Merge "Mock permission checks for NetworkManagementServiceTest"
diff --git a/OWNERS b/OWNERS
index d4d1936..09a721f 100644
--- a/OWNERS
+++ b/OWNERS
@@ -15,6 +15,7 @@
narayan@google.com #{LAST_RESORT_SUGGESTION}
ogunwale@google.com #{LAST_RESORT_SUGGESTION}
roosa@google.com #{LAST_RESORT_SUGGESTION}
+smoreland@google.com #{LAST_RESORT_SUGGESTION}
svetoslavganov@android.com #{LAST_RESORT_SUGGESTION}
svetoslavganov@google.com #{LAST_RESORT_SUGGESTION}
yamasani@google.com #{LAST_RESORT_SUGGESTION}
diff --git a/apex/jobscheduler/framework/java/android/app/AlarmManager.java b/apex/jobscheduler/framework/java/android/app/AlarmManager.java
index dade7c3..53e81c7 100644
--- a/apex/jobscheduler/framework/java/android/app/AlarmManager.java
+++ b/apex/jobscheduler/framework/java/android/app/AlarmManager.java
@@ -27,7 +27,6 @@
import android.annotation.SystemService;
import android.annotation.TestApi;
import android.compat.annotation.ChangeId;
-import android.compat.annotation.Disabled;
import android.compat.annotation.EnabledSince;
import android.compat.annotation.UnsupportedAppUsage;
import android.content.Context;
@@ -285,11 +284,10 @@
* The permission {@link Manifest.permission#SCHEDULE_EXACT_ALARM} will be denied, unless the
* user explicitly allows it from Settings.
*
- * TODO (b/226439802): Either enable it in the next SDK or replace it with a better alternative.
* @hide
*/
@ChangeId
- @Disabled
+ @EnabledSince(targetSdkVersion = Build.VERSION_CODES.TIRAMISU)
public static final long SCHEDULE_EXACT_ALARM_DENIED_BY_DEFAULT = 226439802L;
@UnsupportedAppUsage
diff --git a/apex/jobscheduler/framework/java/android/app/JobSchedulerImpl.java b/apex/jobscheduler/framework/java/android/app/JobSchedulerImpl.java
index 2d3201a..4242cf8 100644
--- a/apex/jobscheduler/framework/java/android/app/JobSchedulerImpl.java
+++ b/apex/jobscheduler/framework/java/android/app/JobSchedulerImpl.java
@@ -158,7 +158,10 @@
android.Manifest.permission.INTERACT_ACROSS_USERS_FULL})
@Override
public void registerUserVisibleJobObserver(@NonNull IUserVisibleJobObserver observer) {
- // TODO(255767350): implement
+ try {
+ mBinder.registerUserVisibleJobObserver(observer);
+ } catch (RemoteException e) {
+ }
}
@RequiresPermission(allOf = {
@@ -166,7 +169,10 @@
android.Manifest.permission.INTERACT_ACROSS_USERS_FULL})
@Override
public void unregisterUserVisibleJobObserver(@NonNull IUserVisibleJobObserver observer) {
- // TODO(255767350): implement
+ try {
+ mBinder.unregisterUserVisibleJobObserver(observer);
+ } catch (RemoteException e) {
+ }
}
@RequiresPermission(allOf = {
@@ -174,6 +180,9 @@
android.Manifest.permission.INTERACT_ACROSS_USERS_FULL})
@Override
public void stopUserVisibleJobsForUser(@NonNull String packageName, int userId) {
- // TODO(255767350): implement
+ try {
+ mBinder.stopUserVisibleJobsForUser(packageName, userId);
+ } catch (RemoteException e) {
+ }
}
}
diff --git a/apex/jobscheduler/framework/java/android/app/job/IJobScheduler.aidl b/apex/jobscheduler/framework/java/android/app/job/IJobScheduler.aidl
index bf29dc9..c87a2af 100644
--- a/apex/jobscheduler/framework/java/android/app/job/IJobScheduler.aidl
+++ b/apex/jobscheduler/framework/java/android/app/job/IJobScheduler.aidl
@@ -16,6 +16,7 @@
package android.app.job;
+import android.app.job.IUserVisibleJobObserver;
import android.app.job.JobInfo;
import android.app.job.JobSnapshot;
import android.app.job.JobWorkItem;
@@ -38,4 +39,10 @@
boolean hasRunLongJobsPermission(String packageName, int userId);
List<JobInfo> getStartedJobs();
ParceledListSlice getAllJobSnapshots();
+ @EnforcePermission(allOf={"MANAGE_ACTIVITY_TASKS", "INTERACT_ACROSS_USERS_FULL"})
+ void registerUserVisibleJobObserver(in IUserVisibleJobObserver observer);
+ @EnforcePermission(allOf={"MANAGE_ACTIVITY_TASKS", "INTERACT_ACROSS_USERS_FULL"})
+ void unregisterUserVisibleJobObserver(in IUserVisibleJobObserver observer);
+ @EnforcePermission(allOf={"MANAGE_ACTIVITY_TASKS", "INTERACT_ACROSS_USERS_FULL"})
+ void stopUserVisibleJobsForUser(String packageName, int userId);
}
diff --git a/apex/jobscheduler/framework/java/android/app/job/JobParameters.java b/apex/jobscheduler/framework/java/android/app/job/JobParameters.java
index ed72530..0205430 100644
--- a/apex/jobscheduler/framework/java/android/app/job/JobParameters.java
+++ b/apex/jobscheduler/framework/java/android/app/job/JobParameters.java
@@ -98,6 +98,12 @@
*/
public static final int INTERNAL_STOP_REASON_SUCCESSFUL_FINISH =
JobProtoEnums.INTERNAL_STOP_REASON_SUCCESSFUL_FINISH; // 10.
+ /**
+ * The user stopped the job via some UI (eg. Task Manager).
+ * @hide
+ */
+ public static final int INTERNAL_STOP_REASON_USER_UI_STOP =
+ JobProtoEnums.INTERNAL_STOP_REASON_USER_UI_STOP; // 11.
/**
* All the stop reason codes. This should be regarded as an immutable array at runtime.
@@ -121,6 +127,7 @@
INTERNAL_STOP_REASON_DATA_CLEARED,
INTERNAL_STOP_REASON_RTC_UPDATED,
INTERNAL_STOP_REASON_SUCCESSFUL_FINISH,
+ INTERNAL_STOP_REASON_USER_UI_STOP,
};
/**
@@ -141,6 +148,7 @@
case INTERNAL_STOP_REASON_DATA_CLEARED: return "data_cleared";
case INTERNAL_STOP_REASON_RTC_UPDATED: return "rtc_updated";
case INTERNAL_STOP_REASON_SUCCESSFUL_FINISH: return "successful_finish";
+ case INTERNAL_STOP_REASON_USER_UI_STOP: return "user_ui_stop";
default: return "unknown:" + reasonCode;
}
}
@@ -230,7 +238,7 @@
public static final int STOP_REASON_APP_STANDBY = 12;
/**
* The user stopped the job. This can happen either through force-stop, adb shell commands,
- * or uninstalling.
+ * uninstalling, or some other UI.
*/
public static final int STOP_REASON_USER = 13;
/** The system is doing some processing that requires stopping this job. */
diff --git a/apex/jobscheduler/framework/java/android/app/job/JobService.java b/apex/jobscheduler/framework/java/android/app/job/JobService.java
index 9297fee..e88e979 100644
--- a/apex/jobscheduler/framework/java/android/app/job/JobService.java
+++ b/apex/jobscheduler/framework/java/android/app/job/JobService.java
@@ -115,9 +115,9 @@
public long getTransferredDownloadBytes(@NonNull JobParameters params,
@Nullable JobWorkItem item) {
if (item == null) {
- return JobService.this.getTransferredDownloadBytes();
+ return JobService.this.getTransferredDownloadBytes(params);
} else {
- return JobService.this.getTransferredDownloadBytes(item);
+ return JobService.this.getTransferredDownloadBytes(params, item);
}
}
@@ -126,9 +126,9 @@
public long getTransferredUploadBytes(@NonNull JobParameters params,
@Nullable JobWorkItem item) {
if (item == null) {
- return JobService.this.getTransferredUploadBytes();
+ return JobService.this.getTransferredUploadBytes(params);
} else {
- return JobService.this.getTransferredUploadBytes(item);
+ return JobService.this.getTransferredUploadBytes(params, item);
}
}
};
@@ -305,7 +305,7 @@
*/
// TODO(255371817): specify the actual time JS will wait for progress before requesting
@BytesLong
- public long getTransferredDownloadBytes() {
+ public long getTransferredDownloadBytes(@NonNull JobParameters params) {
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.
@@ -329,7 +329,7 @@
*/
// TODO(255371817): specify the actual time JS will wait for progress before requesting
@BytesLong
- public long getTransferredUploadBytes() {
+ public long getTransferredUploadBytes(@NonNull JobParameters params) {
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.
@@ -355,9 +355,10 @@
*/
// TODO(255371817): specify the actual time JS will wait for progress before requesting
@BytesLong
- public long getTransferredDownloadBytes(@NonNull JobWorkItem item) {
+ public long getTransferredDownloadBytes(@NonNull JobParameters params,
+ @NonNull JobWorkItem item) {
if (item == null) {
- return getTransferredDownloadBytes();
+ return getTransferredDownloadBytes(params);
}
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
@@ -384,9 +385,10 @@
*/
// TODO(255371817): specify the actual time JS will wait for progress before requesting
@BytesLong
- public long getTransferredUploadBytes(@NonNull JobWorkItem item) {
+ public long getTransferredUploadBytes(@NonNull JobParameters params,
+ @NonNull JobWorkItem item) {
if (item == null) {
- return getTransferredUploadBytes();
+ return getTransferredUploadBytes(params);
}
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
diff --git a/apex/jobscheduler/service/java/com/android/server/job/JobConcurrencyManager.java b/apex/jobscheduler/service/java/com/android/server/job/JobConcurrencyManager.java
index 47f6890..16201b2 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/JobConcurrencyManager.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/JobConcurrencyManager.java
@@ -1184,6 +1184,22 @@
}
@GuardedBy("mLock")
+ void stopUserVisibleJobsLocked(int userId, @NonNull String packageName,
+ @JobParameters.StopReason int reason, int internalReasonCode) {
+ for (int i = mActiveServices.size() - 1; i >= 0; --i) {
+ final JobServiceContext jsc = mActiveServices.get(i);
+ final JobStatus jobStatus = jsc.getRunningJobLocked();
+
+ if (jobStatus != null && userId == jobStatus.getSourceUserId()
+ && jobStatus.getSourcePackageName().equals(packageName)
+ && jobStatus.isUserVisibleJob()) {
+ jsc.cancelExecutingJobLocked(reason, internalReasonCode,
+ JobParameters.getInternalReasonCodeDescription(internalReasonCode));
+ }
+ }
+ }
+
+ @GuardedBy("mLock")
void stopNonReadyActiveJobsLocked() {
for (int i = 0; i < mActiveServices.size(); i++) {
JobServiceContext serviceContext = mActiveServices.get(i);
diff --git a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java
index 9fb2af7..6f58c7ce 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java
@@ -16,11 +16,14 @@
package com.android.server.job;
+import static android.Manifest.permission.INTERACT_ACROSS_USERS_FULL;
+import static android.Manifest.permission.MANAGE_ACTIVITY_TASKS;
import static android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_DISABLED;
import static android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_DISABLED_USER;
import static android.text.format.DateUtils.HOUR_IN_MILLIS;
import static android.text.format.DateUtils.MINUTE_IN_MILLIS;
+import android.annotation.EnforcePermission;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.UserIdInt;
@@ -31,6 +34,7 @@
import android.app.IUidObserver;
import android.app.compat.CompatChanges;
import android.app.job.IJobScheduler;
+import android.app.job.IUserVisibleJobObserver;
import android.app.job.JobInfo;
import android.app.job.JobParameters;
import android.app.job.JobProtoEnums;
@@ -38,6 +42,7 @@
import android.app.job.JobService;
import android.app.job.JobSnapshot;
import android.app.job.JobWorkItem;
+import android.app.job.UserVisibleJobSummary;
import android.app.usage.UsageStatsManager;
import android.app.usage.UsageStatsManagerInternal;
import android.compat.annotation.ChangeId;
@@ -68,6 +73,7 @@
import android.os.Message;
import android.os.ParcelFileDescriptor;
import android.os.Process;
+import android.os.RemoteCallbackList;
import android.os.RemoteException;
import android.os.SystemClock;
import android.os.UserHandle;
@@ -248,6 +254,8 @@
static final int MSG_UID_IDLE = 7;
static final int MSG_CHECK_CHANGED_JOB_LIST = 8;
static final int MSG_CHECK_MEDIA_EXEMPTION = 9;
+ static final int MSG_INFORM_OBSERVER_OF_ALL_USER_VISIBLE_JOBS = 10;
+ static final int MSG_INFORM_OBSERVERS_OF_USER_VISIBLE_JOB_CHANGE = 11;
/** List of controllers that will notify this service of updates to jobs. */
final List<StateController> mControllers;
@@ -258,6 +266,8 @@
private final List<RestrictingController> mRestrictiveControllers;
/** Need direct access to this for testing. */
private final StorageController mStorageController;
+ /** Needed to get estimated transfer time. */
+ private final ConnectivityController mConnectivityController;
/** Need directly for sending uid state changes */
private final DeviceIdleJobsController mDeviceIdleJobsController;
/** Needed to get next estimated launch time. */
@@ -279,6 +289,9 @@
@GuardedBy("mLock")
private final SparseArray<String> mCloudMediaProviderPackages = new SparseArray<>();
+ private final RemoteCallbackList<IUserVisibleJobObserver> mUserVisibleJobObservers =
+ new RemoteCallbackList<>();
+
private final CountQuotaTracker mQuotaTracker;
private static final String QUOTA_TRACKER_SCHEDULE_PERSISTED_TAG = ".schedulePersisted()";
private static final String QUOTA_TRACKER_SCHEDULE_LOGGED =
@@ -453,6 +466,13 @@
case Constants.KEY_RUNTIME_MIN_GUARANTEE_MS:
case Constants.KEY_RUNTIME_MIN_EJ_GUARANTEE_MS:
case Constants.KEY_RUNTIME_MIN_HIGH_PRIORITY_GUARANTEE_MS:
+ case Constants.KEY_RUNTIME_MIN_DATA_TRANSFER_GUARANTEE_MS:
+ case Constants.KEY_RUNTIME_DATA_TRANSFER_LIMIT_MS:
+ case Constants.KEY_RUNTIME_MIN_USER_INITIATED_GUARANTEE_MS:
+ case Constants.KEY_RUNTIME_USER_INITIATED_LIMIT_MS:
+ case Constants.KEY_RUNTIME_MIN_USER_INITIATED_DATA_TRANSFER_GUARANTEE_BUFFER_FACTOR:
+ case Constants.KEY_RUNTIME_MIN_USER_INITIATED_DATA_TRANSFER_GUARANTEE_MS:
+ case Constants.KEY_RUNTIME_USER_INITIATED_DATA_TRANSFER_LIMIT_MS:
if (!runtimeUpdated) {
mConstants.updateRuntimeConstantsLocked();
runtimeUpdated = true;
@@ -544,6 +564,21 @@
private static final String KEY_RUNTIME_MIN_EJ_GUARANTEE_MS = "runtime_min_ej_guarantee_ms";
private static final String KEY_RUNTIME_MIN_HIGH_PRIORITY_GUARANTEE_MS =
"runtime_min_high_priority_guarantee_ms";
+ private static final String KEY_RUNTIME_MIN_DATA_TRANSFER_GUARANTEE_MS =
+ "runtime_min_data_transfer_guarantee_ms";
+ private static final String KEY_RUNTIME_DATA_TRANSFER_LIMIT_MS =
+ "runtime_data_transfer_limit_ms";
+ private static final String KEY_RUNTIME_MIN_USER_INITIATED_GUARANTEE_MS =
+ "runtime_min_user_initiated_guarantee_ms";
+ private static final String KEY_RUNTIME_USER_INITIATED_LIMIT_MS =
+ "runtime_user_initiated_limit_ms";
+ private static final String
+ KEY_RUNTIME_MIN_USER_INITIATED_DATA_TRANSFER_GUARANTEE_BUFFER_FACTOR =
+ "runtime_min_user_initiated_data_transfer_guarantee_buffer_factor";
+ private static final String KEY_RUNTIME_MIN_USER_INITIATED_DATA_TRANSFER_GUARANTEE_MS =
+ "runtime_min_user_initiated_data_transfer_guarantee_ms";
+ private static final String KEY_RUNTIME_USER_INITIATED_DATA_TRANSFER_LIMIT_MS =
+ "runtime_user_initiated_data_transfer_limit_ms";
private static final String KEY_PERSIST_IN_SPLIT_FILES = "persist_in_split_files";
@@ -573,6 +608,20 @@
public static final long DEFAULT_RUNTIME_MIN_EJ_GUARANTEE_MS = 3 * MINUTE_IN_MILLIS;
@VisibleForTesting
static final long DEFAULT_RUNTIME_MIN_HIGH_PRIORITY_GUARANTEE_MS = 5 * MINUTE_IN_MILLIS;
+ public static final long DEFAULT_RUNTIME_MIN_DATA_TRANSFER_GUARANTEE_MS =
+ DEFAULT_RUNTIME_MIN_GUARANTEE_MS;
+ public static final long DEFAULT_RUNTIME_DATA_TRANSFER_LIMIT_MS =
+ DEFAULT_RUNTIME_FREE_QUOTA_MAX_LIMIT_MS;
+ public static final long DEFAULT_RUNTIME_MIN_USER_INITIATED_GUARANTEE_MS =
+ Math.max(10 * MINUTE_IN_MILLIS, DEFAULT_RUNTIME_MIN_GUARANTEE_MS);
+ public static final long DEFAULT_RUNTIME_USER_INITIATED_LIMIT_MS =
+ Math.max(60 * MINUTE_IN_MILLIS, DEFAULT_RUNTIME_FREE_QUOTA_MAX_LIMIT_MS);
+ public static final float
+ DEFAULT_RUNTIME_MIN_USER_INITIATED_DATA_TRANSFER_GUARANTEE_BUFFER_FACTOR = 1.35f;
+ public static final long DEFAULT_RUNTIME_MIN_USER_INITIATED_DATA_TRANSFER_GUARANTEE_MS =
+ Math.max(10 * MINUTE_IN_MILLIS, DEFAULT_RUNTIME_MIN_USER_INITIATED_GUARANTEE_MS);
+ public static final long DEFAULT_RUNTIME_USER_INITIATED_DATA_TRANSFER_LIMIT_MS =
+ Math.max(Long.MAX_VALUE, DEFAULT_RUNTIME_USER_INITIATED_LIMIT_MS);
static final boolean DEFAULT_PERSIST_IN_SPLIT_FILES = true;
private static final boolean DEFAULT_USE_TARE_POLICY = false;
@@ -689,6 +738,49 @@
DEFAULT_RUNTIME_MIN_HIGH_PRIORITY_GUARANTEE_MS;
/**
+ * The minimum amount of time we try to guarantee normal data transfer jobs will run for.
+ */
+ public long RUNTIME_MIN_DATA_TRANSFER_GUARANTEE_MS =
+ DEFAULT_RUNTIME_MIN_DATA_TRANSFER_GUARANTEE_MS;
+
+ /**
+ * The maximum amount of time we will let a normal data transfer job run for. This will only
+ * apply if there are no other limits that apply to the specific data transfer job.
+ */
+ public long RUNTIME_DATA_TRANSFER_LIMIT_MS = DEFAULT_RUNTIME_DATA_TRANSFER_LIMIT_MS;
+
+ /**
+ * The minimum amount of time we try to guarantee normal user-initiated jobs will run for.
+ */
+ public long RUNTIME_MIN_USER_INITIATED_GUARANTEE_MS =
+ DEFAULT_RUNTIME_MIN_USER_INITIATED_GUARANTEE_MS;
+
+ /**
+ * The maximum amount of time we will let a user-initiated job run for. This will only
+ * apply if there are no other limits that apply to the specific user-initiated job.
+ */
+ public long RUNTIME_USER_INITIATED_LIMIT_MS = DEFAULT_RUNTIME_USER_INITIATED_LIMIT_MS;
+
+ /**
+ * A factor to apply to estimated transfer durations for user-initiated data transfer jobs
+ * so that we give some extra time for unexpected situations. This will be at least 1 and
+ * so can just be multiplied with the original value to get the final value.
+ */
+ public float RUNTIME_MIN_USER_INITIATED_DATA_TRANSFER_GUARANTEE_BUFFER_FACTOR =
+ DEFAULT_RUNTIME_MIN_USER_INITIATED_DATA_TRANSFER_GUARANTEE_BUFFER_FACTOR;
+
+ /**
+ * The minimum amount of time we try to guarantee user-initiated data transfer jobs
+ * will run for.
+ */
+ public long RUNTIME_MIN_USER_INITIATED_DATA_TRANSFER_GUARANTEE_MS =
+ DEFAULT_RUNTIME_MIN_USER_INITIATED_DATA_TRANSFER_GUARANTEE_MS;
+
+ /** The maximum amount of time we will let a user-initiated data transfer job run for. */
+ public long RUNTIME_USER_INITIATED_DATA_TRANSFER_LIMIT_MS =
+ DEFAULT_RUNTIME_USER_INITIATED_DATA_TRANSFER_LIMIT_MS;
+
+ /**
* Whether to persist jobs in split files (by UID). If false, all persisted jobs will be
* saved in a single file.
*/
@@ -790,7 +882,14 @@
DeviceConfig.NAMESPACE_JOB_SCHEDULER,
KEY_RUNTIME_FREE_QUOTA_MAX_LIMIT_MS,
KEY_RUNTIME_MIN_GUARANTEE_MS, KEY_RUNTIME_MIN_EJ_GUARANTEE_MS,
- KEY_RUNTIME_MIN_HIGH_PRIORITY_GUARANTEE_MS);
+ KEY_RUNTIME_MIN_HIGH_PRIORITY_GUARANTEE_MS,
+ KEY_RUNTIME_MIN_USER_INITIATED_DATA_TRANSFER_GUARANTEE_BUFFER_FACTOR,
+ KEY_RUNTIME_MIN_DATA_TRANSFER_GUARANTEE_MS,
+ KEY_RUNTIME_DATA_TRANSFER_LIMIT_MS,
+ KEY_RUNTIME_MIN_USER_INITIATED_GUARANTEE_MS,
+ KEY_RUNTIME_USER_INITIATED_LIMIT_MS,
+ KEY_RUNTIME_MIN_USER_INITIATED_DATA_TRANSFER_GUARANTEE_MS,
+ KEY_RUNTIME_USER_INITIATED_DATA_TRANSFER_LIMIT_MS);
// Make sure min runtime for regular jobs is at least 10 minutes.
RUNTIME_MIN_GUARANTEE_MS = Math.max(10 * MINUTE_IN_MILLIS,
@@ -808,6 +907,49 @@
RUNTIME_FREE_QUOTA_MAX_LIMIT_MS = Math.max(RUNTIME_MIN_GUARANTEE_MS,
properties.getLong(KEY_RUNTIME_FREE_QUOTA_MAX_LIMIT_MS,
DEFAULT_RUNTIME_FREE_QUOTA_MAX_LIMIT_MS));
+ // Make sure min runtime is at least as long as regular jobs.
+ RUNTIME_MIN_DATA_TRANSFER_GUARANTEE_MS = Math.max(RUNTIME_MIN_GUARANTEE_MS,
+ properties.getLong(
+ KEY_RUNTIME_MIN_DATA_TRANSFER_GUARANTEE_MS,
+ DEFAULT_RUNTIME_MIN_DATA_TRANSFER_GUARANTEE_MS));
+ // Max limit should be at least the min guarantee AND the free quota.
+ RUNTIME_DATA_TRANSFER_LIMIT_MS = Math.max(RUNTIME_FREE_QUOTA_MAX_LIMIT_MS,
+ Math.max(RUNTIME_MIN_DATA_TRANSFER_GUARANTEE_MS,
+ properties.getLong(
+ KEY_RUNTIME_DATA_TRANSFER_LIMIT_MS,
+ DEFAULT_RUNTIME_DATA_TRANSFER_LIMIT_MS)));
+ // Make sure min runtime is at least as long as regular jobs.
+ RUNTIME_MIN_USER_INITIATED_GUARANTEE_MS = Math.max(RUNTIME_MIN_GUARANTEE_MS,
+ properties.getLong(
+ KEY_RUNTIME_MIN_USER_INITIATED_GUARANTEE_MS,
+ DEFAULT_RUNTIME_MIN_USER_INITIATED_GUARANTEE_MS));
+ // Max limit should be at least the min guarantee AND the free quota.
+ RUNTIME_USER_INITIATED_LIMIT_MS = Math.max(RUNTIME_FREE_QUOTA_MAX_LIMIT_MS,
+ Math.max(RUNTIME_MIN_USER_INITIATED_GUARANTEE_MS,
+ properties.getLong(
+ KEY_RUNTIME_USER_INITIATED_LIMIT_MS,
+ DEFAULT_RUNTIME_USER_INITIATED_LIMIT_MS)));
+ // The buffer factor should be at least 1 (so we don't decrease the time).
+ RUNTIME_MIN_USER_INITIATED_DATA_TRANSFER_GUARANTEE_BUFFER_FACTOR = Math.max(1,
+ properties.getFloat(
+ KEY_RUNTIME_MIN_USER_INITIATED_DATA_TRANSFER_GUARANTEE_BUFFER_FACTOR,
+ DEFAULT_RUNTIME_MIN_USER_INITIATED_DATA_TRANSFER_GUARANTEE_BUFFER_FACTOR
+ ));
+ // Make sure min runtime is at least as long as other user-initiated jobs.
+ RUNTIME_MIN_USER_INITIATED_DATA_TRANSFER_GUARANTEE_MS = Math.max(
+ RUNTIME_MIN_USER_INITIATED_GUARANTEE_MS,
+ properties.getLong(
+ KEY_RUNTIME_MIN_USER_INITIATED_DATA_TRANSFER_GUARANTEE_MS,
+ DEFAULT_RUNTIME_MIN_USER_INITIATED_DATA_TRANSFER_GUARANTEE_MS));
+ // Data transfer requires RUN_LONG_JOBS permission, so the upper limit will be higher
+ // than other jobs.
+ // Max limit should be the min guarantee and the max of other user-initiated jobs.
+ RUNTIME_USER_INITIATED_DATA_TRANSFER_LIMIT_MS = Math.max(
+ RUNTIME_MIN_USER_INITIATED_DATA_TRANSFER_GUARANTEE_MS,
+ Math.max(RUNTIME_USER_INITIATED_LIMIT_MS,
+ properties.getLong(
+ KEY_RUNTIME_USER_INITIATED_DATA_TRANSFER_LIMIT_MS,
+ DEFAULT_RUNTIME_USER_INITIATED_DATA_TRANSFER_LIMIT_MS)));
}
private boolean updateTareSettingsLocked(boolean isTareEnabled) {
@@ -856,6 +998,20 @@
RUNTIME_MIN_HIGH_PRIORITY_GUARANTEE_MS).println();
pw.print(KEY_RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, RUNTIME_FREE_QUOTA_MAX_LIMIT_MS)
.println();
+ pw.print(KEY_RUNTIME_MIN_DATA_TRANSFER_GUARANTEE_MS,
+ RUNTIME_MIN_DATA_TRANSFER_GUARANTEE_MS).println();
+ pw.print(KEY_RUNTIME_DATA_TRANSFER_LIMIT_MS,
+ RUNTIME_DATA_TRANSFER_LIMIT_MS).println();
+ pw.print(KEY_RUNTIME_MIN_USER_INITIATED_GUARANTEE_MS,
+ RUNTIME_MIN_USER_INITIATED_GUARANTEE_MS).println();
+ pw.print(KEY_RUNTIME_USER_INITIATED_LIMIT_MS,
+ RUNTIME_USER_INITIATED_LIMIT_MS).println();
+ pw.print(KEY_RUNTIME_MIN_USER_INITIATED_DATA_TRANSFER_GUARANTEE_BUFFER_FACTOR,
+ RUNTIME_MIN_USER_INITIATED_DATA_TRANSFER_GUARANTEE_BUFFER_FACTOR).println();
+ pw.print(KEY_RUNTIME_MIN_USER_INITIATED_DATA_TRANSFER_GUARANTEE_MS,
+ RUNTIME_MIN_USER_INITIATED_DATA_TRANSFER_GUARANTEE_MS).println();
+ pw.print(KEY_RUNTIME_USER_INITIATED_DATA_TRANSFER_LIMIT_MS,
+ RUNTIME_USER_INITIATED_DATA_TRANSFER_LIMIT_MS).println();
pw.print(KEY_PERSIST_IN_SPLIT_FILES, PERSIST_IN_SPLIT_FILES).println();
@@ -1501,6 +1657,14 @@
}
}
+ private void stopUserVisibleJobsInternal(@NonNull String packageName, int userId) {
+ synchronized (mLock) {
+ mConcurrencyManager.stopUserVisibleJobsLocked(userId, packageName,
+ JobParameters.STOP_REASON_USER,
+ JobParameters.INTERNAL_STOP_REASON_USER_UI_STOP);
+ }
+ }
+
private final Consumer<JobStatus> mCancelJobDueToUserRemovalConsumer = (toRemove) -> {
// There's no guarantee that the process has been stopped by the time we get
// here, but since this is a user-initiated action, it should be fine to just
@@ -1839,9 +2003,9 @@
final FlexibilityController flexibilityController =
new FlexibilityController(this, mPrefetchController);
mControllers.add(flexibilityController);
- final ConnectivityController connectivityController =
+ mConnectivityController =
new ConnectivityController(this, flexibilityController);
- mControllers.add(connectivityController);
+ mControllers.add(mConnectivityController);
mControllers.add(new TimeController(this));
final IdleController idleController = new IdleController(this, flexibilityController);
mControllers.add(idleController);
@@ -1857,16 +2021,16 @@
mDeviceIdleJobsController = new DeviceIdleJobsController(this);
mControllers.add(mDeviceIdleJobsController);
mQuotaController =
- new QuotaController(this, backgroundJobsController, connectivityController);
+ new QuotaController(this, backgroundJobsController, mConnectivityController);
mControllers.add(mQuotaController);
mControllers.add(new ComponentController(this));
mTareController =
- new TareController(this, backgroundJobsController, connectivityController);
+ new TareController(this, backgroundJobsController, mConnectivityController);
mControllers.add(mTareController);
mRestrictiveControllers = new ArrayList<>();
mRestrictiveControllers.add(batteryController);
- mRestrictiveControllers.add(connectivityController);
+ mRestrictiveControllers.add(mConnectivityController);
mRestrictiveControllers.add(idleController);
// Create restrictions
@@ -2159,6 +2323,7 @@
}
delayMillis =
Math.min(delayMillis, JobInfo.MAX_BACKOFF_DELAY_MILLIS);
+ // TODO(255767350): demote all jobs to regular for user stops so they don't keep privileges
JobStatus newJob = new JobStatus(failureToReschedule,
elapsedNowMillis + delayMillis,
JobStatus.NO_LATEST_RUNTIME, numFailures, numSystemStops,
@@ -2509,6 +2674,52 @@
args.recycle();
break;
}
+
+ case MSG_INFORM_OBSERVER_OF_ALL_USER_VISIBLE_JOBS: {
+ final IUserVisibleJobObserver observer =
+ (IUserVisibleJobObserver) message.obj;
+ synchronized (mLock) {
+ for (int i = mConcurrencyManager.mActiveServices.size() - 1; i >= 0;
+ --i) {
+ JobServiceContext context =
+ mConcurrencyManager.mActiveServices.get(i);
+ final JobStatus jobStatus = context.getRunningJobLocked();
+ if (jobStatus != null && jobStatus.isUserVisibleJob()) {
+ try {
+ observer.onUserVisibleJobStateChanged(
+ jobStatus.getUserVisibleJobSummary(),
+ /* isRunning */ true);
+ } catch (RemoteException e) {
+ // Will be unregistered automatically by
+ // RemoteCallbackList's dead-object tracking,
+ // so don't need to remove it here.
+ break;
+ }
+ }
+ }
+ }
+ break;
+ }
+
+ case MSG_INFORM_OBSERVERS_OF_USER_VISIBLE_JOB_CHANGE: {
+ final SomeArgs args = (SomeArgs) message.obj;
+ final JobServiceContext context = (JobServiceContext) args.arg1;
+ final JobStatus jobStatus = (JobStatus) args.arg2;
+ final UserVisibleJobSummary summary = jobStatus.getUserVisibleJobSummary();
+ final boolean isRunning = args.argi1 == 1;
+ for (int i = mUserVisibleJobObservers.beginBroadcast() - 1; i >= 0; --i) {
+ try {
+ mUserVisibleJobObservers.getBroadcastItem(i)
+ .onUserVisibleJobStateChanged(summary, isRunning);
+ } catch (RemoteException e) {
+ // Will be unregistered automatically by RemoteCallbackList's
+ // dead-object tracking, so nothing we need to do here.
+ }
+ }
+ mUserVisibleJobObservers.finishBroadcast();
+ args.recycle();
+ break;
+ }
}
maybeRunPendingJobsLocked();
}
@@ -2962,7 +3173,30 @@
/** Returns the minimum amount of time we should let this job run before timing out. */
public long getMinJobExecutionGuaranteeMs(JobStatus job) {
synchronized (mLock) {
- if (job.shouldTreatAsExpeditedJob()) {
+ final boolean shouldTreatAsDataTransfer = job.getJob().isDataTransfer()
+ && checkRunLongJobsPermission(job.getSourceUid(), job.getSourcePackageName());
+ if (job.shouldTreatAsUserInitiated()) {
+ if (shouldTreatAsDataTransfer) {
+ final long estimatedTransferTimeMs =
+ mConnectivityController.getEstimatedTransferTimeMs(job);
+ if (estimatedTransferTimeMs == ConnectivityController.UNKNOWN_TIME) {
+ return mConstants.RUNTIME_MIN_USER_INITIATED_DATA_TRANSFER_GUARANTEE_MS;
+ }
+ // Try to give the job at least as much time as we think the transfer will take,
+ // but cap it at the maximum limit
+ final long factoredTransferTimeMs = (long) (estimatedTransferTimeMs
+ * mConstants
+ .RUNTIME_MIN_USER_INITIATED_DATA_TRANSFER_GUARANTEE_BUFFER_FACTOR);
+ return Math.min(mConstants.RUNTIME_USER_INITIATED_DATA_TRANSFER_LIMIT_MS,
+ Math.max(factoredTransferTimeMs,
+ mConstants.RUNTIME_MIN_USER_INITIATED_DATA_TRANSFER_GUARANTEE_MS
+ ));
+ }
+ return mConstants.RUNTIME_MIN_USER_INITIATED_GUARANTEE_MS;
+ } else if (shouldTreatAsDataTransfer) {
+ // For now, don't increase a bg data transfer's minimum guarantee.
+ return mConstants.RUNTIME_MIN_DATA_TRANSFER_GUARANTEE_MS;
+ } else if (job.shouldTreatAsExpeditedJob()) {
// Don't guarantee RESTRICTED jobs more than 5 minutes.
return job.getEffectiveStandbyBucket() != RESTRICTED_INDEX
? mConstants.RUNTIME_MIN_EJ_GUARANTEE_MS
@@ -2978,6 +3212,16 @@
/** Returns the maximum amount of time this job could run for. */
public long getMaxJobExecutionTimeMs(JobStatus job) {
synchronized (mLock) {
+ final boolean shouldTreatAsDataTransfer = job.getJob().isDataTransfer()
+ && checkRunLongJobsPermission(job.getSourceUid(), job.getSourcePackageName());
+ if (job.shouldTreatAsUserInitiated()) {
+ if (shouldTreatAsDataTransfer) {
+ return mConstants.RUNTIME_USER_INITIATED_DATA_TRANSFER_LIMIT_MS;
+ }
+ return mConstants.RUNTIME_USER_INITIATED_LIMIT_MS;
+ } else if (shouldTreatAsDataTransfer) {
+ return mConstants.RUNTIME_DATA_TRANSFER_LIMIT_MS;
+ }
return Math.min(mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS,
mConstants.USE_TARE_POLICY
? mTareController.getMaxJobExecutionTimeMsLocked(job)
@@ -3022,6 +3266,16 @@
return adjustJobBias(bias, job);
}
+ void informObserversOfUserVisibleJobChange(JobServiceContext context, JobStatus jobStatus,
+ boolean isRunning) {
+ SomeArgs args = SomeArgs.obtain();
+ args.arg1 = context;
+ args.arg2 = jobStatus;
+ args.argi1 = isRunning ? 1 : 0;
+ mHandler.obtainMessage(MSG_INFORM_OBSERVERS_OF_USER_VISIBLE_JOB_CHANGE, args)
+ .sendToTarget();
+ }
+
private final class BatteryStateTracker extends BroadcastReceiver {
/**
* Track whether we're "charging", where charging means that we're ready to commit to
@@ -3627,13 +3881,6 @@
return checkRunLongJobsPermission(uid, packageName);
}
- private boolean checkRunLongJobsPermission(int packageUid, String packageName) {
- // Returns true if both the appop and permission are granted.
- return PermissionChecker.checkPermissionForPreflight(getContext(),
- android.Manifest.permission.RUN_LONG_JOBS, PermissionChecker.PID_UNKNOWN,
- packageUid, packageName) == PermissionChecker.PERMISSION_GRANTED;
- }
-
/**
* "dumpsys" infrastructure
*/
@@ -3744,6 +3991,38 @@
return new ParceledListSlice<>(snapshots);
}
}
+
+ @Override
+ @EnforcePermission(allOf = {MANAGE_ACTIVITY_TASKS, INTERACT_ACROSS_USERS_FULL})
+ public void registerUserVisibleJobObserver(@NonNull IUserVisibleJobObserver observer) {
+ super.registerUserVisibleJobObserver_enforcePermission();
+ if (observer == null) {
+ throw new NullPointerException("observer");
+ }
+ mUserVisibleJobObservers.register(observer);
+ mHandler.obtainMessage(MSG_INFORM_OBSERVER_OF_ALL_USER_VISIBLE_JOBS, observer)
+ .sendToTarget();
+ }
+
+ @Override
+ @EnforcePermission(allOf = {MANAGE_ACTIVITY_TASKS, INTERACT_ACROSS_USERS_FULL})
+ public void unregisterUserVisibleJobObserver(@NonNull IUserVisibleJobObserver observer) {
+ super.unregisterUserVisibleJobObserver_enforcePermission();
+ if (observer == null) {
+ throw new NullPointerException("observer");
+ }
+ mUserVisibleJobObservers.unregister(observer);
+ }
+
+ @Override
+ @EnforcePermission(allOf = {MANAGE_ACTIVITY_TASKS, INTERACT_ACROSS_USERS_FULL})
+ public void stopUserVisibleJobsForUser(@NonNull String packageName, int userId) {
+ super.stopUserVisibleJobsForUser_enforcePermission();
+ if (packageName == null) {
+ throw new NullPointerException("packageName");
+ }
+ JobSchedulerService.this.stopUserVisibleJobsInternal(packageName, userId);
+ }
}
// Shell command infrastructure: run the given job immediately
@@ -3880,13 +4159,27 @@
}
}
+ private boolean checkRunLongJobsPermission(int packageUid, String packageName) {
+ // Returns true if both the appop and permission are granted.
+ return PermissionChecker.checkPermissionForPreflight(getTestableContext(),
+ android.Manifest.permission.RUN_LONG_JOBS, PermissionChecker.PID_UNKNOWN,
+ packageUid, packageName) == PermissionChecker.PERMISSION_GRANTED;
+ }
+
+ @VisibleForTesting
+ protected ConnectivityController getConnectivityController() {
+ return mConnectivityController;
+ }
+
// Shell command infrastructure
int getJobState(PrintWriter pw, String pkgName, int userId, int jobId) {
try {
final int uid = AppGlobals.getPackageManager().getPackageUid(pkgName, 0,
userId != UserHandle.USER_ALL ? userId : UserHandle.USER_SYSTEM);
if (uid < 0) {
- pw.print("unknown("); pw.print(pkgName); pw.println(")");
+ pw.print("unknown(");
+ pw.print(pkgName);
+ pw.println(")");
return JobSchedulerShellCommand.CMD_ERR_NO_PACKAGE;
}
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 b20eedc..fead68e 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java
@@ -944,6 +944,9 @@
return;
}
scheduleOpTimeOutLocked();
+ if (mRunningJob.isUserVisibleJob()) {
+ mService.informObserversOfUserVisibleJobChange(this, mRunningJob, true);
+ }
break;
default:
Slog.e(TAG, "Handling started job but job wasn't starting! Was "
@@ -1202,6 +1205,9 @@
mPendingDebugStopReason = null;
mNotification = null;
removeOpTimeOutLocked();
+ if (completedJob.isUserVisibleJob()) {
+ mService.informObserversOfUserVisibleJobChange(this, completedJob, false);
+ }
mCompletedListener.onJobCompletedLocked(completedJob, internalStopReason, reschedule);
mJobConcurrencyManager.onJobCompletedLocked(this, completedJob, workType);
}
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 145ac52..a1153e3 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/JobStore.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/JobStore.java
@@ -91,8 +91,11 @@
/** Threshold to adjust how often we want to write to the db. */
private static final long JOB_PERSIST_DELAY = 2000L;
- private static final String JOB_FILE_SPLIT_PREFIX = "jobs_";
+ @VisibleForTesting
+ static final String JOB_FILE_SPLIT_PREFIX = "jobs_";
private static final int ALL_UIDS = -1;
+ @VisibleForTesting
+ static final int INVALID_UID = -2;
final Object mLock;
final Object mWriteScheduleLock; // used solely for invariants around write scheduling
@@ -529,6 +532,25 @@
return values;
}
+ @VisibleForTesting
+ static int extractUidFromJobFileName(@NonNull File file) {
+ final String fileName = file.getName();
+ if (fileName.startsWith(JOB_FILE_SPLIT_PREFIX)) {
+ try {
+ final int subEnd = fileName.length() - 4; // -4 for ".xml"
+ final int uid = Integer.parseInt(
+ fileName.substring(JOB_FILE_SPLIT_PREFIX.length(), subEnd));
+ if (uid < 0) {
+ return INVALID_UID;
+ }
+ return uid;
+ } catch (Exception e) {
+ Slog.e(TAG, "Unexpected file name format", e);
+ }
+ }
+ return INVALID_UID;
+ }
+
/**
* Runnable that writes {@link #mJobSet} out to xml.
* NOTE: This Runnable locks on mLock
@@ -543,6 +565,42 @@
private void prepare() {
mCopyAllJobs = !mUseSplitFiles || mPendingJobWriteUids.get(ALL_UIDS);
+ if (mUseSplitFiles) {
+ // Put the set of changed UIDs in the copy list so that we update each file,
+ // especially if we've dropped all jobs for that UID.
+ if (mPendingJobWriteUids.get(ALL_UIDS)) {
+ // ALL_UIDS is only used when we switch file splitting policy or for tests,
+ // so going through the file list here shouldn't be
+ // a large performance hit on user devices.
+
+ final File[] files;
+ try {
+ files = mJobFileDirectory.listFiles();
+ } catch (SecurityException e) {
+ Slog.wtf(TAG, "Not allowed to read job file directory", e);
+ return;
+ }
+ if (files == null) {
+ Slog.wtfStack(TAG, "Couldn't get job file list");
+ } else {
+ for (File file : files) {
+ final int uid = extractUidFromJobFileName(file);
+ if (uid != INVALID_UID) {
+ mJobStoreCopy.put(uid, new ArrayList<>());
+ }
+ }
+ }
+ } else {
+ for (int i = 0; i < mPendingJobWriteUids.size(); ++i) {
+ mJobStoreCopy.put(mPendingJobWriteUids.keyAt(i), new ArrayList<>());
+ }
+ }
+ } else {
+ // Single file mode.
+ // Put the catchall UID in the copy list so that we update the single file,
+ // especially if we've dropped all persisted jobs.
+ mJobStoreCopy.put(ALL_UIDS, new ArrayList<>());
+ }
}
@Override
diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/ConnectivityController.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/ConnectivityController.java
index 16dd1672..6166921 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/controllers/ConnectivityController.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/ConnectivityController.java
@@ -42,7 +42,6 @@
import android.text.format.DateUtils;
import android.util.ArrayMap;
import android.util.ArraySet;
-import android.util.DataUnit;
import android.util.IndentingPrintWriter;
import android.util.Log;
import android.util.Pools;
@@ -82,6 +81,8 @@
private static final boolean DEBUG = JobSchedulerService.DEBUG
|| Log.isLoggable(TAG, Log.DEBUG);
+ public static final long UNKNOWN_TIME = -1L;
+
// The networking stack has a hard limit so we can't make this configurable.
private static final int MAX_NETWORK_CALLBACKS = 125;
/**
@@ -570,9 +571,8 @@
// If we don't know the bandwidth, all we can do is hope the job finishes the minimum
// chunk in time.
if (bandwidthDown > 0) {
- // Divide by 8 to convert bits to bytes.
- final long estimatedMillis = ((minimumChunkBytes * DateUtils.SECOND_IN_MILLIS)
- / (DataUnit.KIBIBYTES.toBytes(bandwidthDown) / 8));
+ final long estimatedMillis =
+ calculateTransferTimeMs(minimumChunkBytes, bandwidthDown);
if (estimatedMillis > maxJobExecutionTimeMs) {
// If we'd never finish the minimum chunk before the timeout, we'd be insane!
Slog.w(TAG, "Minimum chunk " + minimumChunkBytes + " bytes over "
@@ -585,9 +585,8 @@
final long bandwidthUp = capabilities.getLinkUpstreamBandwidthKbps();
// If we don't know the bandwidth, all we can do is hope the job finishes in time.
if (bandwidthUp > 0) {
- // Divide by 8 to convert bits to bytes.
- final long estimatedMillis = ((minimumChunkBytes * DateUtils.SECOND_IN_MILLIS)
- / (DataUnit.KIBIBYTES.toBytes(bandwidthUp) / 8));
+ final long estimatedMillis =
+ calculateTransferTimeMs(minimumChunkBytes, bandwidthUp);
if (estimatedMillis > maxJobExecutionTimeMs) {
// If we'd never finish the minimum chunk before the timeout, we'd be insane!
Slog.w(TAG, "Minimum chunk " + minimumChunkBytes + " bytes over " + bandwidthUp
@@ -615,9 +614,7 @@
final long bandwidth = capabilities.getLinkDownstreamBandwidthKbps();
// If we don't know the bandwidth, all we can do is hope the job finishes in time.
if (bandwidth > 0) {
- // Divide by 8 to convert bits to bytes.
- final long estimatedMillis = ((downloadBytes * DateUtils.SECOND_IN_MILLIS)
- / (DataUnit.KIBIBYTES.toBytes(bandwidth) / 8));
+ final long estimatedMillis = calculateTransferTimeMs(downloadBytes, bandwidth);
if (estimatedMillis > maxJobExecutionTimeMs) {
// If we'd never finish before the timeout, we'd be insane!
Slog.w(TAG, "Estimated " + downloadBytes + " download bytes over " + bandwidth
@@ -633,9 +630,7 @@
final long bandwidth = capabilities.getLinkUpstreamBandwidthKbps();
// If we don't know the bandwidth, all we can do is hope the job finishes in time.
if (bandwidth > 0) {
- // Divide by 8 to convert bits to bytes.
- final long estimatedMillis = ((uploadBytes * DateUtils.SECOND_IN_MILLIS)
- / (DataUnit.KIBIBYTES.toBytes(bandwidth) / 8));
+ final long estimatedMillis = calculateTransferTimeMs(uploadBytes, bandwidth);
if (estimatedMillis > maxJobExecutionTimeMs) {
// If we'd never finish before the timeout, we'd be insane!
Slog.w(TAG, "Estimated " + uploadBytes + " upload bytes over " + bandwidth
@@ -649,6 +644,48 @@
return false;
}
+ /**
+ * Return the estimated amount of time this job will be transferring data,
+ * based on the current network speed.
+ */
+ public long getEstimatedTransferTimeMs(JobStatus jobStatus) {
+ final long downloadBytes = jobStatus.getEstimatedNetworkDownloadBytes();
+ final long uploadBytes = jobStatus.getEstimatedNetworkUploadBytes();
+ if (downloadBytes == JobInfo.NETWORK_BYTES_UNKNOWN
+ && uploadBytes == JobInfo.NETWORK_BYTES_UNKNOWN) {
+ return UNKNOWN_TIME;
+ }
+ if (jobStatus.network == null) {
+ // This job doesn't have a network assigned.
+ return UNKNOWN_TIME;
+ }
+ NetworkCapabilities capabilities = getNetworkCapabilities(jobStatus.network);
+ if (capabilities == null) {
+ return UNKNOWN_TIME;
+ }
+ final long estimatedDownloadTimeMs = calculateTransferTimeMs(downloadBytes,
+ capabilities.getLinkDownstreamBandwidthKbps());
+ final long estimatedUploadTimeMs = calculateTransferTimeMs(uploadBytes,
+ capabilities.getLinkUpstreamBandwidthKbps());
+ if (estimatedDownloadTimeMs == UNKNOWN_TIME) {
+ return estimatedUploadTimeMs;
+ } else if (estimatedUploadTimeMs == UNKNOWN_TIME) {
+ return estimatedDownloadTimeMs;
+ }
+ return estimatedDownloadTimeMs + estimatedUploadTimeMs;
+ }
+
+ @VisibleForTesting
+ static long calculateTransferTimeMs(long transferBytes, long bandwidthKbps) {
+ if (transferBytes == JobInfo.NETWORK_BYTES_UNKNOWN || bandwidthKbps <= 0) {
+ return UNKNOWN_TIME;
+ }
+ return (transferBytes * DateUtils.SECOND_IN_MILLIS)
+ // Multiply by 1000 to convert kilobits to bits.
+ // Divide by 8 to convert bits to bytes.
+ / (bandwidthKbps * 1000 / 8);
+ }
+
private static boolean isCongestionDelayed(JobStatus jobStatus, Network network,
NetworkCapabilities capabilities, Constants constants) {
// If network is congested, and job is less than 50% through the
diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java
index af8e727..419127e 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java
@@ -29,11 +29,13 @@
import static com.android.server.job.controllers.FlexibilityController.SYSTEM_WIDE_FLEXIBLE_CONSTRAINTS;
import android.annotation.ElapsedRealtimeLong;
+import android.annotation.NonNull;
import android.app.AppGlobals;
import android.app.job.JobInfo;
import android.app.job.JobParameters;
import android.app.job.JobScheduler;
import android.app.job.JobWorkItem;
+import android.app.job.UserVisibleJobSummary;
import android.content.ClipData;
import android.content.ComponentName;
import android.net.Network;
@@ -454,6 +456,12 @@
*/
private boolean mExpeditedTareApproved;
+ /**
+ * Summary describing this job. Lazily created in {@link #getUserVisibleJobSummary()}
+ * since not every job will need it.
+ */
+ private UserVisibleJobSummary mUserVisibleJobSummary;
+
/////// Booleans that track if a job is ready to run. They should be updated whenever dependent
/////// states change.
@@ -1337,6 +1345,36 @@
}
/**
+ * @return true if the job was scheduled as a user-initiated job and it hasn't been downgraded
+ * for any reason.
+ */
+ public boolean shouldTreatAsUserInitiated() {
+ // TODO(248386641): implement
+ return false;
+ }
+
+ /**
+ * Return a summary that uniquely identifies the underlying job.
+ */
+ @NonNull
+ public UserVisibleJobSummary getUserVisibleJobSummary() {
+ if (mUserVisibleJobSummary == null) {
+ mUserVisibleJobSummary = new UserVisibleJobSummary(
+ callingUid, getSourceUserId(), getSourcePackageName(), getJobId());
+ }
+ return mUserVisibleJobSummary;
+ }
+
+ /**
+ * @return true if this is a job whose execution should be made visible to the user.
+ */
+ public boolean isUserVisibleJob() {
+ // TODO(255767350): limit to user-initiated jobs
+ // Placeholder implementation until we have the code in
+ return shouldTreatAsExpeditedJob();
+ }
+
+ /**
* @return true if the job is exempted from Doze restrictions and therefore allowed to run
* in Doze.
*/
diff --git a/api/Android.bp b/api/Android.bp
index b0ce9af..318748e 100644
--- a/api/Android.bp
+++ b/api/Android.bp
@@ -118,9 +118,11 @@
],
system_server_classpath: [
"service-art",
+ "service-configinfrastructure",
"service-healthconnect",
"service-media-s",
"service-permission",
+ "service-rkp",
"service-sdksandbox",
],
}
diff --git a/cmds/bootanimation/BootAnimation.cpp b/cmds/bootanimation/BootAnimation.cpp
index c4d90c6..80512f7 100644
--- a/cmds/bootanimation/BootAnimation.cpp
+++ b/cmds/bootanimation/BootAnimation.cpp
@@ -1112,6 +1112,11 @@
int nextReadPos;
+ if (strlen(l) == 0) {
+ s = ++endl;
+ continue;
+ }
+
int topLineNumbers = sscanf(l, "%d %d %d %d", &width, &height, &fps, &progress);
if (topLineNumbers == 3 || topLineNumbers == 4) {
// SLOGD("> w=%d, h=%d, fps=%d, progress=%d", width, height, fps, progress);
diff --git a/core/api/current.txt b/core/api/current.txt
index 31b9f1a..da0bd8d 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -7283,8 +7283,8 @@
method public android.content.Intent getCropAndSetWallpaperIntent(android.net.Uri);
method public int getDesiredMinimumHeight();
method public int getDesiredMinimumWidth();
- method @RequiresPermission(android.Manifest.permission.READ_EXTERNAL_STORAGE) public android.graphics.drawable.Drawable getDrawable();
- method @RequiresPermission(android.Manifest.permission.READ_EXTERNAL_STORAGE) public android.graphics.drawable.Drawable getFastDrawable();
+ method @Nullable @RequiresPermission(android.Manifest.permission.READ_EXTERNAL_STORAGE) public android.graphics.drawable.Drawable getDrawable();
+ method @Nullable @RequiresPermission(android.Manifest.permission.READ_EXTERNAL_STORAGE) public android.graphics.drawable.Drawable getFastDrawable();
method public static android.app.WallpaperManager getInstance(android.content.Context);
method @Nullable public android.app.WallpaperColors getWallpaperColors(int);
method @RequiresPermission(android.Manifest.permission.READ_EXTERNAL_STORAGE) public android.os.ParcelFileDescriptor getWallpaperFile(int);
@@ -7294,8 +7294,8 @@
method public boolean hasResourceWallpaper(@RawRes int);
method public boolean isSetWallpaperAllowed();
method public boolean isWallpaperSupported();
- method public android.graphics.drawable.Drawable peekDrawable();
- method @RequiresPermission(android.Manifest.permission.READ_EXTERNAL_STORAGE) public android.graphics.drawable.Drawable peekFastDrawable();
+ method @Nullable public android.graphics.drawable.Drawable peekDrawable();
+ method @Nullable @RequiresPermission(android.Manifest.permission.READ_EXTERNAL_STORAGE) public android.graphics.drawable.Drawable peekFastDrawable();
method public void removeOnColorsChangedListener(@NonNull android.app.WallpaperManager.OnColorsChangedListener);
method public void sendWallpaperCommand(android.os.IBinder, String, int, int, int, android.os.Bundle);
method @RequiresPermission(android.Manifest.permission.SET_WALLPAPER) public void setBitmap(android.graphics.Bitmap) throws java.io.IOException;
@@ -7475,7 +7475,7 @@
method public boolean getBluetoothContactSharingDisabled(@NonNull android.content.ComponentName);
method public boolean getCameraDisabled(@Nullable android.content.ComponentName);
method @Deprecated @Nullable public String getCertInstallerPackage(@NonNull android.content.ComponentName) throws java.lang.SecurityException;
- method @Nullable public java.util.Set<java.lang.String> getCrossProfileCalendarPackages(@NonNull android.content.ComponentName);
+ method @Deprecated @Nullable public java.util.Set<java.lang.String> getCrossProfileCalendarPackages(@NonNull android.content.ComponentName);
method public boolean getCrossProfileCallerIdDisabled(@NonNull android.content.ComponentName);
method public boolean getCrossProfileContactsSearchDisabled(@NonNull android.content.ComponentName);
method @NonNull public java.util.Set<java.lang.String> getCrossProfilePackages(@NonNull android.content.ComponentName);
@@ -7624,7 +7624,7 @@
method @Deprecated public void setCertInstallerPackage(@NonNull android.content.ComponentName, @Nullable String) throws java.lang.SecurityException;
method public void setCommonCriteriaModeEnabled(@NonNull android.content.ComponentName, boolean);
method public void setConfiguredNetworksLockdownState(@NonNull android.content.ComponentName, boolean);
- method public void setCrossProfileCalendarPackages(@NonNull android.content.ComponentName, @Nullable java.util.Set<java.lang.String>);
+ method @Deprecated public void setCrossProfileCalendarPackages(@NonNull android.content.ComponentName, @Nullable java.util.Set<java.lang.String>);
method public void setCrossProfileCallerIdDisabled(@NonNull android.content.ComponentName, boolean);
method public void setCrossProfileContactsSearchDisabled(@NonNull android.content.ComponentName, boolean);
method public void setCrossProfilePackages(@NonNull android.content.ComponentName, @NonNull java.util.Set<java.lang.String>);
@@ -11690,7 +11690,7 @@
public class PackageInstaller {
method public void abandonSession(int);
- method public void checkInstallConstraints(@NonNull java.util.List<java.lang.String>, @NonNull android.content.pm.PackageInstaller.InstallConstraints, @NonNull java.util.function.Consumer<android.content.pm.PackageInstaller.InstallConstraintsResult>);
+ method public void checkInstallConstraints(@NonNull java.util.List<java.lang.String>, @NonNull android.content.pm.PackageInstaller.InstallConstraints, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<android.content.pm.PackageInstaller.InstallConstraintsResult>);
method public int createSession(@NonNull android.content.pm.PackageInstaller.SessionParams) throws java.io.IOException;
method @Deprecated @Nullable public android.content.pm.PackageInstaller.SessionInfo getActiveStagedSession();
method @NonNull public java.util.List<android.content.pm.PackageInstaller.SessionInfo> getActiveStagedSessions();
@@ -13096,9 +13096,10 @@
}
public final class GetCredentialOption implements android.os.Parcelable {
- ctor public GetCredentialOption(@NonNull String, @NonNull android.os.Bundle, boolean);
+ ctor public GetCredentialOption(@NonNull String, @NonNull android.os.Bundle, @NonNull android.os.Bundle, boolean);
method public int describeContents();
- method @NonNull public android.os.Bundle getData();
+ method @NonNull public android.os.Bundle getCandidateQueryData();
+ method @NonNull public android.os.Bundle getCredentialRetrievalData();
method @NonNull public String getType();
method public boolean requireSystemProvider();
method public void writeToParcel(@NonNull android.os.Parcel, int);
@@ -17826,6 +17827,7 @@
field @NonNull public static final android.hardware.camera2.CameraCharacteristics.Key<java.lang.Integer> SENSOR_MAX_ANALOG_SENSITIVITY;
field @NonNull public static final android.hardware.camera2.CameraCharacteristics.Key<android.graphics.Rect[]> SENSOR_OPTICAL_BLACK_REGIONS;
field @NonNull public static final android.hardware.camera2.CameraCharacteristics.Key<java.lang.Integer> SENSOR_ORIENTATION;
+ field @NonNull public static final android.hardware.camera2.CameraCharacteristics.Key<java.lang.Integer> SENSOR_READOUT_TIMESTAMP;
field @NonNull public static final android.hardware.camera2.CameraCharacteristics.Key<java.lang.Integer> SENSOR_REFERENCE_ILLUMINANT1;
field @NonNull public static final android.hardware.camera2.CameraCharacteristics.Key<java.lang.Byte> SENSOR_REFERENCE_ILLUMINANT2;
field @NonNull public static final android.hardware.camera2.CameraCharacteristics.Key<int[]> SHADING_AVAILABLE_MODES;
@@ -18194,6 +18196,8 @@
field public static final int SENSOR_INFO_TIMESTAMP_SOURCE_UNKNOWN = 0; // 0x0
field public static final int SENSOR_PIXEL_MODE_DEFAULT = 0; // 0x0
field public static final int SENSOR_PIXEL_MODE_MAXIMUM_RESOLUTION = 1; // 0x1
+ field public static final int SENSOR_READOUT_TIMESTAMP_HARDWARE = 1; // 0x1
+ field public static final int SENSOR_READOUT_TIMESTAMP_NOT_SUPPORTED = 0; // 0x0
field public static final int SENSOR_REFERENCE_ILLUMINANT1_CLOUDY_WEATHER = 10; // 0xa
field public static final int SENSOR_REFERENCE_ILLUMINANT1_COOL_WHITE_FLUORESCENT = 14; // 0xe
field public static final int SENSOR_REFERENCE_ILLUMINANT1_D50 = 23; // 0x17
@@ -18307,6 +18311,7 @@
field @NonNull public static final android.os.Parcelable.Creator<android.hardware.camera2.CaptureRequest> CREATOR;
field @NonNull public static final android.hardware.camera2.CaptureRequest.Key<java.lang.Integer> DISTORTION_CORRECTION_MODE;
field @NonNull public static final android.hardware.camera2.CaptureRequest.Key<java.lang.Integer> EDGE_MODE;
+ field @NonNull public static final android.hardware.camera2.CaptureRequest.Key<java.lang.Integer> EXTENSION_STRENGTH;
field @NonNull public static final android.hardware.camera2.CaptureRequest.Key<java.lang.Integer> FLASH_MODE;
field @NonNull public static final android.hardware.camera2.CaptureRequest.Key<java.lang.Integer> HOT_PIXEL_MODE;
field @NonNull public static final android.hardware.camera2.CaptureRequest.Key<android.location.Location> JPEG_GPS_LOCATION;
@@ -18399,6 +18404,8 @@
field @NonNull public static final android.hardware.camera2.CaptureResult.Key<java.lang.Float> CONTROL_ZOOM_RATIO;
field @NonNull public static final android.hardware.camera2.CaptureResult.Key<java.lang.Integer> DISTORTION_CORRECTION_MODE;
field @NonNull public static final android.hardware.camera2.CaptureResult.Key<java.lang.Integer> EDGE_MODE;
+ field @NonNull public static final android.hardware.camera2.CaptureResult.Key<java.lang.Integer> EXTENSION_CURRENT_TYPE;
+ field @NonNull public static final android.hardware.camera2.CaptureResult.Key<java.lang.Integer> EXTENSION_STRENGTH;
field @NonNull public static final android.hardware.camera2.CaptureResult.Key<java.lang.Integer> FLASH_MODE;
field @NonNull public static final android.hardware.camera2.CaptureResult.Key<java.lang.Integer> FLASH_STATE;
field @NonNull public static final android.hardware.camera2.CaptureResult.Key<java.lang.Integer> HOT_PIXEL_MODE;
@@ -20206,7 +20213,7 @@
public final class AltitudeConverter {
ctor public AltitudeConverter();
- method @WorkerThread public void addMslAltitude(@NonNull android.content.Context, @NonNull android.location.Location) throws java.io.IOException;
+ method @WorkerThread public void addMslAltitudeToLocation(@NonNull android.content.Context, @NonNull android.location.Location) throws java.io.IOException;
}
}
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index 561ec10..137af1a 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -826,7 +826,6 @@
method public int describeContents();
method public int getFpsOverride();
method public float getScalingFactor();
- method @NonNull public android.app.GameModeConfiguration.Builder toBuilder();
method public void writeToParcel(@NonNull android.os.Parcel, int);
field @NonNull public static final android.os.Parcelable.Creator<android.app.GameModeConfiguration> CREATOR;
field public static final int FPS_OVERRIDE_NONE = 0; // 0x0
@@ -834,6 +833,7 @@
public static final class GameModeConfiguration.Builder {
ctor public GameModeConfiguration.Builder();
+ ctor public GameModeConfiguration.Builder(@NonNull android.app.GameModeConfiguration);
method @NonNull public android.app.GameModeConfiguration build();
method @NonNull public android.app.GameModeConfiguration.Builder setFpsOverride(int);
method @NonNull public android.app.GameModeConfiguration.Builder setScalingFactor(float);
@@ -845,7 +845,7 @@
method public int getActiveGameMode();
method @NonNull public int[] getAvailableGameModes();
method @Nullable public android.app.GameModeConfiguration getGameModeConfiguration(int);
- method @NonNull public int[] getOptedInGameModes();
+ method @NonNull public int[] getOverriddenGameModes();
method public boolean isDownscalingAllowed();
method public boolean isFpsOverrideAllowed();
method public void writeToParcel(@NonNull android.os.Parcel, int);
@@ -860,7 +860,7 @@
method @NonNull public android.app.GameModeInfo.Builder setDownscalingAllowed(boolean);
method @NonNull public android.app.GameModeInfo.Builder setFpsOverrideAllowed(boolean);
method @NonNull public android.app.GameModeInfo.Builder setGameModeConfiguration(int, @NonNull android.app.GameModeConfiguration);
- method @NonNull public android.app.GameModeInfo.Builder setOptedInGameModes(@NonNull int[]);
+ method @NonNull public android.app.GameModeInfo.Builder setOverriddenGameModes(@NonNull int[]);
}
public abstract class InstantAppResolverService extends android.app.Service {
@@ -3010,6 +3010,7 @@
method @NonNull public java.util.Set<android.content.ComponentName> getBlockedCrossTaskNavigations();
method public int getDefaultActivityPolicy();
method public int getDefaultNavigationPolicy();
+ method public int getDefaultRecentsPolicy();
method public int getDevicePolicy(int);
method public int getLockState();
method @Nullable public String getName();
@@ -3026,6 +3027,7 @@
field public static final int NAVIGATION_POLICY_DEFAULT_ALLOWED = 0; // 0x0
field public static final int NAVIGATION_POLICY_DEFAULT_BLOCKED = 1; // 0x1
field public static final int POLICY_TYPE_SENSORS = 0; // 0x0
+ field public static final int RECENTS_POLICY_ALLOW_IN_HOST_DEVICE_RECENTS = 1; // 0x1
}
public static final class VirtualDeviceParams.Builder {
@@ -3036,6 +3038,7 @@
method @NonNull public android.companion.virtual.VirtualDeviceParams.Builder setAllowedCrossTaskNavigations(@NonNull java.util.Set<android.content.ComponentName>);
method @NonNull public android.companion.virtual.VirtualDeviceParams.Builder setBlockedActivities(@NonNull java.util.Set<android.content.ComponentName>);
method @NonNull public android.companion.virtual.VirtualDeviceParams.Builder setBlockedCrossTaskNavigations(@NonNull java.util.Set<android.content.ComponentName>);
+ method @NonNull public android.companion.virtual.VirtualDeviceParams.Builder setDefaultRecentsPolicy(int);
method @NonNull public android.companion.virtual.VirtualDeviceParams.Builder setDevicePolicy(int, int);
method @NonNull @RequiresPermission(value=android.Manifest.permission.ADD_ALWAYS_UNLOCKED_DISPLAY, conditional=true) public android.companion.virtual.VirtualDeviceParams.Builder setLockState(int);
method @NonNull public android.companion.virtual.VirtualDeviceParams.Builder setName(@NonNull String);
@@ -3094,7 +3097,7 @@
public class VirtualSensor {
method @NonNull public String getName();
method public int getType();
- method @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public void sendSensorEvent(@NonNull android.companion.virtual.sensor.VirtualSensorEvent);
+ method @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public void sendEvent(@NonNull android.companion.virtual.sensor.VirtualSensorEvent);
}
public static interface VirtualSensor.SensorStateChangeCallback {
@@ -3769,6 +3772,7 @@
field @Deprecated public static final int PROTECTION_FLAG_DOCUMENTER = 262144; // 0x40000
field public static final int PROTECTION_FLAG_INCIDENT_REPORT_APPROVER = 1048576; // 0x100000
field public static final int PROTECTION_FLAG_KNOWN_SIGNER = 134217728; // 0x8000000
+ field public static final int PROTECTION_FLAG_MODULE = 4194304; // 0x400000
field public static final int PROTECTION_FLAG_OEM = 16384; // 0x4000
field public static final int PROTECTION_FLAG_RECENTS = 33554432; // 0x2000000
field public static final int PROTECTION_FLAG_RETAIL_DEMO = 16777216; // 0x1000000
@@ -10221,7 +10225,7 @@
method @Nullable @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.GET_ACCOUNTS_PRIVILEGED}) public android.graphics.Bitmap getUserIcon();
method @Deprecated @android.os.UserManager.UserRestrictionSource @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.QUERY_USERS}) public int getUserRestrictionSource(String, android.os.UserHandle);
method @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.QUERY_USERS}) public java.util.List<android.os.UserManager.EnforcingUser> getUserRestrictionSources(String, android.os.UserHandle);
- method @RequiresPermission(allOf={android.Manifest.permission.READ_PHONE_STATE, android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.INTERACT_ACROSS_USERS}, conditional=true) public int getUserSwitchability();
+ method @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.INTERACT_ACROSS_USERS}) public int getUserSwitchability();
method @NonNull @RequiresPermission(anyOf={"android.permission.INTERACT_ACROSS_USERS", "android.permission.MANAGE_USERS"}) public java.util.Set<android.os.UserHandle> getVisibleUsers();
method @RequiresPermission(android.Manifest.permission.MANAGE_USERS) public boolean hasRestrictedProfiles();
method @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.INTERACT_ACROSS_USERS}, conditional=true) public boolean hasUserRestrictionForUser(@NonNull String, @NonNull android.os.UserHandle);
@@ -10704,7 +10708,7 @@
}
public final class DeviceConfig {
- method @RequiresPermission(android.Manifest.permission.READ_DEVICE_CONFIG) public static void addOnPropertiesChangedListener(@NonNull String, @NonNull java.util.concurrent.Executor, @NonNull android.provider.DeviceConfig.OnPropertiesChangedListener);
+ method public static void addOnPropertiesChangedListener(@NonNull String, @NonNull java.util.concurrent.Executor, @NonNull android.provider.DeviceConfig.OnPropertiesChangedListener);
method @RequiresPermission(android.Manifest.permission.WRITE_DEVICE_CONFIG) public static boolean deleteProperty(@NonNull String, @NonNull String);
method @RequiresPermission(android.Manifest.permission.READ_DEVICE_CONFIG) public static boolean getBoolean(@NonNull String, @NonNull String, boolean);
method @RequiresPermission(android.Manifest.permission.READ_DEVICE_CONFIG) public static float getFloat(@NonNull String, @NonNull String, float);
diff --git a/core/api/test-current.txt b/core/api/test-current.txt
index 43d4530..3d30c0f 100644
--- a/core/api/test-current.txt
+++ b/core/api/test-current.txt
@@ -38,6 +38,7 @@
field public static final String READ_CELL_BROADCASTS = "android.permission.READ_CELL_BROADCASTS";
field public static final String READ_PRIVILEGED_PHONE_STATE = "android.permission.READ_PRIVILEGED_PHONE_STATE";
field public static final String RECORD_BACKGROUND_AUDIO = "android.permission.RECORD_BACKGROUND_AUDIO";
+ field public static final String REMAP_MODIFIER_KEYS = "android.permission.REMAP_MODIFIER_KEYS";
field public static final String REMOVE_TASKS = "android.permission.REMOVE_TASKS";
field public static final String REQUEST_UNIQUE_ID_ATTESTATION = "android.permission.REQUEST_UNIQUE_ID_ATTESTATION";
field public static final String RESET_APP_ERRORS = "android.permission.RESET_APP_ERRORS";
@@ -1294,8 +1295,11 @@
public final class InputManager {
method public void addUniqueIdAssociation(@NonNull String, @NonNull String);
+ method @RequiresPermission(android.Manifest.permission.REMAP_MODIFIER_KEYS) public void clearAllModifierKeyRemappings();
method @Nullable public String getCurrentKeyboardLayoutForInputDevice(@NonNull android.hardware.input.InputDeviceIdentifier);
method @NonNull public java.util.List<java.lang.String> getKeyboardLayoutDescriptorsForInputDevice(@NonNull android.view.InputDevice);
+ method @NonNull @RequiresPermission(android.Manifest.permission.REMAP_MODIFIER_KEYS) public java.util.Map<java.lang.Integer,java.lang.Integer> getModifierKeyRemapping();
+ method @RequiresPermission(android.Manifest.permission.REMAP_MODIFIER_KEYS) public void remapModifierKey(int, int);
method @RequiresPermission(android.Manifest.permission.SET_KEYBOARD_LAYOUT) public void removeKeyboardLayoutForInputDevice(@NonNull android.hardware.input.InputDeviceIdentifier, @NonNull String);
method public void removeUniqueIdAssociation(@NonNull String);
method @RequiresPermission(android.Manifest.permission.SET_KEYBOARD_LAYOUT) public void setCurrentKeyboardLayoutForInputDevice(@NonNull android.hardware.input.InputDeviceIdentifier, @NonNull String);
diff --git a/core/java/Android.bp b/core/java/Android.bp
index 128e3de..738e2de 100644
--- a/core/java/Android.bp
+++ b/core/java/Android.bp
@@ -368,6 +368,20 @@
},
}
+// Build Rust bindings for remote provisioning. Needed by keystore2.
+aidl_interface {
+ name: "android.security.rkp_aidl",
+ unstable: true,
+ srcs: [
+ "android/security/rkp/*.aidl",
+ ],
+ backend: {
+ rust: {
+ enabled: true,
+ },
+ },
+}
+
aidl_interface {
name: "android.debug_aidl",
unstable: true,
diff --git a/core/java/android/app/ActivityManagerInternal.java b/core/java/android/app/ActivityManagerInternal.java
index f62190a..c51e8ae 100644
--- a/core/java/android/app/ActivityManagerInternal.java
+++ b/core/java/android/app/ActivityManagerInternal.java
@@ -902,4 +902,9 @@
*/
public abstract void registerNetworkPolicyUidObserver(@NonNull IUidObserver observer,
int which, int cutpoint, @NonNull String callingPackage);
+
+ /**
+ * Return all client package names of a service.
+ */
+ public abstract ArraySet<String> getClientPackages(String servicePackageName);
}
diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java
index 31cbe28..a4c9f8c 100644
--- a/core/java/android/app/ActivityThread.java
+++ b/core/java/android/app/ActivityThread.java
@@ -1020,6 +1020,12 @@
int flags;
}
+ // A list of receivers and an index into the receiver to be processed next.
+ static final class ReceiverList {
+ List<ReceiverInfo> receivers;
+ int index;
+ }
+
private class ApplicationThread extends IApplicationThread.Stub {
private static final String DB_CONNECTION_INFO_HEADER = " %8s %8s %14s %5s %5s %5s %s";
private static final String DB_CONNECTION_INFO_FORMAT = " %8s %8s %14s %5d %5d %5d %s";
@@ -1036,6 +1042,21 @@
sendMessage(H.RECEIVER, r);
}
+ public final void scheduleReceiverList(List<ReceiverInfo> info) throws RemoteException {
+ for (int i = 0; i < info.size(); i++) {
+ ReceiverInfo r = info.get(i);
+ if (r.registered) {
+ scheduleRegisteredReceiver(r.receiver, r.intent,
+ r.resultCode, r.data, r.extras, r.ordered, r.sticky,
+ r.sendingUser, r.processState);
+ } else {
+ scheduleReceiver(r.intent, r.activityInfo, r.compatInfo,
+ r.resultCode, r.data, r.extras, r.sync,
+ r.sendingUser, r.processState);
+ }
+ }
+ }
+
public final void scheduleCreateBackupAgent(ApplicationInfo app,
int backupMode, int userId, @BackupDestination int backupDestination) {
CreateBackupAgentData d = new CreateBackupAgentData();
diff --git a/core/java/android/app/GameManager.java b/core/java/android/app/GameManager.java
index 2f51b17..c6fa064 100644
--- a/core/java/android/app/GameManager.java
+++ b/core/java/android/app/GameManager.java
@@ -287,6 +287,7 @@
* <p>
* The caller must have {@link android.Manifest.permission#MANAGE_GAME_MODE}.
*
+ * @param packageName The package name of the game to update
* @param gameModeConfig The configuration to use for game mode interventions
* @hide
*/
diff --git a/core/java/android/app/GameModeConfiguration.java b/core/java/android/app/GameModeConfiguration.java
index b081e82..d8be814 100644
--- a/core/java/android/app/GameModeConfiguration.java
+++ b/core/java/android/app/GameModeConfiguration.java
@@ -62,10 +62,16 @@
*/
@SystemApi
public static final class Builder {
- /** Constructs a new Builder for a game mode’s configuration */
+ /** Constructs a new Builder for a game mode’s configuration. */
public Builder() {
}
+ /** Constructs a new builder by copying from an existing game mode configuration. */
+ public Builder(@NonNull GameModeConfiguration configuration) {
+ mFpsOverride = configuration.mFpsOverride;
+ mScalingFactor = configuration.mScalingFactor;
+ }
+
/**
* Sets the scaling factor used for game resolution downscaling.
* <br>
@@ -156,16 +162,6 @@
return mFpsOverride;
}
- /**
- * Converts and returns the game mode config as a new builder.
- */
- @NonNull
- public GameModeConfiguration.Builder toBuilder() {
- return new GameModeConfiguration.Builder()
- .setFpsOverride(mFpsOverride)
- .setScalingFactor(mScalingFactor);
- }
-
@Override
public boolean equals(Object obj) {
if (obj == this) {
diff --git a/core/java/android/app/GameModeInfo.java b/core/java/android/app/GameModeInfo.java
index 31255c2..7dcb3909 100644
--- a/core/java/android/app/GameModeInfo.java
+++ b/core/java/android/app/GameModeInfo.java
@@ -87,12 +87,12 @@
}
/**
- * Sets the opted-in game modes.
+ * Sets the overridden game modes.
*/
@NonNull
- public GameModeInfo.Builder setOptedInGameModes(
- @NonNull @GameManager.GameMode int[] optedInGameModes) {
- mOptedInGameModes = optedInGameModes;
+ public GameModeInfo.Builder setOverriddenGameModes(
+ @NonNull @GameManager.GameMode int[] overriddenGameModes) {
+ mOverriddenGameModes = overriddenGameModes;
return this;
}
@@ -140,12 +140,12 @@
*/
@NonNull
public GameModeInfo build() {
- return new GameModeInfo(mActiveGameMode, mAvailableGameModes, mOptedInGameModes,
+ return new GameModeInfo(mActiveGameMode, mAvailableGameModes, mOverriddenGameModes,
mIsDownscalingAllowed, mIsFpsOverrideAllowed, mConfigMap);
}
private @GameManager.GameMode int[] mAvailableGameModes = new int[]{};
- private @GameManager.GameMode int[] mOptedInGameModes = new int[]{};
+ private @GameManager.GameMode int[] mOverriddenGameModes = new int[]{};
private @GameManager.GameMode int mActiveGameMode;
private boolean mIsDownscalingAllowed;
private boolean mIsFpsOverrideAllowed;
@@ -164,11 +164,11 @@
private GameModeInfo(@GameManager.GameMode int activeGameMode,
@NonNull @GameManager.GameMode int[] availableGameModes,
- @NonNull @GameManager.GameMode int[] optedInGameModes, boolean isDownscalingAllowed,
+ @NonNull @GameManager.GameMode int[] overriddenGameModes, boolean isDownscalingAllowed,
boolean isFpsOverrideAllowed, @NonNull Map<Integer, GameModeConfiguration> configMap) {
mActiveGameMode = activeGameMode;
mAvailableGameModes = Arrays.copyOf(availableGameModes, availableGameModes.length);
- mOptedInGameModes = Arrays.copyOf(optedInGameModes, optedInGameModes.length);
+ mOverriddenGameModes = Arrays.copyOf(overriddenGameModes, overriddenGameModes.length);
mIsDownscalingAllowed = isDownscalingAllowed;
mIsFpsOverrideAllowed = isFpsOverrideAllowed;
mConfigMap = configMap;
@@ -179,7 +179,7 @@
public GameModeInfo(Parcel in) {
mActiveGameMode = in.readInt();
mAvailableGameModes = in.createIntArray();
- mOptedInGameModes = in.createIntArray();
+ mOverriddenGameModes = in.createIntArray();
mIsDownscalingAllowed = in.readBoolean();
mIsFpsOverrideAllowed = in.readBoolean();
mConfigMap = new ArrayMap<>();
@@ -198,8 +198,8 @@
* Gets the collection of {@link GameManager.GameMode} that can be applied to the game.
* <p>
* Available games include all game modes that are either supported by the OEM in device
- * config, or opted in by the game developers in game mode config XML, plus the default enabled
- * modes for any game including {@link GameManager#GAME_MODE_STANDARD} and
+ * config, or overridden by the game developers in game mode config XML, plus the default
+ * enabled modes for any game including {@link GameManager#GAME_MODE_STANDARD} and
* {@link GameManager#GAME_MODE_CUSTOM}.
* <p>
* Also see {@link GameModeInfo}.
@@ -210,19 +210,19 @@
}
/**
- * Gets the collection of {@link GameManager.GameMode} that are opted in by the game.
+ * Gets the collection of {@link GameManager.GameMode} that are overridden by the game.
* <p>
* Also see {@link GameModeInfo}.
*/
@NonNull
- public @GameManager.GameMode int[] getOptedInGameModes() {
- return Arrays.copyOf(mOptedInGameModes, mOptedInGameModes.length);
+ public @GameManager.GameMode int[] getOverriddenGameModes() {
+ return Arrays.copyOf(mOverriddenGameModes, mOverriddenGameModes.length);
}
/**
* Gets the current game mode configuration of a game mode.
* <p>
- * The game mode can be null if it's opted in by the game itself, or not configured in device
+ * The game mode can be null if it's overridden by the game itself, or not configured in device
* config nor set by the user as custom game mode configuration.
*/
public @Nullable GameModeConfiguration getGameModeConfiguration(
@@ -250,7 +250,7 @@
private final @GameManager.GameMode int[] mAvailableGameModes;
- private final @GameManager.GameMode int[] mOptedInGameModes;
+ private final @GameManager.GameMode int[] mOverriddenGameModes;
private final @GameManager.GameMode int mActiveGameMode;
private final boolean mIsDownscalingAllowed;
private final boolean mIsFpsOverrideAllowed;
@@ -265,7 +265,7 @@
public void writeToParcel(@NonNull Parcel dest, int flags) {
dest.writeInt(mActiveGameMode);
dest.writeIntArray(mAvailableGameModes);
- dest.writeIntArray(mOptedInGameModes);
+ dest.writeIntArray(mOverriddenGameModes);
dest.writeBoolean(mIsDownscalingAllowed);
dest.writeBoolean(mIsFpsOverrideAllowed);
dest.writeMap(mConfigMap);
diff --git a/core/java/android/app/IApplicationThread.aidl b/core/java/android/app/IApplicationThread.aidl
index 595c7f7..3984fee 100644
--- a/core/java/android/app/IApplicationThread.aidl
+++ b/core/java/android/app/IApplicationThread.aidl
@@ -20,6 +20,7 @@
import android.app.IInstrumentationWatcher;
import android.app.IUiAutomationConnection;
import android.app.ProfilerInfo;
+import android.app.ReceiverInfo;
import android.app.ResultInfo;
import android.app.servertransaction.ClientTransaction;
import android.content.AutofillOptions;
@@ -66,6 +67,9 @@
in CompatibilityInfo compatInfo,
int resultCode, in String data, in Bundle extras, boolean sync,
int sendingUser, int processState);
+
+ void scheduleReceiverList(in List<ReceiverInfo> info);
+
@UnsupportedAppUsage
void scheduleCreateService(IBinder token, in ServiceInfo info,
in CompatibilityInfo compatInfo, int processState);
diff --git a/core/java/android/app/ReceiverInfo.aidl b/core/java/android/app/ReceiverInfo.aidl
new file mode 100644
index 0000000..d90eee7
--- /dev/null
+++ b/core/java/android/app/ReceiverInfo.aidl
@@ -0,0 +1,60 @@
+/**
+ * Copyright (c) 2022, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app;
+
+import android.content.IIntentReceiver;
+import android.content.Intent;
+import android.content.pm.ActivityInfo;
+import android.content.res.CompatibilityInfo;
+import android.os.Bundle;
+
+/**
+ * Collect the information needed for manifest and registered receivers into a single structure
+ * that can be the element of a list. All fields are already parcelable.
+ * @hide
+ */
+parcelable ReceiverInfo {
+ /**
+ * Fields common to registered and manifest receivers.
+ */
+ Intent intent;
+ String data;
+ Bundle extras;
+ int sendingUser;
+ int processState;
+ int resultCode;
+
+ /**
+ * True if this instance represents a registered receiver and false if this instance
+ * represents a manifest receiver.
+ */
+ boolean registered;
+
+ /**
+ * Fields used only for registered receivers.
+ */
+ IIntentReceiver receiver;
+ boolean ordered;
+ boolean sticky;
+
+ /**
+ * Fields used only for manifest receivers.
+ */
+ ActivityInfo activityInfo;
+ CompatibilityInfo compatInfo;
+ boolean sync;
+}
diff --git a/core/java/android/app/ResourcesManager.java b/core/java/android/app/ResourcesManager.java
index d275c83..7e6386e 100644
--- a/core/java/android/app/ResourcesManager.java
+++ b/core/java/android/app/ResourcesManager.java
@@ -41,6 +41,7 @@
import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.DisplayMetrics;
+import android.util.IndentingPrintWriter;
import android.util.Log;
import android.util.Pair;
import android.util.Slog;
@@ -51,7 +52,6 @@
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.ArrayUtils;
-import com.android.internal.util.IndentingPrintWriter;
import java.io.IOException;
import java.io.PrintWriter;
@@ -173,6 +173,7 @@
/**
* The ApkAssets that are being referenced in the wild that we can reuse.
+ * Used as a lock for itself as well.
*/
private final ArrayMap<ApkKey, WeakReference<ApkAssets>> mCachedApkAssets = new ArrayMap<>();
@@ -286,38 +287,43 @@
* try as hard as possible to release any open FDs.
*/
public void invalidatePath(String path) {
+ final List<ResourcesImpl> implsToFlush = new ArrayList<>();
synchronized (mLock) {
- int count = 0;
-
for (int i = mResourceImpls.size() - 1; i >= 0; i--) {
final ResourcesKey key = mResourceImpls.keyAt(i);
if (key.isPathReferenced(path)) {
- ResourcesImpl impl = mResourceImpls.removeAt(i).get();
- if (impl != null) {
- impl.flushLayoutCache();
+ ResourcesImpl resImpl = mResourceImpls.removeAt(i).get();
+ if (resImpl != null) {
+ implsToFlush.add(resImpl);
}
- count++;
}
}
-
- Log.i(TAG, "Invalidated " + count + " asset managers that referenced " + path);
-
+ }
+ for (int i = 0; i < implsToFlush.size(); i++) {
+ implsToFlush.get(i).flushLayoutCache();
+ }
+ final List<ApkAssets> assetsToClose = new ArrayList<>();
+ synchronized (mCachedApkAssets) {
for (int i = mCachedApkAssets.size() - 1; i >= 0; i--) {
final ApkKey key = mCachedApkAssets.keyAt(i);
if (key.path.equals(path)) {
- WeakReference<ApkAssets> apkAssetsRef = mCachedApkAssets.removeAt(i);
- if (apkAssetsRef != null && apkAssetsRef.get() != null) {
- apkAssetsRef.get().close();
+ final WeakReference<ApkAssets> apkAssetsRef = mCachedApkAssets.removeAt(i);
+ final ApkAssets apkAssets = apkAssetsRef != null ? apkAssetsRef.get() : null;
+ if (apkAssets != null) {
+ assetsToClose.add(apkAssets);
}
}
}
}
+ for (int i = 0; i < assetsToClose.size(); i++) {
+ assetsToClose.get(i).close();
+ }
+ Log.i(TAG,
+ "Invalidated " + implsToFlush.size() + " asset managers that referenced " + path);
}
public Configuration getConfiguration() {
- synchronized (mLock) {
- return mResConfiguration;
- }
+ return mResConfiguration;
}
@VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
@@ -402,14 +408,12 @@
* @param resources The {@link Resources} backing the display adjustments.
*/
public Display getAdjustedDisplay(final int displayId, Resources resources) {
- synchronized (mLock) {
- final DisplayManagerGlobal dm = DisplayManagerGlobal.getInstance();
- if (dm == null) {
- // may be null early in system startup
- return null;
- }
- return dm.getCompatibleDisplay(displayId, resources);
+ final DisplayManagerGlobal dm = DisplayManagerGlobal.getInstance();
+ if (dm == null) {
+ // may be null early in system startup
+ return null;
}
+ return dm.getCompatibleDisplay(displayId, resources);
}
/**
@@ -446,16 +450,14 @@
ApkAssets apkAssets;
// Optimistically check if this ApkAssets exists somewhere else.
- synchronized (mLock) {
- final WeakReference<ApkAssets> apkAssetsRef = mCachedApkAssets.get(key);
- if (apkAssetsRef != null) {
- apkAssets = apkAssetsRef.get();
- if (apkAssets != null && apkAssets.isUpToDate()) {
- return apkAssets;
- } else {
- // Clean up the reference.
- mCachedApkAssets.remove(key);
- }
+ final WeakReference<ApkAssets> apkAssetsRef;
+ synchronized (mCachedApkAssets) {
+ apkAssetsRef = mCachedApkAssets.get(key);
+ }
+ if (apkAssetsRef != null) {
+ apkAssets = apkAssetsRef.get();
+ if (apkAssets != null && apkAssets.isUpToDate()) {
+ return apkAssets;
}
}
@@ -472,7 +474,7 @@
apkAssets = ApkAssets.loadFromPath(key.path, flags);
}
- synchronized (mLock) {
+ synchronized (mCachedApkAssets) {
mCachedApkAssets.put(key, new WeakReference<>(apkAssets));
}
@@ -584,28 +586,33 @@
* @hide
*/
public void dump(String prefix, PrintWriter printWriter) {
+ final int references;
+ final int resImpls;
synchronized (mLock) {
- IndentingPrintWriter pw = new IndentingPrintWriter(printWriter, " ");
- for (int i = 0; i < prefix.length() / 2; i++) {
- pw.increaseIndent();
- }
-
- pw.println("ResourcesManager:");
- pw.increaseIndent();
- pw.print("total apks: ");
- pw.println(countLiveReferences(mCachedApkAssets.values()));
-
- pw.print("resources: ");
-
- int references = countLiveReferences(mResourceReferences);
+ int refs = countLiveReferences(mResourceReferences);
for (ActivityResources activityResources : mActivityResourceReferences.values()) {
- references += activityResources.countLiveReferences();
+ refs += activityResources.countLiveReferences();
}
- pw.println(references);
-
- pw.print("resource impls: ");
- pw.println(countLiveReferences(mResourceImpls.values()));
+ references = refs;
+ resImpls = countLiveReferences(mResourceImpls.values());
}
+ final int liveAssets;
+ synchronized (mCachedApkAssets) {
+ liveAssets = countLiveReferences(mCachedApkAssets.values());
+ }
+
+ final var pw = new IndentingPrintWriter(printWriter, " ");
+ for (int i = 0; i < prefix.length() / 2; i++) {
+ pw.increaseIndent();
+ }
+ pw.println("ResourcesManager:");
+ pw.increaseIndent();
+ pw.print("total apks: ");
+ pw.println(liveAssets);
+ pw.print("resources: ");
+ pw.println(references);
+ pw.print("resource impls: ");
+ pw.println(resImpls);
}
private Configuration generateConfig(@NonNull ResourcesKey key) {
diff --git a/core/java/android/app/StatusBarManager.java b/core/java/android/app/StatusBarManager.java
index a035375..f63f406 100644
--- a/core/java/android/app/StatusBarManager.java
+++ b/core/java/android/app/StatusBarManager.java
@@ -234,7 +234,10 @@
/**
* Session flag for {@link #registerSessionListener} indicating the listener
- * is interested in sessions on the keygaurd
+ * is interested in sessions on the keygaurd.
+ * Keyguard Session Boundaries:
+ * START_SESSION: device starts going to sleep OR the keyguard is newly shown
+ * END_SESSION: device starts going to sleep OR keyguard is no longer showing
* @hide
*/
public static final int SESSION_KEYGUARD = 1 << 0;
diff --git a/core/java/android/app/SystemServiceRegistry.java b/core/java/android/app/SystemServiceRegistry.java
index 762ac23..3730a6f 100644
--- a/core/java/android/app/SystemServiceRegistry.java
+++ b/core/java/android/app/SystemServiceRegistry.java
@@ -51,6 +51,8 @@
import android.app.usage.UsageStatsManager;
import android.app.wallpapereffectsgeneration.IWallpaperEffectsGenerationManager;
import android.app.wallpapereffectsgeneration.WallpaperEffectsGenerationManager;
+import android.app.wearable.IWearableSensingManager;
+import android.app.wearable.WearableSensingManager;
import android.apphibernation.AppHibernationManager;
import android.appwidget.AppWidgetManager;
import android.bluetooth.BluetoothFrameworkInitializer;
@@ -1544,6 +1546,17 @@
IAmbientContextManager.Stub.asInterface(iBinder);
return new AmbientContextManager(ctx.getOuterContext(), manager);
}});
+ registerService(Context.WEARABLE_SENSING_SERVICE, WearableSensingManager.class,
+ new CachedServiceFetcher<WearableSensingManager>() {
+ @Override
+ public WearableSensingManager createService(ContextImpl ctx)
+ throws ServiceNotFoundException {
+ IBinder iBinder = ServiceManager.getServiceOrThrow(
+ Context.WEARABLE_SENSING_SERVICE);
+ IWearableSensingManager manager =
+ IWearableSensingManager.Stub.asInterface(iBinder);
+ return new WearableSensingManager(ctx.getOuterContext(), manager);
+ }});
sInitializing = true;
try {
diff --git a/core/java/android/app/WallpaperManager.java b/core/java/android/app/WallpaperManager.java
index f5d657c..e880432 100644
--- a/core/java/android/app/WallpaperManager.java
+++ b/core/java/android/app/WallpaperManager.java
@@ -807,6 +807,7 @@
* is not able to access the wallpaper.
*/
@RequiresPermission(android.Manifest.permission.READ_EXTERNAL_STORAGE)
+ @Nullable
public Drawable getDrawable() {
final ColorManagementProxy cmProxy = getColorManagementProxy();
Bitmap bm = sGlobals.peekWallpaperBitmap(mContext, true, FLAG_SYSTEM, cmProxy);
@@ -819,6 +820,29 @@
}
/**
+ * Retrieve the requested wallpaper; if
+ * no wallpaper is set, the requested built-in static wallpaper is returned.
+ * This is returned as an
+ * abstract Drawable that you can install in a View to display whatever
+ * wallpaper the user has currently set.
+ * <p>
+ * This method can return null if the requested wallpaper is not available, if
+ * wallpapers are not supported in the current user, or if the calling app is not
+ * permitted to access the requested wallpaper.
+ *
+ * @param which The {@code FLAG_*} identifier of a valid wallpaper type. Throws
+ * IllegalArgumentException if an invalid wallpaper is requested.
+ * @return Returns a Drawable object that will draw the requested wallpaper,
+ * or {@code null} if the requested wallpaper does not exist or if the calling application
+ * is not able to access the wallpaper.
+ * @hide
+ */
+ @RequiresPermission(android.Manifest.permission.READ_EXTERNAL_STORAGE)
+ @Nullable
+ public Drawable getDrawable(@SetWallpaperFlags int which) {
+ return getDrawable();
+ }
+ /**
* Obtain a drawable for the built-in static system wallpaper.
*/
public Drawable getBuiltInDrawable() {
@@ -1037,6 +1061,7 @@
* @return Returns a Drawable object that will draw the wallpaper or a
* null pointer if these is none.
*/
+ @Nullable
public Drawable peekDrawable() {
final ColorManagementProxy cmProxy = getColorManagementProxy();
Bitmap bm = sGlobals.peekWallpaperBitmap(mContext, false, FLAG_SYSTEM, cmProxy);
@@ -1049,6 +1074,23 @@
}
/**
+ * Retrieve the requested wallpaper; if there is no wallpaper set,
+ * a null pointer is returned. This is returned as an
+ * abstract Drawable that you can install in a View to display whatever
+ * wallpaper the user has currently set.
+ *
+ * @param which The {@code FLAG_*} identifier of a valid wallpaper type. Throws
+ * IllegalArgumentException if an invalid wallpaper is requested.
+ * @return Returns a Drawable object that will draw the wallpaper or a null pointer if these
+ * is none.
+ * @hide
+ */
+ @Nullable
+ public Drawable peekDrawable(@SetWallpaperFlags int which) {
+ return peekDrawable();
+ }
+
+ /**
* Like {@link #getDrawable()}, but the returned Drawable has a number
* of limitations to reduce its overhead as much as possible. It will
* never scale the wallpaper (only centering it if the requested bounds
@@ -1062,6 +1104,7 @@
* @return Returns a Drawable object that will draw the wallpaper.
*/
@RequiresPermission(android.Manifest.permission.READ_EXTERNAL_STORAGE)
+ @Nullable
public Drawable getFastDrawable() {
final ColorManagementProxy cmProxy = getColorManagementProxy();
Bitmap bm = sGlobals.peekWallpaperBitmap(mContext, true, FLAG_SYSTEM, cmProxy);
@@ -1072,6 +1115,28 @@
}
/**
+ * Like {@link #getFastDrawable(int)}, but the returned Drawable has a number
+ * of limitations to reduce its overhead as much as possible. It will
+ * never scale the wallpaper (only centering it if the requested bounds
+ * do match the bitmap bounds, which should not be typical), doesn't
+ * allow setting an alpha, color filter, or other attributes, etc. The
+ * bounds of the returned drawable will be initialized to the same bounds
+ * as the wallpaper, so normally you will not need to touch it. The
+ * drawable also assumes that it will be used in a context running in
+ * the same density as the screen (not in density compatibility mode).
+ *
+ * @param which The {@code FLAG_*} identifier of a valid wallpaper type. Throws
+ * IllegalArgumentException if an invalid wallpaper is requested.
+ * @return Returns a Drawable object that will draw the wallpaper.
+ * @hide
+ */
+ @RequiresPermission(android.Manifest.permission.READ_EXTERNAL_STORAGE)
+ @Nullable
+ public Drawable getFastDrawable(@SetWallpaperFlags int which) {
+ return getFastDrawable();
+ }
+
+ /**
* Like {@link #getFastDrawable()}, but if there is no wallpaper set,
* a null pointer is returned.
*
@@ -1079,6 +1144,7 @@
* wallpaper or a null pointer if these is none.
*/
@RequiresPermission(android.Manifest.permission.READ_EXTERNAL_STORAGE)
+ @Nullable
public Drawable peekFastDrawable() {
final ColorManagementProxy cmProxy = getColorManagementProxy();
Bitmap bm = sGlobals.peekWallpaperBitmap(mContext, false, FLAG_SYSTEM, cmProxy);
@@ -1089,6 +1155,22 @@
}
/**
+ * Like {@link #getFastDrawable()}, but if there is no wallpaper set,
+ * a null pointer is returned.
+ *
+ * @param which The {@code FLAG_*} identifier of a valid wallpaper type. Throws
+ * IllegalArgumentException if an invalid wallpaper is requested.
+ * @return Returns an optimized Drawable object that will draw the
+ * wallpaper or a null pointer if these is none.
+ * @hide
+ */
+ @RequiresPermission(android.Manifest.permission.READ_EXTERNAL_STORAGE)
+ @Nullable
+ public Drawable peekFastDrawable(@SetWallpaperFlags int which) {
+ return peekFastDrawable();
+ }
+
+ /**
* Whether the wallpaper supports Wide Color Gamut or not.
* @param which The wallpaper whose image file is to be retrieved. Must be a single
* defined kind of wallpaper, either {@link #FLAG_SYSTEM} or {@link #FLAG_LOCK}.
diff --git a/core/java/android/app/admin/DevicePolicyManager.java b/core/java/android/app/admin/DevicePolicyManager.java
index ecea1bb..0784405 100644
--- a/core/java/android/app/admin/DevicePolicyManager.java
+++ b/core/java/android/app/admin/DevicePolicyManager.java
@@ -14449,7 +14449,9 @@
* @throws SecurityException if {@code admin} is not a profile owner
*
* @see #getCrossProfileCalendarPackages(ComponentName)
+ * @deprecated Use {@link #setCrossProfilePackages(ComponentName, Set)}.
*/
+ @Deprecated
public void setCrossProfileCalendarPackages(@NonNull ComponentName admin,
@Nullable Set<String> packageNames) {
throwIfParentInstance("setCrossProfileCalendarPackages");
@@ -14475,7 +14477,9 @@
* @throws SecurityException if {@code admin} is not a profile owner
*
* @see #setCrossProfileCalendarPackages(ComponentName, Set)
+ * @deprecated Use {@link #setCrossProfilePackages(ComponentName, Set)}.
*/
+ @Deprecated
public @Nullable Set<String> getCrossProfileCalendarPackages(@NonNull ComponentName admin) {
throwIfParentInstance("getCrossProfileCalendarPackages");
if (mService != null) {
diff --git a/core/java/android/app/admin/DevicePolicyManagerInternal.java b/core/java/android/app/admin/DevicePolicyManagerInternal.java
index b6f0916..537b8d4 100644
--- a/core/java/android/app/admin/DevicePolicyManagerInternal.java
+++ b/core/java/android/app/admin/DevicePolicyManagerInternal.java
@@ -269,4 +269,9 @@
* {@link #supportsResetOp(int)} is true.
*/
public abstract void resetOp(int op, String packageName, @UserIdInt int userId);
+
+ /**
+ * Returns whether new "turn off work" behavior is enabled via feature flag.
+ */
+ public abstract boolean isKeepProfilesRunningEnabled();
}
diff --git a/core/java/android/companion/virtual/VirtualDeviceParams.java b/core/java/android/companion/virtual/VirtualDeviceParams.java
index 1cbe910..be77f2b 100644
--- a/core/java/android/companion/virtual/VirtualDeviceParams.java
+++ b/core/java/android/companion/virtual/VirtualDeviceParams.java
@@ -147,6 +147,19 @@
*/
public static final int POLICY_TYPE_SENSORS = 0;
+ /** @hide */
+ @IntDef(flag = true, prefix = "RECENTS_POLICY_",
+ value = {RECENTS_POLICY_ALLOW_IN_HOST_DEVICE_RECENTS})
+ @Retention(RetentionPolicy.SOURCE)
+ @Target({ElementType.TYPE_PARAMETER, ElementType.TYPE_USE})
+ public @interface RecentsPolicy {}
+
+ /**
+ * If set, activities launched on this virtual device are allowed to appear in the host device
+ * of the recently launched activities list.
+ */
+ public static final int RECENTS_POLICY_ALLOW_IN_HOST_DEVICE_RECENTS = 1 << 0;
+
private final int mLockState;
@NonNull private final ArraySet<UserHandle> mUsersWithMatchingAccounts;
@NonNull private final ArraySet<ComponentName> mAllowedCrossTaskNavigations;
@@ -161,6 +174,8 @@
// Mapping of @PolicyType to @DevicePolicy
@NonNull private final SparseIntArray mDevicePolicies;
@NonNull private final List<VirtualSensorConfig> mVirtualSensorConfigs;
+ @RecentsPolicy
+ private final int mDefaultRecentsPolicy;
private VirtualDeviceParams(
@LockState int lockState,
@@ -173,7 +188,8 @@
@ActivityPolicy int defaultActivityPolicy,
@Nullable String name,
@NonNull SparseIntArray devicePolicies,
- @NonNull List<VirtualSensorConfig> virtualSensorConfigs) {
+ @NonNull List<VirtualSensorConfig> virtualSensorConfigs,
+ @RecentsPolicy int defaultRecentsPolicy) {
mLockState = lockState;
mUsersWithMatchingAccounts =
new ArraySet<>(Objects.requireNonNull(usersWithMatchingAccounts));
@@ -188,6 +204,7 @@
mName = name;
mDevicePolicies = Objects.requireNonNull(devicePolicies);
mVirtualSensorConfigs = Objects.requireNonNull(virtualSensorConfigs);
+ mDefaultRecentsPolicy = defaultRecentsPolicy;
}
@SuppressWarnings("unchecked")
@@ -204,6 +221,7 @@
mDevicePolicies = parcel.readSparseIntArray();
mVirtualSensorConfigs = new ArrayList<>();
parcel.readTypedList(mVirtualSensorConfigs, VirtualSensorConfig.CREATOR);
+ mDefaultRecentsPolicy = parcel.readInt();
}
/**
@@ -328,6 +346,16 @@
return mVirtualSensorConfigs;
}
+ /**
+ * Returns the policy of how to handle activities in recents.
+ *
+ * @see RecentsPolicy
+ */
+ @RecentsPolicy
+ public int getDefaultRecentsPolicy() {
+ return mDefaultRecentsPolicy;
+ }
+
@Override
public int describeContents() {
return 0;
@@ -346,6 +374,7 @@
dest.writeString8(mName);
dest.writeSparseIntArray(mDevicePolicies);
dest.writeTypedList(mVirtualSensorConfigs);
+ dest.writeInt(mDefaultRecentsPolicy);
}
@Override
@@ -377,15 +406,17 @@
&& Objects.equals(mAllowedActivities, that.mAllowedActivities)
&& Objects.equals(mBlockedActivities, that.mBlockedActivities)
&& mDefaultActivityPolicy == that.mDefaultActivityPolicy
- && Objects.equals(mName, that.mName);
+ && Objects.equals(mName, that.mName)
+ && mDefaultRecentsPolicy == that.mDefaultRecentsPolicy;
}
@Override
public int hashCode() {
int hashCode = Objects.hash(
mLockState, mUsersWithMatchingAccounts, mAllowedCrossTaskNavigations,
- mBlockedCrossTaskNavigations, mDefaultNavigationPolicy, mAllowedActivities,
- mBlockedActivities, mDefaultActivityPolicy, mName, mDevicePolicies);
+ mBlockedCrossTaskNavigations, mDefaultNavigationPolicy, mAllowedActivities,
+ mBlockedActivities, mDefaultActivityPolicy, mName, mDevicePolicies,
+ mDefaultRecentsPolicy);
for (int i = 0; i < mDevicePolicies.size(); i++) {
hashCode = 31 * hashCode + mDevicePolicies.keyAt(i);
hashCode = 31 * hashCode + mDevicePolicies.valueAt(i);
@@ -407,6 +438,7 @@
+ " mDefaultActivityPolicy=" + mDefaultActivityPolicy
+ " mName=" + mName
+ " mDevicePolicies=" + mDevicePolicies
+ + " mDefaultRecentsPolicy=" + mDefaultRecentsPolicy
+ ")";
}
@@ -442,6 +474,7 @@
@Nullable private String mName;
@NonNull private SparseIntArray mDevicePolicies = new SparseIntArray();
@NonNull private List<VirtualSensorConfig> mVirtualSensorConfigs = new ArrayList<>();
+ private int mDefaultRecentsPolicy;
/**
* Sets the lock state of the device. The permission {@code ADD_ALWAYS_UNLOCKED_DISPLAY}
@@ -647,6 +680,17 @@
}
/**
+ * Sets the policy to indicate how activities are handled in recents.
+ *
+ * @param defaultRecentsPolicy A policy specifying how to handle activities in recents.
+ */
+ @NonNull
+ public Builder setDefaultRecentsPolicy(@RecentsPolicy int defaultRecentsPolicy) {
+ mDefaultRecentsPolicy = defaultRecentsPolicy;
+ return this;
+ }
+
+ /**
* Builds the {@link VirtualDeviceParams} instance.
*
* @throws IllegalArgumentException if there's mismatch between policy definition and
@@ -684,7 +728,8 @@
mDefaultActivityPolicy,
mName,
mDevicePolicies,
- mVirtualSensorConfigs);
+ mVirtualSensorConfigs,
+ mDefaultRecentsPolicy);
}
}
}
diff --git a/core/java/android/companion/virtual/sensor/VirtualSensor.java b/core/java/android/companion/virtual/sensor/VirtualSensor.java
index a184481..58a5387 100644
--- a/core/java/android/companion/virtual/sensor/VirtualSensor.java
+++ b/core/java/android/companion/virtual/sensor/VirtualSensor.java
@@ -20,6 +20,7 @@
import android.annotation.RequiresPermission;
import android.annotation.SystemApi;
import android.companion.virtual.IVirtualDevice;
+import android.hardware.Sensor;
import android.os.IBinder;
import android.os.RemoteException;
@@ -68,8 +69,10 @@
}
/**
- * Returns the
- * <a href="https://source.android.com/devices/sensors/sensor-types">type</a> of the sensor.
+ * Returns the type of the sensor.
+ *
+ * @see Sensor#getType()
+ * @see <a href="https://source.android.com/devices/sensors/sensor-types">Sensor types</a>
*/
public int getType() {
return mType;
@@ -87,7 +90,7 @@
* Send a sensor event to the system.
*/
@RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
- public void sendSensorEvent(@NonNull VirtualSensorEvent event) {
+ public void sendEvent(@NonNull VirtualSensorEvent event) {
try {
mVirtualDevice.sendSensorEvent(mToken, event);
} catch (RemoteException e) {
diff --git a/core/java/android/companion/virtual/sensor/VirtualSensorConfig.java b/core/java/android/companion/virtual/sensor/VirtualSensorConfig.java
index 7982fa5..eb2f9dd 100644
--- a/core/java/android/companion/virtual/sensor/VirtualSensorConfig.java
+++ b/core/java/android/companion/virtual/sensor/VirtualSensorConfig.java
@@ -23,6 +23,7 @@
import android.annotation.Nullable;
import android.annotation.SuppressLint;
import android.annotation.SystemApi;
+import android.hardware.Sensor;
import android.os.Parcel;
import android.os.Parcelable;
@@ -77,8 +78,10 @@
}
/**
- * Returns the
- * <a href="https://source.android.com/devices/sensors/sensor-types">type</a> of the sensor.
+ * Returns the type of the sensor.
+ *
+ * @see Sensor#getType()
+ * @see <a href="https://source.android.com/devices/sensors/sensor-types">Sensor types</a>
*/
public int getType() {
return mType;
@@ -150,8 +153,7 @@
/**
* Creates a new builder.
*
- * @param type The
- * <a href="https://source.android.com/devices/sensors/sensor-types">type</a> of the sensor.
+ * @param type The type of the sensor, matching {@link Sensor#getType}.
* @param name The name of the sensor. Must be unique among all sensors with the same type
* that belong to the same virtual device.
*/
diff --git a/core/java/android/companion/virtual/sensor/VirtualSensorEvent.java b/core/java/android/companion/virtual/sensor/VirtualSensorEvent.java
index 8f8860e..01b4975 100644
--- a/core/java/android/companion/virtual/sensor/VirtualSensorEvent.java
+++ b/core/java/android/companion/virtual/sensor/VirtualSensorEvent.java
@@ -18,6 +18,7 @@
import android.annotation.NonNull;
import android.annotation.SystemApi;
+import android.hardware.Sensor;
import android.os.Parcel;
import android.os.Parcelable;
import android.os.SystemClock;
@@ -60,9 +61,11 @@
}
/**
- * Returns the values of this sensor event. The length and contents depend on the
- * <a href="https://source.android.com/devices/sensors/sensor-types">sensor type</a>.
+ * Returns the values of this sensor event. The length and contents depend on the sensor type.
+ *
* @see android.hardware.SensorEvent#values
+ * @see Sensor#getType()
+ * @see <a href="https://source.android.com/devices/sensors/sensor-types">Sensor types</a>
*/
@NonNull
public float[] getValues() {
@@ -90,7 +93,9 @@
/**
* Creates a new builder.
- * @param values the values of the sensor event. @see android.hardware.SensorEvent#values
+ *
+ * @param values the values of the sensor event.
+ * @see android.hardware.SensorEvent#values
*/
public Builder(@NonNull float[] values) {
mValues = values;
diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java
index 9f9fd3c..df5a1ed 100644
--- a/core/java/android/content/Context.java
+++ b/core/java/android/content/Context.java
@@ -5937,6 +5937,14 @@
public static final String FILE_INTEGRITY_SERVICE = "file_integrity";
/**
+ * Binder service for remote key provisioning.
+ *
+ * @see android.frameworks.rkp.IRemoteProvisioning
+ * @hide
+ */
+ public static final String REMOTE_PROVISIONING_SERVICE = "remote_provisioning";
+
+ /**
* Use with {@link #getSystemService(String)} to retrieve a
* {@link android.hardware.lights.LightsManager} for controlling device lights.
*
diff --git a/core/java/android/content/om/TEST_MAPPING b/core/java/android/content/om/TEST_MAPPING
index 6185cf6..a9aaf1a 100644
--- a/core/java/android/content/om/TEST_MAPPING
+++ b/core/java/android/content/om/TEST_MAPPING
@@ -12,6 +12,9 @@
"name": "OverlayDeviceTests"
},
{
+ "name": "SelfTargetingOverlayDeviceTests"
+ },
+ {
"name": "OverlayHostTests"
},
{
@@ -35,6 +38,9 @@
},
{
"include-filter": "android.content.om.cts"
+ },
+ {
+ "include-filter": "android.content.res.loader.cts"
}
]
}
diff --git a/core/java/android/content/pm/PackageInstaller.java b/core/java/android/content/pm/PackageInstaller.java
index c79f99d..1f01ae9 100644
--- a/core/java/android/content/pm/PackageInstaller.java
+++ b/core/java/android/content/pm/PackageInstaller.java
@@ -867,10 +867,15 @@
*/
public void checkInstallConstraints(@NonNull List<String> packageNames,
@NonNull InstallConstraints constraints,
+ @NonNull @CallbackExecutor Executor executor,
@NonNull Consumer<InstallConstraintsResult> callback) {
+ Objects.requireNonNull(executor);
+ Objects.requireNonNull(callback);
try {
var remoteCallback = new RemoteCallback(b -> {
- callback.accept(b.getParcelable("result", InstallConstraintsResult.class));
+ executor.execute(() -> {
+ callback.accept(b.getParcelable("result", InstallConstraintsResult.class));
+ });
});
mInstaller.checkInstallConstraints(
mInstallerPackageName, packageNames, constraints, remoteCallback);
@@ -3675,7 +3680,7 @@
}
/**
- * The callback result of {@link #checkInstallConstraints(List, InstallConstraints, Consumer)}.
+ * The callback result of {@link #checkInstallConstraints(List, InstallConstraints, Executor, Consumer)}.
*/
@DataClass(genParcelable = true, genHiddenConstructor = true)
public static final class InstallConstraintsResult implements Parcelable {
@@ -3783,7 +3788,7 @@
/**
* A class to encapsulate constraints for installation.
*
- * When used with {@link #checkInstallConstraints(List, InstallConstraints, Consumer)}, it
+ * When used with {@link #checkInstallConstraints(List, InstallConstraints, Executor, Consumer)}, it
* specifies the conditions to check against for the packages in question. This can be used
* by app stores to deliver auto updates without disrupting the user experience (referred as
* gentle update) - for example, an app store might hold off updates when it find out the
diff --git a/core/java/android/content/pm/PackageManager.java b/core/java/android/content/pm/PackageManager.java
index 88b5e02..ec490d1 100644
--- a/core/java/android/content/pm/PackageManager.java
+++ b/core/java/android/content/pm/PackageManager.java
@@ -3849,7 +3849,10 @@
/**
* Feature for {@link #getSystemAvailableFeatures} and {@link #hasSystemFeature}:
- * The device supports running activities on secondary displays.
+ * The device supports running activities on secondary displays. Displays here
+ * refers to both physical and virtual displays. Disabling this feature can impact
+ * support for application projection use-cases and support for virtual devices
+ * on the device.
*/
@SdkConstant(SdkConstantType.FEATURE)
public static final String FEATURE_ACTIVITIES_ON_SECONDARY_DISPLAYS
diff --git a/core/java/android/content/pm/PermissionInfo.java b/core/java/android/content/pm/PermissionInfo.java
index 61fc6f6..4fa80d7 100644
--- a/core/java/android/content/pm/PermissionInfo.java
+++ b/core/java/android/content/pm/PermissionInfo.java
@@ -250,6 +250,16 @@
/**
* Additional flag for {@link #protectionLevel}, corresponding
+ * to the <code>module</code> value of
+ * {@link android.R.attr#protectionLevel}.
+ *
+ * @hide
+ */
+ @SystemApi
+ public static final int PROTECTION_FLAG_MODULE = 0x400000;
+
+ /**
+ * Additional flag for {@link #protectionLevel}, corresponding
* to the <code>companion</code> value of
* {@link android.R.attr#protectionLevel}.
*
@@ -320,6 +330,7 @@
PROTECTION_FLAG_RECENTS,
PROTECTION_FLAG_ROLE,
PROTECTION_FLAG_KNOWN_SIGNER,
+ PROTECTION_FLAG_MODULE,
})
@Retention(RetentionPolicy.SOURCE)
public @interface ProtectionFlags {}
@@ -593,6 +604,9 @@
if ((level & PermissionInfo.PROTECTION_FLAG_KNOWN_SIGNER) != 0) {
protLevel.append("|knownSigner");
}
+ if ((level & PermissionInfo.PROTECTION_FLAG_MODULE) != 0) {
+ protLevel.append(("|module"));
+ }
return protLevel.toString();
}
diff --git a/core/java/android/credentials/CreateCredentialRequest.java b/core/java/android/credentials/CreateCredentialRequest.java
index 4589039..be887a5 100644
--- a/core/java/android/credentials/CreateCredentialRequest.java
+++ b/core/java/android/credentials/CreateCredentialRequest.java
@@ -52,7 +52,7 @@
private final Bundle mCandidateQueryData;
/**
- * Determines whether or not the request must only be fulfilled by a system provider.
+ * Determines whether the request must only be fulfilled by a system provider.
*/
private final boolean mRequireSystemProvider;
diff --git a/core/java/android/credentials/GetCredentialOption.java b/core/java/android/credentials/GetCredentialOption.java
index ed93dae..47731dd 100644
--- a/core/java/android/credentials/GetCredentialOption.java
+++ b/core/java/android/credentials/GetCredentialOption.java
@@ -38,13 +38,20 @@
private final String mType;
/**
- * The request data.
+ * The full request data.
*/
@NonNull
- private final Bundle mData;
+ private final Bundle mCredentialRetrievalData;
/**
- * Determines whether or not the request must only be fulfilled by a system provider.
+ * The partial request data that will be sent to the provider during the initial credential
+ * candidate query stage.
+ */
+ @NonNull
+ private final Bundle mCandidateQueryData;
+
+ /**
+ * Determines whether the request must only be fulfilled by a system provider.
*/
private final boolean mRequireSystemProvider;
@@ -57,11 +64,27 @@
}
/**
- * Returns the request data.
+ * Returns the full request data.
*/
@NonNull
- public Bundle getData() {
- return mData;
+ public Bundle getCredentialRetrievalData() {
+ return mCredentialRetrievalData;
+ }
+
+ /**
+ * Returns the partial request data that will be sent to the provider during the initial
+ * credential candidate query stage.
+ *
+ * For security reason, a provider will receive the request data in two stages. First it gets
+ * this partial request that do not contain sensitive user information; it uses this
+ * information to provide credential candidates that the [@code CredentialManager] will show to
+ * the user. Next, the full request data, {@link #getCredentialRetrievalData()}, will be sent to
+ * a provider only if the user further grants the consent by choosing a candidate from the
+ * provider.
+ */
+ @NonNull
+ public Bundle getCandidateQueryData() {
+ return mCandidateQueryData;
}
/**
@@ -75,7 +98,8 @@
@Override
public void writeToParcel(@NonNull Parcel dest, int flags) {
dest.writeString8(mType);
- dest.writeBundle(mData);
+ dest.writeBundle(mCredentialRetrievalData);
+ dest.writeBundle(mCandidateQueryData);
dest.writeBoolean(mRequireSystemProvider);
}
@@ -88,7 +112,8 @@
public String toString() {
return "GetCredentialOption {"
+ "type=" + mType
- + ", data=" + mData
+ + ", requestData=" + mCredentialRetrievalData
+ + ", candidateQueryData=" + mCandidateQueryData
+ ", requireSystemProvider=" + mRequireSystemProvider
+ "}";
}
@@ -96,44 +121,52 @@
/**
* Constructs a {@link GetCredentialOption}.
*
- * @param type the requested credential type
- * @param data the request data
- * @param requireSystemProvider whether or not the request must only be fulfilled by a system
- * provider
- *
+ * @param type the requested credential type
+ * @param credentialRetrievalData the request data
+ * @param candidateQueryData the partial request data that will be sent to the provider
+ * during the initial credential candidate query stage
+ * @param requireSystemProvider whether the request must only be fulfilled by a system
+ * provider
* @throws IllegalArgumentException If type is empty.
*/
public GetCredentialOption(
@NonNull String type,
- @NonNull Bundle data,
+ @NonNull Bundle credentialRetrievalData,
+ @NonNull Bundle candidateQueryData,
boolean requireSystemProvider) {
mType = Preconditions.checkStringNotEmpty(type, "type must not be empty");
- mData = requireNonNull(data, "data must not be null");
+ mCredentialRetrievalData = requireNonNull(credentialRetrievalData,
+ "requestData must not be null");
+ mCandidateQueryData = requireNonNull(candidateQueryData,
+ "candidateQueryData must not be null");
mRequireSystemProvider = requireSystemProvider;
}
private GetCredentialOption(@NonNull Parcel in) {
String type = in.readString8();
Bundle data = in.readBundle();
+ Bundle candidateQueryData = in.readBundle();
boolean requireSystemProvider = in.readBoolean();
mType = type;
AnnotationValidations.validate(NonNull.class, null, mType);
- mData = data;
- AnnotationValidations.validate(NonNull.class, null, mData);
+ mCredentialRetrievalData = data;
+ AnnotationValidations.validate(NonNull.class, null, mCredentialRetrievalData);
+ mCandidateQueryData = candidateQueryData;
+ AnnotationValidations.validate(NonNull.class, null, mCandidateQueryData);
mRequireSystemProvider = requireSystemProvider;
}
public static final @NonNull Parcelable.Creator<GetCredentialOption> CREATOR =
new Parcelable.Creator<GetCredentialOption>() {
- @Override
- public GetCredentialOption[] newArray(int size) {
- return new GetCredentialOption[size];
- }
+ @Override
+ public GetCredentialOption[] newArray(int size) {
+ return new GetCredentialOption[size];
+ }
- @Override
- public GetCredentialOption createFromParcel(@NonNull Parcel in) {
- return new GetCredentialOption(in);
- }
- };
+ @Override
+ public GetCredentialOption createFromParcel(@NonNull Parcel in) {
+ return new GetCredentialOption(in);
+ }
+ };
}
diff --git a/core/java/android/hardware/camera2/CameraCharacteristics.java b/core/java/android/hardware/camera2/CameraCharacteristics.java
index 1ee2423..6e72b5f 100644
--- a/core/java/android/hardware/camera2/CameraCharacteristics.java
+++ b/core/java/android/hardware/camera2/CameraCharacteristics.java
@@ -4613,7 +4613,6 @@
* <p>This key is available on all devices.</p>
* @see #SENSOR_READOUT_TIMESTAMP_NOT_SUPPORTED
* @see #SENSOR_READOUT_TIMESTAMP_HARDWARE
- * @hide
*/
@PublicKey
@NonNull
@@ -5572,4 +5571,6 @@
+
+
}
diff --git a/core/java/android/hardware/camera2/CameraMetadata.java b/core/java/android/hardware/camera2/CameraMetadata.java
index 44f8b1b..b2428b1 100644
--- a/core/java/android/hardware/camera2/CameraMetadata.java
+++ b/core/java/android/hardware/camera2/CameraMetadata.java
@@ -1695,7 +1695,6 @@
* <p>This camera device doesn't support readout timestamp and onReadoutStarted
* callback.</p>
* @see CameraCharacteristics#SENSOR_READOUT_TIMESTAMP
- * @hide
*/
public static final int SENSOR_READOUT_TIMESTAMP_NOT_SUPPORTED = 0;
@@ -1705,7 +1704,6 @@
* readout timestamp is generated by the camera hardware and it has the same accuracy
* and timing characteristics of the start-of-exposure time.</p>
* @see CameraCharacteristics#SENSOR_READOUT_TIMESTAMP
- * @hide
*/
public static final int SENSOR_READOUT_TIMESTAMP_HARDWARE = 1;
diff --git a/core/java/android/hardware/camera2/CaptureRequest.java b/core/java/android/hardware/camera2/CaptureRequest.java
index ea3e4a8..43bfdcc 100644
--- a/core/java/android/hardware/camera2/CaptureRequest.java
+++ b/core/java/android/hardware/camera2/CaptureRequest.java
@@ -4154,6 +4154,38 @@
public static final Key<Integer> DISTORTION_CORRECTION_MODE =
new Key<Integer>("android.distortionCorrection.mode", int.class);
+ /**
+ * <p>Strength of the extension post-processing effect</p>
+ * <p>This control allows Camera extension clients to configure the strength of the applied
+ * extension effect. Strength equal to 0 means that the extension must not apply any
+ * post-processing and return a regular captured frame. Strength equal to 100 is the
+ * default level of post-processing applied when the control is not supported or not set
+ * by the client. Values between 0 and 100 will have different effect depending on the
+ * extension type as described below:</p>
+ * <ul>
+ * <li>{@link android.hardware.camera2.CameraExtensionCharacteristics#EXTENSION_BOKEH BOKEH} -
+ * the strength is expected to control the amount of blur.</li>
+ * <li>{@link android.hardware.camera2.CameraExtensionCharacteristics#EXTENSION_HDR HDR} and
+ * {@link android.hardware.camera2.CameraExtensionCharacteristics#EXTENSION_NIGHT NIGHT} -
+ * the strength can control the amount of images fused and the brightness of the final image.</li>
+ * <li>{@link android.hardware.camera2.CameraExtensionCharacteristics#EXTENSION_FACE_RETOUCH FACE_RETOUCH} -
+ * the strength value will control the amount of cosmetic enhancement and skin
+ * smoothing.</li>
+ * </ul>
+ * <p>The control will be supported if the capture request key is part of the list generated by
+ * {@link android.hardware.camera2.CameraExtensionCharacteristics#getAvailableCaptureRequestKeys }.
+ * The control is only defined and available to clients sending capture requests via
+ * {@link android.hardware.camera2.CameraExtensionSession }.
+ * The default value is 100.</p>
+ * <p><b>Range of valid values:</b><br>
+ * 0 - 100</p>
+ * <p><b>Optional</b> - The value for this key may be {@code null} on some devices.</p>
+ */
+ @PublicKey
+ @NonNull
+ public static final Key<Integer> EXTENSION_STRENGTH =
+ new Key<Integer>("android.extension.strength", int.class);
+
/*~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~
* End generated code
*~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~O@*/
@@ -4163,4 +4195,6 @@
+
+
}
diff --git a/core/java/android/hardware/camera2/CaptureResult.java b/core/java/android/hardware/camera2/CaptureResult.java
index 285c933..fb52cc6 100644
--- a/core/java/android/hardware/camera2/CaptureResult.java
+++ b/core/java/android/hardware/camera2/CaptureResult.java
@@ -5576,6 +5576,62 @@
public static final Key<Integer> DISTORTION_CORRECTION_MODE =
new Key<Integer>("android.distortionCorrection.mode", int.class);
+ /**
+ * <p>Contains the extension type of the currently active extension</p>
+ * <p>The capture result will only be supported and included by camera extension
+ * {@link android.hardware.camera2.CameraExtensionSession sessions}.
+ * In case the extension session was configured to use
+ * {@link android.hardware.camera2.CameraExtensionCharacteristics#EXTENSION_AUTOMATIC AUTO},
+ * then the extension type value will indicate the currently active extension like
+ * {@link android.hardware.camera2.CameraExtensionCharacteristics#EXTENSION_HDR HDR},
+ * {@link android.hardware.camera2.CameraExtensionCharacteristics#EXTENSION_NIGHT NIGHT} etc.
+ * , and will never return
+ * {@link android.hardware.camera2.CameraExtensionCharacteristics#EXTENSION_AUTOMATIC AUTO}.
+ * In case the extension session was configured to use an extension different from
+ * {@link android.hardware.camera2.CameraExtensionCharacteristics#EXTENSION_AUTOMATIC AUTO},
+ * then the result type will always match with the configured extension type.</p>
+ * <p><b>Range of valid values:</b><br>
+ * Extension type value listed in
+ * {@link android.hardware.camera2.CameraExtensionCharacteristics }</p>
+ * <p><b>Optional</b> - The value for this key may be {@code null} on some devices.</p>
+ */
+ @PublicKey
+ @NonNull
+ public static final Key<Integer> EXTENSION_CURRENT_TYPE =
+ new Key<Integer>("android.extension.currentType", int.class);
+
+ /**
+ * <p>Strength of the extension post-processing effect</p>
+ * <p>This control allows Camera extension clients to configure the strength of the applied
+ * extension effect. Strength equal to 0 means that the extension must not apply any
+ * post-processing and return a regular captured frame. Strength equal to 100 is the
+ * default level of post-processing applied when the control is not supported or not set
+ * by the client. Values between 0 and 100 will have different effect depending on the
+ * extension type as described below:</p>
+ * <ul>
+ * <li>{@link android.hardware.camera2.CameraExtensionCharacteristics#EXTENSION_BOKEH BOKEH} -
+ * the strength is expected to control the amount of blur.</li>
+ * <li>{@link android.hardware.camera2.CameraExtensionCharacteristics#EXTENSION_HDR HDR} and
+ * {@link android.hardware.camera2.CameraExtensionCharacteristics#EXTENSION_NIGHT NIGHT} -
+ * the strength can control the amount of images fused and the brightness of the final image.</li>
+ * <li>{@link android.hardware.camera2.CameraExtensionCharacteristics#EXTENSION_FACE_RETOUCH FACE_RETOUCH} -
+ * the strength value will control the amount of cosmetic enhancement and skin
+ * smoothing.</li>
+ * </ul>
+ * <p>The control will be supported if the capture request key is part of the list generated by
+ * {@link android.hardware.camera2.CameraExtensionCharacteristics#getAvailableCaptureRequestKeys }.
+ * The control is only defined and available to clients sending capture requests via
+ * {@link android.hardware.camera2.CameraExtensionSession }.
+ * The default value is 100.</p>
+ * <p><b>Range of valid values:</b><br>
+ * 0 - 100</p>
+ * <p><b>Optional</b> - The value for this key may be {@code null} on some devices.</p>
+ */
+ @PublicKey
+ @NonNull
+ public static final Key<Integer> EXTENSION_STRENGTH =
+ new Key<Integer>("android.extension.strength", int.class);
+
/*~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~
* End generated code
*~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~O@*/
@@ -5585,4 +5641,6 @@
+
+
}
diff --git a/core/java/android/hardware/display/AmbientDisplayConfiguration.java b/core/java/android/hardware/display/AmbientDisplayConfiguration.java
index 8c71b36..47541ca 100644
--- a/core/java/android/hardware/display/AmbientDisplayConfiguration.java
+++ b/core/java/android/hardware/display/AmbientDisplayConfiguration.java
@@ -40,6 +40,7 @@
private static final String TAG = "AmbientDisplayConfig";
private final Context mContext;
private final boolean mAlwaysOnByDefault;
+ private final boolean mPickupGestureEnabledByDefault;
/** Copied from android.provider.Settings.Secure since these keys are hidden. */
private static final String[] DOZE_SETTINGS = {
@@ -65,6 +66,8 @@
public AmbientDisplayConfiguration(Context context) {
mContext = context;
mAlwaysOnByDefault = mContext.getResources().getBoolean(R.bool.config_dozeAlwaysOnEnabled);
+ mPickupGestureEnabledByDefault =
+ mContext.getResources().getBoolean(R.bool.config_dozePickupGestureEnabled);
}
/** @hide */
@@ -95,7 +98,8 @@
/** @hide */
public boolean pickupGestureEnabled(int user) {
- return boolSettingDefaultOn(Settings.Secure.DOZE_PICK_UP_GESTURE, user)
+ return boolSetting(Settings.Secure.DOZE_PICK_UP_GESTURE, user,
+ mPickupGestureEnabledByDefault ? 1 : 0)
&& dozePickupSensorAvailable();
}
diff --git a/core/java/android/hardware/input/IInputManager.aidl b/core/java/android/hardware/input/IInputManager.aidl
index b26c0a2..eef0f42 100644
--- a/core/java/android/hardware/input/IInputManager.aidl
+++ b/core/java/android/hardware/input/IInputManager.aidl
@@ -123,6 +123,22 @@
String[] getKeyboardLayoutListForInputDevice(in InputDeviceIdentifier identifier, int userId,
in InputMethodInfo imeInfo, in InputMethodSubtype imeSubtype);
+ // Modifier key remapping APIs.
+ @EnforcePermission("REMAP_MODIFIER_KEYS")
+ @JavaPassthrough(annotation="@android.annotation.RequiresPermission(value = "
+ + "android.Manifest.permission.REMAP_MODIFIER_KEYS)")
+ void remapModifierKey(int fromKey, int toKey);
+
+ @EnforcePermission("REMAP_MODIFIER_KEYS")
+ @JavaPassthrough(annotation="@android.annotation.RequiresPermission(value = "
+ + "android.Manifest.permission.REMAP_MODIFIER_KEYS)")
+ void clearAllModifierKeyRemappings();
+
+ @EnforcePermission("REMAP_MODIFIER_KEYS")
+ @JavaPassthrough(annotation="@android.annotation.RequiresPermission(value = "
+ + "android.Manifest.permission.REMAP_MODIFIER_KEYS)")
+ Map getModifierKeyRemapping();
+
// Registers an input devices changed listener.
void registerInputDevicesChangedListener(IInputDevicesChangedListener listener);
diff --git a/core/java/android/hardware/input/InputManager.java b/core/java/android/hardware/input/InputManager.java
index cea3fa1..3735417 100644
--- a/core/java/android/hardware/input/InputManager.java
+++ b/core/java/android/hardware/input/InputManager.java
@@ -78,6 +78,7 @@
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.List;
+import java.util.Map;
import java.util.Objects;
import java.util.concurrent.Executor;
@@ -253,6 +254,31 @@
})
public @interface SwitchState {}
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(prefix = { "REMAPPABLE_MODIFIER_KEY_" }, value = {
+ RemappableModifierKey.REMAPPABLE_MODIFIER_KEY_CTRL_LEFT,
+ RemappableModifierKey.REMAPPABLE_MODIFIER_KEY_CTRL_RIGHT,
+ RemappableModifierKey.REMAPPABLE_MODIFIER_KEY_META_LEFT,
+ RemappableModifierKey.REMAPPABLE_MODIFIER_KEY_META_RIGHT,
+ RemappableModifierKey.REMAPPABLE_MODIFIER_KEY_ALT_LEFT,
+ RemappableModifierKey.REMAPPABLE_MODIFIER_KEY_ALT_RIGHT,
+ RemappableModifierKey.REMAPPABLE_MODIFIER_KEY_SHIFT_LEFT,
+ RemappableModifierKey.REMAPPABLE_MODIFIER_KEY_SHIFT_RIGHT,
+ RemappableModifierKey.REMAPPABLE_MODIFIER_KEY_CAPS_LOCK,
+ })
+ public @interface RemappableModifierKey {
+ int REMAPPABLE_MODIFIER_KEY_CTRL_LEFT = KeyEvent.KEYCODE_CTRL_LEFT;
+ int REMAPPABLE_MODIFIER_KEY_CTRL_RIGHT = KeyEvent.KEYCODE_CTRL_RIGHT;
+ int REMAPPABLE_MODIFIER_KEY_META_LEFT = KeyEvent.KEYCODE_META_LEFT;
+ int REMAPPABLE_MODIFIER_KEY_META_RIGHT = KeyEvent.KEYCODE_META_RIGHT;
+ int REMAPPABLE_MODIFIER_KEY_ALT_LEFT = KeyEvent.KEYCODE_ALT_LEFT;
+ int REMAPPABLE_MODIFIER_KEY_ALT_RIGHT = KeyEvent.KEYCODE_ALT_RIGHT;
+ int REMAPPABLE_MODIFIER_KEY_SHIFT_LEFT = KeyEvent.KEYCODE_SHIFT_LEFT;
+ int REMAPPABLE_MODIFIER_KEY_SHIFT_RIGHT = KeyEvent.KEYCODE_SHIFT_RIGHT;
+ int REMAPPABLE_MODIFIER_KEY_CAPS_LOCK = KeyEvent.KEYCODE_CAPS_LOCK;
+ }
+
/**
* Switch State: Unknown.
*
@@ -854,6 +880,60 @@
}
/**
+ * Remaps modifier keys. Remapping a modifier key to itself will clear any previous remappings
+ * for that key.
+ *
+ * @param fromKey The modifier key getting remapped.
+ * @param toKey The modifier key that it is remapped to.
+ *
+ * @hide
+ */
+ @TestApi
+ @RequiresPermission(Manifest.permission.REMAP_MODIFIER_KEYS)
+ public void remapModifierKey(@RemappableModifierKey int fromKey,
+ @RemappableModifierKey int toKey) {
+ try {
+ mIm.remapModifierKey(fromKey, toKey);
+ } catch (RemoteException ex) {
+ throw ex.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Clears all existing modifier key remappings
+ *
+ * @hide
+ */
+ @TestApi
+ @RequiresPermission(Manifest.permission.REMAP_MODIFIER_KEYS)
+ public void clearAllModifierKeyRemappings() {
+ try {
+ mIm.clearAllModifierKeyRemappings();
+ } catch (RemoteException ex) {
+ throw ex.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Provides the current modifier key remapping
+ *
+ * @return a {fromKey, toKey} map that contains the existing modifier key remappings..
+ * {@link RemappableModifierKey}
+ *
+ * @hide
+ */
+ @TestApi
+ @NonNull
+ @RequiresPermission(Manifest.permission.REMAP_MODIFIER_KEYS)
+ public Map<Integer, Integer> getModifierKeyRemapping() {
+ try {
+ return mIm.getModifierKeyRemapping();
+ } catch (RemoteException ex) {
+ throw ex.rethrowFromSystemServer();
+ }
+ }
+
+ /**
* Gets the TouchCalibration applied to the specified input device's coordinates.
*
* @param inputDeviceDescriptor The input device descriptor.
diff --git a/core/java/android/hardware/input/VirtualDpad.java b/core/java/android/hardware/input/VirtualDpad.java
index 4d61553..8133472 100644
--- a/core/java/android/hardware/input/VirtualDpad.java
+++ b/core/java/android/hardware/input/VirtualDpad.java
@@ -32,8 +32,8 @@
/**
* A virtual dpad representing a key input mechanism on a remote device.
*
- * This registers an InputDevice that is interpreted like a physically-connected device and
- * dispatches received events to it.
+ * <p>This registers an InputDevice that is interpreted like a physically-connected device and
+ * dispatches received events to it.</p>
*
* @hide
*/
@@ -44,6 +44,7 @@
Collections.unmodifiableSet(
new HashSet<>(
Arrays.asList(
+ KeyEvent.KEYCODE_BACK,
KeyEvent.KEYCODE_DPAD_UP,
KeyEvent.KEYCODE_DPAD_DOWN,
KeyEvent.KEYCODE_DPAD_LEFT,
@@ -58,8 +59,15 @@
/**
* Sends a key event to the system.
*
- * Supported key codes are KEYCODE_DPAD_UP, KEYCODE_DPAD_DOWN, KEYCODE_DPAD_LEFT,
- * KEYCODE_DPAD_RIGHT and KEYCODE_DPAD_CENTER,
+ * <p>Supported key codes are:
+ * <ul>
+ * <li>{@link KeyEvent.KEYCODE_DPAD_UP}</li>
+ * <li>{@link KeyEvent.KEYCODE_DPAD_DOWN}</li>
+ * <li>{@link KeyEvent.KEYCODE_DPAD_LEFT}</li>
+ * <li>{@link KeyEvent.KEYCODE_DPAD_RIGHT}</li>
+ * <li>{@link KeyEvent.KEYCODE_DPAD_CENTER}</li>
+ * <li>{@link KeyEvent.KEYCODE_BACK}</li>
+ * </ul>
*
* @param event the event to send
*/
diff --git a/core/java/android/os/IUserManager.aidl b/core/java/android/os/IUserManager.aidl
index d31540a..c2ddf45 100644
--- a/core/java/android/os/IUserManager.aidl
+++ b/core/java/android/os/IUserManager.aidl
@@ -79,6 +79,7 @@
String getUserAccount(int userId);
void setUserAccount(int userId, String accountName);
long getUserCreationTime(int userId);
+ int getUserSwitchability(int userId);
boolean isUserSwitcherEnabled(boolean showEvenIfNotActionable, int mUserId);
boolean isRestricted(int userId);
boolean canHaveRestrictedProfile(int userId);
diff --git a/core/java/android/os/OWNERS b/core/java/android/os/OWNERS
index 5c5af2a..e9a3254 100644
--- a/core/java/android/os/OWNERS
+++ b/core/java/android/os/OWNERS
@@ -71,6 +71,9 @@
# Tracing
per-file Trace.java = file:/TRACE_OWNERS
+# PatternMatcher
+per-file PatternMatcher* = file:/PACKAGE_MANAGER_OWNERS
+
# PermissionEnforcer
per-file PermissionEnforcer.java = tweek@google.com, brufino@google.com
diff --git a/core/java/android/os/UserManager.java b/core/java/android/os/UserManager.java
index 07c4b44..5f2f710 100644
--- a/core/java/android/os/UserManager.java
+++ b/core/java/android/os/UserManager.java
@@ -59,7 +59,6 @@
import android.graphics.drawable.Drawable;
import android.location.LocationManager;
import android.provider.Settings;
-import android.telecom.TelecomManager;
import android.util.AndroidException;
import android.util.ArraySet;
import android.util.Log;
@@ -1748,7 +1747,7 @@
public static final int SWITCHABILITY_STATUS_SYSTEM_USER_LOCKED = 1 << 2;
/**
- * Result returned in {@link #getUserSwitchability()} indicating user swichability.
+ * Result returned in {@link #getUserSwitchability()} indicating user switchability.
* @hide
*/
@Retention(RetentionPolicy.SOURCE)
@@ -2128,20 +2127,16 @@
* @hide
*/
@Deprecated
- @RequiresPermission(allOf = {
- Manifest.permission.READ_PHONE_STATE,
- Manifest.permission.MANAGE_USERS}, // Can be INTERACT_ACROSS_USERS instead.
- conditional = true)
+ @RequiresPermission(anyOf = {android.Manifest.permission.MANAGE_USERS,
+ android.Manifest.permission.INTERACT_ACROSS_USERS})
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
@UserHandleAware
public boolean canSwitchUsers() {
- boolean allowUserSwitchingWhenSystemUserLocked = Settings.Global.getInt(
- mContext.getContentResolver(),
- Settings.Global.ALLOW_USER_SWITCHING_WHEN_SYSTEM_USER_LOCKED, 0) != 0;
- boolean isSystemUserUnlocked = isUserUnlocked(UserHandle.SYSTEM);
- boolean isUserSwitchDisallowed = hasUserRestrictionForUser(DISALLOW_USER_SWITCH, mUserId);
- return (allowUserSwitchingWhenSystemUserLocked || isSystemUserUnlocked) && !inCall()
- && !isUserSwitchDisallowed;
+ try {
+ return mService.getUserSwitchability(mUserId) == SWITCHABILITY_STATUS_OK;
+ } catch (RemoteException re) {
+ throw re.rethrowFromSystemServer();
+ }
}
/**
@@ -2156,9 +2151,8 @@
* @hide
*/
@SystemApi
- @RequiresPermission(allOf = {Manifest.permission.READ_PHONE_STATE,
- android.Manifest.permission.MANAGE_USERS,
- android.Manifest.permission.INTERACT_ACROSS_USERS}, conditional = true)
+ @RequiresPermission(anyOf = {android.Manifest.permission.MANAGE_USERS,
+ android.Manifest.permission.INTERACT_ACROSS_USERS})
@UserHandleAware(enabledSinceTargetSdkVersion = Build.VERSION_CODES.TIRAMISU)
public @UserSwitchabilityResult int getUserSwitchability() {
return getUserSwitchability(UserHandle.of(getContextUserIfAppropriate()));
@@ -2175,31 +2169,14 @@
* @return A {@link UserSwitchabilityResult} flag indicating if the user is switchable.
* @hide
*/
- @RequiresPermission(allOf = {Manifest.permission.READ_PHONE_STATE,
- android.Manifest.permission.MANAGE_USERS,
- android.Manifest.permission.INTERACT_ACROSS_USERS}, conditional = true)
+ @RequiresPermission(anyOf = {android.Manifest.permission.MANAGE_USERS,
+ android.Manifest.permission.INTERACT_ACROSS_USERS})
public @UserSwitchabilityResult int getUserSwitchability(UserHandle userHandle) {
- int flags = SWITCHABILITY_STATUS_OK;
- if (inCall()) {
- flags |= SWITCHABILITY_STATUS_USER_IN_CALL;
+ try {
+ return mService.getUserSwitchability(userHandle.getIdentifier());
+ } catch (RemoteException re) {
+ throw re.rethrowFromSystemServer();
}
- if (hasUserRestrictionForUser(DISALLOW_USER_SWITCH, userHandle)) {
- flags |= SWITCHABILITY_STATUS_USER_SWITCH_DISALLOWED;
- }
-
- // System User is always unlocked in Headless System User Mode, so ignore this flag
- if (!isHeadlessSystemUserMode()) {
- final boolean allowUserSwitchingWhenSystemUserLocked = Settings.Global.getInt(
- mContext.getContentResolver(),
- Settings.Global.ALLOW_USER_SWITCHING_WHEN_SYSTEM_USER_LOCKED, 0) != 0;
- final boolean systemUserUnlocked = isUserUnlocked(UserHandle.SYSTEM);
-
- if (!allowUserSwitchingWhenSystemUserLocked && !systemUserUnlocked) {
- flags |= SWITCHABILITY_STATUS_SYSTEM_USER_LOCKED;
- }
- }
-
- return flags;
}
/**
@@ -5614,11 +5591,6 @@
}
}
- private boolean inCall() {
- final TelecomManager telecomManager = mContext.getSystemService(TelecomManager.class);
- return telecomManager != null && telecomManager.isInCall();
- }
-
/* Cache key for anything that assumes that userIds cannot be re-used without rebooting. */
private static final String CACHE_KEY_STATIC_USER_PROPERTIES = "cache_key.static_user_props";
diff --git a/core/java/android/provider/DeviceConfig.java b/core/java/android/provider/DeviceConfig.java
index ab87820..7df9290 100644
--- a/core/java/android/provider/DeviceConfig.java
+++ b/core/java/android/provider/DeviceConfig.java
@@ -25,7 +25,6 @@
import android.annotation.RequiresPermission;
import android.annotation.SuppressLint;
import android.annotation.SystemApi;
-import android.content.pm.PackageManager;
import android.database.ContentObserver;
import android.net.Uri;
import android.provider.Settings.Config.SyncDisabledMode;
@@ -1168,12 +1167,10 @@
* @see #removeOnPropertiesChangedListener(OnPropertiesChangedListener)
*/
@SystemApi
- @RequiresPermission(READ_DEVICE_CONFIG)
public static void addOnPropertiesChangedListener(
@NonNull String namespace,
@NonNull @CallbackExecutor Executor executor,
@NonNull OnPropertiesChangedListener onPropertiesChangedListener) {
- enforceReadPermission(namespace);
synchronized (sLock) {
Pair<String, Executor> oldNamespace = sListeners.get(onPropertiesChangedListener);
if (oldNamespace == null) {
@@ -1296,20 +1293,6 @@
}
/**
- * Enforces READ_DEVICE_CONFIG permission if namespace is not one of public namespaces.
- * @hide
- */
- public static void enforceReadPermission(@NonNull String namespace) {
- if (Settings.Config.checkCallingOrSelfPermission(READ_DEVICE_CONFIG)
- != PackageManager.PERMISSION_GRANTED) {
- if (!PUBLIC_NAMESPACES.contains(namespace)) {
- throw new SecurityException("Permission denial: reading from settings requires:"
- + READ_DEVICE_CONFIG);
- }
- }
- }
-
- /**
* Returns list of namespaces that can be read without READ_DEVICE_CONFIG_PERMISSION;
* @hide
*/
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index f3e8b7c..eeac00b 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -3366,7 +3366,7 @@
public ArrayMap<String, String> getStringsForPrefix(ContentResolver cr, String prefix,
List<String> names) {
String namespace = prefix.substring(0, prefix.length() - 1);
- DeviceConfig.enforceReadPermission(namespace);
+ Config.enforceReadPermission(namespace);
ArrayMap<String, String> keyValues = new ArrayMap<>();
int currentGeneration = -1;
@@ -9370,6 +9370,14 @@
public static final int DOCK_SETUP_PAUSED = 2;
/**
+ * Indicates that the user has been prompted to start dock setup.
+ * One of the possible states for {@link #DOCK_SETUP_STATE}.
+ *
+ * @hide
+ */
+ public static final int DOCK_SETUP_PROMPTED = 3;
+
+ /**
* Indicates that the user has completed dock setup.
* One of the possible states for {@link #DOCK_SETUP_STATE}.
*
@@ -9383,6 +9391,7 @@
DOCK_SETUP_NOT_STARTED,
DOCK_SETUP_STARTED,
DOCK_SETUP_PAUSED,
+ DOCK_SETUP_PROMPTED,
DOCK_SETUP_COMPLETED
})
public @interface DockSetupState {
@@ -18370,6 +18379,21 @@
.getApplicationContext().checkCallingOrSelfPermission(permission);
}
+ /**
+ * Enforces READ_DEVICE_CONFIG permission if namespace is not one of public namespaces.
+ * @hide
+ */
+ public static void enforceReadPermission(String namespace) {
+ if (ActivityThread.currentApplication().getApplicationContext()
+ .checkCallingOrSelfPermission(Manifest.permission.READ_DEVICE_CONFIG)
+ != PackageManager.PERMISSION_GRANTED) {
+ if (!DeviceConfig.getPublicNamespaces().contains(namespace)) {
+ throw new SecurityException("Permission denial: reading from settings requires:"
+ + Manifest.permission.READ_DEVICE_CONFIG);
+ }
+ }
+ }
+
private static void registerMonitorCallbackAsUser(
@NonNull ContentResolver resolver, @UserIdInt int userHandle,
@NonNull RemoteCallback callback) {
diff --git a/core/java/android/security/rkp/IGetKeyCallback.aidl b/core/java/android/security/rkp/IGetKeyCallback.aidl
new file mode 100644
index 0000000..85ceae62
--- /dev/null
+++ b/core/java/android/security/rkp/IGetKeyCallback.aidl
@@ -0,0 +1,49 @@
+/*
+ * 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.security.rkp;
+
+import android.security.rkp.RemotelyProvisionedKey;
+
+/**
+ * Callback interface for receiving remotely provisioned keys from a
+ * {@link IRegistration}.
+ *
+ * @hide
+ */
+oneway interface IGetKeyCallback {
+ /**
+ * Called in response to {@link IRegistration.getKey}, indicating
+ * a remotely-provisioned key is available.
+ *
+ * @param key The key that was received from the remote provisioning service.
+ */
+ void onSuccess(in RemotelyProvisionedKey key);
+
+ /**
+ * Called when the key request has been successfully cancelled.
+ * @see IRegistration.cancelGetKey
+ */
+ void onCancel();
+
+ /**
+ * Called when an error has occurred while trying to get a remotely provisioned key.
+ *
+ * @param error A description of what failed, suitable for logging.
+ */
+ void onError(String error);
+}
+
diff --git a/core/java/android/security/rkp/IGetRegistrationCallback.aidl b/core/java/android/security/rkp/IGetRegistrationCallback.aidl
new file mode 100644
index 0000000..e375a6f
--- /dev/null
+++ b/core/java/android/security/rkp/IGetRegistrationCallback.aidl
@@ -0,0 +1,49 @@
+/*
+ * 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.security.rkp;
+
+import android.security.rkp.IRegistration;
+
+/**
+ * Callback interface for receiving a remote provisioning registration.
+ * {@link IRegistration}.
+ *
+ * @hide
+ */
+oneway interface IGetRegistrationCallback {
+ /**
+ * Called in response to {@link IRemoteProvisioning.getRegistration}.
+ *
+ * @param registration an IRegistration that is used to fetch remotely
+ * provisioned keys for the given IRemotelyProvisionedComponent.
+ */
+ void onSuccess(in IRegistration registration);
+
+ /**
+ * Called when the get registration request has been successfully cancelled.
+ * @see IRemoteProvisioning.cancelGetRegistration
+ */
+ void onCancel();
+
+ /**
+ * Called when an error has occurred while trying to get a registration.
+ *
+ * @param error A description of what failed, suitable for logging.
+ */
+ void onError(String error);
+}
+
diff --git a/core/java/android/security/rkp/IRegistration.aidl b/core/java/android/security/rkp/IRegistration.aidl
new file mode 100644
index 0000000..6522a45
--- /dev/null
+++ b/core/java/android/security/rkp/IRegistration.aidl
@@ -0,0 +1,85 @@
+/*
+ * 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.security.rkp;
+
+import android.security.rkp.IGetKeyCallback;
+
+/**
+ * This interface is associated with the registration of an
+ * IRemotelyProvisionedComponent. Each component has a unique set of keys
+ * and certificates that are provisioned to the device for attestation. An
+ * IRegistration binder is created by calling
+ * {@link IRemoteProvisioning#getRegistration()}.
+ *
+ * This interface is used to query for available keys and certificates for the
+ * registered component.
+ *
+ * @hide
+ */
+oneway interface IRegistration {
+ /**
+ * Fetch a remotely provisioned key for the given keyId. Keys are unique
+ * per caller/keyId/registration tuple. This ensures that no two
+ * applications are able to correlate keys to uniquely identify a
+ * device/user. Callers receive their key via {@code callback}.
+ *
+ * If a key is available, this call immediately invokes {@code callback}.
+ *
+ * If no keys are immediately available, then this function contacts the
+ * remote provisioning server to provision a key. After provisioning is
+ * completed, the key is passed to {@code callback}.
+ *
+ * @param keyId This is a client-chosen key identifier, used to
+ * differentiate between keys for varying client-specific use-cases. For
+ * example, keystore2 passes the UID of the applications that call it as
+ * the keyId value here, so that each of keystore2's clients gets a unique
+ * key.
+ * @param callback Receives the result of the call. A callback must only
+ * be used with one {@code getKey} call at a time.
+ */
+ void getKey(int keyId, IGetKeyCallback callback);
+
+ /**
+ * Cancel an active request for a remotely provisioned key, as initiated via
+ * {@link getKey}. Upon cancellation, {@code callback.onCancel} will be invoked.
+ */
+ void cancelGetKey(IGetKeyCallback callback);
+
+ /**
+ * Replace an obsolete key blob with an upgraded key blob.
+ * In certain cases, such as security patch level upgrade, keys become "old".
+ * In these cases, the component which supports operations with the remotely
+ * provisioned key blobs must support upgrading the blobs to make them "new"
+ * and usable on the updated system.
+ *
+ * For an example of a remotely provisioned component that has an upgrade
+ * mechanism, see the documentation for IKeyMintDevice.upgradeKey.
+ *
+ * Once a key has been upgraded, the IRegistration where the key is stored
+ * needs to be told about the new blob. After calling storeUpgradedKey,
+ * getKey will return the new key blob instead of the old one.
+ *
+ * Note that this function does NOT extend the lifetime of key blobs. The
+ * certificate for the key is unchanged, and the key will still expire at
+ * the same time it would have if storeUpgradedKey had never been called.
+ *
+ * @param oldKeyBlob The old key blob to be replaced by {@code newKeyBlob}.
+ *
+ * @param newKeyblob The new blob to replace {@code oldKeyBlob}.
+ */
+ void storeUpgradedKey(in byte[] oldKeyBlob, in byte[] newKeyBlob);
+}
diff --git a/core/java/android/security/rkp/IRemoteProvisioning.aidl b/core/java/android/security/rkp/IRemoteProvisioning.aidl
new file mode 100644
index 0000000..23d8159
--- /dev/null
+++ b/core/java/android/security/rkp/IRemoteProvisioning.aidl
@@ -0,0 +1,68 @@
+/*
+ * 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.security.rkp;
+
+import android.security.rkp.IRegistration;
+import android.security.rkp.IGetRegistrationCallback;
+
+/**
+ * {@link IRemoteProvisioning} is the interface provided to use the remote key
+ * provisioning functionality from the Remote Key Provisioning Daemon (RKPD).
+ * This would be the first service that RKPD clients would interact with. The
+ * intent is for the clients to get the {@link IRegistration} object from this
+ * interface and use it for actual remote provisioning work.
+ *
+ * @hide
+ */
+oneway interface IRemoteProvisioning {
+ /**
+ * Takes a remotely provisioned component service name and gets a
+ * registration bound to that service and the caller's UID.
+ *
+ * @param irpcName The name of the {@code IRemotelyProvisionedComponent}
+ * for which remotely provisioned keys should be managed.
+ * @param callback Receives the result of the call. A callback must only
+ * be used with one {@code getRegistration} call at a time.
+ *
+ * Notes:
+ * - This function will attempt to get the service named by irpcName. This
+ * implies that a lazy/dynamic aidl service will be instantiated, and this
+ * function blocks until the service is up. Upon return, any binder tokens
+ * are dropped, allowing the lazy/dynamic service to shutdown.
+ * - The created registration object is unique per caller. If two different
+ * UIDs call getRegistration with the same irpcName, they will receive
+ * different registrations. This prevents two different applications from
+ * being able to see the same keys.
+ * - This function is idempotent per calling UID. Additional calls to
+ * getRegistration with the same parameters, from the same caller, will have
+ * no side effects.
+ * - A callback may only be associated with one getRegistration call at a time.
+ * If the callback is used multiple times, this API will return an error.
+ *
+ * @see IRegistration#getKey()
+ * @see IRemotelyProvisionedComponent
+ *
+ */
+ void getRegistration(String irpcName, IGetRegistrationCallback callback);
+
+ /**
+ * Cancel any active {@link getRegistration} call associated with the given
+ * callback. If no getRegistration call is currently active, this function is
+ * a noop.
+ */
+ void cancelGetRegistration(IGetRegistrationCallback callback);
+}
diff --git a/core/java/android/security/rkp/OWNERS b/core/java/android/security/rkp/OWNERS
new file mode 100644
index 0000000..fd43089
--- /dev/null
+++ b/core/java/android/security/rkp/OWNERS
@@ -0,0 +1,5 @@
+# Bug component: 1084908
+
+jbires@google.com
+sethmo@google.com
+vikramgaur@google.com
diff --git a/core/java/android/security/rkp/RemotelyProvisionedKey.aidl b/core/java/android/security/rkp/RemotelyProvisionedKey.aidl
new file mode 100644
index 0000000..207f18f
--- /dev/null
+++ b/core/java/android/security/rkp/RemotelyProvisionedKey.aidl
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2022, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.security.rkp;
+
+/**
+ * A {@link RemotelyProvisionedKey} holds an attestation key and the
+ * corresponding remotely provisioned certificate chain.
+ *
+ * @hide
+ */
+@RustDerive(Eq=true, PartialEq=true)
+parcelable RemotelyProvisionedKey {
+ /**
+ * The remotely-provisioned key that may be used to sign attestations. The
+ * format of this key is opaque, and need only be understood by the
+ * IRemotelyProvisionedComponent that generated it.
+ *
+ * Any private key material contained within this blob must be encrypted.
+ *
+ * @see IRemotelyProvisionedComponent
+ */
+ byte[] keyBlob;
+
+ /**
+ * Sequence of DER-encoded X.509 certificates that make up the attestation
+ * key's certificate chain. This is the binary encoding for a chain that is
+ * supported by Java's CertificateFactory.generateCertificates API.
+ */
+ byte[] encodedCertChain;
+}
diff --git a/core/java/android/service/controls/ControlsProviderService.java b/core/java/android/service/controls/ControlsProviderService.java
index d2a4ae2..9396a88 100644
--- a/core/java/android/service/controls/ControlsProviderService.java
+++ b/core/java/android/service/controls/ControlsProviderService.java
@@ -69,6 +69,18 @@
"android.service.controls.META_DATA_PANEL_ACTIVITY";
/**
+ * Boolean extra containing the value of
+ * {@link android.provider.Settings.Secure#LOCKSCREEN_ALLOW_TRIVIAL_CONTROLS}.
+ *
+ * This is passed with the intent when the panel specified by {@link #META_DATA_PANEL_ACTIVITY}
+ * is launched.
+ *
+ * @hide
+ */
+ public static final String EXTRA_LOCKSCREEN_ALLOW_TRIVIAL_CONTROLS =
+ "android.service.controls.EXTRA_LOCKSCREEN_ALLOW_TRIVIAL_CONTROLS";
+
+ /**
* @hide
*/
@SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
diff --git a/core/java/android/service/notification/NotificationListenerService.java b/core/java/android/service/notification/NotificationListenerService.java
index e821af1..d113a3c 100644
--- a/core/java/android/service/notification/NotificationListenerService.java
+++ b/core/java/android/service/notification/NotificationListenerService.java
@@ -267,6 +267,8 @@
* will be restored via NotificationListeners#notifyPostedLocked()
*/
public static final int REASON_LOCKDOWN = 23;
+ // If adding a new notification cancellation reason, you must also add handling for it in
+ // NotificationCancelledEvent.fromCancelReason.
/**
* @hide
diff --git a/core/java/android/view/DisplayEventReceiver.java b/core/java/android/view/DisplayEventReceiver.java
index 169c8c5..ce7606a0 100644
--- a/core/java/android/view/DisplayEventReceiver.java
+++ b/core/java/android/view/DisplayEventReceiver.java
@@ -227,8 +227,10 @@
* timebase.
* @param physicalDisplayId Stable display ID that uniquely describes a (display, port) pair.
* @param modeId The new mode Id
+ * @param renderPeriod The render frame period, which is a multiple of the mode's vsync period
*/
- public void onModeChanged(long timestampNanos, long physicalDisplayId, int modeId) {
+ public void onModeChanged(long timestampNanos, long physicalDisplayId, int modeId,
+ long renderPeriod) {
}
/**
@@ -303,8 +305,9 @@
// Called from native code.
@SuppressWarnings("unused")
- private void dispatchModeChanged(long timestampNanos, long physicalDisplayId, int modeId) {
- onModeChanged(timestampNanos, physicalDisplayId, modeId);
+ private void dispatchModeChanged(long timestampNanos, long physicalDisplayId, int modeId,
+ long renderPeriod) {
+ onModeChanged(timestampNanos, physicalDisplayId, modeId, renderPeriod);
}
// Called from native code.
diff --git a/core/java/android/view/DisplayInfo.java b/core/java/android/view/DisplayInfo.java
index 138017c..85cb517 100644
--- a/core/java/android/view/DisplayInfo.java
+++ b/core/java/android/view/DisplayInfo.java
@@ -180,6 +180,16 @@
public int modeId;
/**
+ * The render frame rate this display is scheduled at, which is a divisor of the active mode
+ * refresh rate. This is the rate SurfaceFlinger would consume frames and would be observable
+ * by applications via the cadence of {@link android.view.Choreographer} callbacks and
+ * by backpressure when submitting buffers as fast as possible.
+ * Apps can call {@link android.view.Display#getRefreshRate} to query this value.
+ *
+ */
+ public float renderFrameRate;
+
+ /**
* The default display mode.
*/
public int defaultModeId;
@@ -376,6 +386,7 @@
&& Objects.equals(displayCutout, other.displayCutout)
&& rotation == other.rotation
&& modeId == other.modeId
+ && renderFrameRate == other.renderFrameRate
&& defaultModeId == other.defaultModeId
&& Arrays.equals(supportedModes, other.supportedModes)
&& colorMode == other.colorMode
@@ -428,6 +439,7 @@
displayCutout = other.displayCutout;
rotation = other.rotation;
modeId = other.modeId;
+ renderFrameRate = other.renderFrameRate;
defaultModeId = other.defaultModeId;
supportedModes = Arrays.copyOf(other.supportedModes, other.supportedModes.length);
colorMode = other.colorMode;
@@ -475,6 +487,7 @@
displayCutout = DisplayCutout.ParcelableWrapper.readCutoutFromParcel(source);
rotation = source.readInt();
modeId = source.readInt();
+ renderFrameRate = source.readFloat();
defaultModeId = source.readInt();
int nModes = source.readInt();
supportedModes = new Display.Mode[nModes];
@@ -535,6 +548,7 @@
DisplayCutout.ParcelableWrapper.writeCutoutToParcel(displayCutout, dest, flags);
dest.writeInt(rotation);
dest.writeInt(modeId);
+ dest.writeFloat(renderFrameRate);
dest.writeInt(defaultModeId);
dest.writeInt(supportedModes.length);
for (int i = 0; i < supportedModes.length; i++) {
@@ -764,6 +778,7 @@
sb.append(presentationDeadlineNanos);
sb.append(", mode ");
sb.append(modeId);
+ sb.append(renderFrameRate);
sb.append(", defaultMode ");
sb.append(defaultModeId);
sb.append(", modes ");
diff --git a/core/java/android/view/KeyCharacterMap.java b/core/java/android/view/KeyCharacterMap.java
index a6f88a7..ea5d9a6 100644
--- a/core/java/android/view/KeyCharacterMap.java
+++ b/core/java/android/view/KeyCharacterMap.java
@@ -177,6 +177,8 @@
private static final int ACCENT_UMLAUT = '\u00A8';
private static final int ACCENT_VERTICAL_LINE_ABOVE = '\u02C8';
private static final int ACCENT_VERTICAL_LINE_BELOW = '\u02CC';
+ private static final int ACCENT_APOSTROPHE = '\'';
+ private static final int ACCENT_QUOTATION_MARK = '"';
/* Legacy dead key display characters used in previous versions of the API.
* We still support these characters by mapping them to their non-legacy version. */
@@ -204,8 +206,6 @@
addCombining('\u030A', ACCENT_RING_ABOVE);
addCombining('\u030B', ACCENT_DOUBLE_ACUTE);
addCombining('\u030C', ACCENT_CARON);
- addCombining('\u030D', ACCENT_VERTICAL_LINE_ABOVE);
- //addCombining('\u030E', ACCENT_DOUBLE_VERTICAL_LINE_ABOVE);
//addCombining('\u030F', ACCENT_DOUBLE_GRAVE);
//addCombining('\u0310', ACCENT_CANDRABINDU);
//addCombining('\u0311', ACCENT_INVERTED_BREVE);
@@ -229,11 +229,17 @@
sCombiningToAccent.append('\u0340', ACCENT_GRAVE);
sCombiningToAccent.append('\u0341', ACCENT_ACUTE);
sCombiningToAccent.append('\u0343', ACCENT_COMMA_ABOVE);
+ sCombiningToAccent.append('\u030D', ACCENT_APOSTROPHE);
+ sCombiningToAccent.append('\u030E', ACCENT_QUOTATION_MARK);
// One-way legacy mappings to preserve compatibility with older applications.
sAccentToCombining.append(ACCENT_GRAVE_LEGACY, '\u0300');
sAccentToCombining.append(ACCENT_CIRCUMFLEX_LEGACY, '\u0302');
sAccentToCombining.append(ACCENT_TILDE_LEGACY, '\u0303');
+
+ // One-way mappings to use the preferred accent
+ sAccentToCombining.append(ACCENT_APOSTROPHE, '\u0301');
+ sAccentToCombining.append(ACCENT_QUOTATION_MARK, '\u0308');
}
private static void addCombining(int combining, int accent) {
diff --git a/core/java/android/view/SurfaceControl.java b/core/java/android/view/SurfaceControl.java
index 30e7a7a..277b90c 100644
--- a/core/java/android/view/SurfaceControl.java
+++ b/core/java/android/view/SurfaceControl.java
@@ -187,8 +187,8 @@
int L, int T, int R, int B);
private static native void nativeSetDisplaySize(long transactionObj, IBinder displayToken,
int width, int height);
- private static native StaticDisplayInfo nativeGetStaticDisplayInfo(IBinder displayToken);
- private static native DynamicDisplayInfo nativeGetDynamicDisplayInfo(IBinder displayToken);
+ private static native StaticDisplayInfo nativeGetStaticDisplayInfo(long displayId);
+ private static native DynamicDisplayInfo nativeGetDynamicDisplayInfo(long displayId);
private static native DisplayedContentSamplingAttributes
nativeGetDisplayedContentSamplingAttributes(IBinder displayToken);
private static native boolean nativeSetDisplayedContentSamplingEnabled(IBinder displayToken,
@@ -1504,6 +1504,7 @@
public static final class DynamicDisplayInfo {
public DisplayMode[] supportedDisplayModes;
public int activeDisplayModeId;
+ public float renderFrameRate;
public int[] supportedColorModes;
public int activeColorMode;
@@ -1520,6 +1521,7 @@
return "DynamicDisplayInfo{"
+ "supportedDisplayModes=" + Arrays.toString(supportedDisplayModes)
+ ", activeDisplayModeId=" + activeDisplayModeId
+ + ", renderFrameRate=" + renderFrameRate
+ ", supportedColorModes=" + Arrays.toString(supportedColorModes)
+ ", activeColorMode=" + activeColorMode
+ ", hdrCapabilities=" + hdrCapabilities
@@ -1535,6 +1537,7 @@
DynamicDisplayInfo that = (DynamicDisplayInfo) o;
return Arrays.equals(supportedDisplayModes, that.supportedDisplayModes)
&& activeDisplayModeId == that.activeDisplayModeId
+ && renderFrameRate == that.renderFrameRate
&& Arrays.equals(supportedColorModes, that.supportedColorModes)
&& activeColorMode == that.activeColorMode
&& Objects.equals(hdrCapabilities, that.hdrCapabilities)
@@ -1544,7 +1547,7 @@
@Override
public int hashCode() {
return Objects.hash(Arrays.hashCode(supportedDisplayModes), activeDisplayModeId,
- activeColorMode, hdrCapabilities);
+ renderFrameRate, activeColorMode, hdrCapabilities);
}
}
@@ -1624,21 +1627,15 @@
/**
* @hide
*/
- public static StaticDisplayInfo getStaticDisplayInfo(IBinder displayToken) {
- if (displayToken == null) {
- throw new IllegalArgumentException("displayToken must not be null");
- }
- return nativeGetStaticDisplayInfo(displayToken);
+ public static StaticDisplayInfo getStaticDisplayInfo(long displayId) {
+ return nativeGetStaticDisplayInfo(displayId);
}
/**
* @hide
*/
- public static DynamicDisplayInfo getDynamicDisplayInfo(IBinder displayToken) {
- if (displayToken == null) {
- throw new IllegalArgumentException("displayToken must not be null");
- }
- return nativeGetDynamicDisplayInfo(displayToken);
+ public static DynamicDisplayInfo getDynamicDisplayInfo(long displayId) {
+ return nativeGetDynamicDisplayInfo(displayId);
}
/**
diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java
index bfb489e..727011c 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -471,16 +471,6 @@
private boolean mAppVisibilityChanged;
int mOrigWindowType = -1;
- /** Whether the window had focus during the most recent traversal. */
- boolean mHadWindowFocus;
-
- /**
- * Whether the window lost focus during a previous traversal and has not
- * yet gained it back. Used to determine whether a WINDOW_STATE_CHANGE
- * accessibility events should be sent during traversal.
- */
- boolean mLostWindowFocus;
-
// Set to true if the owner of this window is in the stopped state,
// so the window should no longer be active.
@UnsupportedAppUsage
@@ -3630,20 +3620,8 @@
}
final boolean changedVisibility = (viewVisibilityChanged || mFirst) && isViewVisible;
- final boolean hasWindowFocus = mAttachInfo.mHasWindowFocus && isViewVisible;
- final boolean regainedFocus = hasWindowFocus && mLostWindowFocus;
- if (regainedFocus) {
- mLostWindowFocus = false;
- } else if (!hasWindowFocus && mHadWindowFocus) {
- mLostWindowFocus = true;
- }
-
- if (changedVisibility || regainedFocus) {
- // Toasts are presented as notifications - don't present them as windows as well
- boolean isToast = mWindowAttributes.type == TYPE_TOAST;
- if (!isToast) {
- host.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
- }
+ if (changedVisibility) {
+ maybeFireAccessibilityWindowStateChangedEvent();
}
mFirst = false;
@@ -3651,8 +3629,8 @@
mNewSurfaceNeeded = false;
mActivityRelaunched = false;
mViewVisibility = viewVisibility;
- mHadWindowFocus = hasWindowFocus;
+ final boolean hasWindowFocus = mAttachInfo.mHasWindowFocus && isViewVisible;
mImeFocusController.onTraversal(hasWindowFocus, mWindowAttributes);
if ((relayoutResult & WindowManagerGlobal.RELAYOUT_RES_FIRST_TIME) != 0) {
@@ -3895,6 +3873,8 @@
~WindowManager.LayoutParams
.SOFT_INPUT_IS_FORWARD_NAVIGATION;
+ maybeFireAccessibilityWindowStateChangedEvent();
+
// Refocusing a window that has a focused view should fire a
// focus event for the view since the global focused view changed.
fireAccessibilityFocusEventIfHasFocusedNode();
@@ -3922,6 +3902,14 @@
ensureTouchModeLocally(inTouchMode);
}
+ private void maybeFireAccessibilityWindowStateChangedEvent() {
+ // Toasts are presented as notifications - don't present them as windows as well.
+ boolean isToast = mWindowAttributes != null && (mWindowAttributes.type == TYPE_TOAST);
+ if (!isToast && mView != null) {
+ mView.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
+ }
+ }
+
private void fireAccessibilityFocusEventIfHasFocusedNode() {
if (!AccessibilityManager.getInstance(mContext).isEnabled()) {
return;
diff --git a/core/java/android/window/DisplayWindowPolicyController.java b/core/java/android/window/DisplayWindowPolicyController.java
index f55932e..e027934 100644
--- a/core/java/android/window/DisplayWindowPolicyController.java
+++ b/core/java/android/window/DisplayWindowPolicyController.java
@@ -128,9 +128,10 @@
ActivityInfo activityInfo, int windowFlags, int systemWindowFlags);
/**
- * Returns {@code true} if the tasks which is on this virtual display can be showed on Recents.
+ * Returns {@code true} if the tasks which is on this virtual display can be showed in the
+ * host device of the recently launched activities list.
*/
- public abstract boolean canShowTasksInRecents();
+ public abstract boolean canShowTasksInHostDeviceRecents();
/**
* This is called when the top activity of the display is changed.
diff --git a/core/java/com/android/internal/accessibility/AccessibilityShortcutController.java b/core/java/com/android/internal/accessibility/AccessibilityShortcutController.java
index 43be031..e0b0110 100644
--- a/core/java/com/android/internal/accessibility/AccessibilityShortcutController.java
+++ b/core/java/com/android/internal/accessibility/AccessibilityShortcutController.java
@@ -226,9 +226,7 @@
Slog.d(TAG, "Accessibility shortcut activated");
final ContentResolver cr = mContext.getContentResolver();
final int userId = ActivityManager.getCurrentUser();
- final int dialogAlreadyShown = Settings.Secure.getIntForUser(
- cr, Settings.Secure.ACCESSIBILITY_SHORTCUT_DIALOG_SHOWN, DialogStatus.NOT_SHOWN,
- userId);
+
// Play a notification vibration
Vibrator vibrator = (Vibrator) mContext.getSystemService(Context.VIBRATOR_SERVICE);
if ((vibrator != null) && vibrator.hasVibrator()) {
@@ -239,7 +237,7 @@
vibrator.vibrate(vibePattern, -1, VIBRATION_ATTRIBUTES);
}
- if (dialogAlreadyShown == DialogStatus.NOT_SHOWN) {
+ if (shouldShowDialog()) {
// The first time, we show a warning rather than toggle the service to give the user a
// chance to turn off this feature before stuff gets enabled.
mAlertDialog = createShortcutWarningDialog(userId);
@@ -269,6 +267,20 @@
}
}
+ /** Whether the warning dialog should be shown instead of performing the shortcut. */
+ private boolean shouldShowDialog() {
+ if (hasFeatureLeanback()) {
+ // Never show the dialog on TV, instead always perform the shortcut directly.
+ return false;
+ }
+ final ContentResolver cr = mContext.getContentResolver();
+ final int userId = ActivityManager.getCurrentUser();
+ final int dialogAlreadyShown = Settings.Secure.getIntForUser(cr,
+ Settings.Secure.ACCESSIBILITY_SHORTCUT_DIALOG_SHOWN, DialogStatus.NOT_SHOWN,
+ userId);
+ return dialogAlreadyShown == DialogStatus.NOT_SHOWN;
+ }
+
/**
* Show toast to alert the user that the accessibility shortcut turned on or off an
* accessibility service.
diff --git a/core/java/com/android/internal/content/om/TEST_MAPPING b/core/java/com/android/internal/content/om/TEST_MAPPING
index 4cb595b..98dadce7 100644
--- a/core/java/com/android/internal/content/om/TEST_MAPPING
+++ b/core/java/com/android/internal/content/om/TEST_MAPPING
@@ -7,6 +7,28 @@
"include-filter": "com.android.internal.content."
}
]
+ },
+ {
+ "name": "SelfTargetingOverlayDeviceTests"
+ }
+ ],
+ "presubmit-large": [
+ {
+ "name": "CtsContentTestCases",
+ "options": [
+ {
+ "exclude-annotation": "androidx.test.filters.FlakyTest"
+ },
+ {
+ "exclude-annotation": "org.junit.Ignore"
+ },
+ {
+ "include-filter": "android.content.om.cts"
+ },
+ {
+ "include-filter": "android.content.res.loader.cts"
+ }
+ ]
}
]
-}
\ No newline at end of file
+}
diff --git a/core/java/com/android/internal/jank/InteractionJankMonitor.java b/core/java/com/android/internal/jank/InteractionJankMonitor.java
index 614f962..75f0bf5 100644
--- a/core/java/com/android/internal/jank/InteractionJankMonitor.java
+++ b/core/java/com/android/internal/jank/InteractionJankMonitor.java
@@ -97,6 +97,7 @@
import android.os.HandlerExecutor;
import android.os.HandlerThread;
import android.provider.DeviceConfig;
+import android.provider.Settings;
import android.text.TextUtils;
import android.util.Log;
import android.util.SparseArray;
@@ -415,7 +416,7 @@
@VisibleForTesting
public InteractionJankMonitor(@NonNull HandlerThread worker) {
// Check permission early.
- DeviceConfig.enforceReadPermission(
+ Settings.Config.enforceReadPermission(
DeviceConfig.NAMESPACE_INTERACTION_JANK_MONITOR);
mRunningTrackers = new SparseArray<>();
diff --git a/core/java/com/android/internal/power/ModemPowerProfile.java b/core/java/com/android/internal/power/ModemPowerProfile.java
index afea69a..a555ae3 100644
--- a/core/java/com/android/internal/power/ModemPowerProfile.java
+++ b/core/java/com/android/internal/power/ModemPowerProfile.java
@@ -158,11 +158,11 @@
private static final SparseArray<String> MODEM_TX_LEVEL_NAMES = new SparseArray<>(5);
static {
- MODEM_DRAIN_TYPE_NAMES.put(MODEM_TX_LEVEL_0, "0");
- MODEM_DRAIN_TYPE_NAMES.put(MODEM_TX_LEVEL_1, "1");
- MODEM_DRAIN_TYPE_NAMES.put(MODEM_TX_LEVEL_2, "2");
- MODEM_DRAIN_TYPE_NAMES.put(MODEM_TX_LEVEL_3, "3");
- MODEM_DRAIN_TYPE_NAMES.put(MODEM_TX_LEVEL_4, "4");
+ MODEM_TX_LEVEL_NAMES.put(MODEM_TX_LEVEL_0, "0");
+ MODEM_TX_LEVEL_NAMES.put(MODEM_TX_LEVEL_1, "1");
+ MODEM_TX_LEVEL_NAMES.put(MODEM_TX_LEVEL_2, "2");
+ MODEM_TX_LEVEL_NAMES.put(MODEM_TX_LEVEL_3, "3");
+ MODEM_TX_LEVEL_NAMES.put(MODEM_TX_LEVEL_4, "4");
}
private static final int[] MODEM_TX_LEVEL_MAP = new int[]{
@@ -418,7 +418,10 @@
return Double.NaN;
}
- private static String keyToString(int key) {
+ /**
+ * Returns a human readable version of a key.
+ */
+ public static String keyToString(int key) {
StringBuilder sb = new StringBuilder();
final int drainType = key & MODEM_DRAIN_TYPE_MASK;
appendFieldToString(sb, "drain", MODEM_DRAIN_TYPE_NAMES, drainType);
@@ -427,6 +430,7 @@
if (drainType == MODEM_DRAIN_TYPE_TX) {
final int txLevel = key & MODEM_TX_LEVEL_MASK;
appendFieldToString(sb, "level", MODEM_TX_LEVEL_NAMES, txLevel);
+ sb.append(",");
}
final int ratType = key & MODEM_RAT_TYPE_MASK;
diff --git a/core/java/com/android/internal/statusbar/IStatusBar.aidl b/core/java/com/android/internal/statusbar/IStatusBar.aidl
index 1460a69..db288c0 100644
--- a/core/java/com/android/internal/statusbar/IStatusBar.aidl
+++ b/core/java/com/android/internal/statusbar/IStatusBar.aidl
@@ -330,4 +330,11 @@
/** Called when requested to go to fullscreen from the active split app. */
void goToFullscreenFromSplit();
+
+ /**
+ * Enters stage split from a current running app.
+ *
+ * @param leftOrTop indicates where the stage split is.
+ */
+ void enterStageSplitFromRunningApp(boolean leftOrTop);
}
diff --git a/core/jni/Android.bp b/core/jni/Android.bp
index 1bc903a..a43f0b3 100644
--- a/core/jni/Android.bp
+++ b/core/jni/Android.bp
@@ -339,6 +339,9 @@
"dnsproxyd_protocol_headers",
"libtextclassifier_hash_headers",
],
+ runtime_libs: [
+ "libidmap2",
+ ],
},
host: {
cflags: [
diff --git a/core/jni/TEST_MAPPING b/core/jni/TEST_MAPPING
index 004c30e..2844856 100644
--- a/core/jni/TEST_MAPPING
+++ b/core/jni/TEST_MAPPING
@@ -11,6 +11,29 @@
}
],
"file_patterns": ["CharsetUtils|FastData"]
+ },
+ {
+ "name": "SelfTargetingOverlayDeviceTests",
+ "file_patterns": ["Overlay"]
+ }
+ ],
+ "presubmit-large": [
+ {
+ "name": "CtsContentTestCases",
+ "options": [
+ {
+ "exclude-annotation": "androidx.test.filters.FlakyTest"
+ },
+ {
+ "exclude-annotation": "org.junit.Ignore"
+ },
+ {
+ "include-filter": "android.content.om.cts"
+ },
+ {
+ "include-filter": "android.content.res.loader.cts"
+ }
+ ]
}
]
}
diff --git a/core/jni/android_view_DisplayEventReceiver.cpp b/core/jni/android_view_DisplayEventReceiver.cpp
index 019e3bd..a8d8a43 100644
--- a/core/jni/android_view_DisplayEventReceiver.cpp
+++ b/core/jni/android_view_DisplayEventReceiver.cpp
@@ -80,7 +80,7 @@
VsyncEventData vsyncEventData) override;
void dispatchHotplug(nsecs_t timestamp, PhysicalDisplayId displayId, bool connected) override;
void dispatchModeChanged(nsecs_t timestamp, PhysicalDisplayId displayId, int32_t modeId,
- nsecs_t vsyncPeriod) override;
+ nsecs_t renderPeriod) override;
void dispatchFrameRateOverrides(nsecs_t timestamp, PhysicalDisplayId displayId,
std::vector<FrameRateOverride> overrides) override;
void dispatchNullEvent(nsecs_t timestamp, PhysicalDisplayId displayId) override {}
@@ -168,14 +168,14 @@
}
void NativeDisplayEventReceiver::dispatchModeChanged(nsecs_t timestamp, PhysicalDisplayId displayId,
- int32_t modeId, nsecs_t) {
+ int32_t modeId, nsecs_t renderPeriod) {
JNIEnv* env = AndroidRuntime::getJNIEnv();
ScopedLocalRef<jobject> receiverObj(env, GetReferent(env, mReceiverWeakGlobal));
if (receiverObj.get()) {
ALOGV("receiver %p ~ Invoking mode changed handler.", this);
env->CallVoidMethod(receiverObj.get(), gDisplayEventReceiverClassInfo.dispatchModeChanged,
- timestamp, displayId.value, modeId);
+ timestamp, displayId.value, modeId, renderPeriod);
ALOGV("receiver %p ~ Returned from mode changed handler.", this);
}
@@ -290,7 +290,7 @@
gDisplayEventReceiverClassInfo.clazz, "dispatchHotplug", "(JJZ)V");
gDisplayEventReceiverClassInfo.dispatchModeChanged =
GetMethodIDOrDie(env, gDisplayEventReceiverClassInfo.clazz, "dispatchModeChanged",
- "(JJI)V");
+ "(JJIJ)V");
gDisplayEventReceiverClassInfo.dispatchFrameRateOverrides =
GetMethodIDOrDie(env, gDisplayEventReceiverClassInfo.clazz,
"dispatchFrameRateOverrides",
diff --git a/core/jni/android_view_SurfaceControl.cpp b/core/jni/android_view_SurfaceControl.cpp
index 2c5386d..593482c 100644
--- a/core/jni/android_view_SurfaceControl.cpp
+++ b/core/jni/android_view_SurfaceControl.cpp
@@ -109,6 +109,7 @@
jmethodID ctor;
jfieldID supportedDisplayModes;
jfieldID activeDisplayModeId;
+ jfieldID renderFrameRate;
jfieldID supportedColorModes;
jfieldID activeColorMode;
jfieldID hdrCapabilities;
@@ -1098,10 +1099,9 @@
connectionToSinkType);
}
-static jobject nativeGetStaticDisplayInfo(JNIEnv* env, jclass clazz, jobject tokenObj) {
+static jobject nativeGetStaticDisplayInfo(JNIEnv* env, jclass clazz, jlong id) {
ui::StaticDisplayInfo info;
- if (const auto token = ibinderForJavaObject(env, tokenObj);
- !token || SurfaceComposerClient::getStaticDisplayInfo(token, &info) != NO_ERROR) {
+ if (SurfaceComposerClient::getStaticDisplayInfo(id, &info) != NO_ERROR) {
return nullptr;
}
@@ -1159,10 +1159,9 @@
capabilities.getDesiredMinLuminance());
}
-static jobject nativeGetDynamicDisplayInfo(JNIEnv* env, jclass clazz, jobject tokenObj) {
+static jobject nativeGetDynamicDisplayInfo(JNIEnv* env, jclass clazz, jlong displayId) {
ui::DynamicDisplayInfo info;
- if (const auto token = ibinderForJavaObject(env, tokenObj);
- !token || SurfaceComposerClient::getDynamicDisplayInfo(token, &info) != NO_ERROR) {
+ if (SurfaceComposerClient::getDynamicDisplayInfoFromId(displayId, &info) != NO_ERROR) {
return nullptr;
}
@@ -1184,6 +1183,7 @@
env->SetObjectField(object, gDynamicDisplayInfoClassInfo.supportedDisplayModes, modesArray);
env->SetIntField(object, gDynamicDisplayInfoClassInfo.activeDisplayModeId,
info.activeDisplayModeId);
+ env->SetFloatField(object, gDynamicDisplayInfoClassInfo.renderFrameRate, info.renderFrameRate);
jintArray colorModesArray = env->NewIntArray(info.supportedColorModes.size());
if (colorModesArray == NULL) {
@@ -2018,10 +2018,10 @@
{"nativeSetDisplaySize", "(JLandroid/os/IBinder;II)V",
(void*)nativeSetDisplaySize },
{"nativeGetStaticDisplayInfo",
- "(Landroid/os/IBinder;)Landroid/view/SurfaceControl$StaticDisplayInfo;",
+ "(J)Landroid/view/SurfaceControl$StaticDisplayInfo;",
(void*)nativeGetStaticDisplayInfo },
{"nativeGetDynamicDisplayInfo",
- "(Landroid/os/IBinder;)Landroid/view/SurfaceControl$DynamicDisplayInfo;",
+ "(J)Landroid/view/SurfaceControl$DynamicDisplayInfo;",
(void*)nativeGetDynamicDisplayInfo },
{"nativeSetDesiredDisplayModeSpecs",
"(Landroid/os/IBinder;Landroid/view/SurfaceControl$DesiredDisplayModeSpecs;)Z",
@@ -2174,6 +2174,8 @@
"[Landroid/view/SurfaceControl$DisplayMode;");
gDynamicDisplayInfoClassInfo.activeDisplayModeId =
GetFieldIDOrDie(env, dynamicInfoClazz, "activeDisplayModeId", "I");
+ gDynamicDisplayInfoClassInfo.renderFrameRate =
+ GetFieldIDOrDie(env, dynamicInfoClazz, "renderFrameRate", "F");
gDynamicDisplayInfoClassInfo.supportedColorModes =
GetFieldIDOrDie(env, dynamicInfoClazz, "supportedColorModes", "[I");
gDynamicDisplayInfoClassInfo.activeColorMode =
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index fb451dd..dc24b0f9 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -4629,6 +4629,8 @@
<p>Protection level: appop
-->
<permission android:name="android.permission.SCHEDULE_EXACT_ALARM"
+ android:label="@string/permlab_schedule_exact_alarm"
+ android:description="@string/permdesc_schedule_exact_alarm"
android:protectionLevel="normal|appop"/>
<!-- Allows apps to use exact alarms just like with {@link
@@ -4654,6 +4656,8 @@
lower standby bucket.
-->
<permission android:name="android.permission.USE_EXACT_ALARM"
+ android:label="@string/permlab_use_exact_alarm"
+ android:description="@string/permdesc_use_exact_alarm"
android:protectionLevel="normal"/>
<!-- Allows an application to query tablet mode state and monitor changes
@@ -6772,6 +6776,14 @@
@hide -->
<permission android:name="android.permission.HANDLE_QUERY_PACKAGE_RESTART"
android:protectionLevel="signature" />
+
+ <!-- Allows low-level access to re-mapping modifier keys.
+ <p>Not for use by third-party applications.
+ @hide
+ @TestApi -->
+ <permission android:name="android.permission.REMAP_MODIFIER_KEYS"
+ android:protectionLevel="signature" />
+
<uses-permission android:name="android.permission.HANDLE_QUERY_PACKAGE_RESTART" />
<!-- Allows financed device kiosk apps to perform actions on the Device Lock service
diff --git a/core/res/res/values/attrs_manifest.xml b/core/res/res/values/attrs_manifest.xml
index c8a65a7..d4644c5 100644
--- a/core/res/res/values/attrs_manifest.xml
+++ b/core/res/res/values/attrs_manifest.xml
@@ -294,6 +294,9 @@
<!-- Additional flag from base permission type: this permission can be automatically
granted to the system app predictor -->
<flag name="appPredictor" value="0x200000" />
+ <!-- Additional flag from base permission type: this permission can also be granted if the
+ requesting application is included in the mainline module}. -->
+ <flag name="module" value="0x400000" />
<!-- Additional flag from base permission type: this permission can be automatically
granted to the system companion device manager service -->
<flag name="companion" value="0x800000" />
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index 9a585a1..1d18d41 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -2507,6 +2507,10 @@
states. -->
<bool name="config_dozeAlwaysOnDisplayAvailable">false</bool>
+ <!-- Control whether the pickup gesture is enabled by default. This value will be used
+ during initialization when the setting is still null. -->
+ <bool name="config_dozePickupGestureEnabled">true</bool>
+
<!-- Control whether the always on display mode is enabled by default. This value will be used
during initialization when the setting is still null. -->
<bool name="config_dozeAlwaysOnEnabled">true</bool>
diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml
index 39012ae..18a5c72 100644
--- a/core/res/res/values/strings.xml
+++ b/core/res/res/values/strings.xml
@@ -1133,6 +1133,16 @@
<string name="permdesc_useDataInBackground">This app can use data in the background. This may increase data usage.</string>
<!-- Title of an application permission, listed so the user can choose whether they want to allow the application to do this. -->
+ <string name="permlab_schedule_exact_alarm">Schedule precisely timed actions</string>
+ <!-- Description of an application permission, listed so the user can choose whether they want to allow the application to do this. -->
+ <string name="permdesc_schedule_exact_alarm">This app can schedule work to happen at a desired time in the future. This also means that the app can run when you\u2019re not actively using the device.</string>
+
+ <!-- Title of an application permission, listed so the user can choose whether they want to allow the application to do this. -->
+ <string name="permlab_use_exact_alarm">Schedule alarms or event reminders</string>
+ <!-- Description of an application permission, listed so the user can choose whether they want to allow the application to do this. -->
+ <string name="permdesc_use_exact_alarm">This app can schedule actions like alarms and reminders to notify you at a desired time in the future.</string>
+
+ <!-- Title of an application permission, listed so the user can choose whether they want to allow the application to do this. -->
<string name="permlab_persistentActivity">make app always run</string>
<!-- Description of an application permission, listed so the user can choose whether they want to allow the application to do this. -->
<string name="permdesc_persistentActivity" product="tablet">Allows the app to make parts of itself persistent in memory. This can limit memory available to other apps slowing down the tablet.</string>
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index ace7e4c..54be568 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -3826,6 +3826,7 @@
<java-symbol type="string" name="config_cameraShutterSound" />
<java-symbol type="integer" name="config_autoGroupAtCount" />
<java-symbol type="bool" name="config_dozeAlwaysOnDisplayAvailable" />
+ <java-symbol type="bool" name="config_dozePickupGestureEnabled" />
<java-symbol type="bool" name="config_dozeAlwaysOnEnabled" />
<java-symbol type="bool" name="config_dozeSupportsAodWallpaper" />
<java-symbol type="bool" name="config_displayBlanksAfterDoze" />
diff --git a/core/res/res/xml/bookmarks.xml b/core/res/res/xml/bookmarks.xml
index 454f456..3087aaa 100644
--- a/core/res/res/xml/bookmarks.xml
+++ b/core/res/res/xml/bookmarks.xml
@@ -19,23 +19,20 @@
Bookmarks for vendor apps should be added to a bookmarks resource overlay; not here.
Typical shortcuts (not necessarily defined here):
- 'a': Calculator
'b': Browser
'c': Contacts
'e': Email
'g': GMail
- 'l': Calendar
+ 'k': Calendar
'm': Maps
'p': Music
's': SMS
't': Talk
+ 'u': Calculator
'y': YouTube
-->
<bookmarks>
<bookmark
- category="android.intent.category.APP_CALCULATOR"
- shortcut="a" />
- <bookmark
category="android.intent.category.APP_BROWSER"
shortcut="b" />
<bookmark
@@ -46,7 +43,7 @@
shortcut="e" />
<bookmark
category="android.intent.category.APP_CALENDAR"
- shortcut="l" />
+ shortcut="k" />
<bookmark
category="android.intent.category.APP_MAPS"
shortcut="m" />
@@ -56,4 +53,7 @@
<bookmark
category="android.intent.category.APP_MESSAGING"
shortcut="s" />
+ <bookmark
+ category="android.intent.category.APP_CALCULATOR"
+ shortcut="u" />
</bookmarks>
diff --git a/core/tests/GameManagerTests/src/android/app/GameModeConfigurationTest.java b/core/tests/GameManagerTests/src/android/app/GameModeConfigurationTest.java
index 7462bcf..b3e74d3 100644
--- a/core/tests/GameManagerTests/src/android/app/GameModeConfigurationTest.java
+++ b/core/tests/GameManagerTests/src/android/app/GameModeConfigurationTest.java
@@ -77,10 +77,10 @@
}
@Test
- public void testToBuilder() {
+ public void testBuilderConstructor() {
GameModeConfiguration config = new GameModeConfiguration
.Builder().setFpsOverride(40).setScalingFactor(0.5f).build();
- GameModeConfiguration newConfig = config.toBuilder().build();
+ GameModeConfiguration newConfig = new GameModeConfiguration.Builder(config).build();
assertEquals(config, newConfig);
}
diff --git a/core/tests/GameManagerTests/src/android/app/GameModeInfoTest.java b/core/tests/GameManagerTests/src/android/app/GameModeInfoTest.java
index ecd9b6b8..5fa6084 100644
--- a/core/tests/GameManagerTests/src/android/app/GameModeInfoTest.java
+++ b/core/tests/GameManagerTests/src/android/app/GameModeInfoTest.java
@@ -43,7 +43,7 @@
int[] availableGameModes =
new int[]{GameManager.GAME_MODE_STANDARD, GameManager.GAME_MODE_PERFORMANCE,
GameManager.GAME_MODE_BATTERY, GameManager.GAME_MODE_CUSTOM};
- int[] optedInGameModes = new int[]{GameManager.GAME_MODE_PERFORMANCE};
+ int[] overriddenGameModes = new int[]{GameManager.GAME_MODE_PERFORMANCE};
GameModeConfiguration batteryConfig = new GameModeConfiguration
.Builder().setFpsOverride(40).setScalingFactor(0.5f).build();
GameModeConfiguration performanceConfig = new GameModeConfiguration
@@ -51,7 +51,7 @@
GameModeInfo gameModeInfo = new GameModeInfo.Builder()
.setActiveGameMode(activeGameMode)
.setAvailableGameModes(availableGameModes)
- .setOptedInGameModes(optedInGameModes)
+ .setOverriddenGameModes(overriddenGameModes)
.setDownscalingAllowed(true)
.setFpsOverrideAllowed(false)
.setGameModeConfiguration(GameManager.GAME_MODE_BATTERY, batteryConfig)
@@ -59,7 +59,7 @@
.build();
assertArrayEquals(availableGameModes, gameModeInfo.getAvailableGameModes());
- assertArrayEquals(optedInGameModes, gameModeInfo.getOptedInGameModes());
+ assertArrayEquals(overriddenGameModes, gameModeInfo.getOverriddenGameModes());
assertEquals(activeGameMode, gameModeInfo.getActiveGameMode());
assertTrue(gameModeInfo.isDownscalingAllowed());
assertFalse(gameModeInfo.isFpsOverrideAllowed());
@@ -75,8 +75,8 @@
assertEquals(gameModeInfo.getActiveGameMode(), newGameModeInfo.getActiveGameMode());
assertArrayEquals(gameModeInfo.getAvailableGameModes(),
newGameModeInfo.getAvailableGameModes());
- assertArrayEquals(gameModeInfo.getOptedInGameModes(),
- newGameModeInfo.getOptedInGameModes());
+ assertArrayEquals(gameModeInfo.getOverriddenGameModes(),
+ newGameModeInfo.getOverriddenGameModes());
assertTrue(newGameModeInfo.isDownscalingAllowed());
assertFalse(newGameModeInfo.isFpsOverrideAllowed());
assertEquals(performanceConfig,
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationRunner.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationRunner.java
index 322f854..dcc12ac 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationRunner.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationRunner.java
@@ -19,6 +19,7 @@
import static android.os.Process.THREAD_PRIORITY_DISPLAY;
import static android.view.RemoteAnimationTarget.MODE_CHANGING;
import static android.view.RemoteAnimationTarget.MODE_CLOSING;
+import static android.view.RemoteAnimationTarget.MODE_OPENING;
import static android.view.WindowManager.TRANSIT_OLD_ACTIVITY_CLOSE;
import static android.view.WindowManager.TRANSIT_OLD_ACTIVITY_OPEN;
import static android.view.WindowManager.TRANSIT_OLD_TASK_FRAGMENT_CHANGE;
@@ -253,6 +254,10 @@
@NonNull
private List<TaskFragmentAnimationAdapter> createChangeAnimationAdapters(
@NonNull RemoteAnimationTarget[] targets) {
+ if (shouldUseJumpCutForChangeAnimation(targets)) {
+ return new ArrayList<>();
+ }
+
final List<TaskFragmentAnimationAdapter> adapters = new ArrayList<>();
for (RemoteAnimationTarget target : targets) {
if (target.mode == MODE_CHANGING) {
@@ -282,4 +287,24 @@
}
return adapters;
}
+
+ /**
+ * Whether we should use jump cut for the change transition.
+ * This normally happens when opening a new secondary with the existing primary using a
+ * different split layout. This can be complicated, like from horizontal to vertical split with
+ * new split pairs.
+ * Uses a jump cut animation to simplify.
+ */
+ private boolean shouldUseJumpCutForChangeAnimation(@NonNull RemoteAnimationTarget[] targets) {
+ boolean hasOpeningWindow = false;
+ boolean hasClosingWindow = false;
+ for (RemoteAnimationTarget target : targets) {
+ if (target.hasAnimatingParent) {
+ continue;
+ }
+ hasOpeningWindow |= target.mode == MODE_OPENING;
+ hasClosingWindow |= target.mode == MODE_CLOSING;
+ }
+ return hasOpeningWindow && hasClosingWindow;
+ }
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunner.java
index c0a6456..164d2f1 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunner.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunner.java
@@ -17,6 +17,7 @@
package com.android.wm.shell.activityembedding;
import static android.view.WindowManager.TRANSIT_CHANGE;
+import static android.view.WindowManager.TRANSIT_CLOSE;
import static android.view.WindowManagerPolicyConstants.TYPE_LAYER_OFFSET;
import static android.window.TransitionInfo.FLAG_IS_BEHIND_STARTING_WINDOW;
@@ -112,23 +113,30 @@
@NonNull List<Consumer<SurfaceControl.Transaction>> postStartTransactionCallbacks) {
final List<ActivityEmbeddingAnimationAdapter> adapters = createAnimationAdapters(info,
startTransaction);
- addEdgeExtensionIfNeeded(startTransaction, finishTransaction, postStartTransactionCallbacks,
- adapters);
- addBackgroundColorIfNeeded(info, startTransaction, finishTransaction, adapters);
- long duration = 0;
- for (ActivityEmbeddingAnimationAdapter adapter : adapters) {
- duration = Math.max(duration, adapter.getDurationHint());
- }
final ValueAnimator animator = ValueAnimator.ofFloat(0, 1);
- animator.setDuration(duration);
- animator.addUpdateListener((anim) -> {
- // Update all adapters in the same transaction.
- final SurfaceControl.Transaction t = new SurfaceControl.Transaction();
+ long duration = 0;
+ if (adapters.isEmpty()) {
+ // Jump cut
+ // No need to modify the animator, but to update the startTransaction with the changes'
+ // ending states.
+ prepareForJumpCut(info, startTransaction);
+ } else {
+ addEdgeExtensionIfNeeded(startTransaction, finishTransaction,
+ postStartTransactionCallbacks, adapters);
+ addBackgroundColorIfNeeded(info, startTransaction, finishTransaction, adapters);
for (ActivityEmbeddingAnimationAdapter adapter : adapters) {
- adapter.onAnimationUpdate(t, animator.getCurrentPlayTime());
+ duration = Math.max(duration, adapter.getDurationHint());
}
- t.apply();
- });
+ animator.addUpdateListener((anim) -> {
+ // Update all adapters in the same transaction.
+ final SurfaceControl.Transaction t = new SurfaceControl.Transaction();
+ for (ActivityEmbeddingAnimationAdapter adapter : adapters) {
+ adapter.onAnimationUpdate(t, animator.getCurrentPlayTime());
+ }
+ t.apply();
+ });
+ }
+ animator.setDuration(duration);
animator.addListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animation) {}
@@ -292,6 +300,10 @@
@NonNull
private List<ActivityEmbeddingAnimationAdapter> createChangeAnimationAdapters(
@NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction startTransaction) {
+ if (shouldUseJumpCutForChangeTransition(info)) {
+ return new ArrayList<>();
+ }
+
final List<ActivityEmbeddingAnimationAdapter> adapters = new ArrayList<>();
final Set<TransitionInfo.Change> handledChanges = new ArraySet<>();
@@ -374,9 +386,11 @@
}
final Animation animation;
- if (change.getParent() != null
- && handledChanges.contains(info.getChange(change.getParent()))) {
- // No-op if it will be covered by the changing parent window.
+ if ((change.getParent() != null
+ && handledChanges.contains(info.getChange(change.getParent())))
+ || change.getMode() == TRANSIT_CHANGE) {
+ // No-op if it will be covered by the changing parent window, or it is a changing
+ // window without bounds change.
animation = ActivityEmbeddingAnimationSpec.createNoopAnimation(change);
} else if (Transitions.isClosingType(change.getMode())) {
animation = mAnimationSpec.createChangeBoundsCloseAnimation(change, parentBounds);
@@ -421,6 +435,74 @@
animationChange.getLeash(), cropBounds, Integer.MAX_VALUE);
}
+ /**
+ * Whether we should use jump cut for the change transition.
+ * This normally happens when opening a new secondary with the existing primary using a
+ * different split layout. This can be complicated, like from horizontal to vertical split with
+ * new split pairs.
+ * Uses a jump cut animation to simplify.
+ */
+ private boolean shouldUseJumpCutForChangeTransition(@NonNull TransitionInfo info) {
+ // There can be reparenting of changing Activity to new open TaskFragment, so we need to
+ // exclude both in the first iteration.
+ final List<TransitionInfo.Change> changingChanges = new ArrayList<>();
+ for (TransitionInfo.Change change : info.getChanges()) {
+ if (change.getMode() != TRANSIT_CHANGE
+ || change.getStartAbsBounds().equals(change.getEndAbsBounds())) {
+ continue;
+ }
+ changingChanges.add(change);
+ final WindowContainerToken parentToken = change.getParent();
+ if (parentToken != null) {
+ // When the parent window is also included in the transition as an opening window,
+ // we would like to animate the parent window instead.
+ final TransitionInfo.Change parentChange = info.getChange(parentToken);
+ if (parentChange != null && Transitions.isOpeningType(parentChange.getMode())) {
+ changingChanges.add(parentChange);
+ }
+ }
+ }
+ if (changingChanges.isEmpty()) {
+ // No changing target found.
+ return true;
+ }
+
+ // Check if the transition contains both opening and closing windows.
+ boolean hasOpeningWindow = false;
+ boolean hasClosingWindow = false;
+ for (TransitionInfo.Change change : info.getChanges()) {
+ if (changingChanges.contains(change)) {
+ continue;
+ }
+ if (change.getParent() != null
+ && changingChanges.contains(info.getChange(change.getParent()))) {
+ // No-op if it will be covered by the changing parent window.
+ continue;
+ }
+ hasOpeningWindow |= Transitions.isOpeningType(change.getMode());
+ hasClosingWindow |= Transitions.isClosingType(change.getMode());
+ }
+ return hasOpeningWindow && hasClosingWindow;
+ }
+
+ /** Updates the changes to end states in {@code startTransaction} for jump cut animation. */
+ private void prepareForJumpCut(@NonNull TransitionInfo info,
+ @NonNull SurfaceControl.Transaction startTransaction) {
+ for (TransitionInfo.Change change : info.getChanges()) {
+ final SurfaceControl leash = change.getLeash();
+ startTransaction.setPosition(leash,
+ change.getEndRelOffset().x, change.getEndRelOffset().y);
+ startTransaction.setWindowCrop(leash,
+ change.getEndAbsBounds().width(), change.getEndAbsBounds().height());
+ if (change.getMode() == TRANSIT_CLOSE) {
+ startTransaction.hide(leash);
+ } else {
+ startTransaction.show(leash);
+ startTransaction.setAlpha(leash, 1f);
+ }
+ }
+ }
+
/** To provide an {@link Animation} based on the transition infos. */
private interface AnimationProvider {
Animation get(@NonNull TransitionInfo info, @NonNull TransitionInfo.Change change,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitDecorManager.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitDecorManager.java
index 6e116b9..c836b95 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitDecorManager.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitDecorManager.java
@@ -51,8 +51,6 @@
import com.android.wm.shell.common.ScreenshotUtils;
import com.android.wm.shell.common.SurfaceUtils;
-import java.util.function.Consumer;
-
/**
* Handles split decor like showing resizing hint for a specific split.
*/
@@ -72,17 +70,18 @@
private SurfaceControl mIconLeash;
private SurfaceControl mBackgroundLeash;
private SurfaceControl mGapBackgroundLeash;
+ private SurfaceControl mScreenshot;
private boolean mShown;
private boolean mIsResizing;
private final Rect mBounds = new Rect();
- private final Rect mResizingBounds = new Rect();
private final Rect mTempRect = new Rect();
private ValueAnimator mFadeAnimator;
private int mIconSize;
private int mOffsetX;
private int mOffsetY;
+ private int mRunningAnimationCount = 0;
public SplitDecorManager(Configuration configuration, IconProvider iconProvider,
SurfaceSession surfaceSession) {
@@ -173,7 +172,6 @@
mIsResizing = true;
mBounds.set(newBounds);
}
- mResizingBounds.set(newBounds);
mOffsetX = offsetX;
mOffsetY = offsetY;
@@ -227,33 +225,41 @@
t.setVisibility(mBackgroundLeash, show);
t.setVisibility(mIconLeash, show);
} else {
- startFadeAnimation(show, null /* finishedConsumer */);
+ startFadeAnimation(show, false, null);
}
mShown = show;
}
}
/** Stops showing resizing hint. */
- public void onResized(SurfaceControl.Transaction t) {
- if (!mShown && mIsResizing) {
- mTempRect.set(mResizingBounds);
- mTempRect.offsetTo(-mOffsetX, -mOffsetY);
- final SurfaceControl screenshot = ScreenshotUtils.takeScreenshot(t,
- mHostLeash, mTempRect, Integer.MAX_VALUE - 1);
+ public void onResized(SurfaceControl.Transaction t, Runnable animFinishedCallback) {
+ if (mScreenshot != null) {
+ t.setPosition(mScreenshot, mOffsetX, mOffsetY);
final SurfaceControl.Transaction animT = new SurfaceControl.Transaction();
final ValueAnimator va = ValueAnimator.ofFloat(1, 0);
va.addUpdateListener(valueAnimator -> {
final float progress = (float) valueAnimator.getAnimatedValue();
- animT.setAlpha(screenshot, progress);
+ animT.setAlpha(mScreenshot, progress);
animT.apply();
});
va.addListener(new AnimatorListenerAdapter() {
@Override
+ public void onAnimationStart(Animator animation) {
+ mRunningAnimationCount++;
+ }
+
+ @Override
public void onAnimationEnd(@androidx.annotation.NonNull Animator animation) {
- animT.remove(screenshot);
+ mRunningAnimationCount--;
+ animT.remove(mScreenshot);
animT.apply();
animT.close();
+ mScreenshot = null;
+
+ if (mRunningAnimationCount == 0 && animFinishedCallback != null) {
+ animFinishedCallback.run();
+ }
}
});
va.start();
@@ -285,10 +291,34 @@
mFadeAnimator.cancel();
}
if (mShown) {
- fadeOutDecor(null /* finishedCallback */);
+ fadeOutDecor(animFinishedCallback);
} else {
// Decor surface is hidden so release it directly.
releaseDecor(t);
+ if (mRunningAnimationCount == 0 && animFinishedCallback != null) {
+ animFinishedCallback.run();
+ }
+ }
+ }
+
+ /** Screenshot host leash and attach on it if meet some conditions */
+ public void screenshotIfNeeded(SurfaceControl.Transaction t) {
+ if (!mShown && mIsResizing) {
+ mTempRect.set(mBounds);
+ mTempRect.offsetTo(0, 0);
+ mScreenshot = ScreenshotUtils.takeScreenshot(t, mHostLeash, mTempRect,
+ Integer.MAX_VALUE - 1);
+ }
+ }
+
+ /** Set screenshot and attach on host leash it if meet some conditions */
+ public void setScreenshotIfNeeded(SurfaceControl screenshot, SurfaceControl.Transaction t) {
+ if (screenshot == null || !screenshot.isValid()) return;
+
+ if (!mShown && mIsResizing) {
+ mScreenshot = screenshot;
+ t.reparent(screenshot, mHostLeash);
+ t.setLayer(screenshot, Integer.MAX_VALUE - 1);
}
}
@@ -296,18 +326,15 @@
* directly. */
public void fadeOutDecor(Runnable finishedCallback) {
if (mShown) {
- startFadeAnimation(false /* show */, transaction -> {
- releaseDecor(transaction);
- if (finishedCallback != null) finishedCallback.run();
- });
+ startFadeAnimation(false /* show */, true, finishedCallback);
mShown = false;
} else {
if (finishedCallback != null) finishedCallback.run();
}
}
- private void startFadeAnimation(boolean show,
- Consumer<SurfaceControl.Transaction> finishedConsumer) {
+ private void startFadeAnimation(boolean show, boolean releaseSurface,
+ Runnable finishedCallback) {
final SurfaceControl.Transaction animT = new SurfaceControl.Transaction();
mFadeAnimator = ValueAnimator.ofFloat(0f, 1f);
mFadeAnimator.setDuration(FADE_DURATION);
@@ -324,6 +351,7 @@
mFadeAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationStart(@NonNull Animator animation) {
+ mRunningAnimationCount++;
if (show) {
animT.show(mBackgroundLeash).show(mIconLeash);
}
@@ -335,6 +363,7 @@
@Override
public void onAnimationEnd(@NonNull Animator animation) {
+ mRunningAnimationCount--;
if (!show) {
if (mBackgroundLeash != null) {
animT.hide(mBackgroundLeash);
@@ -343,11 +372,15 @@
animT.hide(mIconLeash);
}
}
- if (finishedConsumer != null) {
- finishedConsumer.accept(animT);
+ if (releaseSurface) {
+ releaseDecor(animT);
}
animT.apply();
animT.close();
+
+ if (mRunningAnimationCount == 0 && finishedCallback != null) {
+ finishedCallback.run();
+ }
}
});
mFadeAnimator.start();
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java
index 400039b..9329d02 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java
@@ -529,10 +529,24 @@
@SplitPosition int splitPosition, float splitRatio, RemoteAnimationAdapter adapter,
InstanceId instanceId) {
Intent fillInIntent = null;
- if (launchSameComponentAdjacently(pendingIntent, splitPosition, taskId)
- && supportMultiInstancesSplit(pendingIntent.getIntent().getComponent())) {
- fillInIntent = new Intent();
- fillInIntent.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK);
+ if (launchSameComponentAdjacently(pendingIntent, splitPosition, taskId)) {
+ if (supportMultiInstancesSplit(pendingIntent.getIntent().getComponent())) {
+ fillInIntent = new Intent();
+ fillInIntent.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK);
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN, "Adding MULTIPLE_TASK");
+ } else {
+ try {
+ adapter.getRunner().onAnimationCancelled(false /* isKeyguardOccluded */);
+ ActivityTaskManager.getService().startActivityFromRecents(taskId, options2);
+ } catch (RemoteException e) {
+ Slog.e(TAG, "Error starting remote animation", e);
+ }
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN,
+ "Cancel entering split as not supporting multi-instances");
+ Toast.makeText(mContext, R.string.dock_multi_instances_not_supported_text,
+ Toast.LENGTH_SHORT).show();
+ return;
+ }
}
mStageCoordinator.startIntentAndTaskWithLegacyTransition(pendingIntent, fillInIntent,
options1, taskId, options2, splitPosition, splitRatio, adapter, instanceId);
@@ -542,10 +556,17 @@
int taskId, @Nullable Bundle options2, @SplitPosition int splitPosition,
float splitRatio, @Nullable RemoteTransition remoteTransition, InstanceId instanceId) {
Intent fillInIntent = null;
- if (launchSameComponentAdjacently(pendingIntent, splitPosition, taskId)
- && supportMultiInstancesSplit(pendingIntent.getIntent().getComponent())) {
- fillInIntent = new Intent();
- fillInIntent.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK);
+ if (launchSameComponentAdjacently(pendingIntent, splitPosition, taskId)) {
+ if (supportMultiInstancesSplit(pendingIntent.getIntent().getComponent())) {
+ fillInIntent = new Intent();
+ fillInIntent.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK);
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN, "Adding MULTIPLE_TASK");
+ } else {
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN,
+ "Cancel entering split as not supporting multi-instances");
+ Toast.makeText(mContext, R.string.dock_multi_instances_not_supported_text,
+ Toast.LENGTH_SHORT).show();
+ }
}
mStageCoordinator.startIntentAndTask(pendingIntent, fillInIntent, options1, taskId,
options2, splitPosition, splitRatio, remoteTransition, instanceId);
@@ -557,12 +578,26 @@
float splitRatio, RemoteAnimationAdapter adapter, InstanceId instanceId) {
Intent fillInIntent1 = null;
Intent fillInIntent2 = null;
- if (launchSameComponentAdjacently(pendingIntent1, pendingIntent2)
- && supportMultiInstancesSplit(pendingIntent1.getIntent().getComponent())) {
- fillInIntent1 = new Intent();
- fillInIntent1.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK);
- fillInIntent2 = new Intent();
- fillInIntent2.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK);
+ if (launchSameComponentAdjacently(pendingIntent1, pendingIntent2)) {
+ if (supportMultiInstancesSplit(pendingIntent1.getIntent().getComponent())) {
+ fillInIntent1 = new Intent();
+ fillInIntent1.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK);
+ fillInIntent2 = new Intent();
+ fillInIntent2.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK);
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN, "Adding MULTIPLE_TASK");
+ } else {
+ try {
+ adapter.getRunner().onAnimationCancelled(false /* isKeyguardOccluded */);
+ pendingIntent1.send();
+ } catch (RemoteException | PendingIntent.CanceledException e) {
+ Slog.e(TAG, "Error starting remote animation", e);
+ }
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN,
+ "Cancel entering split as not supporting multi-instances");
+ Toast.makeText(mContext, R.string.dock_multi_instances_not_supported_text,
+ Toast.LENGTH_SHORT).show();
+ return;
+ }
}
mStageCoordinator.startIntentsWithLegacyTransition(pendingIntent1, fillInIntent1, options1,
pendingIntent2, fillInIntent2, options2, splitPosition, splitRatio, adapter,
@@ -601,6 +636,8 @@
mStageCoordinator.switchSplitPosition("startIntent");
return;
} else {
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN,
+ "Cancel entering split as not supporting multi-instances");
Toast.makeText(mContext, R.string.dock_multi_instances_not_supported_text,
Toast.LENGTH_SHORT).show();
return;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTransitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTransitions.java
index 21a1310..1cf3a89 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTransitions.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTransitions.java
@@ -47,6 +47,7 @@
import com.android.internal.protolog.common.ProtoLog;
import com.android.wm.shell.common.TransactionPool;
+import com.android.wm.shell.common.split.SplitDecorManager;
import com.android.wm.shell.protolog.ShellProtoLogGroup;
import com.android.wm.shell.transition.OneShotRemoteHandler;
import com.android.wm.shell.transition.Transitions;
@@ -64,6 +65,7 @@
DismissTransition mPendingDismiss = null;
TransitSession mPendingEnter = null;
TransitSession mPendingRecent = null;
+ TransitSession mPendingResize = null;
private IBinder mAnimatingTransition = null;
OneShotRemoteHandler mPendingRemoteHandler = null;
@@ -177,6 +179,43 @@
onFinish(null /* wct */, null /* wctCB */);
}
+ void applyResizeTransition(@NonNull IBinder transition, @NonNull TransitionInfo info,
+ @NonNull SurfaceControl.Transaction startTransaction,
+ @NonNull SurfaceControl.Transaction finishTransaction,
+ @NonNull Transitions.TransitionFinishCallback finishCallback,
+ @NonNull WindowContainerToken mainRoot, @NonNull WindowContainerToken sideRoot,
+ @NonNull SplitDecorManager mainDecor, @NonNull SplitDecorManager sideDecor) {
+ mFinishCallback = finishCallback;
+ mAnimatingTransition = transition;
+ mFinishTransaction = finishTransaction;
+
+ for (int i = info.getChanges().size() - 1; i >= 0; --i) {
+ final TransitionInfo.Change change = info.getChanges().get(i);
+ if (mainRoot.equals(change.getContainer()) || sideRoot.equals(change.getContainer())) {
+ final SurfaceControl leash = change.getLeash();
+ startTransaction.setPosition(leash, change.getEndAbsBounds().left,
+ change.getEndAbsBounds().top);
+ startTransaction.setWindowCrop(leash, change.getEndAbsBounds().width(),
+ change.getEndAbsBounds().height());
+
+ SplitDecorManager decor = mainRoot.equals(change.getContainer())
+ ? mainDecor : sideDecor;
+ ValueAnimator va = new ValueAnimator();
+ mAnimations.add(va);
+ decor.setScreenshotIfNeeded(change.getSnapshot(), startTransaction);
+ decor.onResized(startTransaction, () -> {
+ mTransitions.getMainExecutor().execute(() -> {
+ mAnimations.remove(va);
+ onFinish(null /* wct */, null /* wctCB */);
+ });
+ });
+ }
+ }
+
+ startTransaction.apply();
+ onFinish(null /* wct */, null /* wctCB */);
+ }
+
boolean isPendingTransition(IBinder transition) {
return getPendingTransition(transition) != null;
}
@@ -193,6 +232,10 @@
return mPendingDismiss != null && mPendingDismiss.mTransition == transition;
}
+ boolean isPendingResize(IBinder transition) {
+ return mPendingResize != null && mPendingResize.mTransition == transition;
+ }
+
@Nullable
private TransitSession getPendingTransition(IBinder transition) {
if (isPendingEnter(transition)) {
@@ -201,11 +244,14 @@
return mPendingRecent;
} else if (isPendingDismiss(transition)) {
return mPendingDismiss;
+ } else if (isPendingResize(transition)) {
+ return mPendingResize;
}
return null;
}
+
/** Starts a transition to enter split with a remote transition animator. */
IBinder startEnterTransition(
@WindowManager.TransitionType int transitType,
@@ -258,6 +304,21 @@
exitReasonToString(reason), stageTypeToString(dismissTop));
}
+ IBinder startResizeTransition(WindowContainerTransaction wct,
+ Transitions.TransitionHandler handler,
+ @Nullable TransitionFinishedCallback finishCallback) {
+ IBinder transition = mTransitions.startTransition(TRANSIT_CHANGE, wct, handler);
+ setResizeTransition(transition, finishCallback);
+ return transition;
+ }
+
+ void setResizeTransition(@NonNull IBinder transition,
+ @Nullable TransitionFinishedCallback finishCallback) {
+ mPendingResize = new TransitSession(transition, null /* consumedCb */, finishCallback);
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " splitTransition "
+ + " deduced Resize split screen");
+ }
+
void setRecentTransition(@NonNull IBinder transition,
@Nullable RemoteTransition remoteTransition,
@Nullable TransitionFinishedCallback finishCallback) {
@@ -324,6 +385,9 @@
mPendingRecent.onConsumed(aborted);
mPendingRecent = null;
mPendingRemoteHandler = null;
+ } else if (isPendingResize(transition)) {
+ mPendingResize.onConsumed(aborted);
+ mPendingResize = null;
}
}
@@ -340,6 +404,9 @@
} else if (isPendingDismiss(mAnimatingTransition)) {
mPendingDismiss.onFinished(wct, mFinishTransaction);
mPendingDismiss = null;
+ } else if (isPendingResize(mAnimatingTransition)) {
+ mPendingResize.onFinished(wct, mFinishTransaction);
+ mPendingResize = null;
}
mPendingRemoteHandler = null;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitscreenEventLogger.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitscreenEventLogger.java
index 2dc4a04..1016e1b 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitscreenEventLogger.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitscreenEventLogger.java
@@ -23,6 +23,7 @@
import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__EXIT_REASON__APP_FINISHED;
import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__EXIT_REASON__DEVICE_FOLDED;
import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__EXIT_REASON__DRAG_DIVIDER;
+import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__EXIT_REASON__FULLSCREEN_SHORTCUT;
import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__EXIT_REASON__RETURN_HOME;
import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__EXIT_REASON__ROOT_TASK_VANISHED;
import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__EXIT_REASON__SCREEN_LOCKED;
@@ -38,6 +39,7 @@
import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_APP_FINISHED;
import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_DEVICE_FOLDED;
import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_DRAG_DIVIDER;
+import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_FULLSCREEN_SHORTCUT;
import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_RETURN_HOME;
import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_ROOT_TASK_VANISHED;
import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_SCREEN_LOCKED;
@@ -180,6 +182,8 @@
return SPLITSCREEN_UICHANGED__EXIT_REASON__SCREEN_LOCKED;
case EXIT_REASON_SCREEN_LOCKED_SHOW_ON_TOP:
return SPLITSCREEN_UICHANGED__EXIT_REASON__SCREEN_LOCKED_SHOW_ON_TOP;
+ case EXIT_REASON_FULLSCREEN_SHORTCUT:
+ return SPLITSCREEN_UICHANGED__EXIT_REASON__FULLSCREEN_SHORTCUT;
case EXIT_REASON_UNKNOWN:
// Fall through
default:
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
index aa0512b..da8dc87 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
@@ -456,8 +456,6 @@
void startIntentLegacy(PendingIntent intent, Intent fillInIntent, @SplitPosition int position,
@Nullable Bundle options) {
final boolean isEnteringSplit = !isSplitActive();
- final WindowContainerTransaction evictWct = new WindowContainerTransaction();
- prepareEvictChildTasks(position, evictWct);
LegacyTransitions.ILegacyTransition transition = new LegacyTransitions.ILegacyTransition() {
@Override
@@ -465,22 +463,21 @@
RemoteAnimationTarget[] wallpapers, RemoteAnimationTarget[] nonApps,
IRemoteAnimationFinishedCallback finishedCallback,
SurfaceControl.Transaction t) {
- if (isEnteringSplit) {
- boolean openingToSide = false;
- if (apps != null) {
- for (int i = 0; i < apps.length; ++i) {
- if (apps[i].mode == MODE_OPENING
- && mSideStage.containsTask(apps[i].taskId)) {
- openingToSide = true;
- break;
- }
+ boolean openingToSide = false;
+ if (apps != null) {
+ for (int i = 0; i < apps.length; ++i) {
+ if (apps[i].mode == MODE_OPENING
+ && mSideStage.containsTask(apps[i].taskId)) {
+ openingToSide = true;
+ break;
}
}
- if (!openingToSide) {
- mMainExecutor.execute(() -> exitSplitScreen(
- mSideStage.getChildCount() == 0 ? mMainStage : mSideStage,
- EXIT_REASON_UNKNOWN));
- }
+ }
+
+ if (isEnteringSplit && !openingToSide) {
+ mMainExecutor.execute(() -> exitSplitScreen(
+ mSideStage.getChildCount() == 0 ? mMainStage : mSideStage,
+ EXIT_REASON_UNKNOWN));
}
if (apps != null) {
@@ -500,7 +497,12 @@
}
}
- mSyncQueue.queue(evictWct);
+
+ if (!isEnteringSplit && openingToSide) {
+ final WindowContainerTransaction evictWct = new WindowContainerTransaction();
+ prepareEvictNonOpeningChildTasks(position, apps, evictWct);
+ mSyncQueue.queue(evictWct);
+ }
}
};
@@ -1667,15 +1669,29 @@
public void onLayoutSizeChanged(SplitLayout layout) {
// Reset this flag every time onLayoutSizeChanged.
mShowDecorImmediately = false;
+
+ if (!ENABLE_SHELL_TRANSITIONS) {
+ // Only need screenshot for legacy case because shell transition should screenshot
+ // itself during transition.
+ final SurfaceControl.Transaction startT = mTransactionPool.acquire();
+ mMainStage.screenshotIfNeeded(startT);
+ mSideStage.screenshotIfNeeded(startT);
+ mTransactionPool.release(startT);
+ }
+
final WindowContainerTransaction wct = new WindowContainerTransaction();
updateWindowBounds(layout, wct);
sendOnBoundsChanged();
- mSyncQueue.queue(wct);
- mSyncQueue.runInSync(t -> {
- updateSurfaceBounds(layout, t, false /* applyResizingOffset */);
- mMainStage.onResized(t);
- mSideStage.onResized(t);
- });
+ if (ENABLE_SHELL_TRANSITIONS) {
+ mSplitTransitions.startResizeTransition(wct, this, null /* callback */);
+ } else {
+ mSyncQueue.queue(wct);
+ mSyncQueue.runInSync(t -> {
+ updateSurfaceBounds(layout, t, false /* applyResizingOffset */);
+ mMainStage.onResized(t);
+ mSideStage.onResized(t);
+ });
+ }
mLogger.logResize(mSplitLayout.getDividerPositionAsFraction());
}
@@ -2029,6 +2045,12 @@
} else if (mSplitTransitions.isPendingDismiss(transition)) {
shouldAnimate = startPendingDismissAnimation(
mSplitTransitions.mPendingDismiss, info, startTransaction, finishTransaction);
+ } else if (mSplitTransitions.isPendingResize(transition)) {
+ mSplitTransitions.applyResizeTransition(transition, info, startTransaction,
+ finishTransaction, finishCallback, mMainStage.mRootTaskInfo.token,
+ mSideStage.mRootTaskInfo.token, mMainStage.getSplitDecorManager(),
+ mSideStage.getSplitDecorManager());
+ return true;
}
if (!shouldAnimate) return false;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskListener.java
index 358f712..8a52c87 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskListener.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskListener.java
@@ -292,7 +292,13 @@
void onResized(SurfaceControl.Transaction t) {
if (mSplitDecorManager != null) {
- mSplitDecorManager.onResized(t);
+ mSplitDecorManager.onResized(t, null);
+ }
+ }
+
+ void screenshotIfNeeded(SurfaceControl.Transaction t) {
+ if (mSplitDecorManager != null) {
+ mSplitDecorManager.screenshotIfNeeded(t);
}
}
@@ -304,6 +310,10 @@
}
}
+ SplitDecorManager getSplitDecorManager() {
+ return mSplitDecorManager;
+ }
+
void addTask(ActivityManager.RunningTaskInfo task, WindowContainerTransaction wct) {
// Clear overridden bounds and windowing mode to make sure the child task can inherit
// windowing mode and bounds from split root.
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/tv/OWNERS b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/tv/OWNERS
new file mode 100644
index 0000000..736d4cf
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/tv/OWNERS
@@ -0,0 +1,3 @@
+# WM shell sub-module TV pip owners
+galinap@google.com
+bronger@google.com
\ No newline at end of file
diff --git a/libs/hwui/jni/Mesh.cpp b/libs/hwui/jni/Mesh.cpp
index 109aac3..7c732d7 100644
--- a/libs/hwui/jni/Mesh.cpp
+++ b/libs/hwui/jni/Mesh.cpp
@@ -37,6 +37,16 @@
return indexBuffer;
}
+// TODO(b/260252882): undefine SK_LEGACY_MESH_MAKE and remove this.
+template <typename T>
+SkMesh get_mesh_from_result(T&& result) {
+#ifdef SK_LEGACY_MESH_MAKE
+ return result;
+#else
+ return result.mesh;
+#endif
+}
+
static jlong make(JNIEnv* env, jobject, jlong meshSpec, jint mode, jobject vertexBuffer,
jboolean isDirect, jint vertexCount, jint vertexOffset, jint left, jint top,
jint right, jint bottom) {
@@ -44,8 +54,8 @@
sk_sp<SkMesh::VertexBuffer> skVertexBuffer =
genVertexBuffer(env, vertexBuffer, vertexCount * skMeshSpec->stride(), isDirect);
auto skRect = SkRect::MakeLTRB(left, top, right, bottom);
- auto mesh = SkMesh::Make(skMeshSpec, SkMesh::Mode(mode), skVertexBuffer, vertexCount,
- vertexOffset, nullptr, skRect);
+ auto mesh = get_mesh_from_result(SkMesh::Make(skMeshSpec, SkMesh::Mode(mode), skVertexBuffer,
+ vertexCount, vertexOffset, nullptr, skRect));
auto meshPtr = std::make_unique<MeshWrapper>(MeshWrapper{mesh, MeshUniformBuilder(skMeshSpec)});
return reinterpret_cast<jlong>(meshPtr.release());
}
@@ -60,9 +70,9 @@
sk_sp<SkMesh::IndexBuffer> skIndexBuffer =
genIndexBuffer(env, indexBuffer, indexCount * gIndexByteSize, isIndexDirect);
auto skRect = SkRect::MakeLTRB(left, top, right, bottom);
- auto mesh = SkMesh::MakeIndexed(skMeshSpec, SkMesh::Mode(mode), skVertexBuffer, vertexCount,
- vertexOffset, skIndexBuffer, indexCount, indexOffset, nullptr,
- skRect);
+ auto mesh = get_mesh_from_result(SkMesh::MakeIndexed(
+ skMeshSpec, SkMesh::Mode(mode), skVertexBuffer, vertexCount, vertexOffset,
+ skIndexBuffer, indexCount, indexOffset, nullptr, skRect));
auto meshPtr = std::make_unique<MeshWrapper>(MeshWrapper{mesh, MeshUniformBuilder(skMeshSpec)});
return reinterpret_cast<jlong>(meshPtr.release());
}
@@ -71,14 +81,15 @@
auto wrapper = reinterpret_cast<MeshWrapper*>(meshWrapper);
auto mesh = wrapper->mesh;
if (indexed) {
- wrapper->mesh = SkMesh::MakeIndexed(
+ wrapper->mesh = get_mesh_from_result(SkMesh::MakeIndexed(
sk_ref_sp(mesh.spec()), mesh.mode(), sk_ref_sp(mesh.vertexBuffer()),
mesh.vertexCount(), mesh.vertexOffset(), sk_ref_sp(mesh.indexBuffer()),
- mesh.indexCount(), mesh.indexOffset(), wrapper->builder.fUniforms, mesh.bounds());
+ mesh.indexCount(), mesh.indexOffset(), wrapper->builder.fUniforms, mesh.bounds()));
} else {
- wrapper->mesh = SkMesh::Make(
- sk_ref_sp(mesh.spec()), mesh.mode(), sk_ref_sp(mesh.vertexBuffer()),
- mesh.vertexCount(), mesh.vertexOffset(), wrapper->builder.fUniforms, mesh.bounds());
+ wrapper->mesh = get_mesh_from_result(
+ SkMesh::Make(sk_ref_sp(mesh.spec()), mesh.mode(), sk_ref_sp(mesh.vertexBuffer()),
+ mesh.vertexCount(), mesh.vertexOffset(), wrapper->builder.fUniforms,
+ mesh.bounds()));
}
}
diff --git a/libs/hwui/tests/common/TestContext.cpp b/libs/hwui/tests/common/TestContext.cpp
index 0faa8f4..fd596d9 100644
--- a/libs/hwui/tests/common/TestContext.cpp
+++ b/libs/hwui/tests/common/TestContext.cpp
@@ -31,10 +31,8 @@
const std::vector<PhysicalDisplayId> ids = SurfaceComposerClient::getPhysicalDisplayIds();
LOG_ALWAYS_FATAL_IF(ids.empty(), "%s: No displays", __FUNCTION__);
- const sp<IBinder> token = SurfaceComposerClient::getPhysicalDisplayToken(ids.front());
- LOG_ALWAYS_FATAL_IF(!token, "%s: No internal display", __FUNCTION__);
-
- const status_t status = SurfaceComposerClient::getStaticDisplayInfo(token, &info);
+ const status_t status =
+ SurfaceComposerClient::getStaticDisplayInfo(ids.front().value, &info);
LOG_ALWAYS_FATAL_IF(status, "%s: Failed to get display info", __FUNCTION__);
#endif
return info;
diff --git a/location/java/android/location/altitude/AltitudeConverter.java b/location/java/android/location/altitude/AltitudeConverter.java
index 506128e..d46b4d2 100644
--- a/location/java/android/location/altitude/AltitudeConverter.java
+++ b/location/java/android/location/altitude/AltitudeConverter.java
@@ -151,20 +151,17 @@
* altitude accuracy if the {@code location} has a finite and non-negative vertical accuracy;
* otherwise, does not add a corresponding accuracy.
*
- * <p>Must be called off the main thread as data may be loaded from raw assets. Throws an
- * {@link IOException} if an I/O error occurs when loading data.
+ * <p>Must be called off the main thread as data may be loaded from raw assets.
*
- * <p>Throws an {@link IllegalArgumentException} if the {@code location} has an invalid
- * latitude, longitude, or altitude above WGS84. Specifically:
- *
- * <ul>
- * <li>The latitude must be between -90 and 90, both inclusive.
- * <li>The longitude must be between -180 and 180, both inclusive.
- * <li>The altitude above WGS84 must be finite.
- * </ul>
+ * @throws IOException if an I/O error occurs when loading data from raw assets.
+ * @throws IllegalArgumentException if the {@code location} has an invalid latitude, longitude,
+ * or altitude above WGS84. Specifically, the latitude must be
+ * between -90 and 90 (both inclusive), the longitude must be
+ * between -180 and 180 (both inclusive), and the altitude
+ * above WGS84 must be finite.
*/
@WorkerThread
- public void addMslAltitude(@NonNull Context context, @NonNull Location location)
+ public void addMslAltitudeToLocation(@NonNull Context context, @NonNull Location location)
throws IOException {
validate(location);
MapParamsProto params = GeoidHeightMap.getParams(context);
diff --git a/location/java/com/android/internal/location/altitude/GeoidHeightMap.java b/location/java/com/android/internal/location/altitude/GeoidHeightMap.java
index e113ab4..6430eb4 100644
--- a/location/java/com/android/internal/location/altitude/GeoidHeightMap.java
+++ b/location/java/com/android/internal/location/altitude/GeoidHeightMap.java
@@ -293,8 +293,6 @@
try (InputStream is = context.getApplicationContext().getAssets().open(
"geoid_height_map/tile-" + diskTokens[i] + ".pb")) {
tile = S2TileProto.parseFrom(is.readAllBytes());
- } catch (IOException e) {
- throw new RuntimeException(e);
}
mergeFromDiskTile(params, tile, cacheKeys, diskTokens, i, loadedTiles);
}
diff --git a/media/java/android/media/MediaRouter2Manager.java b/media/java/android/media/MediaRouter2Manager.java
index 7786f61..aea6bcb 100644
--- a/media/java/android/media/MediaRouter2Manager.java
+++ b/media/java/android/media/MediaRouter2Manager.java
@@ -76,13 +76,7 @@
private static MediaRouter2Manager sInstance;
private final MediaSessionManager mMediaSessionManager;
-
- final String mPackageName;
-
- private final Context mContext;
-
private final Client mClient;
-
private final IMediaRouterService mMediaRouterService;
private final AtomicInteger mScanRequestCount = new AtomicInteger(/* initialValue= */ 0);
final Handler mHandler;
@@ -120,16 +114,14 @@
}
private MediaRouter2Manager(Context context) {
- mContext = context.getApplicationContext();
mMediaRouterService = IMediaRouterService.Stub.asInterface(
ServiceManager.getService(Context.MEDIA_ROUTER_SERVICE));
mMediaSessionManager = (MediaSessionManager) context
.getSystemService(Context.MEDIA_SESSION_SERVICE);
- mPackageName = mContext.getPackageName();
mHandler = new Handler(context.getMainLooper());
mClient = new Client();
try {
- mMediaRouterService.registerManager(mClient, mPackageName);
+ mMediaRouterService.registerManager(mClient, context.getPackageName());
} catch (RemoteException ex) {
throw ex.rethrowFromSystemServer();
}
diff --git a/media/java/android/media/RingtoneManager.java b/media/java/android/media/RingtoneManager.java
index f15f443..1e270b1 100644
--- a/media/java/android/media/RingtoneManager.java
+++ b/media/java/android/media/RingtoneManager.java
@@ -826,6 +826,19 @@
if(!isInternalRingtoneUri(ringtoneUri)) {
ringtoneUri = ContentProvider.maybeAddUserId(ringtoneUri, context.getUserId());
}
+
+ final String mimeType = resolver.getType(ringtoneUri);
+ if (mimeType == null) {
+ Log.e(TAG, "setActualDefaultRingtoneUri for URI:" + ringtoneUri
+ + " ignored: failure to find mimeType (no access from this context?)");
+ return;
+ }
+ if (!(mimeType.startsWith("audio/") || mimeType.equals("application/ogg"))) {
+ Log.e(TAG, "setActualDefaultRingtoneUri for URI:" + ringtoneUri
+ + " ignored: associated mimeType:" + mimeType + " is not an audio type");
+ return;
+ }
+
Settings.System.putStringForUser(resolver, setting,
ringtoneUri != null ? ringtoneUri.toString() : null, context.getUserId());
diff --git a/media/tests/AudioPolicyTest/AndroidManifest.xml b/media/tests/AudioPolicyTest/AndroidManifest.xml
index f696735..5c911b1 100644
--- a/media/tests/AudioPolicyTest/AndroidManifest.xml
+++ b/media/tests/AudioPolicyTest/AndroidManifest.xml
@@ -24,13 +24,22 @@
<application>
<uses-library android:name="android.test.runner" />
- <activity android:label="@string/app_name" android:name="AudioPolicyTestActivity"
+ <activity android:label="@string/app_name" android:name="AudioVolumeTestActivity"
android:screenOrientation="landscape" android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
+ <activity android:label="@string/app_name" android:name="AudioPolicyDeathTestActivity"
+ android:screenOrientation="landscape"
+ android:process=":AudioPolicyDeathTestActivityProcess"
+ android:exported="true">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+ <category android:name="android.intent.category.LAUNCHER"/>
+ </intent-filter>
+ </activity>
</application>
<instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
diff --git a/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/AudioPolicyDeathTest.java b/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/AudioPolicyDeathTest.java
new file mode 100644
index 0000000..841804b
--- /dev/null
+++ b/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/AudioPolicyDeathTest.java
@@ -0,0 +1,132 @@
+/*
+ * 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.audiopolicytest;
+
+import static androidx.test.core.app.ApplicationProvider.getApplicationContext;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.PackageManager;
+import android.media.AudioAttributes;
+import android.media.AudioFormat;
+import android.media.AudioManager;
+import android.media.AudioTrack;
+import android.platform.test.annotations.Presubmit;
+import android.util.Log;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@Presubmit
+@RunWith(AndroidJUnit4.class)
+public class AudioPolicyDeathTest {
+ private static final String TAG = "AudioPolicyDeathTest";
+
+ private static final int SAMPLE_RATE = 48000;
+ private static final int PLAYBACK_TIME_MS = 2000;
+
+ private static final IntentFilter AUDIO_NOISY_INTENT_FILTER =
+ new IntentFilter(AudioManager.ACTION_AUDIO_BECOMING_NOISY);
+
+ private class MyBroadcastReceiver extends BroadcastReceiver {
+ private boolean mReceived = false;
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (AudioManager.ACTION_AUDIO_BECOMING_NOISY.equals(intent.getAction())) {
+ synchronized (this) {
+ mReceived = true;
+ notify();
+ }
+ }
+ }
+
+ public synchronized boolean received() {
+ return mReceived;
+ }
+ }
+ private final MyBroadcastReceiver mReceiver = new MyBroadcastReceiver();
+
+ private Context mContext;
+
+ @Before
+ public void setUp() {
+ mContext = getApplicationContext();
+ assertEquals(PackageManager.PERMISSION_GRANTED,
+ mContext.checkSelfPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING));
+ }
+
+ //-----------------------------------------------------------------
+ // Tests that an AUDIO_BECOMING_NOISY intent is broadcast when an app having registered
+ // a dynamic audio policy that intercepts an active media playback dies
+ //-----------------------------------------------------------------
+ @Test
+ public void testPolicyClientDeathSendBecomingNoisyIntent() {
+ mContext.registerReceiver(mReceiver, AUDIO_NOISY_INTENT_FILTER);
+
+ // Launch process registering a dynamic auido policy and dying after PLAYBACK_TIME_MS/2 ms
+ Intent intent = new Intent(mContext, AudioPolicyDeathTestActivity.class);
+ intent.putExtra("captureDurationMs", PLAYBACK_TIME_MS / 2);
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
+ mContext.startActivity(intent);
+
+ AudioTrack track = createAudioTrack();
+ track.play();
+ synchronized (mReceiver) {
+ long startTimeMs = System.currentTimeMillis();
+ long elapsedTimeMs = 0;
+ while (elapsedTimeMs < PLAYBACK_TIME_MS && !mReceiver.received()) {
+ try {
+ mReceiver.wait(PLAYBACK_TIME_MS - elapsedTimeMs);
+ } catch (InterruptedException e) {
+ Log.w(TAG, "wait interrupted");
+ }
+ elapsedTimeMs = System.currentTimeMillis() - startTimeMs;
+ }
+ }
+
+ track.stop();
+ track.release();
+
+ assertTrue(mReceiver.received());
+ }
+
+ private AudioTrack createAudioTrack() {
+ AudioFormat format = new AudioFormat.Builder()
+ .setChannelMask(AudioFormat.CHANNEL_OUT_STEREO)
+ .setEncoding(AudioFormat.ENCODING_PCM_16BIT)
+ .setSampleRate(SAMPLE_RATE)
+ .build();
+
+ short[] data = new short[PLAYBACK_TIME_MS * SAMPLE_RATE * format.getChannelCount() / 1000];
+ AudioAttributes attributes =
+ new AudioAttributes.Builder().setUsage(AudioAttributes.USAGE_MEDIA).build();
+
+ AudioTrack track = new AudioTrack(attributes, format, data.length,
+ AudioTrack.MODE_STATIC, AudioManager.AUDIO_SESSION_ID_GENERATE);
+ track.write(data, 0, data.length);
+
+ return track;
+ }
+}
diff --git a/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/AudioPolicyDeathTestActivity.java b/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/AudioPolicyDeathTestActivity.java
new file mode 100644
index 0000000..957e719
--- /dev/null
+++ b/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/AudioPolicyDeathTestActivity.java
@@ -0,0 +1,132 @@
+/*
+ * 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.audiopolicytest;
+
+import android.app.Activity;
+import android.media.AudioAttributes;
+import android.media.AudioFormat;
+import android.media.AudioManager;
+import android.media.AudioRecord;
+import android.media.audiopolicy.AudioMix;
+import android.media.audiopolicy.AudioMixingRule;
+import android.media.audiopolicy.AudioPolicy;
+import android.os.Bundle;
+import android.os.Looper;
+import android.util.Log;
+
+// This activity will register a dynamic audio policy to intercept media playback and launch
+// a thread that will capture audio from the policy mix and crash after the time indicated by
+// intent extra "captureDurationMs" has elapsed
+public class AudioPolicyDeathTestActivity extends Activity {
+ private static final String TAG = "AudioPolicyDeathTestActivity";
+
+ private static final int SAMPLE_RATE = 48000;
+ private static final int RECORD_TIME_MS = 1000;
+
+ private AudioManager mAudioManager = null;
+ private AudioPolicy mAudioPolicy = null;
+
+ public AudioPolicyDeathTestActivity() {
+ }
+
+ @Override
+ public void onCreate(Bundle icicle) {
+ super.onCreate(icicle);
+
+ mAudioManager = getApplicationContext().getSystemService(AudioManager.class);
+
+ AudioAttributes attributes = new AudioAttributes.Builder()
+ .setUsage(AudioAttributes.USAGE_MEDIA).build();
+ AudioMixingRule.Builder audioMixingRuleBuilder = new AudioMixingRule.Builder()
+ .addRule(attributes, AudioMixingRule.RULE_MATCH_ATTRIBUTE_USAGE);
+
+ AudioFormat audioFormat = new AudioFormat.Builder()
+ .setEncoding(AudioFormat.ENCODING_PCM_16BIT)
+ .setChannelMask(AudioFormat.CHANNEL_OUT_STEREO)
+ .setSampleRate(SAMPLE_RATE)
+ .build();
+
+ AudioMix audioMix = new AudioMix.Builder(audioMixingRuleBuilder.build())
+ .setFormat(audioFormat)
+ .setRouteFlags(AudioMix.ROUTE_FLAG_LOOP_BACK)
+ .build();
+
+ AudioPolicy.Builder audioPolicyBuilder = new AudioPolicy.Builder(getApplicationContext());
+ audioPolicyBuilder.addMix(audioMix)
+ .setLooper(Looper.getMainLooper());
+ mAudioPolicy = audioPolicyBuilder.build();
+
+ int result = mAudioManager.registerAudioPolicy(mAudioPolicy);
+ if (result != AudioManager.SUCCESS) {
+ Log.w(TAG, "registerAudioPolicy failed, status: " + result);
+ return;
+ }
+ AudioRecord audioRecord = mAudioPolicy.createAudioRecordSink(audioMix);
+ if (audioRecord == null) {
+ Log.w(TAG, "AudioRecord creation failed");
+ return;
+ }
+
+ int captureDurationMs = getIntent().getIntExtra("captureDurationMs", RECORD_TIME_MS);
+ AudioCapturingThread thread = new AudioCapturingThread(audioRecord, captureDurationMs);
+ thread.start();
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ if (mAudioManager != null && mAudioPolicy != null) {
+ mAudioManager.unregisterAudioPolicy(mAudioPolicy);
+ }
+ }
+
+ // A thread that captures audio from the supplied AudioRecord and crashes after the supplied
+ // duration has elapsed
+ private static class AudioCapturingThread extends Thread {
+ private final AudioRecord mAudioRecord;
+ private final int mDurationMs;
+
+ AudioCapturingThread(AudioRecord record, int durationMs) {
+ super();
+ mAudioRecord = record;
+ mDurationMs = durationMs;
+ }
+
+ @Override
+ @SuppressWarnings("ConstantOverflow")
+ public void run() {
+ int samplesLeft = mDurationMs * SAMPLE_RATE * mAudioRecord.getChannelCount() / 1000;
+ short[] readBuffer = new short[samplesLeft / 10];
+ mAudioRecord.startRecording();
+ long startTimeMs = System.currentTimeMillis();
+ long elapsedTimeMs = 0;
+ do {
+ int read = readBuffer.length < samplesLeft ? readBuffer.length : samplesLeft;
+ read = mAudioRecord.read(readBuffer, 0, read);
+ elapsedTimeMs = System.currentTimeMillis() - startTimeMs;
+ if (read < 0) {
+ Log.w(TAG, "read error: " + read);
+ break;
+ }
+ samplesLeft -= read;
+ } while (elapsedTimeMs < mDurationMs && samplesLeft > 0);
+
+ // force process to crash
+ int i = 1 / 0;
+ }
+ }
+}
diff --git a/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/AudioPolicyTestActivity.java b/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/AudioVolumeTestActivity.java
similarity index 91%
rename from media/tests/AudioPolicyTest/src/com/android/audiopolicytest/AudioPolicyTestActivity.java
rename to media/tests/AudioPolicyTest/src/com/android/audiopolicytest/AudioVolumeTestActivity.java
index e31c01a..8f61815 100644
--- a/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/AudioPolicyTestActivity.java
+++ b/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/AudioVolumeTestActivity.java
@@ -19,9 +19,9 @@
import android.app.Activity;
import android.os.Bundle;
-public class AudioPolicyTestActivity extends Activity {
+public class AudioVolumeTestActivity extends Activity {
- public AudioPolicyTestActivity() {
+ public AudioVolumeTestActivity() {
}
/** Called when the activity is first created. */
diff --git a/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/AudioVolumesTestRule.java b/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/AudioVolumesTestRule.java
index fc3b198..c6ec7a6 100644
--- a/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/AudioVolumesTestRule.java
+++ b/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/AudioVolumesTestRule.java
@@ -100,7 +100,7 @@
@Before
public void setUp() throws Exception {
- ActivityScenario.launch(AudioPolicyTestActivity.class);
+ ActivityScenario.launch(AudioVolumeTestActivity.class);
mContext = getApplicationContext();
mAudioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt b/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt
index 1db1d1c..f801ba6 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt
@@ -40,12 +40,9 @@
import android.os.Bundle
import android.os.ResultReceiver
import android.service.credentials.CredentialProviderService
-import com.android.credentialmanager.createflow.ActiveEntry
import com.android.credentialmanager.createflow.CreateCredentialUiState
-import com.android.credentialmanager.createflow.CreateScreenState
import com.android.credentialmanager.createflow.EnabledProviderInfo
import com.android.credentialmanager.createflow.RemoteInfo
-import com.android.credentialmanager.createflow.RequestDisplayInfo
import com.android.credentialmanager.getflow.GetCredentialUiState
import com.android.credentialmanager.getflow.GetScreenState
import com.android.credentialmanager.jetpack.developer.CreatePasswordRequest.Companion.toBundle
@@ -146,20 +143,30 @@
providerDisabledList, context)
var defaultProvider: EnabledProviderInfo? = null
var remoteEntry: RemoteInfo? = null
+ var createOptionSize = 0
+ var lastSeenProviderWithNonEmptyCreateOptions: EnabledProviderInfo? = null
providerEnabledList.forEach{providerInfo -> providerInfo.createOptions =
providerInfo.createOptions.sortedWith(compareBy { it.lastUsedTimeMillis }).reversed()
if (providerInfo.isDefault) {defaultProvider = providerInfo}
if (providerInfo.remoteEntry != null) {
remoteEntry = providerInfo.remoteEntry!!
}
+ if (providerInfo.createOptions.isNotEmpty()) {
+ createOptionSize += providerInfo.createOptions.size
+ lastSeenProviderWithNonEmptyCreateOptions = providerInfo
+ }
}
return CreateCredentialUiState(
enabledProviders = providerEnabledList,
disabledProviders = providerDisabledList,
- toCreateScreenState(requestDisplayInfo, defaultProvider, remoteEntry),
+ CreateFlowUtils.toCreateScreenState(
+ createOptionSize, false,
+ requestDisplayInfo, defaultProvider, remoteEntry),
requestDisplayInfo,
false,
- toActiveEntry(defaultProvider, remoteEntry),
+ CreateFlowUtils.toActiveEntry(
+ /*defaultProvider=*/defaultProvider, createOptionSize,
+ lastSeenProviderWithNonEmptyCreateOptions, remoteEntry),
)
}
@@ -194,6 +201,7 @@
.setRemoteEntry(
newRemoteEntry("key2", "subkey-1")
)
+ .setIsDefaultProvider(true)
.build(),
CreateCredentialProviderData
.Builder("com.dashlane")
@@ -510,45 +518,11 @@
GetCredentialRequest.Builder()
.addGetCredentialOption(
GetCredentialOption(
- TYPE_PUBLIC_KEY_CREDENTIAL, Bundle(), /*requireSystemProvider=*/ false)
+ TYPE_PUBLIC_KEY_CREDENTIAL, Bundle(), Bundle(), /*requireSystemProvider=*/ false)
)
.build(),
/*isFirstUsage=*/false,
"tribank.us"
)
}
-
- private fun toCreateScreenState(
- requestDisplayInfo: RequestDisplayInfo,
- defaultProvider: EnabledProviderInfo?,
- remoteEntry: RemoteInfo?,
- ): CreateScreenState {
- return if (
- defaultProvider != null && defaultProvider.createOptions.isEmpty() && remoteEntry != null
- ){
- CreateScreenState.EXTERNAL_ONLY_SELECTION
- } else if (defaultProvider == null || defaultProvider.createOptions.isEmpty()) {
- if (requestDisplayInfo.type == TYPE_PUBLIC_KEY_CREDENTIAL) {
- CreateScreenState.PASSKEY_INTRO
- } else {
- CreateScreenState.PROVIDER_SELECTION
- }
- } else {
- CreateScreenState.CREATION_OPTION_SELECTION
- }
- }
-
- private fun toActiveEntry(
- defaultProvider: EnabledProviderInfo?,
- remoteEntry: RemoteInfo?,
- ): ActiveEntry? {
- return if (
- defaultProvider != null && defaultProvider.createOptions.isNotEmpty()
- ) {
- ActiveEntry(defaultProvider, defaultProvider.createOptions.first())
- } else if (
- defaultProvider != null && defaultProvider.createOptions.isEmpty() && remoteEntry != null) {
- ActiveEntry(defaultProvider, remoteEntry)
- } else null
- }
}
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt b/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt
index 357c55d..0d7e819 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt
@@ -28,6 +28,9 @@
import com.android.credentialmanager.createflow.CreateOptionInfo
import com.android.credentialmanager.createflow.RemoteInfo
import com.android.credentialmanager.createflow.RequestDisplayInfo
+import com.android.credentialmanager.createflow.EnabledProviderInfo
+import com.android.credentialmanager.createflow.CreateScreenState
+import com.android.credentialmanager.createflow.ActiveEntry
import com.android.credentialmanager.getflow.ActionEntryInfo
import com.android.credentialmanager.getflow.AuthenticationEntryInfo
import com.android.credentialmanager.getflow.CredentialEntryInfo
@@ -36,6 +39,7 @@
import com.android.credentialmanager.jetpack.developer.CreateCredentialRequest
import com.android.credentialmanager.jetpack.developer.CreatePasswordRequest
import com.android.credentialmanager.jetpack.developer.CreatePublicKeyCredentialRequest
+import com.android.credentialmanager.jetpack.developer.PublicKeyCredential.Companion.TYPE_PUBLIC_KEY_CREDENTIAL
import com.android.credentialmanager.jetpack.provider.ActionUi
import com.android.credentialmanager.jetpack.provider.CredentialEntryUi
import com.android.credentialmanager.jetpack.provider.SaveEntryUi
@@ -243,7 +247,8 @@
createCredentialRequestJetpack.password,
createCredentialRequestJetpack.type,
requestInfo.appPackageName,
- context.getDrawable(R.drawable.ic_password)!!
+ context.getDrawable(R.drawable.ic_password)!!,
+ requestInfo.isFirstUsage
)
}
is CreatePublicKeyCredentialRequest -> {
@@ -261,7 +266,8 @@
displayName,
createCredentialRequestJetpack.type,
requestInfo.appPackageName,
- context.getDrawable(R.drawable.ic_passkey)!!)
+ context.getDrawable(R.drawable.ic_passkey)!!,
+ requestInfo.isFirstUsage)
}
// TODO: correctly parsing for other sign-ins
else -> {
@@ -270,11 +276,58 @@
"Elisa Beckett",
"other-sign-ins",
requestInfo.appPackageName,
- context.getDrawable(R.drawable.ic_other_sign_in)!!)
+ context.getDrawable(R.drawable.ic_other_sign_in)!!,
+ requestInfo.isFirstUsage)
}
}
}
+ fun toCreateScreenState(
+ createOptionSize: Int,
+ isOnPasskeyIntroStateAlready: Boolean,
+ requestDisplayInfo: RequestDisplayInfo,
+ defaultProvider: EnabledProviderInfo?,
+ remoteEntry: RemoteInfo?,
+ ): CreateScreenState {
+ return if (requestDisplayInfo.isFirstUsage && requestDisplayInfo
+ .type == TYPE_PUBLIC_KEY_CREDENTIAL && !isOnPasskeyIntroStateAlready) {
+ CreateScreenState.PASSKEY_INTRO
+ } else if (
+ (defaultProvider == null || defaultProvider.createOptions.isEmpty()
+ ) && createOptionSize > 1) {
+ CreateScreenState.PROVIDER_SELECTION
+ } else if (
+ ((defaultProvider == null || defaultProvider.createOptions.isEmpty()
+ ) && createOptionSize == 1) || (
+ defaultProvider != null && defaultProvider.createOptions.isNotEmpty())) {
+ CreateScreenState.CREATION_OPTION_SELECTION
+ } else if (createOptionSize == 0 && remoteEntry != null) {
+ CreateScreenState.EXTERNAL_ONLY_SELECTION
+ } else {
+ // TODO: properly handle error and gracefully finish itself
+ throw java.lang.IllegalStateException("Empty provider list.")
+ }
+ }
+
+ fun toActiveEntry(
+ defaultProvider: EnabledProviderInfo?,
+ createOptionSize: Int,
+ lastSeenProviderWithNonEmptyCreateOptions: EnabledProviderInfo?,
+ remoteEntry: RemoteInfo?,
+ ): ActiveEntry? {
+ return if (
+ defaultProvider != null && defaultProvider.createOptions.isEmpty() && remoteEntry != null) {
+ ActiveEntry(defaultProvider, remoteEntry)
+ } else if (
+ defaultProvider != null && defaultProvider.createOptions.isNotEmpty()
+ ) {
+ ActiveEntry(defaultProvider, defaultProvider.createOptions.first())
+ } else if (createOptionSize == 1) {
+ ActiveEntry(lastSeenProviderWithNonEmptyCreateOptions!!,
+ lastSeenProviderWithNonEmptyCreateOptions.createOptions.first())
+ } else null
+ }
+
private fun toCreationOptionInfoList(
providerId: String,
creationEntries: List<Entry>,
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialComponents.kt b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialComponents.kt
index 5d22bfd..b417b38 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialComponents.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialComponents.kt
@@ -118,7 +118,7 @@
onCancel = viewModel::onCancel,
)
}
- } else if (uiState.hidden && uiState.selectedEntry != null) {
+ } else if (uiState.selectedEntry != null && !uiState.providerActivityPending) {
viewModel.launchProviderUi(providerActivityLauncher)
}
},
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialViewModel.kt b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialViewModel.kt
index 6f74998..518aaee 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialViewModel.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialViewModel.kt
@@ -27,6 +27,7 @@
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
+import com.android.credentialmanager.CreateFlowUtils
import com.android.credentialmanager.CredentialManagerRepo
import com.android.credentialmanager.common.DialogResult
import com.android.credentialmanager.common.ProviderActivityResult
@@ -41,6 +42,7 @@
val activeEntry: ActiveEntry? = null,
val selectedEntry: EntryInfo? = null,
val hidden: Boolean = false,
+ val providerActivityPending: Boolean = false,
)
class CreateCredentialViewModel(
@@ -60,24 +62,26 @@
fun onConfirmIntro() {
var createOptionSize = 0
+ var lastSeenProviderWithNonEmptyCreateOptions: EnabledProviderInfo? = null
+ var remoteEntry: RemoteInfo? = null
uiState.enabledProviders.forEach {
- enabledProvider -> createOptionSize += enabledProvider.createOptions.size}
- uiState = if (createOptionSize > 1) {
- uiState.copy(
- currentScreenState = CreateScreenState.PROVIDER_SELECTION,
- showActiveEntryOnly = true
- )
- } else if (createOptionSize == 1){
- uiState.copy(
- currentScreenState = CreateScreenState.CREATION_OPTION_SELECTION,
- showActiveEntryOnly = false,
- activeEntry = ActiveEntry(uiState.enabledProviders.first(),
- uiState.enabledProviders.first().createOptions.first()
- )
- )
- } else {
- throw java.lang.IllegalStateException("Empty provider list.")
+ enabledProvider ->
+ if (enabledProvider.createOptions.isNotEmpty()) {
+ createOptionSize += enabledProvider.createOptions.size
+ lastSeenProviderWithNonEmptyCreateOptions = enabledProvider
+ }
+ if (enabledProvider.remoteEntry != null) {
+ remoteEntry = enabledProvider.remoteEntry!!
+ }
}
+ uiState = uiState.copy(
+ currentScreenState = CreateFlowUtils.toCreateScreenState(
+ createOptionSize, true,
+ uiState.requestDisplayInfo, null, remoteEntry),
+ showActiveEntryOnly = createOptionSize > 1,
+ activeEntry = CreateFlowUtils.toActiveEntry(
+ null, createOptionSize, lastSeenProviderWithNonEmptyCreateOptions, remoteEntry),
+ )
}
fun getProviderInfoByName(providerName: String): EnabledProviderInfo {
@@ -159,6 +163,9 @@
) {
val entry = uiState.selectedEntry
if (entry != null && entry.pendingIntent != null) {
+ uiState = uiState.copy(
+ providerActivityPending = true,
+ )
val intentSenderRequest = IntentSenderRequest.Builder(entry.pendingIntent)
.setFillInIntent(entry.fillInIntent).build()
launcher.launch(intentSenderRequest)
@@ -189,6 +196,7 @@
uiState = uiState.copy(
selectedEntry = null,
hidden = false,
+ providerActivityPending = false,
)
} else {
if (entry != null) {
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateModel.kt b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateModel.kt
index 9ac524a..21abe08 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateModel.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateModel.kt
@@ -77,6 +77,7 @@
val type: String,
val appDomainName: String,
val typeIcon: Drawable,
+ val isFirstUsage: Boolean,
)
/**
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt
index 21342a1..d6d7122 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt
@@ -97,7 +97,7 @@
onBackButtonClicked = viewModel::onBackToPrimarySelectionScreen,
)
}
- } else if (uiState.hidden && uiState.selectedEntry != null) {
+ } else if (uiState.selectedEntry != null && !uiState.providerActivityPending) {
viewModel.launchProviderUi(providerActivityLauncher)
}
},
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialViewModel.kt b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialViewModel.kt
index 33e7021..7b80124 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialViewModel.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialViewModel.kt
@@ -41,6 +41,7 @@
val providerDisplayInfo: ProviderDisplayInfo = toProviderDisplayInfo(providerInfoList),
val selectedEntry: EntryInfo? = null,
val hidden: Boolean = false,
+ val providerActivityPending: Boolean = false,
)
class GetCredentialViewModel(
@@ -79,6 +80,9 @@
) {
val entry = uiState.selectedEntry
if (entry != null && entry.pendingIntent != null) {
+ uiState = uiState.copy(
+ providerActivityPending = true,
+ )
val intentSenderRequest = IntentSenderRequest.Builder(entry.pendingIntent)
.setFillInIntent(entry.fillInIntent).build()
launcher.launch(intentSenderRequest)
@@ -96,6 +100,7 @@
uiState = uiState.copy(
selectedEntry = null,
hidden = false,
+ providerActivityPending = false,
)
} else {
if (entry != null) {
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/GetCredentialOption.kt b/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/GetCredentialOption.kt
index eb65241..ef48a77 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/GetCredentialOption.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/GetCredentialOption.kt
@@ -28,9 +28,9 @@
* otherwise
*/
open class GetCredentialOption(
- val type: String,
- val data: Bundle,
- val requireSystemProvider: Boolean,
+ val type: String,
+ val data: Bundle,
+ val requireSystemProvider: Boolean,
) {
companion object {
@JvmStatic
@@ -38,14 +38,20 @@
return try {
when (from.type) {
Credential.TYPE_PASSWORD_CREDENTIAL ->
- GetPasswordOption.createFrom(from.data)
+ GetPasswordOption.createFrom(from.credentialRetrievalData)
PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL ->
- GetPublicKeyCredentialBaseOption.createFrom(from.data)
+ GetPublicKeyCredentialBaseOption.createFrom(from.credentialRetrievalData)
else ->
- GetCredentialOption(from.type, from.data, from.requireSystemProvider())
+ GetCredentialOption(
+ from.type, from.credentialRetrievalData, from.requireSystemProvider()
+ )
}
} catch (e: FrameworkClassParsingException) {
- GetCredentialOption(from.type, from.data, from.requireSystemProvider())
+ GetCredentialOption(
+ from.type,
+ from.credentialRetrievalData,
+ from.requireSystemProvider()
+ )
}
}
}
diff --git a/packages/InputDevices/res/raw/keyboard_layout_english_us_intl.kcm b/packages/InputDevices/res/raw/keyboard_layout_english_us_intl.kcm
index 66c1c98..aa31493 100644
--- a/packages/InputDevices/res/raw/keyboard_layout_english_us_intl.kcm
+++ b/packages/InputDevices/res/raw/keyboard_layout_english_us_intl.kcm
@@ -292,10 +292,10 @@
key APOSTROPHE {
label: '\''
- base: '\''
- shift: '"'
- ralt: '\u0301'
- shift+ralt: '\u0308'
+ base: '\u030D'
+ shift: '\u030E'
+ ralt: '\u00B4'
+ shift+ralt: '\u00A8'
}
### ROW 4
diff --git a/packages/SettingsLib/Spa/gallery/AndroidManifest.xml b/packages/SettingsLib/Spa/gallery/AndroidManifest.xml
index 37d6b42..d32d659 100644
--- a/packages/SettingsLib/Spa/gallery/AndroidManifest.xml
+++ b/packages/SettingsLib/Spa/gallery/AndroidManifest.xml
@@ -38,7 +38,6 @@
<provider
android:name="com.android.settingslib.spa.search.SpaSearchProvider"
android:authorities="com.android.spa.gallery.search.provider"
- android:enabled="true"
android:exported="false">
</provider>
@@ -67,7 +66,6 @@
<provider
android:name="com.android.settingslib.spa.debug.DebugProvider"
android:authorities="com.android.spa.gallery.debug.provider"
- android:enabled="true"
android:exported="false">
</provider>
diff --git a/packages/SettingsLib/Spa/screenshot/Android.bp b/packages/SettingsLib/Spa/screenshot/Android.bp
new file mode 100644
index 0000000..4e6b646
--- /dev/null
+++ b/packages/SettingsLib/Spa/screenshot/Android.bp
@@ -0,0 +1,41 @@
+//
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+package {
+ default_applicable_licenses: ["frameworks_base_license"],
+}
+
+android_test {
+ name: "SpaScreenshotTests",
+ test_suites: ["device-tests"],
+
+ asset_dirs: ["assets"],
+
+ srcs: ["src/**/*.kt"],
+
+ certificate: "platform",
+
+ static_libs: [
+ "SpaLib",
+ "SpaLibTestUtils",
+ "androidx.compose.runtime_runtime",
+ "androidx.test.ext.junit",
+ "androidx.test.runner",
+ "mockito-target-minus-junit4",
+ "platform-screenshot-diff-core",
+ ],
+ kotlincflags: ["-Xjvm-default=all"],
+}
diff --git a/packages/SettingsLib/Spa/screenshot/AndroidManifest.xml b/packages/SettingsLib/Spa/screenshot/AndroidManifest.xml
new file mode 100644
index 0000000..d59a154
--- /dev/null
+++ b/packages/SettingsLib/Spa/screenshot/AndroidManifest.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ Copyright (C) 2022 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.settingslib.spa.screenshot">
+
+ <uses-sdk android:minSdkVersion="21"/>
+
+ <application>
+ <uses-library android:name="android.test.runner" />
+ <activity android:name=".DebugActivity" android:exported="true" />
+ </application>
+
+ <instrumentation
+ android:name="androidx.test.runner.AndroidJUnitRunner"
+ android:label="Screenshot tests for SpaLib"
+ android:targetPackage="com.android.settingslib.spa.screenshot">
+ </instrumentation>
+</manifest>
diff --git a/packages/SettingsLib/Spa/screenshot/AndroidTest.xml b/packages/SettingsLib/Spa/screenshot/AndroidTest.xml
new file mode 100644
index 0000000..e0c08e8
--- /dev/null
+++ b/packages/SettingsLib/Spa/screenshot/AndroidTest.xml
@@ -0,0 +1,36 @@
+<!--
+ Copyright (C) 2022 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<configuration description="Runs screendiff tests.">
+ <option name="test-suite-tag" value="apct-instrumentation" />
+ <option name="test-suite-tag" value="apct" />
+ <target_preparer class="com.android.tradefed.targetprep.DeviceSetup">
+ <option name="optimized-property-setting" value="true" />
+ </target_preparer>
+ <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+ <option name="cleanup-apks" value="true" />
+ <option name="test-file-name" value="SpaScreenshotTests.apk" />
+ </target_preparer>
+ <metrics_collector class="com.android.tradefed.device.metric.FilePullerLogCollector">
+ <option name="directory-keys"
+ value="/data/user/0/com.android.settingslib.spa.screenshot/files/settings_screenshots" />
+ <option name="collect-on-run-ended-only" value="true" />
+ </metrics_collector>
+ <test class="com.android.tradefed.testtype.AndroidJUnitTest">
+ <option name="package" value="com.android.settingslib.spa.screenshot" />
+ <option name="runner" value="androidx.test.runner.AndroidJUnitRunner" />
+ </test>
+</configuration>
diff --git a/packages/SettingsLib/Spa/screenshot/assets/phone/dark_landscape_preference.png b/packages/SettingsLib/Spa/screenshot/assets/phone/dark_landscape_preference.png
new file mode 100644
index 0000000..6086e2d
--- /dev/null
+++ b/packages/SettingsLib/Spa/screenshot/assets/phone/dark_landscape_preference.png
Binary files differ
diff --git a/packages/SettingsLib/Spa/screenshot/assets/phone/dark_portrait_preference.png b/packages/SettingsLib/Spa/screenshot/assets/phone/dark_portrait_preference.png
new file mode 100644
index 0000000..aa6c5b7
--- /dev/null
+++ b/packages/SettingsLib/Spa/screenshot/assets/phone/dark_portrait_preference.png
Binary files differ
diff --git a/packages/SettingsLib/Spa/screenshot/assets/phone/light_landscape_preference.png b/packages/SettingsLib/Spa/screenshot/assets/phone/light_landscape_preference.png
new file mode 100644
index 0000000..cac990c
--- /dev/null
+++ b/packages/SettingsLib/Spa/screenshot/assets/phone/light_landscape_preference.png
Binary files differ
diff --git a/packages/SettingsLib/Spa/screenshot/assets/phone/light_portrait_preference.png b/packages/SettingsLib/Spa/screenshot/assets/phone/light_portrait_preference.png
new file mode 100644
index 0000000..f6298c0
--- /dev/null
+++ b/packages/SettingsLib/Spa/screenshot/assets/phone/light_portrait_preference.png
Binary files differ
diff --git a/packages/SettingsLib/Spa/screenshot/assets/tablet/dark_landscape_preference.png b/packages/SettingsLib/Spa/screenshot/assets/tablet/dark_landscape_preference.png
new file mode 100644
index 0000000..9391eeb
--- /dev/null
+++ b/packages/SettingsLib/Spa/screenshot/assets/tablet/dark_landscape_preference.png
Binary files differ
diff --git a/packages/SettingsLib/Spa/screenshot/assets/tablet/dark_portrait_preference.png b/packages/SettingsLib/Spa/screenshot/assets/tablet/dark_portrait_preference.png
new file mode 100644
index 0000000..94e2843
--- /dev/null
+++ b/packages/SettingsLib/Spa/screenshot/assets/tablet/dark_portrait_preference.png
Binary files differ
diff --git a/packages/SettingsLib/Spa/screenshot/assets/tablet/light_landscape_preference.png b/packages/SettingsLib/Spa/screenshot/assets/tablet/light_landscape_preference.png
new file mode 100644
index 0000000..b1d03c3
--- /dev/null
+++ b/packages/SettingsLib/Spa/screenshot/assets/tablet/light_landscape_preference.png
Binary files differ
diff --git a/packages/SettingsLib/Spa/screenshot/assets/tablet/light_portrait_preference.png b/packages/SettingsLib/Spa/screenshot/assets/tablet/light_portrait_preference.png
new file mode 100644
index 0000000..95f19da
--- /dev/null
+++ b/packages/SettingsLib/Spa/screenshot/assets/tablet/light_portrait_preference.png
Binary files differ
diff --git a/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/util/Bitmap.kt b/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/util/Bitmap.kt
new file mode 100644
index 0000000..814d4a1
--- /dev/null
+++ b/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/util/Bitmap.kt
@@ -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.settingslib.spa.screenshot
+
+import android.graphics.Bitmap
+import android.graphics.Canvas
+import android.os.Build
+import android.view.View
+import platform.test.screenshot.matchers.MSSIMMatcher
+import platform.test.screenshot.matchers.PixelPerfectMatcher
+
+/** Draw this [View] into a [Bitmap]. */
+// TODO(b/195673633): Remove this once Compose screenshot tests use hardware rendering for their
+// tests.
+fun View.drawIntoBitmap(): Bitmap {
+ val bitmap =
+ Bitmap.createBitmap(
+ measuredWidth,
+ measuredHeight,
+ Bitmap.Config.ARGB_8888,
+ )
+ val canvas = Canvas(bitmap)
+ draw(canvas)
+ return bitmap
+}
+
+/**
+ * The [BitmapMatcher][platform.test.screenshot.matchers.BitmapMatcher] that should be used for
+ * screenshot *unit* tests.
+ */
+val UnitTestBitmapMatcher =
+ if (Build.CPU_ABI == "x86_64") {
+ // Different CPU architectures can sometimes end up rendering differently, so we can't do
+ // pixel-perfect matching on different architectures using the same golden. Given that our
+ // presubmits are run on cf_x86_64_phone, our goldens should be perfectly matched on the
+ // x86_64 architecture and use the Structural Similarity Index on others.
+ // TODO(b/237511747): Run our screenshot presubmit tests on arm64 instead so that we can
+ // do pixel perfect matching both at presubmit time and at development time with actual
+ // devices.
+ PixelPerfectMatcher()
+ } else {
+ MSSIMMatcher()
+ }
+
+/**
+ * The [BitmapMatcher][platform.test.screenshot.matchers.BitmapMatcher] that should be used for
+ * screenshot *unit* tests.
+ *
+ * We use the Structural Similarity Index for integration tests because they usually contain
+ * additional information and noise that shouldn't break the test.
+ */
+val IntegrationTestBitmapMatcher = MSSIMMatcher()
diff --git a/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/util/DefaultDeviceEmulationSpec.kt b/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/util/DefaultDeviceEmulationSpec.kt
new file mode 100644
index 0000000..d7f42b3
--- /dev/null
+++ b/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/util/DefaultDeviceEmulationSpec.kt
@@ -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.settingslib.spa.screenshot
+
+import platform.test.screenshot.DeviceEmulationSpec
+import platform.test.screenshot.DisplaySpec
+
+/**
+ * The emulations specs for all 8 permutations of:
+ * - phone or tablet.
+ * - dark of light mode.
+ * - portrait or landscape.
+ */
+val DeviceEmulationSpec.Companion.PhoneAndTabletFull
+ get() = PhoneAndTabletFullSpec
+
+private val PhoneAndTabletFullSpec =
+ DeviceEmulationSpec.forDisplays(Displays.Phone, Displays.Tablet)
+
+/**
+ * The emulations specs of:
+ * - phone + light mode + portrait.
+ * - phone + light mode + landscape.
+ * - tablet + dark mode + portrait.
+ *
+ * This allows to test the most important permutations of a screen/layout with only 3
+ * configurations.
+ */
+val DeviceEmulationSpec.Companion.PhoneAndTabletMinimal
+ get() = PhoneAndTabletMinimalSpec
+
+private val PhoneAndTabletMinimalSpec =
+ DeviceEmulationSpec.forDisplays(Displays.Phone, isDarkTheme = false) +
+ DeviceEmulationSpec.forDisplays(Displays.Tablet, isDarkTheme = true, isLandscape = false)
+
+object Displays {
+ val Phone =
+ DisplaySpec(
+ "phone",
+ width = 1440,
+ height = 3120,
+ densityDpi = 560,
+ )
+
+ val Tablet =
+ DisplaySpec(
+ "tablet",
+ width = 2560,
+ height = 1600,
+ densityDpi = 320,
+ )
+}
diff --git a/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/util/SettingsGoldenImagePathManager.kt b/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/util/SettingsGoldenImagePathManager.kt
new file mode 100644
index 0000000..25bc098
--- /dev/null
+++ b/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/util/SettingsGoldenImagePathManager.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.spa.screenshot
+
+import androidx.test.platform.app.InstrumentationRegistry
+import platform.test.screenshot.GoldenImagePathManager
+import platform.test.screenshot.PathConfig
+
+/** A [GoldenImagePathManager] that should be used for all Settings screenshot tests. */
+class SettingsGoldenImagePathManager(
+ pathConfig: PathConfig,
+ assetsPathRelativeToBuildRoot: String
+) :
+ GoldenImagePathManager(
+ appContext = InstrumentationRegistry.getInstrumentation().context,
+ assetsPathRelativeToBuildRoot = assetsPathRelativeToBuildRoot,
+ deviceLocalPath =
+ InstrumentationRegistry.getInstrumentation()
+ .targetContext
+ .filesDir
+ .absolutePath
+ .toString() + "/settings_screenshots",
+ pathConfig = pathConfig,
+ ) {
+ override fun toString(): String {
+ // This string is appended to all actual/expected screenshots on the device, so make sure
+ // it is a static value.
+ return "SettingsGoldenImagePathManager"
+ }
+}
diff --git a/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/util/SettingsScreenshotTestRule.kt b/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/util/SettingsScreenshotTestRule.kt
new file mode 100644
index 0000000..7a7cf31
--- /dev/null
+++ b/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/util/SettingsScreenshotTestRule.kt
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.spa.screenshot
+
+import androidx.activity.ComponentActivity
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Surface
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.platform.ViewRootForTest
+import androidx.compose.ui.test.junit4.createAndroidComposeRule
+import androidx.compose.ui.test.onRoot
+import com.android.settingslib.spa.framework.theme.SettingsTheme
+import org.junit.rules.RuleChain
+import org.junit.rules.TestRule
+import org.junit.runner.Description
+import org.junit.runners.model.Statement
+import platform.test.screenshot.DeviceEmulationRule
+import platform.test.screenshot.DeviceEmulationSpec
+import platform.test.screenshot.MaterialYouColorsRule
+import platform.test.screenshot.ScreenshotTestRule
+import platform.test.screenshot.getEmulatedDevicePathConfig
+
+/** A rule for Settings screenshot diff tests. */
+class SettingsScreenshotTestRule(
+ emulationSpec: DeviceEmulationSpec,
+ assetsPathRelativeToBuildRoot: String
+) : TestRule {
+ private val colorsRule = MaterialYouColorsRule()
+ private val deviceEmulationRule = DeviceEmulationRule(emulationSpec)
+ private val screenshotRule =
+ ScreenshotTestRule(
+ SettingsGoldenImagePathManager(
+ getEmulatedDevicePathConfig(emulationSpec),
+ assetsPathRelativeToBuildRoot
+ )
+ )
+ private val composeRule = createAndroidComposeRule<ComponentActivity>()
+ private val delegateRule =
+ RuleChain.outerRule(colorsRule)
+ .around(deviceEmulationRule)
+ .around(screenshotRule)
+ .around(composeRule)
+ private val matcher = UnitTestBitmapMatcher
+
+ override fun apply(base: Statement, description: Description): Statement {
+ return delegateRule.apply(base, description)
+ }
+
+ /**
+ * Compare [content] with the golden image identified by [goldenIdentifier] in the context of
+ * [testSpec].
+ */
+ fun screenshotTest(
+ goldenIdentifier: String,
+ content: @Composable () -> Unit,
+ ) {
+ // Make sure that the activity draws full screen and fits the whole display.
+ val activity = composeRule.activity
+ activity.mainExecutor.execute { activity.window.setDecorFitsSystemWindows(false) }
+
+ // Set the content using the AndroidComposeRule to make sure that the Activity is set up
+ // correctly.
+ composeRule.setContent {
+ SettingsTheme {
+ Surface(
+ color = MaterialTheme.colorScheme.background,
+ ) {
+ content()
+ }
+ }
+ }
+ composeRule.waitForIdle()
+
+ val view = (composeRule.onRoot().fetchSemanticsNode().root as ViewRootForTest).view
+ screenshotRule.assertBitmapAgainstGolden(view.drawIntoBitmap(), goldenIdentifier, matcher)
+ }
+}
diff --git a/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/widget/PreferenceScreenshotTest.kt b/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/widget/PreferenceScreenshotTest.kt
new file mode 100644
index 0000000..9631826
--- /dev/null
+++ b/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/widget/PreferenceScreenshotTest.kt
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.spa.screenshot
+
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.outlined.Autorenew
+import androidx.compose.material.icons.outlined.DisabledByDefault
+import androidx.compose.runtime.Composable
+import com.android.settingslib.spa.framework.compose.toState
+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.SettingsIcon
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+import platform.test.screenshot.DeviceEmulationSpec
+
+/** A screenshot test for ExampleFeature. */
+@RunWith(Parameterized::class)
+class PreferenceScreenshotTest(emulationSpec: DeviceEmulationSpec) {
+ companion object {
+ @Parameterized.Parameters(name = "{0}")
+ @JvmStatic
+ fun getTestSpecs() = DeviceEmulationSpec.PhoneAndTabletFull
+ private const val TITLE = "Title"
+ private const val SUMMARY = "Summary"
+ private const val LONG_SUMMARY =
+ "Long long long long long long long long long long long long long long long summary"
+ }
+
+ @get:Rule
+ val screenshotRule =
+ SettingsScreenshotTestRule(
+ emulationSpec,
+ "frameworks/base/packages/SettingsLib/Spa/screenshot/assets"
+ )
+
+ @Test
+ fun testPreference() {
+ screenshotRule.screenshotTest("preference") {
+ RegularScaffold(title = "Preference") {
+ Preference(object : PreferenceModel {
+ override val title = TITLE
+ })
+
+ Preference(object : PreferenceModel {
+ override val title = TITLE
+ override val summary = SUMMARY.toState()
+ })
+
+ Preference(object : PreferenceModel {
+ override val title = TITLE
+ override val summary = LONG_SUMMARY.toState()
+ })
+
+ Preference(object : PreferenceModel {
+ override val title = TITLE
+ override val summary = SUMMARY.toState()
+ override val enabled = false.toState()
+ override val icon = @Composable {
+ SettingsIcon(imageVector = Icons.Outlined.DisabledByDefault)
+ }
+ })
+
+ Preference(object : PreferenceModel {
+ override val title = TITLE
+ override val summary = SUMMARY.toState()
+ override val icon = @Composable {
+ SettingsIcon(imageVector = Icons.Outlined.Autorenew)
+ }
+ })
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/debug/DebugFormat.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/debug/DebugFormat.kt
index 5873635..6ecf9c3 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/debug/DebugFormat.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/debug/DebugFormat.kt
@@ -40,7 +40,7 @@
}
fun SettingsPage.debugArguments(): String {
- val normArguments = parameter.normalize(arguments)
+ val normArguments = parameter.normalize(arguments, eraseRuntimeValues = true)
if (normArguments == null || normArguments.isEmpty) return "[No arguments]"
return normArguments.toString().removeRange(0, 6)
}
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/debug/DebugProvider.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/debug/DebugProvider.kt
index 59ec985..838c0cf 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/debug/DebugProvider.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/debug/DebugProvider.kt
@@ -27,11 +27,7 @@
import android.database.MatrixCursor
import android.net.Uri
import android.util.Log
-import com.android.settingslib.spa.framework.common.ColumnEnum
-import com.android.settingslib.spa.framework.common.QueryEnum
import com.android.settingslib.spa.framework.common.SpaEnvironmentFactory
-import com.android.settingslib.spa.framework.common.addUri
-import com.android.settingslib.spa.framework.common.getColumns
import com.android.settingslib.spa.framework.util.KEY_DESTINATION
import com.android.settingslib.spa.framework.util.KEY_HIGHLIGHT_ENTRY
import com.android.settingslib.spa.framework.util.KEY_SESSION_SOURCE_NAME
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/ProviderColumn.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/debug/ProviderColumn.kt
similarity index 60%
rename from packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/ProviderColumn.kt
rename to packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/debug/ProviderColumn.kt
index 61b46be..bb9a134 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/ProviderColumn.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/debug/ProviderColumn.kt
@@ -14,10 +14,9 @@
* limitations under the License.
*/
-package com.android.settingslib.spa.framework.common
+package com.android.settingslib.spa.debug
import android.content.UriMatcher
-import androidx.annotation.VisibleForTesting
/**
* Enum to define all column names in provider.
@@ -39,12 +38,6 @@
ENTRY_INTENT_URI("entryIntent"),
ENTRY_HIERARCHY_PATH("entryPath"),
ENTRY_START_ADB("entryStartAdb"),
-
- // Columns related to search
- SEARCH_TITLE("searchTitle"),
- SEARCH_KEYWORD("searchKw"),
- SEARCH_PATH("searchPath"),
- SEARCH_STATUS_DISABLED("searchDisabled"),
}
/**
@@ -89,54 +82,16 @@
ColumnEnum.ENTRY_HIERARCHY_PATH,
)
),
-
- SEARCH_STATIC_DATA_QUERY(
- "search_static", 301,
- listOf(
- ColumnEnum.ENTRY_ID,
- ColumnEnum.ENTRY_INTENT_URI,
- ColumnEnum.SEARCH_TITLE,
- ColumnEnum.SEARCH_KEYWORD,
- ColumnEnum.SEARCH_PATH,
- )
- ),
- SEARCH_DYNAMIC_DATA_QUERY(
- "search_dynamic", 302,
- listOf(
- ColumnEnum.ENTRY_ID,
- ColumnEnum.ENTRY_INTENT_URI,
- ColumnEnum.SEARCH_TITLE,
- ColumnEnum.SEARCH_KEYWORD,
- ColumnEnum.SEARCH_PATH,
- )
- ),
- SEARCH_IMMUTABLE_STATUS_DATA_QUERY(
- "search_immutable_status", 303,
- listOf(
- ColumnEnum.ENTRY_ID,
- ColumnEnum.SEARCH_STATUS_DISABLED,
- )
- ),
- SEARCH_MUTABLE_STATUS_DATA_QUERY(
- "search_mutable_status", 304,
- listOf(
- ColumnEnum.ENTRY_ID,
- ColumnEnum.SEARCH_STATUS_DISABLED,
- )
- ),
}
-@VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
-fun QueryEnum.getColumns(): Array<String> {
+internal fun QueryEnum.getColumns(): Array<String> {
return columnNames.map { it.id }.toTypedArray()
}
-@VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
-fun QueryEnum.getIndex(name: ColumnEnum): Int {
+internal fun QueryEnum.getIndex(name: ColumnEnum): Int {
return columnNames.indexOf(name)
}
-@VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
-fun QueryEnum.addUri(uriMatcher: UriMatcher, authority: String) {
+internal fun QueryEnum.addUri(uriMatcher: UriMatcher, authority: String) {
uriMatcher.addURI(authority, queryPath, queryMatchCode)
}
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsEntry.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsEntry.kt
index 702c075..4985b44 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsEntry.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsEntry.kt
@@ -35,6 +35,8 @@
get() = null
val isHighlighted: Boolean
get() = false
+ val arguments: Bundle?
+ get() = null
}
val LocalEntryDataProvider =
@@ -121,11 +123,11 @@
}
private fun fullArgument(runtimeArguments: Bundle? = null): Bundle {
- val arguments = Bundle()
- if (owner.arguments != null) arguments.putAll(owner.arguments)
- // Put runtime args later, which can override page args.
- if (runtimeArguments != null) arguments.putAll(runtimeArguments)
- return arguments
+ return Bundle().apply {
+ if (owner.arguments != null) putAll(owner.arguments)
+ // Put runtime args later, which can override page args.
+ if (runtimeArguments != null) putAll(runtimeArguments)
+ }
}
fun getStatusData(runtimeArguments: Bundle? = null): EntryStatusData? {
@@ -142,19 +144,21 @@
@Composable
fun UiLayout(runtimeArguments: Bundle? = null) {
- CompositionLocalProvider(provideLocalEntryData()) {
- uiLayoutImpl(fullArgument(runtimeArguments))
+ val arguments = remember { fullArgument(runtimeArguments) }
+ CompositionLocalProvider(provideLocalEntryData(arguments)) {
+ uiLayoutImpl(arguments)
}
}
@Composable
- fun provideLocalEntryData(): ProvidedValue<EntryData> {
+ fun provideLocalEntryData(arguments: Bundle): ProvidedValue<EntryData> {
val controller = LocalNavController.current
return LocalEntryDataProvider provides remember {
object : EntryData {
override val pageId = containerPage().id
override val entryId = id
override val isHighlighted = controller.highlightEntryId == id
+ override val arguments = arguments
}
}
}
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsPage.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsPage.kt
index 7a39b73..2bfa2a4 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsPage.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsPage.kt
@@ -69,7 +69,7 @@
parameter: List<NamedNavArgument> = emptyList(),
arguments: Bundle? = null
): String {
- val normArguments = parameter.normalize(arguments)
+ val normArguments = parameter.normalize(arguments, eraseRuntimeValues = true)
return "$name:${normArguments?.toString()}".toHashId()
}
}
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/util/EntryLogger.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/util/EntryLogger.kt
index 1c88187..8ff4368 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/util/EntryLogger.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/util/EntryLogger.kt
@@ -28,9 +28,12 @@
@Composable
fun logEntryEvent(): (event: LogEvent, extraData: Bundle) -> Unit {
val entryId = LocalEntryDataProvider.current.entryId ?: return { _, _ -> }
+ val arguments = LocalEntryDataProvider.current.arguments
return { event, extraData ->
SpaEnvironmentFactory.instance.logger.event(
- entryId, event, category = LogCategory.VIEW, extraData = extraData
+ entryId, event, category = LogCategory.VIEW, extraData = extraData.apply {
+ if (arguments != null) putAll(arguments)
+ }
)
}
}
@@ -40,7 +43,7 @@
if (onClick == null) return null
val logEvent = logEntryEvent()
return {
- logEvent(LogEvent.ENTRY_CLICK, Bundle.EMPTY)
+ logEvent(LogEvent.ENTRY_CLICK, bundleOf())
onClick()
}
}
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/util/PageLogger.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/util/PageLogger.kt
index a881254..271443e 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/util/PageLogger.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/util/PageLogger.kt
@@ -48,7 +48,10 @@
extraData = bundleOf(
LOG_DATA_DISPLAY_NAME to page.displayName,
LOG_DATA_SESSION_NAME to navController.sessionSourceName,
- )
+ ).apply {
+ val normArguments = parameter.normalize(arguments)
+ if (normArguments != null) putAll(normArguments)
+ }
)
}
if (event == Lifecycle.Event.ON_START) {
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/util/Parameter.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/util/Parameter.kt
index f10d3b0..be303f0 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/util/Parameter.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/util/Parameter.kt
@@ -47,12 +47,15 @@
return argsArray.joinToString("") { arg -> "/$arg" }
}
-fun List<NamedNavArgument>.normalize(arguments: Bundle? = null): Bundle? {
+fun List<NamedNavArgument>.normalize(
+ arguments: Bundle? = null,
+ eraseRuntimeValues: Boolean = false
+): Bundle? {
if (this.isEmpty()) return null
val normArgs = Bundle()
for (navArg in this) {
// Erase value of runtime parameters.
- if (navArg.isRuntimeParam()) {
+ if (navArg.isRuntimeParam() && eraseRuntimeValues) {
normArgs.putString(navArg.name, null)
continue
}
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/search/SpaSearchContract.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/search/SpaSearchContract.kt
new file mode 100644
index 0000000..2301f04
--- /dev/null
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/search/SpaSearchContract.kt
@@ -0,0 +1,97 @@
+/*
+ * 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.search
+
+/**
+ * Intent action used to identify SpaSearchProvider instances. This is used in the {@code
+ * <intent-filter>} of a {@code <provider>}.
+ */
+const val PROVIDER_INTERFACE = "android.content.action.SPA_SEARCH_PROVIDER"
+
+/** ContentProvider path for search static data */
+const val SEARCH_STATIC_DATA = "search_static_data"
+
+/** ContentProvider path for search dynamic data */
+const val SEARCH_DYNAMIC_DATA = "search_dynamic_data"
+
+/** ContentProvider path for search immutable status */
+const val SEARCH_IMMUTABLE_STATUS = "search_immutable_status"
+
+/** ContentProvider path for search mutable status */
+const val SEARCH_MUTABLE_STATUS = "search_mutable_status"
+
+/** Enum to define all column names in provider. */
+enum class ColumnEnum(val id: String) {
+ ENTRY_ID("entryId"),
+ SEARCH_TITLE("searchTitle"),
+ SEARCH_KEYWORD("searchKw"),
+ SEARCH_PATH("searchPath"),
+ INTENT_TARGET_PACKAGE("intentTargetPackage"),
+ INTENT_TARGET_CLASS("intentTargetClass"),
+ INTENT_EXTRAS("intentExtras"),
+ SLICE_URI("sliceUri"),
+ LEGACY_KEY("legacyKey"),
+ ENTRY_DISABLED("entryDisabled"),
+}
+
+/** Enum to define all queries supported in the provider. */
+@SuppressWarnings("Immutable")
+enum class QueryEnum(
+ val queryPath: String,
+ val columnNames: List<ColumnEnum>
+) {
+ SEARCH_STATIC_DATA_QUERY(
+ SEARCH_STATIC_DATA,
+ listOf(
+ ColumnEnum.ENTRY_ID,
+ ColumnEnum.SEARCH_TITLE,
+ ColumnEnum.SEARCH_KEYWORD,
+ ColumnEnum.SEARCH_PATH,
+ ColumnEnum.INTENT_TARGET_PACKAGE,
+ ColumnEnum.INTENT_TARGET_CLASS,
+ ColumnEnum.INTENT_EXTRAS,
+ ColumnEnum.SLICE_URI,
+ ColumnEnum.LEGACY_KEY
+ )
+ ),
+ SEARCH_DYNAMIC_DATA_QUERY(
+ SEARCH_DYNAMIC_DATA,
+ listOf(
+ ColumnEnum.ENTRY_ID,
+ ColumnEnum.SEARCH_TITLE,
+ ColumnEnum.SEARCH_KEYWORD,
+ ColumnEnum.SEARCH_PATH,
+ ColumnEnum.INTENT_TARGET_PACKAGE,
+ ColumnEnum.INTENT_TARGET_CLASS,
+ ColumnEnum.SLICE_URI,
+ ColumnEnum.LEGACY_KEY
+ )
+ ),
+ SEARCH_IMMUTABLE_STATUS_DATA_QUERY(
+ SEARCH_IMMUTABLE_STATUS,
+ listOf(
+ ColumnEnum.ENTRY_ID,
+ ColumnEnum.ENTRY_DISABLED,
+ )
+ ),
+ SEARCH_MUTABLE_STATUS_DATA_QUERY(
+ SEARCH_MUTABLE_STATUS,
+ listOf(
+ ColumnEnum.ENTRY_ID,
+ ColumnEnum.ENTRY_DISABLED,
+ )
+ ),
+}
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/search/SpaSearchProvider.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/search/SpaSearchProvider.kt
index 02aed1c..21bc75a 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/search/SpaSearchProvider.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/search/SpaSearchProvider.kt
@@ -19,22 +19,22 @@
import android.content.ContentProvider
import android.content.ContentValues
import android.content.Context
-import android.content.Intent
import android.content.UriMatcher
import android.content.pm.ProviderInfo
import android.database.Cursor
import android.database.MatrixCursor
import android.net.Uri
+import android.os.Parcel
+import android.os.Parcelable
import android.util.Log
import androidx.annotation.VisibleForTesting
-import com.android.settingslib.spa.framework.common.ColumnEnum
-import com.android.settingslib.spa.framework.common.QueryEnum
+import com.android.settingslib.spa.framework.common.EntryStatusData
import com.android.settingslib.spa.framework.common.SettingsEntry
import com.android.settingslib.spa.framework.common.SpaEnvironmentFactory
-import com.android.settingslib.spa.framework.common.addUri
-import com.android.settingslib.spa.framework.common.getColumns
import com.android.settingslib.spa.framework.util.SESSION_SEARCH
import com.android.settingslib.spa.framework.util.createIntent
+import com.android.settingslib.spa.slice.fromEntry
+
private const val TAG = "SpaSearchProvider"
@@ -42,18 +42,25 @@
* The content provider to return entry related data, which can be used for search and hierarchy.
* One can query the provider result by:
* $ adb shell content query --uri content://<AuthorityPath>/<QueryPath>
- * For gallery, AuthorityPath = com.android.spa.gallery.provider
- * For Settings, AuthorityPath = com.android.settings.spa.provider
+ * For gallery, AuthorityPath = com.android.spa.gallery.search.provider
+ * For Settings, AuthorityPath = com.android.settings.spa.search.provider"
* Some examples:
- * $ adb shell content query --uri content://<AuthorityPath>/search_static
- * $ adb shell content query --uri content://<AuthorityPath>/search_dynamic
- * $ adb shell content query --uri content://<AuthorityPath>/search_mutable_status
+ * $ adb shell content query --uri content://<AuthorityPath>/search_static_data
+ * $ adb shell content query --uri content://<AuthorityPath>/search_dynamic_data
* $ adb shell content query --uri content://<AuthorityPath>/search_immutable_status
+ * $ adb shell content query --uri content://<AuthorityPath>/search_mutable_status
*/
class SpaSearchProvider : ContentProvider() {
private val spaEnvironment get() = SpaEnvironmentFactory.instance
private val uriMatcher = UriMatcher(UriMatcher.NO_MATCH)
+ private val queryMatchCode = mapOf(
+ SEARCH_STATIC_DATA to 301,
+ SEARCH_DYNAMIC_DATA to 302,
+ SEARCH_MUTABLE_STATUS to 303,
+ SEARCH_IMMUTABLE_STATUS to 304
+ )
+
override fun delete(uri: Uri, selection: String?, selectionArgs: Array<String>?): Int {
TODO("Implement this to handle requests to delete one or more rows")
}
@@ -85,10 +92,9 @@
override fun attachInfo(context: Context?, info: ProviderInfo?) {
if (info != null) {
- QueryEnum.SEARCH_STATIC_DATA_QUERY.addUri(uriMatcher, info.authority)
- QueryEnum.SEARCH_DYNAMIC_DATA_QUERY.addUri(uriMatcher, info.authority)
- QueryEnum.SEARCH_MUTABLE_STATUS_DATA_QUERY.addUri(uriMatcher, info.authority)
- QueryEnum.SEARCH_IMMUTABLE_STATUS_DATA_QUERY.addUri(uriMatcher, info.authority)
+ for (entry in queryMatchCode) {
+ uriMatcher.addURI(info.authority, entry.key, entry.value)
+ }
}
super.attachInfo(context, info)
}
@@ -102,11 +108,11 @@
): Cursor? {
return try {
when (uriMatcher.match(uri)) {
- QueryEnum.SEARCH_STATIC_DATA_QUERY.queryMatchCode -> querySearchStaticData()
- QueryEnum.SEARCH_DYNAMIC_DATA_QUERY.queryMatchCode -> querySearchDynamicData()
- QueryEnum.SEARCH_MUTABLE_STATUS_DATA_QUERY.queryMatchCode ->
+ queryMatchCode[SEARCH_STATIC_DATA] -> querySearchStaticData()
+ queryMatchCode[SEARCH_DYNAMIC_DATA] -> querySearchDynamicData()
+ queryMatchCode[SEARCH_MUTABLE_STATUS] ->
querySearchMutableStatusData()
- QueryEnum.SEARCH_IMMUTABLE_STATUS_DATA_QUERY.queryMatchCode ->
+ queryMatchCode[SEARCH_IMMUTABLE_STATUS] ->
querySearchImmutableStatusData()
else -> throw UnsupportedOperationException("Unknown Uri $uri")
}
@@ -167,23 +173,45 @@
// Fetch search data. We can add runtime arguments later if necessary
val searchData = entry.getSearchData() ?: return
- val intent = entry.createIntent(SESSION_SEARCH) ?: Intent()
- cursor.newRow()
- .add(ColumnEnum.ENTRY_ID.id, entry.id)
- .add(ColumnEnum.ENTRY_INTENT_URI.id, intent.toUri(Intent.URI_INTENT_SCHEME))
+ val intent = entry.createIntent(SESSION_SEARCH)
+ val row = cursor.newRow().add(ColumnEnum.ENTRY_ID.id, entry.id)
.add(ColumnEnum.SEARCH_TITLE.id, searchData.title)
.add(ColumnEnum.SEARCH_KEYWORD.id, searchData.keyword)
.add(
ColumnEnum.SEARCH_PATH.id,
entryRepository.getEntryPathWithTitle(entry.id, searchData.title)
)
+ intent?.let {
+ row.add(ColumnEnum.INTENT_TARGET_PACKAGE.id, spaEnvironment.appContext.packageName)
+ .add(ColumnEnum.INTENT_TARGET_CLASS.id, spaEnvironment.browseActivityClass?.name)
+ .add(ColumnEnum.INTENT_EXTRAS.id, marshall(intent.extras))
+ }
+ if (entry.hasSliceSupport)
+ row.add(
+ ColumnEnum.SLICE_URI.id, Uri.Builder()
+ .fromEntry(entry, spaEnvironment.sliceProviderAuthorities)
+ )
+ // TODO: support legacy key
}
private fun fetchStatusData(entry: SettingsEntry, cursor: MatrixCursor) {
// Fetch status data. We can add runtime arguments later if necessary
- val statusData = entry.getStatusData() ?: return
+ val statusData = entry.getStatusData() ?: EntryStatusData()
cursor.newRow()
.add(ColumnEnum.ENTRY_ID.id, entry.id)
- .add(ColumnEnum.SEARCH_STATUS_DISABLED.id, statusData.isDisabled)
+ .add(ColumnEnum.ENTRY_DISABLED.id, statusData.isDisabled)
+ }
+
+ private fun QueryEnum.getColumns(): Array<String> {
+ return columnNames.map { it.id }.toTypedArray()
+ }
+
+ private fun marshall(parcelable: Parcelable?): ByteArray? {
+ if (parcelable == null) return null
+ val parcel = Parcel.obtain()
+ parcelable.writeToParcel(parcel, 0)
+ val bytes = parcel.marshall()
+ parcel.recycle()
+ return bytes
}
}
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/slice/provider/Demo.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/slice/provider/Demo.kt
index b65b91f..3d7d565 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/slice/provider/Demo.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/slice/provider/Demo.kt
@@ -24,6 +24,7 @@
import androidx.slice.SliceManager
import androidx.slice.builders.ListBuilder
import androidx.slice.builders.SliceAction
+import androidx.slice.core.R
import com.android.settingslib.spa.framework.common.SpaEnvironmentFactory
import com.android.settingslib.spa.slice.createBroadcastPendingIntent
import com.android.settingslib.spa.slice.createBrowsePendingIntent
@@ -52,10 +53,7 @@
private fun createSliceAction(context: Context, intent: PendingIntent): SliceAction {
return SliceAction.create(
intent,
- IconCompat.createWithResource(
- context,
- com.google.android.material.R.drawable.navigation_empty_icon
- ),
+ IconCompat.createWithResource(context, R.drawable.notification_action_background),
ListBuilder.ICON_IMAGE,
"Enter app"
)
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/CustomizedAppBar.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/CustomizedAppBar.kt
index 7db1ca1..ae88ed7 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/CustomizedAppBar.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/CustomizedAppBar.kt
@@ -132,7 +132,10 @@
.asPaddingValues()
.horizontalValues()
)
- .padding(horizontal = SettingsDimension.itemPaddingAround),
+ .padding(
+ start = SettingsDimension.itemPaddingAround,
+ end = SettingsDimension.itemPaddingEnd,
+ ),
overflow = TextOverflow.Ellipsis,
maxLines = maxLines,
)
diff --git a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/util/ParameterTest.kt b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/util/ParameterTest.kt
index 48ebd8d..0aa846c 100644
--- a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/util/ParameterTest.kt
+++ b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/util/ParameterTest.kt
@@ -88,7 +88,7 @@
val emptyParam = navArguments.normalize()
assertThat(emptyParam).isNotNull()
assertThat(emptyParam.toString()).isEqualTo(
- "Bundle[{rt_param=null, unset_string_param=null, unset_int_param=null}]"
+ "Bundle[{unset_rt_param=null, unset_string_param=null, unset_int_param=null}]"
)
val setPartialParam = navArguments.normalize(
@@ -99,7 +99,7 @@
)
assertThat(setPartialParam).isNotNull()
assertThat(setPartialParam.toString()).isEqualTo(
- "Bundle[{rt_param=null, string_param=myStr, unset_int_param=null}]"
+ "Bundle[{rt_param=rtStr, string_param=myStr, unset_int_param=null}]"
)
val setAllParam = navArguments.normalize(
@@ -107,7 +107,8 @@
"string_param" to "myStr",
"int_param" to 10,
"rt_param" to "rtStr",
- )
+ ),
+ eraseRuntimeValues = true
)
assertThat(setAllParam).isNotNull()
assertThat(setAllParam.toString()).isEqualTo(
diff --git a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/search/SpaSearchProviderTest.kt b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/search/SpaSearchProviderTest.kt
index cdb0f3a..831aded 100644
--- a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/search/SpaSearchProviderTest.kt
+++ b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/search/SpaSearchProviderTest.kt
@@ -18,15 +18,14 @@
import android.content.Context
import android.database.Cursor
+import android.os.Bundle
+import android.os.Parcel
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
-import com.android.settingslib.spa.framework.common.ColumnEnum
-import com.android.settingslib.spa.framework.common.QueryEnum
import com.android.settingslib.spa.framework.common.SettingsEntryBuilder
import com.android.settingslib.spa.framework.common.SettingsPage
import com.android.settingslib.spa.framework.common.SpaEnvironmentFactory
import com.android.settingslib.spa.framework.common.createSettingsPage
-import com.android.settingslib.spa.framework.common.getIndex
import com.android.settingslib.spa.tests.testutils.SpaEnvironmentForTest
import com.android.settingslib.spa.tests.testutils.SppForSearch
import com.google.common.truth.Truth
@@ -39,51 +38,145 @@
private val spaEnvironment =
SpaEnvironmentForTest(context, listOf(SppForSearch.createSettingsPage()))
private val searchProvider = SpaSearchProvider()
+ private val pageOwner = spaEnvironment.createPage("SppForSearch")
@Test
fun testQuerySearchStatusData() {
SpaEnvironmentFactory.reset(spaEnvironment)
- val pageOwner = spaEnvironment.createPage("SppForSearch")
val immutableStatus = searchProvider.querySearchImmutableStatusData()
- Truth.assertThat(immutableStatus.count).isEqualTo(1)
+ Truth.assertThat(immutableStatus.count).isEqualTo(2)
immutableStatus.moveToFirst()
immutableStatus.checkValue(
QueryEnum.SEARCH_IMMUTABLE_STATUS_DATA_QUERY,
ColumnEnum.ENTRY_ID,
+ pageOwner.getEntryId("SearchStaticWithNoStatus")
+ )
+ immutableStatus.checkValue(
+ QueryEnum.SEARCH_IMMUTABLE_STATUS_DATA_QUERY,
+ ColumnEnum.ENTRY_DISABLED,
+ false.toString()
+ )
+
+ immutableStatus.moveToNext()
+ immutableStatus.checkValue(
+ QueryEnum.SEARCH_IMMUTABLE_STATUS_DATA_QUERY,
+ ColumnEnum.ENTRY_ID,
pageOwner.getEntryId("SearchDynamicWithImmutableStatus")
)
+ immutableStatus.checkValue(
+ QueryEnum.SEARCH_IMMUTABLE_STATUS_DATA_QUERY, ColumnEnum.ENTRY_DISABLED, true.toString()
+ )
val mutableStatus = searchProvider.querySearchMutableStatusData()
Truth.assertThat(mutableStatus.count).isEqualTo(2)
mutableStatus.moveToFirst()
mutableStatus.checkValue(
- QueryEnum.SEARCH_IMMUTABLE_STATUS_DATA_QUERY,
+ QueryEnum.SEARCH_MUTABLE_STATUS_DATA_QUERY,
ColumnEnum.ENTRY_ID,
pageOwner.getEntryId("SearchStaticWithMutableStatus")
)
+ mutableStatus.checkValue(
+ QueryEnum.SEARCH_MUTABLE_STATUS_DATA_QUERY, ColumnEnum.ENTRY_DISABLED, false.toString()
+ )
mutableStatus.moveToNext()
mutableStatus.checkValue(
- QueryEnum.SEARCH_IMMUTABLE_STATUS_DATA_QUERY,
+ QueryEnum.SEARCH_MUTABLE_STATUS_DATA_QUERY,
ColumnEnum.ENTRY_ID,
pageOwner.getEntryId("SearchDynamicWithMutableStatus")
)
+ mutableStatus.checkValue(
+ QueryEnum.SEARCH_MUTABLE_STATUS_DATA_QUERY, ColumnEnum.ENTRY_DISABLED, true.toString()
+ )
}
@Test
fun testQuerySearchIndexData() {
SpaEnvironmentFactory.reset(spaEnvironment)
+
val staticData = searchProvider.querySearchStaticData()
Truth.assertThat(staticData.count).isEqualTo(2)
+ staticData.moveToFirst()
+ staticData.checkValue(
+ QueryEnum.SEARCH_STATIC_DATA_QUERY,
+ ColumnEnum.ENTRY_ID,
+ pageOwner.getEntryId("SearchStaticWithNoStatus")
+ )
+ staticData.checkValue(
+ QueryEnum.SEARCH_STATIC_DATA_QUERY, ColumnEnum.SEARCH_TITLE, "SearchStaticWithNoStatus"
+ )
+ staticData.checkValue(
+ QueryEnum.SEARCH_STATIC_DATA_QUERY, ColumnEnum.SEARCH_KEYWORD, listOf("").toString()
+ )
+ staticData.checkValue(
+ QueryEnum.SEARCH_STATIC_DATA_QUERY,
+ ColumnEnum.SEARCH_PATH,
+ listOf("SearchStaticWithNoStatus", "SppForSearch").toString()
+ )
+ staticData.checkValue(
+ QueryEnum.SEARCH_STATIC_DATA_QUERY,
+ ColumnEnum.INTENT_TARGET_PACKAGE,
+ spaEnvironment.appContext.packageName
+ )
+ staticData.checkValue(
+ QueryEnum.SEARCH_STATIC_DATA_QUERY,
+ ColumnEnum.INTENT_TARGET_CLASS,
+ "com.android.settingslib.spa.tests.testutils.BlankActivity"
+ )
+
+ // Check extras in intent
+ val bundle =
+ staticData.getExtras(QueryEnum.SEARCH_STATIC_DATA_QUERY, ColumnEnum.INTENT_EXTRAS)
+ Truth.assertThat(bundle).isNotNull()
+ Truth.assertThat(bundle!!.size()).isEqualTo(3)
+ Truth.assertThat(bundle.getString("spaActivityDestination")).isEqualTo("SppForSearch")
+ Truth.assertThat(bundle.getString("highlightEntry"))
+ .isEqualTo(pageOwner.getEntryId("SearchStaticWithNoStatus"))
+ Truth.assertThat(bundle.getString("sessionSource")).isEqualTo("search")
+
+ staticData.moveToNext()
+ staticData.checkValue(
+ QueryEnum.SEARCH_STATIC_DATA_QUERY,
+ ColumnEnum.ENTRY_ID,
+ pageOwner.getEntryId("SearchStaticWithMutableStatus")
+ )
val dynamicData = searchProvider.querySearchDynamicData()
Truth.assertThat(dynamicData.count).isEqualTo(2)
+ dynamicData.moveToFirst()
+ dynamicData.checkValue(
+ QueryEnum.SEARCH_DYNAMIC_DATA_QUERY,
+ ColumnEnum.ENTRY_ID,
+ pageOwner.getEntryId("SearchDynamicWithMutableStatus")
+ )
+
+ dynamicData.moveToNext()
+ dynamicData.checkValue(
+ QueryEnum.SEARCH_DYNAMIC_DATA_QUERY,
+ ColumnEnum.ENTRY_ID,
+ pageOwner.getEntryId("SearchDynamicWithImmutableStatus")
+ )
+ dynamicData.checkValue(
+ QueryEnum.SEARCH_DYNAMIC_DATA_QUERY,
+ ColumnEnum.SEARCH_KEYWORD,
+ listOf("kw1", "kw2").toString()
+ )
}
}
private fun Cursor.checkValue(query: QueryEnum, column: ColumnEnum, value: String) {
- Truth.assertThat(getString(query.getIndex(column))).isEqualTo(value)
+ Truth.assertThat(getString(query.columnNames.indexOf(column))).isEqualTo(value)
+}
+
+private fun Cursor.getExtras(query: QueryEnum, column: ColumnEnum): Bundle? {
+ val extrasByte = getBlob(query.columnNames.indexOf(column)) ?: return null
+ val parcel = Parcel.obtain()
+ parcel.unmarshall(extrasByte, 0, extrasByte.size)
+ parcel.setDataPosition(0)
+ val bundle = Bundle()
+ bundle.readFromParcel(parcel)
+ return bundle
}
private fun SettingsPage.getEntryId(name: String): String {
diff --git a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/tests/testutils/UniqueIdHelper.kt b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/tests/testutils/UniqueIdHelper.kt
index 7e51fea..ce9b791 100644
--- a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/tests/testutils/UniqueIdHelper.kt
+++ b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/tests/testutils/UniqueIdHelper.kt
@@ -27,7 +27,7 @@
parameter: List<NamedNavArgument> = emptyList(),
arguments: Bundle? = null
): String {
- val normArguments = parameter.normalize(arguments)
+ val normArguments = parameter.normalize(arguments, eraseRuntimeValues = true)
return "$name:${normArguments?.toString()}".toHashId()
}
diff --git a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/button/ActionButtonsTest.kt b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/button/ActionButtonsTest.kt
index 8101a94..f59b0de 100644
--- a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/button/ActionButtonsTest.kt
+++ b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/button/ActionButtonsTest.kt
@@ -17,11 +17,13 @@
package com.android.settingslib.spa.widget.button
import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.outlined.Close
import androidx.compose.material.icons.outlined.Launch
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.getBoundsInRoot
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
@@ -66,4 +68,19 @@
assertThat(clicked).isTrue()
}
+
+ @Test
+ fun twoButtons_positionIsAligned() {
+ composeTestRule.setContent {
+ ActionButtons(
+ listOf(
+ ActionButton(text = "Open", imageVector = Icons.Outlined.Launch) {},
+ ActionButton(text = "Close", imageVector = Icons.Outlined.Close) {},
+ )
+ )
+ }
+
+ assertThat(composeTestRule.onNodeWithText("Open").getBoundsInRoot().top)
+ .isEqualTo(composeTestRule.onNodeWithText("Close").getBoundsInRoot().top)
+ }
}
diff --git a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/ui/FooterTest.kt b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/ui/FooterTest.kt
new file mode 100644
index 0000000..9d0501f
--- /dev/null
+++ b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/ui/FooterTest.kt
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.spa.widget.ui
+
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.assertIsNotDisplayed
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithText
+import androidx.compose.ui.test.onRoot
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class FooterTest {
+ @get:Rule
+ val composeTestRule = createComposeRule()
+
+ @Test
+ fun footer_isEmpty() {
+ composeTestRule.setContent {
+ Footer(footerText = "")
+ }
+
+ composeTestRule.onRoot().assertIsNotDisplayed()
+ }
+
+ @Test
+ fun footer_notEmpty_displayed() {
+ composeTestRule.setContent {
+ Footer(footerText = FOOTER_TEXT)
+ }
+
+ composeTestRule.onNodeWithText(FOOTER_TEXT).assertIsDisplayed()
+ }
+
+ private companion object {
+ const val FOOTER_TEXT = "Footer text"
+ }
+}
diff --git a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/framework/compose/DisposableBroadcastReceiverAsUserTest.kt b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/framework/compose/DisposableBroadcastReceiverAsUserTest.kt
new file mode 100644
index 0000000..01f4cc6
--- /dev/null
+++ b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/framework/compose/DisposableBroadcastReceiverAsUserTest.kt
@@ -0,0 +1,108 @@
+/*
+ * 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.framework.compose
+
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import android.os.UserHandle
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.any
+import org.mockito.junit.MockitoJUnit
+import org.mockito.junit.MockitoRule
+import org.mockito.Mockito.`when` as whenever
+
+@RunWith(AndroidJUnit4::class)
+class DisposableBroadcastReceiverAsUserTest {
+ @get:Rule
+ val composeTestRule = createComposeRule()
+
+ @get:Rule
+ val mockito: MockitoRule = MockitoJUnit.rule()
+
+ @Mock
+ private lateinit var context: Context
+
+ private var registeredBroadcastReceiver: BroadcastReceiver? = null
+
+ @Before
+ fun setUp() {
+ whenever(context.registerReceiverAsUser(any(), any(), any(), any(), any()))
+ .thenAnswer {
+ registeredBroadcastReceiver = it.arguments[0] as BroadcastReceiver
+ null
+ }
+ }
+
+ @Test
+ fun broadcastReceiver_registered() {
+ composeTestRule.setContent {
+ CompositionLocalProvider(LocalContext provides context) {
+ DisposableBroadcastReceiverAsUser(IntentFilter(), USER_HANDLE) {}
+ }
+ }
+
+ assertThat(registeredBroadcastReceiver).isNotNull()
+ }
+
+ @Test
+ fun broadcastReceiver_isCalledOnReceive() {
+ var onReceiveIsCalled = false
+ composeTestRule.setContent {
+ CompositionLocalProvider(LocalContext provides context) {
+ DisposableBroadcastReceiverAsUser(IntentFilter(), USER_HANDLE) {
+ onReceiveIsCalled = true
+ }
+ }
+ }
+
+ registeredBroadcastReceiver!!.onReceive(context, Intent())
+
+ assertThat(onReceiveIsCalled).isTrue()
+ }
+
+ @Test
+ fun broadcastReceiver_onStartIsCalled() {
+ var onStartIsCalled = false
+ composeTestRule.setContent {
+ CompositionLocalProvider(LocalContext provides context) {
+ DisposableBroadcastReceiverAsUser(
+ intentFilter = IntentFilter(),
+ userHandle = USER_HANDLE,
+ onStart = { onStartIsCalled = true },
+ onReceive = {},
+ )
+ }
+ }
+
+ assertThat(onStartIsCalled).isTrue()
+ }
+
+ private companion object {
+ val USER_HANDLE: UserHandle = UserHandle.of(0)
+ }
+}
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java
index 61c7fb9..2951001 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java
@@ -163,6 +163,11 @@
mUnpairing = false;
}
+ /** Clears any pending messages in the message queue. */
+ public void release() {
+ mHandler.removeCallbacksAndMessages(null);
+ }
+
private void initDrawableCache() {
int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
int cacheSize = maxMemory / 8;
@@ -1441,11 +1446,13 @@
final boolean tmpJustDiscovered = mJustDiscovered;
final HearingAidInfo tmpHearingAidInfo = mHearingAidInfo;
// Set main device from sub device
+ release();
mDevice = mSubDevice.mDevice;
mRssi = mSubDevice.mRssi;
mJustDiscovered = mSubDevice.mJustDiscovered;
mHearingAidInfo = mSubDevice.mHearingAidInfo;
// Set sub device from backup
+ mSubDevice.release();
mSubDevice.mDevice = tmpDevice;
mSubDevice.mRssi = tmpRssi;
mSubDevice.mJustDiscovered = tmpJustDiscovered;
@@ -1471,6 +1478,7 @@
* Remove a device from the member device sets.
*/
public void removeMemberDevice(CachedBluetoothDevice memberDevice) {
+ memberDevice.release();
mMemberDevices.remove(memberDevice);
}
@@ -1488,11 +1496,13 @@
final short tmpRssi = mRssi;
final boolean tmpJustDiscovered = mJustDiscovered;
// Set main device from sub device
+ release();
mDevice = newMainDevice.mDevice;
mRssi = newMainDevice.mRssi;
mJustDiscovered = newMainDevice.mJustDiscovered;
// Set sub device from backup
+ newMainDevice.release();
newMainDevice.mDevice = tmpDevice;
newMainDevice.mRssi = tmpRssi;
newMainDevice.mJustDiscovered = tmpJustDiscovered;
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceManager.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceManager.java
index dd56bde..221836b 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceManager.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceManager.java
@@ -223,8 +223,14 @@
public synchronized void clearNonBondedDevices() {
clearNonBondedSubDevices();
- mCachedDevices.removeIf(cachedDevice
- -> cachedDevice.getBondState() == BluetoothDevice.BOND_NONE);
+ final List<CachedBluetoothDevice> removedCachedDevice = new ArrayList<>();
+ mCachedDevices.stream()
+ .filter(cachedDevice -> cachedDevice.getBondState() == BluetoothDevice.BOND_NONE)
+ .forEach(cachedDevice -> {
+ cachedDevice.release();
+ removedCachedDevice.add(cachedDevice);
+ });
+ mCachedDevices.removeAll(removedCachedDevice);
}
private void clearNonBondedSubDevices() {
@@ -245,6 +251,7 @@
if (subDevice != null
&& subDevice.getDevice().getBondState() == BluetoothDevice.BOND_NONE) {
// Sub device exists and it is not bonded
+ subDevice.release();
cachedDevice.setSubDevice(null);
}
}
@@ -294,6 +301,7 @@
}
if (cachedDevice.getBondState() != BluetoothDevice.BOND_BONDED) {
cachedDevice.setJustDiscovered(false);
+ cachedDevice.release();
mCachedDevices.remove(i);
}
}
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/HapClientProfile.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/HapClientProfile.java
index f06aab3..6b7f733 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/HapClientProfile.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/HapClientProfile.java
@@ -176,11 +176,11 @@
* @param device is the device for which we want to know if supports synchronized presets
* @return {@code true} if the device supports synchronized presets
*/
- public boolean supportSynchronizedPresets(@NonNull BluetoothDevice device) {
+ public boolean supportsSynchronizedPresets(@NonNull BluetoothDevice device) {
if (mService == null) {
return false;
}
- return mService.supportSynchronizedPresets(device);
+ return mService.supportsSynchronizedPresets(device);
}
/**
@@ -189,11 +189,11 @@
* @param device is the device for which we want to know if supports independent presets
* @return {@code true} if the device supports independent presets
*/
- public boolean supportIndependentPresets(@NonNull BluetoothDevice device) {
+ public boolean supportsIndependentPresets(@NonNull BluetoothDevice device) {
if (mService == null) {
return false;
}
- return mService.supportIndependentPresets(device);
+ return mService.supportsIndependentPresets(device);
}
/**
@@ -202,11 +202,11 @@
* @param device is the device for which we want to know if supports dynamic presets
* @return {@code true} if the device supports dynamic presets
*/
- public boolean supportDynamicPresets(@NonNull BluetoothDevice device) {
+ public boolean supportsDynamicPresets(@NonNull BluetoothDevice device) {
if (mService == null) {
return false;
}
- return mService.supportDynamicPresets(device);
+ return mService.supportsDynamicPresets(device);
}
/**
@@ -215,11 +215,11 @@
* @param device is the device for which we want to know if supports writable presets
* @return {@code true} if the device supports writable presets
*/
- public boolean supportWritablePresets(@NonNull BluetoothDevice device) {
+ public boolean supportsWritablePresets(@NonNull BluetoothDevice device) {
if (mService == null) {
return false;
}
- return mService.supportWritablePresets(device);
+ return mService.supportsWritablePresets(device);
}
@Override
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 65671a2..77c19a1 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
@@ -998,10 +998,12 @@
mCachedDevice.switchSubDeviceContent();
+ verify(mCachedDevice).release();
assertThat(mCachedDevice.mRssi).isEqualTo(RSSI_2);
assertThat(mCachedDevice.mJustDiscovered).isEqualTo(JUSTDISCOVERED_2);
assertThat(mCachedDevice.mDevice).isEqualTo(mSubDevice);
assertThat(mCachedDevice.getDeviceSide()).isEqualTo(HearingAidInfo.DeviceSide.SIDE_RIGHT);
+ verify(mSubCachedDevice).release();
assertThat(mSubCachedDevice.mRssi).isEqualTo(RSSI_1);
assertThat(mSubCachedDevice.mJustDiscovered).isEqualTo(JUSTDISCOVERED_1);
assertThat(mSubCachedDevice.mDevice).isEqualTo(mDevice);
diff --git a/packages/SettingsProvider/res/xml/bookmarks.xml b/packages/SettingsProvider/res/xml/bookmarks.xml
index 454f456..4b97b47 100644
--- a/packages/SettingsProvider/res/xml/bookmarks.xml
+++ b/packages/SettingsProvider/res/xml/bookmarks.xml
@@ -19,7 +19,6 @@
Bookmarks for vendor apps should be added to a bookmarks resource overlay; not here.
Typical shortcuts (not necessarily defined here):
- 'a': Calculator
'b': Browser
'c': Contacts
'e': Email
@@ -29,13 +28,11 @@
'p': Music
's': SMS
't': Talk
+ 'u': Calculator
'y': YouTube
-->
<bookmarks>
<bookmark
- category="android.intent.category.APP_CALCULATOR"
- shortcut="a" />
- <bookmark
category="android.intent.category.APP_BROWSER"
shortcut="b" />
<bookmark
@@ -46,7 +43,7 @@
shortcut="e" />
<bookmark
category="android.intent.category.APP_CALENDAR"
- shortcut="l" />
+ shortcut="k" />
<bookmark
category="android.intent.category.APP_MAPS"
shortcut="m" />
@@ -56,4 +53,7 @@
<bookmark
category="android.intent.category.APP_MESSAGING"
shortcut="s" />
+ <bookmark
+ category="android.intent.category.APP_CALCULATOR"
+ shortcut="u" />
</bookmarks>
diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java
index 503859b..9192086 100644
--- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java
+++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java
@@ -1144,7 +1144,7 @@
Slog.v(LOG_TAG, "getConfigSetting(" + name + ")");
}
- DeviceConfig.enforceReadPermission(/*namespace=*/name.split("/")[0]);
+ Settings.Config.enforceReadPermission(/*namespace=*/name.split("/")[0]);
// Get the value.
synchronized (mLock) {
@@ -1317,7 +1317,7 @@
Slog.v(LOG_TAG, "getAllConfigFlags() for " + prefix);
}
- DeviceConfig.enforceReadPermission(
+ Settings.Config.enforceReadPermission(
prefix != null ? prefix.split("/")[0] : null);
synchronized (mLock) {
diff --git a/packages/Shell/AndroidManifest.xml b/packages/Shell/AndroidManifest.xml
index d3ba5e6..68455c8 100644
--- a/packages/Shell/AndroidManifest.xml
+++ b/packages/Shell/AndroidManifest.xml
@@ -360,6 +360,9 @@
<!-- Permission needed to test wallpaper dimming -->
<uses-permission android:name="android.permission.SET_WALLPAPER_DIM_AMOUNT" />
+ <!-- Permission needed to test wallpapers supporting ambient mode -->
+ <uses-permission android:name="android.permission.AMBIENT_WALLPAPER" />
+
<!-- Permission required to test ContentResolver caching. -->
<uses-permission android:name="android.permission.CACHE_CONTENT" />
@@ -774,6 +777,9 @@
<!-- Permissions required for CTS test - CtsAppFgsTestCases -->
<uses-permission android:name="android.permission.USE_EXACT_ALARM" />
+ <!-- Permission required for CTS test - CtsHardwareTestCases -->
+ <uses-permission android:name="android.permission.REMAP_MODIFIER_KEYS" />
+
<!-- Permissions required for CTS test - CtsAppFgsTestCases -->
<uses-permission android:name="android.permission.health.READ_ACTIVE_CALORIES_BURNED" />
<uses-permission android:name="android.permission.health.READ_BASAL_BODY_TEMPERATURE" />
diff --git a/packages/SystemUI/animation/Android.bp b/packages/SystemUI/animation/Android.bp
index 17ad55f..8acc2f8 100644
--- a/packages/SystemUI/animation/Android.bp
+++ b/packages/SystemUI/animation/Android.bp
@@ -44,23 +44,3 @@
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
deleted file mode 100644
index 3dc8510..0000000
--- a/packages/SystemUI/animation/TEST_MAPPING
+++ /dev/null
@@ -1,7 +0,0 @@
-{
- "presubmit": [
- {
- "name": "SystemUIAnimationLibTests"
- }
- ]
-}
diff --git a/packages/SystemUI/res-keyguard/layout/fgs_footer.xml b/packages/SystemUI/res-keyguard/layout/fgs_footer.xml
deleted file mode 100644
index ee588f99..0000000
--- a/packages/SystemUI/res-keyguard/layout/fgs_footer.xml
+++ /dev/null
@@ -1,105 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
- Copyright (C) 2022 The Android Open Source Project
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
--->
-<!-- TODO(b/242040009): Remove this file. -->
-<FrameLayout
- xmlns:android="http://schemas.android.com/apk/res/android"
- android:layout_width="0dp"
- android:layout_height="@dimen/qs_security_footer_single_line_height"
- android:layout_weight="1"
- android:gravity="center"
- android:clickable="true"
- android:visibility="gone">
-
- <LinearLayout
- android:id="@+id/fgs_text_container"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:layout_marginEnd="@dimen/qs_footer_action_inset"
- android:background="@drawable/qs_security_footer_background"
- android:layout_gravity="end"
- android:gravity="center"
- android:paddingHorizontal="@dimen/qs_footer_padding"
- >
-
- <ImageView
- android:id="@+id/primary_footer_icon"
- android:layout_width="@dimen/qs_footer_icon_size"
- android:layout_height="@dimen/qs_footer_icon_size"
- android:gravity="start"
- android:layout_marginEnd="12dp"
- android:contentDescription="@null"
- android:src="@drawable/ic_info_outline"
- android:tint="?android:attr/textColorSecondary" />
-
- <TextView
- android:id="@+id/footer_text"
- android:layout_width="0dp"
- android:layout_height="wrap_content"
- android:layout_weight="1"
- android:maxLines="1"
- android:ellipsize="end"
- android:textAppearance="@style/TextAppearance.QS.SecurityFooter"
- android:textColor="?android:attr/textColorSecondary"/>
-
- <ImageView
- android:id="@+id/fgs_new"
- android:layout_width="12dp"
- android:layout_height="12dp"
- android:scaleType="fitCenter"
- android:src="@drawable/fgs_dot"
- android:contentDescription="@string/fgs_dot_content_description"
- />
-
- <ImageView
- android:id="@+id/footer_icon"
- android:layout_width="@dimen/qs_footer_icon_size"
- android:layout_height="@dimen/qs_footer_icon_size"
- android:layout_marginStart="8dp"
- android:contentDescription="@null"
- android:src="@*android:drawable/ic_chevron_end"
- android:autoMirrored="true"
- android:tint="?android:attr/textColorSecondary" />
- </LinearLayout>
-
- <FrameLayout
- android:id="@+id/fgs_number_container"
- android:layout_width="@dimen/qs_footer_action_button_size"
- android:layout_height="@dimen/qs_footer_action_button_size"
- android:background="@drawable/qs_footer_action_circle"
- android:focusable="true"
- android:visibility="gone">
-
- <TextView
- android:id="@+id/fgs_number"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:textAppearance="@style/TextAppearance.QS.SecurityFooter"
- android:layout_gravity="center"
- android:textColor="?android:attr/textColorPrimary"
- android:textSize="18sp"/>
- <ImageView
- android:id="@+id/fgs_collapsed_new"
- android:layout_width="12dp"
- android:layout_height="12dp"
- android:scaleType="fitCenter"
- android:layout_gravity="bottom|end"
- android:src="@drawable/fgs_dot"
- android:contentDescription="@string/fgs_dot_content_description"
- />
- </FrameLayout>
-
-</FrameLayout>
\ No newline at end of file
diff --git a/packages/SystemUI/res-keyguard/layout/footer_actions.xml b/packages/SystemUI/res-keyguard/layout/footer_actions.xml
index 2261ae8..4a2a1cb 100644
--- a/packages/SystemUI/res-keyguard/layout/footer_actions.xml
+++ b/packages/SystemUI/res-keyguard/layout/footer_actions.xml
@@ -16,10 +16,8 @@
-->
<!-- Action buttons for footer in QS/QQS, containing settings button, power off button etc -->
-<!-- TODO(b/242040009): Clean up this file. -->
-<com.android.systemui.qs.FooterActionsView
+<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
android:layout_width="match_parent"
android:layout_height="@dimen/footer_actions_height"
android:elevation="@dimen/qs_panel_elevation"
@@ -28,74 +26,4 @@
android:background="@drawable/qs_footer_actions_background"
android:gravity="center_vertical|end"
android:layout_gravity="bottom"
->
-
- <LinearLayout
- android:id="@+id/security_footers_container"
- android:orientation="horizontal"
- android:layout_height="@dimen/qs_footer_action_button_size"
- android:layout_width="0dp"
- android:layout_weight="1"
- />
-
- <!-- Negative margin equal to -->
- <LinearLayout
- android:layout_height="match_parent"
- android:layout_width="wrap_content"
- android:layout_marginEnd="@dimen/qs_footer_action_inset_negative"
- >
-
- <com.android.systemui.statusbar.phone.MultiUserSwitch
- android:id="@id/multi_user_switch"
- android:layout_width="@dimen/qs_footer_action_button_size"
- android:layout_height="@dimen/qs_footer_action_button_size"
- android:background="@drawable/qs_footer_action_circle"
- android:focusable="true">
-
- <ImageView
- android:id="@+id/multi_user_avatar"
- android:layout_width="@dimen/qs_footer_icon_size"
- android:layout_height="@dimen/qs_footer_icon_size"
- android:layout_gravity="center"
- android:scaleType="centerInside" />
- </com.android.systemui.statusbar.phone.MultiUserSwitch>
-
- <com.android.systemui.statusbar.AlphaOptimizedFrameLayout
- android:id="@id/settings_button_container"
- android:layout_width="@dimen/qs_footer_action_button_size"
- android:layout_height="@dimen/qs_footer_action_button_size"
- android:background="@drawable/qs_footer_action_circle"
- android:clipChildren="false"
- android:clipToPadding="false">
-
- <com.android.systemui.statusbar.phone.SettingsButton
- android:id="@+id/settings_button"
- android:layout_width="@dimen/qs_footer_icon_size"
- android:layout_height="@dimen/qs_footer_icon_size"
- android:layout_gravity="center"
- android:background="@android:color/transparent"
- android:focusable="false"
- android:clickable="false"
- android:importantForAccessibility="yes"
- android:contentDescription="@string/accessibility_quick_settings_settings"
- android:scaleType="centerInside"
- android:src="@drawable/ic_settings"
- android:tint="?android:attr/textColorPrimary" />
-
- </com.android.systemui.statusbar.AlphaOptimizedFrameLayout>
-
- <com.android.systemui.statusbar.AlphaOptimizedImageView
- android:id="@id/pm_lite"
- android:layout_width="@dimen/qs_footer_action_button_size"
- android:layout_height="@dimen/qs_footer_action_button_size"
- android:background="@drawable/qs_footer_action_circle_color"
- android:clickable="true"
- android:clipToPadding="false"
- android:focusable="true"
- android:padding="@dimen/qs_footer_icon_padding"
- android:src="@*android:drawable/ic_lock_power_off"
- android:contentDescription="@string/accessibility_quick_settings_power_menu"
- android:tint="?androidprv:attr/textColorOnAccent" />
-
- </LinearLayout>
-</com.android.systemui.qs.FooterActionsView>
\ No newline at end of file
+/>
\ No newline at end of file
diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_num_pad_key.xml b/packages/SystemUI/res-keyguard/layout/keyguard_num_pad_key.xml
index 316ad39..411fea5 100644
--- a/packages/SystemUI/res-keyguard/layout/keyguard_num_pad_key.xml
+++ b/packages/SystemUI/res-keyguard/layout/keyguard_num_pad_key.xml
@@ -18,8 +18,6 @@
<TextView
android:id="@+id/digit_text"
style="@style/Widget.TextView.NumPadKey.Digit"
- android:autoSizeMaxTextSize="32sp"
- android:autoSizeTextType="uniform"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
/>
diff --git a/packages/SystemUI/res/drawable/ic_circle_check_box.xml b/packages/SystemUI/res/drawable/ic_circle_check_box.xml
index b44a32d..00c10ce 100644
--- a/packages/SystemUI/res/drawable/ic_circle_check_box.xml
+++ b/packages/SystemUI/res/drawable/ic_circle_check_box.xml
@@ -18,7 +18,7 @@
<item
android:id="@+id/checked"
android:state_checked="true"
- android:drawable="@drawable/media_output_status_check" />
+ android:drawable="@drawable/media_output_status_filled_checked" />
<item
android:id="@+id/unchecked"
android:state_checked="false"
diff --git a/packages/SystemUI/res/drawable/media_output_dialog_seekbar_background.xml b/packages/SystemUI/res/drawable/media_output_dialog_seekbar_background.xml
index 55dce8f..43cf003 100644
--- a/packages/SystemUI/res/drawable/media_output_dialog_seekbar_background.xml
+++ b/packages/SystemUI/res/drawable/media_output_dialog_seekbar_background.xml
@@ -17,7 +17,10 @@
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:id="@android:id/background">
<shape>
- <corners android:radius="28dp" />
+ <corners
+ android:bottomRightRadius="28dp"
+ android:topRightRadius="28dp"
+ />
<solid android:color="@android:color/transparent" />
<size
android:height="64dp"/>
diff --git a/packages/SystemUI/res/drawable/media_output_icon_volume_off.xml b/packages/SystemUI/res/drawable/media_output_icon_volume_off.xml
new file mode 100644
index 0000000..f29f44c
--- /dev/null
+++ b/packages/SystemUI/res/drawable/media_output_icon_volume_off.xml
@@ -0,0 +1,27 @@
+<!--
+ ~ 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="48dp"
+ android:height="48dp"
+ android:viewportWidth="48"
+ android:viewportHeight="48"
+ android:tint="?attr/colorControlNormal"
+ android:autoMirrored="true">
+ <path
+ android:fillColor="@color/media_dialog_item_main_content"
+ android:pathData="M40.65,45.2 L34.05,38.6Q32.65,39.6 31.025,40.325Q29.4,41.05 27.65,41.45V38.35Q28.8,38 29.875,37.575Q30.95,37.15 31.9,36.45L23.65,28.15V40L13.65,30H5.65V18H13.45L2.45,7L4.6,4.85L42.8,43ZM38.85,33.6 L36.7,31.45Q37.7,29.75 38.175,27.85Q38.65,25.95 38.65,23.95Q38.65,18.8 35.65,14.725Q32.65,10.65 27.65,9.55V6.45Q33.85,7.85 37.75,12.725Q41.65,17.6 41.65,23.95Q41.65,26.5 40.95,28.95Q40.25,31.4 38.85,33.6ZM32.15,26.9 L27.65,22.4V15.9Q30,17 31.325,19.2Q32.65,21.4 32.65,24Q32.65,24.75 32.525,25.475Q32.4,26.2 32.15,26.9ZM23.65,18.4 L18.45,13.2 23.65,8ZM20.65,32.7V25.2L16.45,21H8.65V27H14.95ZM18.55,23.1Z"/>
+</vector>
diff --git a/packages/SystemUI/res/drawable/media_output_item_check_box.xml b/packages/SystemUI/res/drawable/media_output_item_check_box.xml
new file mode 100644
index 0000000..a0742900
--- /dev/null
+++ b/packages/SystemUI/res/drawable/media_output_item_check_box.xml
@@ -0,0 +1,26 @@
+<!--
+ ~ Copyright (C) 2022 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item
+ android:id="@+id/checked"
+ android:state_checked="true"
+ android:drawable="@drawable/media_output_status_checked" />
+ <item
+ android:id="@+id/unchecked"
+ android:state_checked="false"
+ android:drawable="@drawable/media_output_status_selectable" />
+</selector>
diff --git a/packages/SystemUI/res/drawable/media_output_status_checked.xml b/packages/SystemUI/res/drawable/media_output_status_checked.xml
new file mode 100644
index 0000000..8f83ee2
--- /dev/null
+++ b/packages/SystemUI/res/drawable/media_output_status_checked.xml
@@ -0,0 +1,26 @@
+<!--
+ ~ Copyright (C) 2022 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<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="@android:color/white"
+ android:pathData="M9.55,18 L3.85,12.3 5.275,10.875 9.55,15.15 18.725,5.975 20.15,7.4Z"/>
+</vector>
diff --git a/packages/SystemUI/res/drawable/media_output_status_check.xml b/packages/SystemUI/res/drawable/media_output_status_filled_checked.xml
similarity index 100%
rename from packages/SystemUI/res/drawable/media_output_status_check.xml
rename to packages/SystemUI/res/drawable/media_output_status_filled_checked.xml
diff --git a/packages/SystemUI/res/drawable/media_output_status_selectable.xml b/packages/SystemUI/res/drawable/media_output_status_selectable.xml
new file mode 100644
index 0000000..5465aa7
--- /dev/null
+++ b/packages/SystemUI/res/drawable/media_output_status_selectable.xml
@@ -0,0 +1,26 @@
+<!--
+ ~ Copyright (C) 2022 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<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="@android:color/white"
+ android:pathData="M11,19V13H5V11H11V5H13V11H19V13H13V19Z"/>
+</vector>
diff --git a/packages/SystemUI/res/drawable/media_output_title_icon_area.xml b/packages/SystemUI/res/drawable/media_output_title_icon_area.xml
new file mode 100644
index 0000000..b937937
--- /dev/null
+++ b/packages/SystemUI/res/drawable/media_output_title_icon_area.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.
+ -->
+
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="rectangle">
+ <corners
+ android:bottomLeftRadius="28dp"
+ android:topLeftRadius="28dp"
+ android:bottomRightRadius="0dp"
+ android:topRightRadius="0dp"/>
+ <solid android:color="@color/media_dialog_item_background" />
+</shape>
\ No newline at end of file
diff --git a/packages/SystemUI/res/layout/media_output_list_item_advanced.xml b/packages/SystemUI/res/layout/media_output_list_item_advanced.xml
new file mode 100644
index 0000000..d49b9f1
--- /dev/null
+++ b/packages/SystemUI/res/layout/media_output_list_item_advanced.xml
@@ -0,0 +1,156 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2020 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<FrameLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/device_container"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+ <FrameLayout
+ android:layout_width="match_parent"
+ android:layout_height="64dp"
+ android:id="@+id/item_layout"
+ android:background="@drawable/media_output_item_background"
+ android:layout_marginStart="16dp"
+ android:layout_marginEnd="80dp"
+ android:layout_marginBottom="12dp">
+ <FrameLayout
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_gravity="center_vertical|start">
+ <com.android.systemui.media.dialog.MediaOutputSeekbar
+ android:id="@+id/volume_seekbar"
+ android:splitTrack="false"
+ android:visibility="gone"
+ android:paddingStart="64dp"
+ android:paddingEnd="0dp"
+ android:background="@null"
+ android:contentDescription="@string/media_output_dialog_accessibility_seekbar"
+ android:progressDrawable="@drawable/media_output_dialog_seekbar_background"
+ android:thumb="@null"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"/>
+ </FrameLayout>
+
+ <FrameLayout
+ android:id="@+id/icon_area"
+ android:layout_width="64dp"
+ android:layout_height="64dp"
+ android:background="@drawable/media_output_title_icon_area"
+ android:layout_gravity="center_vertical|start">
+ <ImageView
+ android:id="@+id/title_icon"
+ android:layout_width="24dp"
+ android:layout_height="24dp"
+ android:animateLayoutChanges="true"
+ android:layout_gravity="center"/>
+ <TextView
+ android:id="@+id/volume_value"
+ android:animateLayoutChanges="true"
+ android:layout_gravity="center"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:fontFamily="@*android:string/config_headlineFontFamilyMedium"
+ android:textSize="16sp"
+ android:visibility="gone"/>
+ </FrameLayout>
+
+ <TextView
+ android:id="@+id/title"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_vertical|start"
+ android:layout_marginStart="72dp"
+ android:layout_marginEnd="56dp"
+ android:ellipsize="end"
+ android:maxLines="1"
+ android:fontFamily="@*android:string/config_headlineFontFamilyMedium"
+ android:textSize="16sp"/>
+
+ <LinearLayout
+ android:id="@+id/two_line_layout"
+ android:orientation="vertical"
+ android:layout_width="wrap_content"
+ android:layout_gravity="center_vertical|start"
+ android:layout_height="48dp"
+ android:layout_marginEnd="56dp"
+ android:layout_marginStart="72dp">
+ <TextView
+ android:id="@+id/two_line_title"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:ellipsize="end"
+ android:maxLines="1"
+ android:fontFamily="@*android:string/config_headlineFontFamilyMedium"
+ android:textColor="@color/media_dialog_item_main_content"
+ android:textSize="16sp"/>
+ <TextView
+ android:id="@+id/subtitle"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:ellipsize="end"
+ android:maxLines="1"
+ android:textColor="@color/media_dialog_item_main_content"
+ android:textSize="14sp"
+ android:fontFamily="@*android:string/config_bodyFontFamily"
+ android:visibility="gone"/>
+ </LinearLayout>
+
+ <ProgressBar
+ android:id="@+id/volume_indeterminate_progress"
+ style="?android:attr/progressBarStyleSmallTitle"
+ android:layout_width="24dp"
+ android:layout_height="24dp"
+ android:layout_marginEnd="16dp"
+ android:indeterminate="true"
+ android:layout_gravity="end|center"
+ android:indeterminateOnly="true"
+ android:visibility="gone"/>
+
+ <ImageView
+ android:id="@+id/media_output_item_status"
+ android:layout_width="24dp"
+ android:layout_height="24dp"
+ android:layout_marginEnd="16dp"
+ android:indeterminate="true"
+ android:layout_gravity="end|center"
+ android:indeterminateOnly="true"
+ android:importantForAccessibility="no"
+ android:visibility="gone"/>
+ </FrameLayout>
+ <FrameLayout
+ android:id="@+id/end_action_area"
+ android:layout_width="64dp"
+ android:layout_height="64dp"
+ android:visibility="gone"
+ android:layout_marginBottom="6dp"
+ android:layout_marginEnd="8dp"
+ android:layout_gravity="end|center"
+ android:gravity="center"
+ android:background="@drawable/media_output_item_background_active">
+ <CheckBox
+ android:id="@+id/check_box"
+ android:focusable="false"
+ android:importantForAccessibility="no"
+ android:layout_gravity="center"
+ android:layout_width="24dp"
+ android:layout_height="24dp"
+ android:button="@drawable/media_output_item_check_box"
+ android:visibility="gone"
+ />
+ </FrameLayout>
+</FrameLayout>
\ No newline at end of file
diff --git a/packages/SystemUI/res/layout/quick_settings_security_footer.xml b/packages/SystemUI/res/layout/quick_settings_security_footer.xml
deleted file mode 100644
index 194f3dd..0000000
--- a/packages/SystemUI/res/layout/quick_settings_security_footer.xml
+++ /dev/null
@@ -1,61 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
- Copyright (C) 2014 The Android Open Source Project
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
--->
-<!-- TODO(b/242040009): Remove this file. -->
-<LinearLayout
- xmlns:android="http://schemas.android.com/apk/res/android"
- android:layout_width="0dp"
- android:layout_height="@dimen/qs_security_footer_single_line_height"
- android:layout_weight="1"
- android:clickable="true"
- android:orientation="horizontal"
- android:paddingHorizontal="@dimen/qs_footer_padding"
- android:gravity="center_vertical"
- android:layout_gravity="center_vertical|center_horizontal"
- android:layout_marginEnd="@dimen/qs_footer_action_inset"
- android:background="@drawable/qs_security_footer_background"
- >
-
- <ImageView
- android:id="@+id/primary_footer_icon"
- android:layout_width="@dimen/qs_footer_icon_size"
- android:layout_height="@dimen/qs_footer_icon_size"
- android:gravity="start"
- android:layout_marginEnd="12dp"
- android:contentDescription="@null"
- android:tint="?android:attr/textColorSecondary" />
-
- <TextView
- android:id="@+id/footer_text"
- android:layout_width="0dp"
- android:layout_height="wrap_content"
- android:layout_weight="1"
- android:singleLine="true"
- android:ellipsize="end"
- android:textAppearance="@style/TextAppearance.QS.SecurityFooter"
- android:textColor="?android:attr/textColorSecondary"/>
-
- <ImageView
- android:id="@+id/footer_icon"
- android:layout_width="@dimen/qs_footer_icon_size"
- android:layout_height="@dimen/qs_footer_icon_size"
- android:layout_marginStart="8dp"
- android:contentDescription="@null"
- android:src="@*android:drawable/ic_chevron_end"
- android:autoMirrored="true"
- android:tint="?android:attr/textColorSecondary" />
-
-</LinearLayout>
diff --git a/packages/SystemUI/res/layout/super_notification_shade.xml b/packages/SystemUI/res/layout/super_notification_shade.xml
index bafdb11..4abc176 100644
--- a/packages/SystemUI/res/layout/super_notification_shade.xml
+++ b/packages/SystemUI/res/layout/super_notification_shade.xml
@@ -98,16 +98,18 @@
android:singleLine="true"
android:ellipsize="marquee"
android:focusable="true" />
- <FrameLayout android:id="@+id/keyguard_bouncer_container"
- android:layout_height="0dp"
- android:layout_width="match_parent"
- android:layout_weight="1"
- android:background="@android:color/transparent"
- android:visibility="invisible"
- android:clipChildren="false"
- android:clipToPadding="false" />
</LinearLayout>
+ <FrameLayout android:id="@+id/keyguard_bouncer_container"
+ android:paddingTop="@dimen/status_bar_height"
+ android:layout_height="match_parent"
+ android:layout_width="match_parent"
+ android:layout_weight="1"
+ android:background="@android:color/transparent"
+ android:visibility="invisible"
+ android:clipChildren="false"
+ android:clipToPadding="false" />
+
<com.android.systemui.biometrics.AuthRippleView
android:id="@+id/auth_ripple"
android:layout_width="match_parent"
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index dc7e4e4..73baeac 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -1222,6 +1222,8 @@
<dimen name="media_output_dialog_app_tier_icon_size">20dp</dimen>
<dimen name="media_output_dialog_background_radius">16dp</dimen>
<dimen name="media_output_dialog_active_background_radius">28dp</dimen>
+ <dimen name="media_output_dialog_default_margin_end">16dp</dimen>
+ <dimen name="media_output_dialog_selectable_margin_end">80dp</dimen>
<!-- Distance that the full shade transition takes in order to complete by tapping on a button
like "expand". -->
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index 9ee918d..c0d69d8 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -2481,7 +2481,7 @@
<!-- Controls menu, edit [CHAR_LIMIT=30] -->
<string name="controls_menu_edit">Edit controls</string>
- <!-- Title for the media output group dialog with media related devices [CHAR LIMIT=50] -->
+ <!-- Title for the media output dialog with media related devices [CHAR LIMIT=50] -->
<string name="media_output_dialog_add_output">Add outputs</string>
<!-- Title for the media output slice with group devices [CHAR LIMIT=50] -->
<string name="media_output_dialog_group">Group</string>
@@ -2505,6 +2505,8 @@
<string name="media_output_dialog_accessibility_title">Available devices for audio output.</string>
<!-- Accessibility text describing purpose of seekbar in media output dialog. [CHAR LIMIT=NONE] -->
<string name="media_output_dialog_accessibility_seekbar">Volume</string>
+ <!-- Summary for media output volume of a device in percentage [CHAR LIMIT=NONE] -->
+ <string name="media_output_dialog_volume_percentage"><xliff:g id="percentage" example="10">%1$d</xliff:g>%%</string>
<!-- Media Output Broadcast Dialog -->
<!-- Title for Broadcast First Notify Dialog [CHAR LIMIT=60] -->
diff --git a/packages/SystemUI/screenshot/src/com/android/systemui/testing/screenshot/ViewScreenshotTestRule.kt b/packages/SystemUI/screenshot/src/com/android/systemui/testing/screenshot/ViewScreenshotTestRule.kt
index 6d0cc5e..738b37c 100644
--- a/packages/SystemUI/screenshot/src/com/android/systemui/testing/screenshot/ViewScreenshotTestRule.kt
+++ b/packages/SystemUI/screenshot/src/com/android/systemui/testing/screenshot/ViewScreenshotTestRule.kt
@@ -34,7 +34,6 @@
import platform.test.screenshot.DeviceEmulationRule
import platform.test.screenshot.DeviceEmulationSpec
import platform.test.screenshot.MaterialYouColorsRule
-import platform.test.screenshot.PathConfig
import platform.test.screenshot.ScreenshotTestRule
import platform.test.screenshot.getEmulatedDevicePathConfig
import platform.test.screenshot.matchers.BitmapMatcher
@@ -43,7 +42,6 @@
class ViewScreenshotTestRule(
emulationSpec: DeviceEmulationSpec,
private val matcher: BitmapMatcher = UnitTestBitmapMatcher,
- pathConfig: PathConfig = getEmulatedDevicePathConfig(emulationSpec),
assetsPathRelativeToBuildRoot: String
) : TestRule {
private val colorsRule = MaterialYouColorsRule()
diff --git a/packages/SystemUI/shared/src/com/android/systemui/flags/FlagManager.kt b/packages/SystemUI/shared/src/com/android/systemui/flags/FlagManager.kt
index d172690..d85292a 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/flags/FlagManager.kt
+++ b/packages/SystemUI/shared/src/com/android/systemui/flags/FlagManager.kt
@@ -38,6 +38,7 @@
const val ACTION_SET_FLAG = "com.android.systemui.action.SET_FLAG"
const val ACTION_GET_FLAGS = "com.android.systemui.action.GET_FLAGS"
const val FLAGS_PERMISSION = "com.android.systemui.permission.FLAGS"
+ const val ACTION_SYSUI_STARTED = "com.android.systemui.STARTED"
const val EXTRA_ID = "id"
const val EXTRA_VALUE = "value"
const val EXTRA_FLAGS = "flags"
diff --git a/packages/SystemUI/src/com/android/systemui/util/condition/CombinedCondition.kt b/packages/SystemUI/shared/src/com/android/systemui/shared/condition/CombinedCondition.kt
similarity index 96%
rename from packages/SystemUI/src/com/android/systemui/util/condition/CombinedCondition.kt
rename to packages/SystemUI/shared/src/com/android/systemui/shared/condition/CombinedCondition.kt
index da81d54..2d83458 100644
--- a/packages/SystemUI/src/com/android/systemui/util/condition/CombinedCondition.kt
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/condition/CombinedCondition.kt
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.systemui.util.condition
+package com.android.systemui.shared.condition
/**
* A higher order [Condition] which combines multiple conditions with a specified
diff --git a/packages/SystemUI/src/com/android/systemui/util/condition/Condition.java b/packages/SystemUI/shared/src/com/android/systemui/shared/condition/Condition.java
similarity index 80%
rename from packages/SystemUI/src/com/android/systemui/util/condition/Condition.java
rename to packages/SystemUI/shared/src/com/android/systemui/shared/condition/Condition.java
index b39adef..cc48090e 100644
--- a/packages/SystemUI/src/com/android/systemui/util/condition/Condition.java
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/condition/Condition.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2021 The Android Open Source Project
+ * Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,13 +14,14 @@
* limitations under the License.
*/
-package com.android.systemui.util.condition;
+package com.android.systemui.shared.condition;
import android.util.Log;
-import com.android.systemui.statusbar.policy.CallbackController;
-
-import org.jetbrains.annotations.NotNull;
+import androidx.annotation.NonNull;
+import androidx.lifecycle.Lifecycle;
+import androidx.lifecycle.LifecycleEventObserver;
+import androidx.lifecycle.LifecycleOwner;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
@@ -33,7 +34,7 @@
* Base class for a condition that needs to be fulfilled in order for {@link Monitor} to inform
* its callbacks.
*/
-public abstract class Condition implements CallbackController<Condition.Callback> {
+public abstract class Condition {
private final String mTag = getClass().getSimpleName();
private final ArrayList<WeakReference<Callback>> mCallbacks = new ArrayList<>();
@@ -79,8 +80,7 @@
* Registers a callback to receive updates once started. This should be called before
* {@link #start()}. Also triggers the callback immediately if already started.
*/
- @Override
- public void addCallback(@NotNull Callback callback) {
+ public void addCallback(@NonNull Callback callback) {
if (shouldLog()) Log.d(mTag, "adding callback");
mCallbacks.add(new WeakReference<>(callback));
@@ -96,8 +96,7 @@
/**
* Removes the provided callback from further receiving updates.
*/
- @Override
- public void removeCallback(@NotNull Callback callback) {
+ public void removeCallback(@NonNull Callback callback) {
if (shouldLog()) Log.d(mTag, "removing callback");
final Iterator<WeakReference<Callback>> iterator = mCallbacks.iterator();
while (iterator.hasNext()) {
@@ -116,6 +115,29 @@
}
/**
+ * Wrapper to {@link #addCallback(Callback)} when a lifecycle is in the resumed state
+ * and {@link #removeCallback(Callback)} when not resumed automatically.
+ */
+ public Callback observe(LifecycleOwner owner, Callback listener) {
+ return observe(owner.getLifecycle(), listener);
+ }
+
+ /**
+ * Wrapper to {@link #addCallback(Callback)} when a lifecycle is in the resumed state
+ * and {@link #removeCallback(Condition.Callback)} when not resumed automatically.
+ */
+ public Callback observe(Lifecycle lifecycle, Callback listener) {
+ lifecycle.addObserver((LifecycleEventObserver) (lifecycleOwner, event) -> {
+ if (event == Lifecycle.Event.ON_RESUME) {
+ addCallback(listener);
+ } else if (event == Lifecycle.Event.ON_PAUSE) {
+ removeCallback(listener);
+ }
+ });
+ return listener;
+ }
+
+ /**
* Updates the value for whether the condition has been fulfilled, and sends an update if the
* value changes and any callback is registered.
*
@@ -187,7 +209,7 @@
* Creates a new condition which will only be true when both this condition and all the provided
* conditions are true.
*/
- public Condition and(Collection<Condition> others) {
+ public Condition and(@NonNull Collection<Condition> others) {
final List<Condition> conditions = new ArrayList<>(others);
conditions.add(this);
return new CombinedCondition(conditions, Evaluator.OP_AND);
@@ -197,7 +219,7 @@
* Creates a new condition which will only be true when both this condition and the provided
* condition is true.
*/
- public Condition and(Condition other) {
+ public Condition and(@NonNull Condition other) {
return new CombinedCondition(Arrays.asList(this, other), Evaluator.OP_AND);
}
@@ -205,7 +227,7 @@
* Creates a new condition which will only be true when either this condition or any of the
* provided conditions are true.
*/
- public Condition or(Collection<Condition> others) {
+ public Condition or(@NonNull Collection<Condition> others) {
final List<Condition> conditions = new ArrayList<>(others);
conditions.add(this);
return new CombinedCondition(conditions, Evaluator.OP_OR);
@@ -215,7 +237,7 @@
* Creates a new condition which will only be true when either this condition or the provided
* condition is true.
*/
- public Condition or(Condition other) {
+ public Condition or(@NonNull Condition other) {
return new CombinedCondition(Arrays.asList(this, other), Evaluator.OP_OR);
}
diff --git a/packages/SystemUI/src/com/android/systemui/util/condition/Evaluator.kt b/packages/SystemUI/shared/src/com/android/systemui/shared/condition/Evaluator.kt
similarity index 81%
rename from packages/SystemUI/src/com/android/systemui/util/condition/Evaluator.kt
rename to packages/SystemUI/shared/src/com/android/systemui/shared/condition/Evaluator.kt
index cf44e84..23742c5 100644
--- a/packages/SystemUI/src/com/android/systemui/util/condition/Evaluator.kt
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/condition/Evaluator.kt
@@ -1,4 +1,20 @@
-package com.android.systemui.util.condition
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.shared.condition
import android.annotation.IntDef
diff --git a/packages/SystemUI/src/com/android/systemui/util/condition/Monitor.java b/packages/SystemUI/shared/src/com/android/systemui/shared/condition/Monitor.java
similarity index 96%
rename from packages/SystemUI/src/com/android/systemui/util/condition/Monitor.java
rename to packages/SystemUI/shared/src/com/android/systemui/shared/condition/Monitor.java
index 24bc907..95675ce 100644
--- a/packages/SystemUI/src/com/android/systemui/util/condition/Monitor.java
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/condition/Monitor.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2021 The Android Open Source Project
+ * Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,14 +14,14 @@
* limitations under the License.
*/
-package com.android.systemui.util.condition;
+package com.android.systemui.shared.condition;
import android.util.ArraySet;
import android.util.Log;
-import com.android.systemui.dagger.qualifiers.Main;
+import androidx.annotation.NonNull;
-import org.jetbrains.annotations.NotNull;
+import com.android.systemui.dagger.qualifiers.Main;
import java.util.ArrayList;
import java.util.Collections;
@@ -100,7 +100,7 @@
* @param subscription A {@link Subscription} detailing the desired conditions and callback.
* @return A {@link Subscription.Token} that can be used to remove the subscription.
*/
- public Subscription.Token addSubscription(@NotNull Subscription subscription) {
+ public Subscription.Token addSubscription(@NonNull Subscription subscription) {
final Subscription.Token token = new Subscription.Token();
final SubscriptionState state = new SubscriptionState(subscription);
@@ -131,7 +131,7 @@
* @param token The {@link Subscription.Token} returned when the {@link Subscription} was
* originally added.
*/
- public void removeSubscription(@NotNull Subscription.Token token) {
+ public void removeSubscription(@NonNull Subscription.Token token) {
mExecutor.execute(() -> {
if (shouldLog()) Log.d(mTag, "removing subscription");
if (!mSubscriptions.containsKey(token)) {
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardAbsKeyInputViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardAbsKeyInputViewController.java
index 860c8e3..7da27b1 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardAbsKeyInputViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardAbsKeyInputViewController.java
@@ -260,7 +260,8 @@
if (reason != PROMPT_REASON_NONE) {
int promtReasonStringRes = mView.getPromptReasonStringRes(reason);
if (promtReasonStringRes != 0) {
- mMessageAreaController.setMessage(promtReasonStringRes);
+ mMessageAreaController.setMessage(
+ mView.getResources().getString(promtReasonStringRes), false);
}
}
}
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardInputViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardInputViewController.java
index 2e9ad58..53b569a 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardInputViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardInputViewController.java
@@ -143,7 +143,9 @@
public void startAppearAnimation() {
if (TextUtils.isEmpty(mMessageAreaController.getMessage())) {
- mMessageAreaController.setMessage(getInitialMessageResId());
+ mMessageAreaController.setMessage(
+ mView.getResources().getString(getInitialMessageResId()),
+ /* animate= */ false);
}
mView.startAppearAnimation();
}
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPINView.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPINView.java
index 5d86ccd..67e3400 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardPINView.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPINView.java
@@ -52,6 +52,7 @@
private int mYTransOffset;
private View mBouncerMessageView;
@DevicePostureInt private int mLastDevicePosture = DEVICE_POSTURE_UNKNOWN;
+ public static final long ANIMATION_DURATION = 650;
public KeyguardPINView(Context context) {
this(context, null);
@@ -181,7 +182,7 @@
if (mAppearAnimator.isRunning()) {
mAppearAnimator.cancel();
}
- mAppearAnimator.setDuration(650);
+ mAppearAnimator.setDuration(ANIMATION_DURATION);
mAppearAnimator.addUpdateListener(animation -> animate(animation.getAnimatedFraction()));
mAppearAnimator.start();
}
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java
index 8f3484a..5d7a6f1 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java
@@ -36,8 +36,11 @@
import static java.lang.Integer.max;
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
+import android.animation.ValueAnimator;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.admin.DevicePolicyManager;
@@ -967,11 +970,23 @@
}
mUserSwitcherViewGroup.setAlpha(0f);
- ObjectAnimator alphaAnim = ObjectAnimator.ofFloat(mUserSwitcherViewGroup, View.ALPHA,
- 1f);
- alphaAnim.setInterpolator(Interpolators.ALPHA_IN);
- alphaAnim.setDuration(500);
- alphaAnim.start();
+ ValueAnimator animator = ValueAnimator.ofFloat(0f, 1f);
+ int yTrans = mView.getResources().getDimensionPixelSize(R.dimen.pin_view_trans_y_entry);
+ animator.setInterpolator(Interpolators.STANDARD_DECELERATE);
+ animator.setDuration(650);
+ animator.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ mUserSwitcherViewGroup.setAlpha(1f);
+ mUserSwitcherViewGroup.setTranslationY(0f);
+ }
+ });
+ animator.addUpdateListener(animation -> {
+ float value = (float) animation.getAnimatedValue();
+ mUserSwitcherViewGroup.setAlpha(value);
+ mUserSwitcherViewGroup.setTranslationY(yTrans - yTrans * value);
+ });
+ animator.start();
}
@Override
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
index 51ade29..84ef505 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
@@ -382,6 +382,9 @@
private static final int HAL_ERROR_RETRY_MAX = 20;
@VisibleForTesting
+ protected static final int HAL_POWER_PRESS_TIMEOUT = 50; // ms
+
+ @VisibleForTesting
protected final Runnable mFpCancelNotReceived = this::onFingerprintCancelNotReceived;
private final Runnable mFaceCancelNotReceived = this::onFaceCancelNotReceived;
@@ -902,7 +905,7 @@
}
}
- private final Runnable mRetryFingerprintAuthentication = new Runnable() {
+ private final Runnable mRetryFingerprintAuthenticationAfterHwUnavailable = new Runnable() {
@SuppressLint("MissingPermission")
@Override
public void run() {
@@ -911,7 +914,8 @@
updateFingerprintListeningState(BIOMETRIC_ACTION_UPDATE);
} else if (mHardwareFingerprintUnavailableRetryCount < HAL_ERROR_RETRY_MAX) {
mHardwareFingerprintUnavailableRetryCount++;
- mHandler.postDelayed(mRetryFingerprintAuthentication, HAL_ERROR_RETRY_TIMEOUT);
+ mHandler.postDelayed(mRetryFingerprintAuthenticationAfterHwUnavailable,
+ HAL_ERROR_RETRY_TIMEOUT);
}
}
};
@@ -941,12 +945,16 @@
if (msgId == FingerprintManager.FINGERPRINT_ERROR_HW_UNAVAILABLE) {
mLogger.logRetryAfterFpErrorWithDelay(msgId, errString, HAL_ERROR_RETRY_TIMEOUT);
- mHandler.postDelayed(mRetryFingerprintAuthentication, HAL_ERROR_RETRY_TIMEOUT);
+ mHandler.postDelayed(mRetryFingerprintAuthenticationAfterHwUnavailable,
+ HAL_ERROR_RETRY_TIMEOUT);
}
if (msgId == FingerprintManager.BIOMETRIC_ERROR_POWER_PRESSED) {
- mLogger.logRetryAfterFpErrorWithDelay(msgId, errString, 0);
- updateFingerprintListeningState(BIOMETRIC_ACTION_START);
+ mLogger.logRetryAfterFpErrorWithDelay(msgId, errString, HAL_POWER_PRESS_TIMEOUT);
+ mHandler.postDelayed(() -> {
+ mLogger.d("Retrying fingerprint listening after power pressed error.");
+ updateFingerprintListeningState(BIOMETRIC_ACTION_START);
+ }, HAL_POWER_PRESS_TIMEOUT);
}
boolean lockedOutStateChanged = false;
diff --git a/packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt b/packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt
index b66ae28..ceebe4c 100644
--- a/packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt
+++ b/packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt
@@ -231,7 +231,7 @@
int2 = delay
str1 = "$errString"
}, {
- "Fingerprint retrying auth after $int2 ms due to($int1) -> $str1"
+ "Fingerprint scheduling retry auth after $int2 ms due to($int1) -> $str1"
})
}
diff --git a/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java b/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java
index 02a6d7b..e6f559b 100644
--- a/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java
+++ b/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java
@@ -210,8 +210,10 @@
(FaceScanningOverlay) getOverlayView(mFaceScanningViewId);
if (faceScanningOverlay != null) {
faceScanningOverlay.setHideOverlayRunnable(() -> {
+ Trace.beginSection("ScreenDecorations#hideOverlayRunnable");
updateOverlayWindowVisibilityIfViewExists(
faceScanningOverlay.findViewById(mFaceScanningViewId));
+ Trace.endSection();
});
faceScanningOverlay.enableShowProtection(false);
}
@@ -273,16 +275,18 @@
if (mOverlays == null || !shouldOptimizeVisibility()) {
return;
}
-
+ Trace.beginSection("ScreenDecorations#updateOverlayWindowVisibilityIfViewExists");
for (final OverlayWindow overlay : mOverlays) {
if (overlay == null) {
continue;
}
if (overlay.getView(view.getId()) != null) {
overlay.getRootView().setVisibility(getWindowVisibility(overlay, true));
+ Trace.endSection();
return;
}
}
+ Trace.endSection();
});
}
@@ -370,6 +374,7 @@
}
private void startOnScreenDecorationsThread() {
+ Trace.beginSection("ScreenDecorations#startOnScreenDecorationsThread");
mWindowManager = mContext.getSystemService(WindowManager.class);
mDisplayManager = mContext.getSystemService(DisplayManager.class);
mContext.getDisplay().getDisplayInfo(mDisplayInfo);
@@ -472,6 +477,7 @@
mDisplayManager.registerDisplayListener(mDisplayListener, mHandler);
updateConfiguration();
+ Trace.endSection();
}
@VisibleForTesting
@@ -521,6 +527,12 @@
}
private void setupDecorations() {
+ Trace.beginSection("ScreenDecorations#setupDecorations");
+ setupDecorationsInner();
+ Trace.endSection();
+ }
+
+ private void setupDecorationsInner() {
if (hasRoundedCorners() || shouldDrawCutout() || isPrivacyDotEnabled()
|| mFaceScanningFactory.getHasProviders()) {
@@ -573,7 +585,11 @@
return;
}
- mMainExecutor.execute(() -> mTunerService.addTunable(this, SIZE));
+ mMainExecutor.execute(() -> {
+ Trace.beginSection("ScreenDecorations#addTunable");
+ mTunerService.addTunable(this, SIZE);
+ Trace.endSection();
+ });
// Watch color inversion and invert the overlay as needed.
if (mColorInversionSetting == null) {
@@ -593,7 +609,11 @@
mUserTracker.addCallback(mUserChangedCallback, mExecutor);
mIsRegistered = true;
} else {
- mMainExecutor.execute(() -> mTunerService.removeTunable(this));
+ mMainExecutor.execute(() -> {
+ Trace.beginSection("ScreenDecorations#removeTunable");
+ mTunerService.removeTunable(this);
+ Trace.endSection();
+ });
if (mColorInversionSetting != null) {
mColorInversionSetting.setListening(false);
@@ -939,6 +959,7 @@
}
mExecutor.execute(() -> {
+ Trace.beginSection("ScreenDecorations#onConfigurationChanged");
int oldRotation = mRotation;
mPendingConfigChange = false;
updateConfiguration();
@@ -951,6 +972,7 @@
// the updated rotation).
updateLayoutParams();
}
+ Trace.endSection();
});
}
@@ -1119,6 +1141,7 @@
if (mOverlays == null || !SIZE.equals(key)) {
return;
}
+ Trace.beginSection("ScreenDecorations#onTuningChanged");
try {
final int sizeFactor = Integer.parseInt(newValue);
mRoundedCornerResDelegate.setTuningSizeFactor(sizeFactor);
@@ -1132,6 +1155,7 @@
R.id.rounded_corner_bottom_right
});
updateHwLayerRoundedCornerExistAndSize();
+ Trace.endSection();
});
}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java
index 19b0548..7fd4d6a 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java
@@ -97,6 +97,7 @@
import java.util.concurrent.Executor;
import javax.inject.Inject;
+import javax.inject.Provider;
import kotlin.Unit;
@@ -747,7 +748,7 @@
@NonNull SystemUIDialogManager dialogManager,
@NonNull LatencyTracker latencyTracker,
@NonNull ActivityLaunchAnimator activityLaunchAnimator,
- @NonNull Optional<AlternateUdfpsTouchProvider> alternateTouchProvider,
+ @NonNull Optional<Provider<AlternateUdfpsTouchProvider>> alternateTouchProvider,
@NonNull @BiometricsBackground Executor biometricsExecutor,
@NonNull PrimaryBouncerInteractor primaryBouncerInteractor,
@NonNull SinglePointerTouchProcessor singlePointerTouchProcessor) {
@@ -779,7 +780,7 @@
mUnlockedScreenOffAnimationController = unlockedScreenOffAnimationController;
mLatencyTracker = latencyTracker;
mActivityLaunchAnimator = activityLaunchAnimator;
- mAlternateTouchProvider = alternateTouchProvider.orElse(null);
+ mAlternateTouchProvider = alternateTouchProvider.map(Provider::get).orElse(null);
mSensorProps = new FingerprintSensorPropertiesInternal(
-1 /* sensorId */,
SensorProperties.STRENGTH_CONVENIENCE,
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsOverlay.kt b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsOverlay.kt
index 142642a..802b9b6 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsOverlay.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsOverlay.kt
@@ -42,6 +42,7 @@
import java.util.Optional
import java.util.concurrent.Executor
import javax.inject.Inject
+import javax.inject.Provider
import kotlin.math.cos
import kotlin.math.pow
import kotlin.math.sin
@@ -64,7 +65,7 @@
private val fingerprintManager: FingerprintManager?,
private val handler: Handler,
private val biometricExecutor: Executor,
- private val alternateTouchProvider: Optional<AlternateUdfpsTouchProvider>,
+ private val alternateTouchProvider: Optional<Provider<AlternateUdfpsTouchProvider>>,
@Main private val fgExecutor: DelayableExecutor,
private val keyguardUpdateMonitor: KeyguardUpdateMonitor,
private val authController: AuthController,
@@ -126,6 +127,7 @@
if (!processedMotionEvent && goodOverlap) {
biometricExecutor.execute {
alternateTouchProvider
+ .map(Provider<AlternateUdfpsTouchProvider>::get)
.get()
.onPointerDown(
requestId,
@@ -142,7 +144,10 @@
view.configureDisplay {
biometricExecutor.execute {
- alternateTouchProvider.get().onUiReady()
+ alternateTouchProvider
+ .map(Provider<AlternateUdfpsTouchProvider>::get)
+ .get()
+ .onUiReady()
}
}
@@ -158,7 +163,10 @@
MotionEvent.ACTION_CANCEL -> {
if (processedMotionEvent && alternateTouchProvider.isPresent) {
biometricExecutor.execute {
- alternateTouchProvider.get().onPointerUp(requestId)
+ alternateTouchProvider
+ .map(Provider<AlternateUdfpsTouchProvider>::get)
+ .get()
+ .onPointerUp(requestId)
}
fgExecutor.execute {
if (keyguardUpdateMonitor.isFingerprintDetectionRunning) {
@@ -241,7 +249,10 @@
if (overlayView != null && isShowing && alternateTouchProvider.isPresent) {
if (processedMotionEvent) {
biometricExecutor.execute {
- alternateTouchProvider.get().onPointerUp(requestId)
+ alternateTouchProvider
+ .map(Provider<AlternateUdfpsTouchProvider>::get)
+ .get()
+ .onPointerUp(requestId)
}
fgExecutor.execute {
if (keyguardUpdateMonitor.isFingerprintDetectionRunning) {
diff --git a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayController.java b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayController.java
index fb37def..63c2065 100644
--- a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayController.java
+++ b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayController.java
@@ -301,7 +301,9 @@
} else {
mView.showDefaultTextPreview();
}
- maybeShowRemoteCopy(clipData);
+ if (!isRemote) {
+ maybeShowRemoteCopy(clipData);
+ }
animateIn();
mView.announceForAccessibility(accessibilityAnnouncement);
if (isRemote) {
diff --git a/packages/SystemUI/src/com/android/systemui/common/shared/model/TintedIcon.kt b/packages/SystemUI/src/com/android/systemui/common/shared/model/TintedIcon.kt
new file mode 100644
index 0000000..5dabbbb
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/common/shared/model/TintedIcon.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.common.shared.model
+
+import androidx.annotation.AttrRes
+
+/** Models an icon with a specific tint. */
+data class TintedIcon(
+ val icon: Icon,
+ @AttrRes val tintAttr: Int?,
+)
diff --git a/packages/SystemUI/src/com/android/systemui/common/ui/binder/TintedIconViewBinder.kt b/packages/SystemUI/src/com/android/systemui/common/ui/binder/TintedIconViewBinder.kt
new file mode 100644
index 0000000..dea8cfd
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/common/ui/binder/TintedIconViewBinder.kt
@@ -0,0 +1,42 @@
+/*
+ * 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.common.ui.binder
+
+import android.widget.ImageView
+import com.android.settingslib.Utils
+import com.android.systemui.common.shared.model.TintedIcon
+
+object TintedIconViewBinder {
+ /**
+ * Binds the given tinted icon to the view.
+ *
+ * [TintedIcon.tintAttr] will always be applied, meaning that if it is null, then the tint
+ * *will* be reset to null.
+ */
+ fun bind(
+ tintedIcon: TintedIcon,
+ view: ImageView,
+ ) {
+ IconViewBinder.bind(tintedIcon.icon, view)
+ view.imageTintList =
+ if (tintedIcon.tintAttr != null) {
+ Utils.getColorAttr(view.context, tintedIcon.tintAttr)
+ } else {
+ null
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/controls/dagger/ControlsComponent.kt b/packages/SystemUI/src/com/android/systemui/controls/dagger/ControlsComponent.kt
index 77d0496e4..27466d4 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/dagger/ControlsComponent.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/dagger/ControlsComponent.kt
@@ -19,7 +19,7 @@
import android.content.Context
import com.android.internal.widget.LockPatternUtils
import com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_BOOT
-import com.android.systemui.controls.ControlsSettingsRepository
+import com.android.systemui.controls.settings.ControlsSettingsRepository
import com.android.systemui.controls.controller.ControlsController
import com.android.systemui.controls.controller.ControlsTileResourceConfiguration
import com.android.systemui.controls.controller.ControlsTileResourceConfigurationImpl
diff --git a/packages/SystemUI/src/com/android/systemui/controls/dagger/ControlsModule.kt b/packages/SystemUI/src/com/android/systemui/controls/dagger/ControlsModule.kt
index 9ae605e..6d6410d 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/dagger/ControlsModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/dagger/ControlsModule.kt
@@ -20,8 +20,8 @@
import android.content.pm.PackageManager
import com.android.systemui.controls.ControlsMetricsLogger
import com.android.systemui.controls.ControlsMetricsLoggerImpl
-import com.android.systemui.controls.ControlsSettingsRepository
-import com.android.systemui.controls.ControlsSettingsRepositoryImpl
+import com.android.systemui.controls.settings.ControlsSettingsRepository
+import com.android.systemui.controls.settings.ControlsSettingsRepositoryImpl
import com.android.systemui.controls.controller.ControlsBindingController
import com.android.systemui.controls.controller.ControlsBindingControllerImpl
import com.android.systemui.controls.controller.ControlsController
@@ -34,6 +34,8 @@
import com.android.systemui.controls.management.ControlsListingControllerImpl
import com.android.systemui.controls.management.ControlsProviderSelectorActivity
import com.android.systemui.controls.management.ControlsRequestDialog
+import com.android.systemui.controls.settings.ControlsSettingsDialogManager
+import com.android.systemui.controls.settings.ControlsSettingsDialogManagerImpl
import com.android.systemui.controls.ui.ControlActionCoordinator
import com.android.systemui.controls.ui.ControlActionCoordinatorImpl
import com.android.systemui.controls.ui.ControlsActivity
@@ -90,6 +92,11 @@
): ControlsSettingsRepository
@Binds
+ abstract fun provideDialogManager(
+ manager: ControlsSettingsDialogManagerImpl
+ ): ControlsSettingsDialogManager
+
+ @Binds
abstract fun provideMetricsLogger(logger: ControlsMetricsLoggerImpl): ControlsMetricsLogger
@Binds
diff --git a/packages/SystemUI/src/com/android/systemui/controls/settings/ControlsSettingsDialogManager.kt b/packages/SystemUI/src/com/android/systemui/controls/settings/ControlsSettingsDialogManager.kt
new file mode 100644
index 0000000..bb2e2d7
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/controls/settings/ControlsSettingsDialogManager.kt
@@ -0,0 +1,231 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package com.android.systemui.controls.settings
+
+import android.app.AlertDialog
+import android.content.Context
+import android.content.Context.MODE_PRIVATE
+import android.content.DialogInterface
+import android.content.SharedPreferences
+import android.provider.Settings
+import androidx.annotation.VisibleForTesting
+import com.android.systemui.R
+import com.android.systemui.controls.settings.ControlsSettingsDialogManager.Companion.MAX_NUMBER_ATTEMPTS_CONTROLS_DIALOG
+import com.android.systemui.controls.settings.ControlsSettingsDialogManager.Companion.PREFS_SETTINGS_DIALOG_ATTEMPTS
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.plugins.ActivityStarter
+import com.android.systemui.settings.UserFileManager
+import com.android.systemui.settings.UserTracker
+import com.android.systemui.statusbar.phone.SystemUIDialog
+import com.android.systemui.statusbar.policy.DeviceControlsControllerImpl
+import com.android.systemui.util.settings.SecureSettings
+import javax.inject.Inject
+
+/**
+ * Manager to display a dialog to prompt user to enable controls related Settings:
+ *
+ * * [Settings.Secure.LOCKSCREEN_SHOW_CONTROLS]
+ * * [Settings.Secure.LOCKSCREEN_ALLOW_TRIVIAL_CONTROLS]
+ */
+interface ControlsSettingsDialogManager {
+
+ /**
+ * Shows the corresponding dialog. In order for a dialog to appear, the following must be true
+ *
+ * * At least one of the Settings in [ControlsSettingsRepository] are `false`.
+ * * The dialog has not been seen by the user too many times (as defined by
+ * [MAX_NUMBER_ATTEMPTS_CONTROLS_DIALOG]).
+ *
+ * When the dialogs are shown, the following outcomes are possible:
+ * * User cancels the dialog by clicking outside or going back: we register that the dialog was
+ * seen but the settings don't change.
+ * * User responds negatively to the dialog: we register that the user doesn't want to change
+ * the settings (dialog will not appear again) and the settings don't change.
+ * * User responds positively to the dialog: the settings are set to `true` and the dialog will
+ * not appear again.
+ * * SystemUI closes the dialogs (for example, the activity showing it is closed). In this case,
+ * we don't modify anything.
+ *
+ * Of those four scenarios, only the first three will cause [onAttemptCompleted] to be called.
+ * It will also be called if the dialogs are not shown.
+ */
+ fun maybeShowDialog(activityContext: Context, onAttemptCompleted: () -> Unit)
+
+ /**
+ * Closes the dialog without registering anything from the user. The state of the settings after
+ * this is called will be the same as before the dialogs were shown.
+ */
+ fun closeDialog()
+
+ companion object {
+ @VisibleForTesting internal const val MAX_NUMBER_ATTEMPTS_CONTROLS_DIALOG = 2
+ @VisibleForTesting
+ internal const val PREFS_SETTINGS_DIALOG_ATTEMPTS = "show_settings_attempts"
+ }
+}
+
+@SysUISingleton
+class ControlsSettingsDialogManagerImpl
+@VisibleForTesting
+internal constructor(
+ private val secureSettings: SecureSettings,
+ private val userFileManager: UserFileManager,
+ private val controlsSettingsRepository: ControlsSettingsRepository,
+ private val userTracker: UserTracker,
+ private val activityStarter: ActivityStarter,
+ private val dialogProvider: (context: Context, theme: Int) -> AlertDialog
+) : ControlsSettingsDialogManager {
+
+ @Inject
+ constructor(
+ secureSettings: SecureSettings,
+ userFileManager: UserFileManager,
+ controlsSettingsRepository: ControlsSettingsRepository,
+ userTracker: UserTracker,
+ activityStarter: ActivityStarter
+ ) : this(
+ secureSettings,
+ userFileManager,
+ controlsSettingsRepository,
+ userTracker,
+ activityStarter,
+ { context, theme -> SettingsDialog(context, theme) }
+ )
+
+ private var dialog: AlertDialog? = null
+ private set
+
+ private val showDeviceControlsInLockscreen: Boolean
+ get() = controlsSettingsRepository.canShowControlsInLockscreen.value
+
+ private val allowTrivialControls: Boolean
+ get() = controlsSettingsRepository.allowActionOnTrivialControlsInLockscreen.value
+
+ override fun maybeShowDialog(activityContext: Context, onAttemptCompleted: () -> Unit) {
+ closeDialog()
+
+ val prefs =
+ userFileManager.getSharedPreferences(
+ DeviceControlsControllerImpl.PREFS_CONTROLS_FILE,
+ MODE_PRIVATE,
+ userTracker.userId
+ )
+ val attempts = prefs.getInt(PREFS_SETTINGS_DIALOG_ATTEMPTS, 0)
+ if (
+ attempts >= MAX_NUMBER_ATTEMPTS_CONTROLS_DIALOG ||
+ (showDeviceControlsInLockscreen && allowTrivialControls)
+ ) {
+ onAttemptCompleted()
+ return
+ }
+
+ val listener = DialogListener(prefs, attempts, onAttemptCompleted)
+ val d =
+ dialogProvider(activityContext, R.style.Theme_SystemUI_Dialog).apply {
+ setIcon(R.drawable.ic_warning)
+ setOnCancelListener(listener)
+ setNeutralButton(R.string.controls_settings_dialog_neutral_button, listener)
+ setPositiveButton(R.string.controls_settings_dialog_positive_button, listener)
+ if (showDeviceControlsInLockscreen) {
+ setTitle(R.string.controls_settings_trivial_controls_dialog_title)
+ setMessage(R.string.controls_settings_trivial_controls_dialog_message)
+ } else {
+ setTitle(R.string.controls_settings_show_controls_dialog_title)
+ setMessage(R.string.controls_settings_show_controls_dialog_message)
+ }
+ }
+
+ SystemUIDialog.registerDismissListener(d) { dialog = null }
+ SystemUIDialog.setDialogSize(d)
+ SystemUIDialog.setShowForAllUsers(d, true)
+ dialog = d
+ d.show()
+ }
+
+ private fun turnOnSettingSecurely(settings: List<String>) {
+ val action =
+ ActivityStarter.OnDismissAction {
+ settings.forEach { setting ->
+ secureSettings.putIntForUser(setting, 1, userTracker.userId)
+ }
+ true
+ }
+ activityStarter.dismissKeyguardThenExecute(
+ action,
+ /* cancel */ null,
+ /* afterKeyguardGone */ true
+ )
+ }
+
+ override fun closeDialog() {
+ dialog?.dismiss()
+ }
+
+ private inner class DialogListener(
+ private val prefs: SharedPreferences,
+ private val attempts: Int,
+ private val onComplete: () -> Unit
+ ) : DialogInterface.OnClickListener, DialogInterface.OnCancelListener {
+ override fun onClick(dialog: DialogInterface?, which: Int) {
+ if (dialog == null) return
+ if (which == DialogInterface.BUTTON_POSITIVE) {
+ val settings = mutableListOf(Settings.Secure.LOCKSCREEN_ALLOW_TRIVIAL_CONTROLS)
+ if (!showDeviceControlsInLockscreen) {
+ settings.add(Settings.Secure.LOCKSCREEN_SHOW_CONTROLS)
+ }
+ turnOnSettingSecurely(settings)
+ }
+ if (attempts != MAX_NUMBER_ATTEMPTS_CONTROLS_DIALOG) {
+ prefs
+ .edit()
+ .putInt(PREFS_SETTINGS_DIALOG_ATTEMPTS, MAX_NUMBER_ATTEMPTS_CONTROLS_DIALOG)
+ .apply()
+ }
+ onComplete()
+ }
+
+ override fun onCancel(dialog: DialogInterface?) {
+ if (dialog == null) return
+ if (attempts < MAX_NUMBER_ATTEMPTS_CONTROLS_DIALOG) {
+ prefs.edit().putInt(PREFS_SETTINGS_DIALOG_ATTEMPTS, attempts + 1).apply()
+ }
+ onComplete()
+ }
+ }
+
+ private fun AlertDialog.setNeutralButton(
+ msgId: Int,
+ listener: DialogInterface.OnClickListener
+ ) {
+ setButton(DialogInterface.BUTTON_NEUTRAL, context.getText(msgId), listener)
+ }
+
+ private fun AlertDialog.setPositiveButton(
+ msgId: Int,
+ listener: DialogInterface.OnClickListener
+ ) {
+ setButton(DialogInterface.BUTTON_POSITIVE, context.getText(msgId), listener)
+ }
+
+ private fun AlertDialog.setMessage(msgId: Int) {
+ setMessage(context.getText(msgId))
+ }
+
+ /** This is necessary because the constructors are `protected`. */
+ private class SettingsDialog(context: Context, theme: Int) : AlertDialog(context, theme)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/controls/ControlsSettingsRepository.kt b/packages/SystemUI/src/com/android/systemui/controls/settings/ControlsSettingsRepository.kt
similarity index 95%
rename from packages/SystemUI/src/com/android/systemui/controls/ControlsSettingsRepository.kt
rename to packages/SystemUI/src/com/android/systemui/controls/settings/ControlsSettingsRepository.kt
index 3d10ab9..df2831c 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/ControlsSettingsRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/settings/ControlsSettingsRepository.kt
@@ -15,7 +15,7 @@
*
*/
-package com.android.systemui.controls
+package com.android.systemui.controls.settings
import kotlinx.coroutines.flow.StateFlow
diff --git a/packages/SystemUI/src/com/android/systemui/controls/ControlsSettingsRepositoryImpl.kt b/packages/SystemUI/src/com/android/systemui/controls/settings/ControlsSettingsRepositoryImpl.kt
similarity index 98%
rename from packages/SystemUI/src/com/android/systemui/controls/ControlsSettingsRepositoryImpl.kt
rename to packages/SystemUI/src/com/android/systemui/controls/settings/ControlsSettingsRepositoryImpl.kt
index 9dc422a..8e3b510 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/ControlsSettingsRepositoryImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/settings/ControlsSettingsRepositoryImpl.kt
@@ -15,7 +15,7 @@
*
*/
-package com.android.systemui.controls
+package com.android.systemui.controls.settings
import android.provider.Settings
import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlActionCoordinatorImpl.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlActionCoordinatorImpl.kt
index 041ed1d..99a10a3 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlActionCoordinatorImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlActionCoordinatorImpl.kt
@@ -19,15 +19,12 @@
import android.annotation.AnyThread
import android.annotation.MainThread
import android.app.Activity
-import android.app.AlertDialog
import android.app.Dialog
import android.app.PendingIntent
import android.content.Context
import android.content.pm.PackageManager
import android.content.pm.ResolveInfo
-import android.os.UserHandle
import android.os.VibrationEffect
-import android.provider.Settings.Secure
import android.service.controls.Control
import android.service.controls.actions.BooleanAction
import android.service.controls.actions.CommandAction
@@ -35,39 +32,36 @@
import android.util.Log
import android.view.HapticFeedbackConstants
import com.android.internal.annotations.VisibleForTesting
-import com.android.systemui.R
import com.android.systemui.broadcast.BroadcastSender
import com.android.systemui.controls.ControlsMetricsLogger
-import com.android.systemui.controls.ControlsSettingsRepository
+import com.android.systemui.controls.settings.ControlsSettingsDialogManager
+import com.android.systemui.controls.settings.ControlsSettingsRepository
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.flags.Flags
import com.android.systemui.plugins.ActivityStarter
-import com.android.systemui.settings.UserContextProvider
import com.android.systemui.statusbar.VibratorHelper
-import com.android.systemui.statusbar.phone.SystemUIDialog
-import com.android.systemui.statusbar.policy.DeviceControlsControllerImpl.Companion.PREFS_CONTROLS_FILE
-import com.android.systemui.statusbar.policy.DeviceControlsControllerImpl.Companion.PREFS_SETTINGS_DIALOG_ATTEMPTS
import com.android.systemui.statusbar.policy.KeyguardStateController
import com.android.systemui.util.concurrency.DelayableExecutor
-import com.android.systemui.util.settings.SecureSettings
import com.android.wm.shell.TaskViewFactory
import java.util.Optional
import javax.inject.Inject
@SysUISingleton
class ControlActionCoordinatorImpl @Inject constructor(
- private val context: Context,
- private val bgExecutor: DelayableExecutor,
- @Main private val uiExecutor: DelayableExecutor,
- private val activityStarter: ActivityStarter,
- private val broadcastSender: BroadcastSender,
- private val keyguardStateController: KeyguardStateController,
- private val taskViewFactory: Optional<TaskViewFactory>,
- private val controlsMetricsLogger: ControlsMetricsLogger,
- private val vibrator: VibratorHelper,
- private val secureSettings: SecureSettings,
- private val userContextProvider: UserContextProvider,
- private val controlsSettingsRepository: ControlsSettingsRepository,
+ private val context: Context,
+ private val bgExecutor: DelayableExecutor,
+ @Main private val uiExecutor: DelayableExecutor,
+ private val activityStarter: ActivityStarter,
+ private val broadcastSender: BroadcastSender,
+ private val keyguardStateController: KeyguardStateController,
+ private val taskViewFactory: Optional<TaskViewFactory>,
+ private val controlsMetricsLogger: ControlsMetricsLogger,
+ private val vibrator: VibratorHelper,
+ private val controlsSettingsRepository: ControlsSettingsRepository,
+ private val controlsSettingsDialogManager: ControlsSettingsDialogManager,
+ private val featureFlags: FeatureFlags,
) : ControlActionCoordinator {
private var dialog: Dialog? = null
private var pendingAction: Action? = null
@@ -76,16 +70,16 @@
get() = !keyguardStateController.isUnlocked()
private val allowTrivialControls: Boolean
get() = controlsSettingsRepository.allowActionOnTrivialControlsInLockscreen.value
- private val showDeviceControlsInLockscreen: Boolean
- get() = controlsSettingsRepository.canShowControlsInLockscreen.value
override lateinit var activityContext: Context
companion object {
private const val RESPONSE_TIMEOUT_IN_MILLIS = 3000L
- private const val MAX_NUMBER_ATTEMPTS_CONTROLS_DIALOG = 2
}
override fun closeDialogs() {
+ if (!featureFlags.isEnabled(Flags.USE_APP_PANELS)) {
+ controlsSettingsDialogManager.closeDialog()
+ }
val isActivityFinishing =
(activityContext as? Activity)?.let { it.isFinishing || it.isDestroyed }
if (isActivityFinishing == true) {
@@ -253,71 +247,9 @@
if (action.authIsRequired) {
return
}
- val prefs = userContextProvider.userContext.getSharedPreferences(
- PREFS_CONTROLS_FILE, Context.MODE_PRIVATE)
- val attempts = prefs.getInt(PREFS_SETTINGS_DIALOG_ATTEMPTS, 0)
- if (attempts >= MAX_NUMBER_ATTEMPTS_CONTROLS_DIALOG ||
- (showDeviceControlsInLockscreen && allowTrivialControls)) {
- return
+ if (!featureFlags.isEnabled(Flags.USE_APP_PANELS)) {
+ controlsSettingsDialogManager.maybeShowDialog(activityContext) {}
}
- val builder = AlertDialog
- .Builder(activityContext, R.style.Theme_SystemUI_Dialog)
- .setIcon(R.drawable.ic_warning)
- .setOnCancelListener {
- if (attempts < MAX_NUMBER_ATTEMPTS_CONTROLS_DIALOG) {
- prefs.edit().putInt(PREFS_SETTINGS_DIALOG_ATTEMPTS, attempts + 1)
- .commit()
- }
- true
- }
- .setNeutralButton(R.string.controls_settings_dialog_neutral_button) { _, _ ->
- if (attempts != MAX_NUMBER_ATTEMPTS_CONTROLS_DIALOG) {
- prefs.edit().putInt(PREFS_SETTINGS_DIALOG_ATTEMPTS,
- MAX_NUMBER_ATTEMPTS_CONTROLS_DIALOG)
- .commit()
- }
- true
- }
-
- if (showDeviceControlsInLockscreen) {
- dialog = builder
- .setTitle(R.string.controls_settings_trivial_controls_dialog_title)
- .setMessage(R.string.controls_settings_trivial_controls_dialog_message)
- .setPositiveButton(R.string.controls_settings_dialog_positive_button) { _, _ ->
- if (attempts != MAX_NUMBER_ATTEMPTS_CONTROLS_DIALOG) {
- prefs.edit().putInt(PREFS_SETTINGS_DIALOG_ATTEMPTS,
- MAX_NUMBER_ATTEMPTS_CONTROLS_DIALOG)
- .commit()
- }
- secureSettings.putIntForUser(Secure.LOCKSCREEN_ALLOW_TRIVIAL_CONTROLS, 1,
- UserHandle.USER_CURRENT)
- true
- }
- .create()
- } else {
- dialog = builder
- .setTitle(R.string.controls_settings_show_controls_dialog_title)
- .setMessage(R.string.controls_settings_show_controls_dialog_message)
- .setPositiveButton(R.string.controls_settings_dialog_positive_button) { _, _ ->
- if (attempts != MAX_NUMBER_ATTEMPTS_CONTROLS_DIALOG) {
- prefs.edit().putInt(PREFS_SETTINGS_DIALOG_ATTEMPTS,
- MAX_NUMBER_ATTEMPTS_CONTROLS_DIALOG)
- .commit()
- }
- secureSettings.putIntForUser(Secure.LOCKSCREEN_SHOW_CONTROLS,
- 1, UserHandle.USER_CURRENT)
- secureSettings.putIntForUser(Secure.LOCKSCREEN_ALLOW_TRIVIAL_CONTROLS,
- 1, UserHandle.USER_CURRENT)
- true
- }
- .create()
- }
-
- SystemUIDialog.registerDismissListener(dialog)
- SystemUIDialog.setDialogSize(dialog)
-
- dialog?.create()
- dialog?.show()
}
@VisibleForTesting
diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsActivity.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsActivity.kt
index bd704c1..5d611c4 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsActivity.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsActivity.kt
@@ -32,8 +32,10 @@
import com.android.systemui.R
import com.android.systemui.broadcast.BroadcastDispatcher
import com.android.systemui.controls.management.ControlsAnimations
+import com.android.systemui.controls.settings.ControlsSettingsDialogManager
import com.android.systemui.flags.FeatureFlags
import com.android.systemui.flags.Flags
+import com.android.systemui.statusbar.policy.KeyguardStateController
import javax.inject.Inject
/**
@@ -47,7 +49,9 @@
private val uiController: ControlsUiController,
private val broadcastDispatcher: BroadcastDispatcher,
private val dreamManager: IDreamManager,
- private val featureFlags: FeatureFlags
+ private val featureFlags: FeatureFlags,
+ private val controlsSettingsDialogManager: ControlsSettingsDialogManager,
+ private val keyguardStateController: KeyguardStateController
) : ComponentActivity() {
private lateinit var parent: ViewGroup
@@ -92,7 +96,13 @@
parent = requireViewById<ViewGroup>(R.id.global_actions_controls)
parent.alpha = 0f
- uiController.show(parent, { finishOrReturnToDream() }, this)
+ if (featureFlags.isEnabled(Flags.USE_APP_PANELS) && !keyguardStateController.isUnlocked) {
+ controlsSettingsDialogManager.maybeShowDialog(this) {
+ uiController.show(parent, { finishOrReturnToDream() }, this)
+ }
+ } else {
+ uiController.show(parent, { finishOrReturnToDream() }, this)
+ }
ControlsAnimations.enterAnimation(parent).start()
}
@@ -124,6 +134,7 @@
mExitToDream = false
uiController.hide()
+ controlsSettingsDialogManager.closeDialog()
}
override fun onDestroy() {
diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt
index 4c8e1ac..fb678aa 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt
@@ -28,6 +28,7 @@
import android.graphics.drawable.Drawable
import android.graphics.drawable.LayerDrawable
import android.service.controls.Control
+import android.service.controls.ControlsProviderService
import android.util.Log
import android.view.ContextThemeWrapper
import android.view.LayoutInflater
@@ -48,6 +49,7 @@
import com.android.systemui.R
import com.android.systemui.controls.ControlsMetricsLogger
import com.android.systemui.controls.ControlsServiceInfo
+import com.android.systemui.controls.settings.ControlsSettingsRepository
import com.android.systemui.controls.CustomIconCache
import com.android.systemui.controls.controller.ControlsController
import com.android.systemui.controls.controller.StructureInfo
@@ -96,6 +98,7 @@
private val userFileManager: UserFileManager,
private val userTracker: UserTracker,
private val taskViewFactory: Optional<TaskViewFactory>,
+ private val controlsSettingsRepository: ControlsSettingsRepository,
dumpManager: DumpManager
) : ControlsUiController, Dumpable {
@@ -354,7 +357,6 @@
} else {
items[0]
}
-
maybeUpdateSelectedItem(selectionItem)
createControlsSpaceFrame()
@@ -374,11 +376,20 @@
}
private fun createPanelView(componentName: ComponentName) {
- val pendingIntent = PendingIntent.getActivity(
+ val setting = controlsSettingsRepository
+ .allowActionOnTrivialControlsInLockscreen.value
+ val pendingIntent = PendingIntent.getActivityAsUser(
context,
0,
- Intent().setComponent(componentName),
- PendingIntent.FLAG_IMMUTABLE
+ Intent()
+ .setComponent(componentName)
+ .putExtra(
+ ControlsProviderService.EXTRA_LOCKSCREEN_ALLOW_TRIVIAL_CONTROLS,
+ setting
+ ),
+ PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT,
+ null,
+ userTracker.userHandle
)
parent.requireViewById<View>(R.id.controls_scroll_view).visibility = View.GONE
@@ -698,6 +709,8 @@
println("hidden: $hidden")
println("selectedItem: $selectedItem")
println("lastSelections: $lastSelections")
+ println("setting: ${controlsSettingsRepository
+ .allowActionOnTrivialControlsInLockscreen.value}")
}
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagsDebug.java b/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagsDebug.java
index 81df4ed..267e036 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagsDebug.java
+++ b/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagsDebug.java
@@ -87,7 +87,7 @@
new ServerFlagReader.ChangeListener() {
@Override
public void onChange() {
- mRestarter.restart();
+ mRestarter.restartSystemUI();
}
};
@@ -327,9 +327,7 @@
Log.i(TAG, "SystemUI Restart Suppressed");
return;
}
- Log.i(TAG, "Restarting SystemUI");
- // SysUI starts back when up exited. Is there a better way to do this?
- System.exit(0);
+ mRestarter.restartSystemUI();
}
private void restartAndroid(boolean requestSuppress) {
@@ -337,7 +335,7 @@
Log.i(TAG, "Android Restart Suppressed");
return;
}
- mRestarter.restart();
+ mRestarter.restartAndroid();
}
void setBooleanFlagInternal(Flag<?> flag, boolean value) {
diff --git a/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagsDebugRestarter.kt b/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagsDebugRestarter.kt
index 3d9f627..069e612 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagsDebugRestarter.kt
+++ b/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagsDebugRestarter.kt
@@ -28,6 +28,8 @@
private val systemExitRestarter: SystemExitRestarter,
) : Restarter {
+ private var androidRestartRequested = false
+
val observer =
object : WakefulnessLifecycle.Observer {
override fun onFinishedGoingToSleep() {
@@ -36,8 +38,18 @@
}
}
- override fun restart() {
- Log.d(FeatureFlagsDebug.TAG, "Restart requested. Restarting on next screen off.")
+ override fun restartSystemUI() {
+ Log.d(FeatureFlagsDebug.TAG, "SystemUI Restart requested. Restarting on next screen off.")
+ scheduleRestart()
+ }
+
+ override fun restartAndroid() {
+ Log.d(FeatureFlagsDebug.TAG, "Android Restart requested. Restarting on next screen off.")
+ androidRestartRequested = true
+ scheduleRestart()
+ }
+
+ fun scheduleRestart() {
if (wakefulnessLifecycle.wakefulness == WakefulnessLifecycle.WAKEFULNESS_ASLEEP) {
restartNow()
} else {
@@ -46,6 +58,10 @@
}
private fun restartNow() {
- systemExitRestarter.restart()
+ if (androidRestartRequested) {
+ systemExitRestarter.restartAndroid()
+ } else {
+ systemExitRestarter.restartSystemUI()
+ }
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagsDebugStartable.kt b/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagsDebugStartable.kt
index 7189f00..b94d781 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagsDebugStartable.kt
+++ b/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagsDebugStartable.kt
@@ -16,7 +16,9 @@
package com.android.systemui.flags
+import android.content.Intent
import com.android.systemui.CoreStartable
+import com.android.systemui.broadcast.BroadcastSender
import com.android.systemui.dump.DumpManager
import com.android.systemui.statusbar.commandline.CommandRegistry
import dagger.Binds
@@ -31,7 +33,8 @@
dumpManager: DumpManager,
private val commandRegistry: CommandRegistry,
private val flagCommand: FlagCommand,
- private val featureFlags: FeatureFlagsDebug
+ private val featureFlags: FeatureFlagsDebug,
+ private val broadcastSender: BroadcastSender
) : CoreStartable {
init {
@@ -43,6 +46,8 @@
override fun start() {
featureFlags.init()
commandRegistry.registerCommand(FlagCommand.FLAG_COMMAND) { flagCommand }
+ val intent = Intent(FlagManager.ACTION_SYSUI_STARTED)
+ broadcastSender.sendBroadcast(intent)
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagsRelease.java b/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagsRelease.java
index 3c83682..8bddacc 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagsRelease.java
+++ b/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagsRelease.java
@@ -61,7 +61,7 @@
new ServerFlagReader.ChangeListener() {
@Override
public void onChange() {
- mRestarter.restart();
+ mRestarter.restartSystemUI();
}
};
diff --git a/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagsReleaseRestarter.kt b/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagsReleaseRestarter.kt
index a3f0f66..7ff3876 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagsReleaseRestarter.kt
+++ b/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagsReleaseRestarter.kt
@@ -34,35 +34,48 @@
@Background private val bgExecutor: DelayableExecutor,
private val systemExitRestarter: SystemExitRestarter
) : Restarter {
- var shouldRestart = false
+ var listenersAdded = false
var pendingRestart: Runnable? = null
+ var androidRestartRequested = false
val observer =
object : WakefulnessLifecycle.Observer {
override fun onFinishedGoingToSleep() {
- maybeScheduleRestart()
+ scheduleRestart()
}
}
val batteryCallback =
object : BatteryController.BatteryStateChangeCallback {
override fun onBatteryLevelChanged(level: Int, pluggedIn: Boolean, charging: Boolean) {
- maybeScheduleRestart()
+ scheduleRestart()
}
}
- override fun restart() {
- Log.d(FeatureFlagsDebug.TAG, "Restart requested. Restarting when plugged in and idle.")
- if (!shouldRestart) {
- // Don't bother scheduling twice.
- shouldRestart = true
- wakefulnessLifecycle.addObserver(observer)
- batteryController.addCallback(batteryCallback)
- maybeScheduleRestart()
- }
+ override fun restartSystemUI() {
+ Log.d(
+ FeatureFlagsDebug.TAG,
+ "SystemUI Restart requested. Restarting when plugged in and idle."
+ )
+ scheduleRestart()
}
- private fun maybeScheduleRestart() {
+ override fun restartAndroid() {
+ Log.d(
+ FeatureFlagsDebug.TAG,
+ "Android Restart requested. Restarting when plugged in and idle."
+ )
+ androidRestartRequested = true
+ scheduleRestart()
+ }
+
+ private fun scheduleRestart() {
+ // Don't bother adding listeners twice.
+ if (!listenersAdded) {
+ listenersAdded = true
+ wakefulnessLifecycle.addObserver(observer)
+ batteryController.addCallback(batteryCallback)
+ }
if (
wakefulnessLifecycle.wakefulness == WAKEFULNESS_ASLEEP && batteryController.isPluggedIn
) {
@@ -77,6 +90,10 @@
private fun restartNow() {
Log.d(FeatureFlagsRelease.TAG, "Restarting due to systemui flag change")
- systemExitRestarter.restart()
+ if (androidRestartRequested) {
+ systemExitRestarter.restartAndroid()
+ } else {
+ systemExitRestarter.restartSystemUI()
+ }
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
index ae2e1d1..4a7ff07 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
+++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
@@ -71,6 +71,9 @@
val NOTIFICATION_MEMORY_MONITOR_ENABLED =
releasedFlag(112, "notification_memory_monitor_enabled")
+ val NOTIFICATION_MEMORY_LOGGING_ENABLED =
+ unreleasedFlag(119, "notification_memory_logging_enabled", teamfood = true)
+
// TODO(b/254512731): Tracking Bug
@JvmField
val NOTIFICATION_DISMISSAL_FADE =
@@ -91,6 +94,7 @@
unreleasedFlag(259217907, "notification_group_dismissal_animation", teamfood = true)
// TODO(b/257506350): Tracking Bug
+ @JvmField
val FSI_CHROME = unreleasedFlag(117, "fsi_chrome")
@JvmField
@@ -98,7 +102,7 @@
unreleasedFlag(259395680, "simplified_appear_fraction", teamfood = true)
// TODO(b/257315550): Tracking Bug
- val NO_HUN_FOR_OLD_WHEN = unreleasedFlag(118, "no_hun_for_old_when")
+ val NO_HUN_FOR_OLD_WHEN = unreleasedFlag(118, "no_hun_for_old_when", teamfood = true)
val FILTER_UNSEEN_NOTIFS_ON_KEYGUARD =
unreleasedFlag(254647461, "filter_unseen_notifs_on_keyguard", teamfood = true)
@@ -204,9 +208,6 @@
"full_screen_user_switcher"
)
- // TODO(b/254512678): Tracking Bug
- @JvmField val NEW_FOOTER_ACTIONS = releasedFlag(507, "new_footer_actions")
-
// TODO(b/244064524): Tracking Bug
@JvmField val QS_SECONDARY_DATA_SUB_INFO = releasedFlag(508, "qs_secondary_data_sub_info")
@@ -435,6 +436,11 @@
val WARN_ON_BLOCKING_BINDER_TRANSACTIONS =
unreleasedFlag(2400, "warn_on_blocking_binder_transactions")
+ // 2500 - output switcher
+ // TODO(b/261538825): Tracking Bug
+ @JvmField
+ val OUTPUT_SWITCHER_ADVANCED_LAYOUT = unreleasedFlag(2500, "output_switcher_advanced_layout")
+
// TODO(b259590361): Tracking bug
val EXPERIMENTAL_FLAG = unreleasedFlag(2, "exp_flag_release")
}
diff --git a/packages/SystemUI/src/com/android/systemui/flags/Restarter.kt b/packages/SystemUI/src/com/android/systemui/flags/Restarter.kt
index 8f095a2..ce8b821 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/Restarter.kt
+++ b/packages/SystemUI/src/com/android/systemui/flags/Restarter.kt
@@ -16,5 +16,7 @@
package com.android.systemui.flags
interface Restarter {
- fun restart()
-}
\ No newline at end of file
+ fun restartSystemUI()
+
+ fun restartAndroid()
+}
diff --git a/packages/SystemUI/src/com/android/systemui/flags/SystemExitRestarter.kt b/packages/SystemUI/src/com/android/systemui/flags/SystemExitRestarter.kt
index f1b1be4..89daa64 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/SystemExitRestarter.kt
+++ b/packages/SystemUI/src/com/android/systemui/flags/SystemExitRestarter.kt
@@ -16,10 +16,19 @@
package com.android.systemui.flags
+import com.android.internal.statusbar.IStatusBarService
import javax.inject.Inject
-class SystemExitRestarter @Inject constructor() : Restarter {
- override fun restart() {
+class SystemExitRestarter
+@Inject
+constructor(
+ private val barService: IStatusBarService,
+) : Restarter {
+ override fun restartAndroid() {
+ barService.restart()
+ }
+
+ override fun restartSystemUI() {
System.exit(0)
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardUnlockAnimationController.kt b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardUnlockAnimationController.kt
index c4eac1c..c0d6cc9 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardUnlockAnimationController.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardUnlockAnimationController.kt
@@ -824,7 +824,11 @@
surfaceBehindEntryAnimator.cancel()
surfaceBehindAlpha = 1f
setSurfaceBehindAppearAmount(1f)
- launcherUnlockController?.setUnlockAmount(1f, false /* forceIfAnimating */)
+ try {
+ launcherUnlockController?.setUnlockAmount(1f, false /* forceIfAnimating */)
+ } catch (e: RemoteException) {
+ Log.e(TAG, "Remote exception in notifyFinishedKeyguardExitAnimation", e)
+ }
// That target is no longer valid since the animation finished, null it out.
surfaceBehindRemoteAnimationTargets = null
diff --git a/packages/SystemUI/src/com/android/systemui/log/SessionTracker.java b/packages/SystemUI/src/com/android/systemui/log/SessionTracker.java
index c7e4c5e..b98a92f 100644
--- a/packages/SystemUI/src/com/android/systemui/log/SessionTracker.java
+++ b/packages/SystemUI/src/com/android/systemui/log/SessionTracker.java
@@ -49,7 +49,9 @@
@SysUISingleton
public class SessionTracker implements CoreStartable {
private static final String TAG = "SessionTracker";
- private static final boolean DEBUG = false;
+
+ // To enable logs: `adb shell setprop log.tag.SessionTracker DEBUG` & restart sysui
+ private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
// At most 20 bits: ~1m possibilities, ~0.5% probability of collision in 100 values
private final InstanceIdSequence mInstanceIdGenerator = new InstanceIdSequence(1 << 20);
@@ -81,8 +83,8 @@
mKeyguardUpdateMonitor.registerCallback(mKeyguardUpdateMonitorCallback);
mKeyguardStateController.addCallback(mKeyguardStateCallback);
- mKeyguardSessionStarted = mKeyguardStateController.isShowing();
- if (mKeyguardSessionStarted) {
+ if (mKeyguardStateController.isShowing()) {
+ mKeyguardSessionStarted = true;
startSession(SESSION_KEYGUARD);
}
}
@@ -136,12 +138,11 @@
new KeyguardUpdateMonitorCallback() {
@Override
public void onStartedGoingToSleep(int why) {
- // we need to register to the KeyguardUpdateMonitor lifecycle b/c it gets called
- // before the WakefulnessLifecycle
if (mKeyguardSessionStarted) {
- return;
+ endSession(SESSION_KEYGUARD);
}
+ // Start a new session whenever the device goes to sleep
mKeyguardSessionStarted = true;
startSession(SESSION_KEYGUARD);
}
@@ -154,6 +155,9 @@
boolean wasSessionStarted = mKeyguardSessionStarted;
boolean keyguardShowing = mKeyguardStateController.isShowing();
if (keyguardShowing && !wasSessionStarted) {
+ // the keyguard can start showing without the device going to sleep (ie: lockdown
+ // from the power button), so we start a new keyguard session when the keyguard is
+ // newly shown in addition to when the device starts going to sleep
mKeyguardSessionStarted = true;
startSession(SESSION_KEYGUARD);
} else if (!keyguardShowing && wasSessionStarted) {
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaDataManager.kt b/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaDataManager.kt
index 3012bb4..2dd339d 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaDataManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaDataManager.kt
@@ -422,6 +422,7 @@
appUid = appUid
)
mediaEntries.put(packageName, resumeData)
+ logSingleVsMultipleMediaAdded(appUid, packageName, instanceId)
logger.logResumeMediaAdded(appUid, packageName, instanceId)
}
backgroundExecutor.execute {
@@ -812,6 +813,7 @@
val appUid = appInfo?.uid ?: Process.INVALID_UID
if (logEvent) {
+ logSingleVsMultipleMediaAdded(appUid, sbn.packageName, instanceId)
logger.logActiveMediaAdded(appUid, sbn.packageName, instanceId, playbackLocation)
} else if (playbackLocation != currentEntry?.playbackLocation) {
logger.logPlaybackLocationChange(appUid, sbn.packageName, instanceId, playbackLocation)
@@ -855,6 +857,20 @@
}
}
+ private fun logSingleVsMultipleMediaAdded(
+ appUid: Int,
+ packageName: String,
+ instanceId: InstanceId
+ ) {
+ if (mediaEntries.size == 1) {
+ logger.logSingleMediaPlayerInCarousel(appUid, packageName, instanceId)
+ } else if (mediaEntries.size == 2) {
+ // Since this method is only called when there is a new media session added.
+ // logging needed once there is more than one media session in carousel.
+ logger.logMultipleMediaPlayersInCarousel(appUid, packageName, instanceId)
+ }
+ }
+
private fun getAppInfoFromPackage(packageName: String): ApplicationInfo? {
try {
return context.packageManager.getApplicationInfo(packageName, 0)
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaUiEventLogger.kt b/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaUiEventLogger.kt
index 3ad8c21..ea943be 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaUiEventLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaUiEventLogger.kt
@@ -213,6 +213,24 @@
instanceId
)
}
+
+ fun logSingleMediaPlayerInCarousel(uid: Int, packageName: String, instanceId: InstanceId) {
+ logger.logWithInstanceId(
+ MediaUiEvent.MEDIA_CAROUSEL_SINGLE_PLAYER,
+ uid,
+ packageName,
+ instanceId
+ )
+ }
+
+ fun logMultipleMediaPlayersInCarousel(uid: Int, packageName: String, instanceId: InstanceId) {
+ logger.logWithInstanceId(
+ MediaUiEvent.MEDIA_CAROUSEL_MULTIPLE_PLAYERS,
+ uid,
+ packageName,
+ instanceId
+ )
+ }
}
enum class MediaUiEvent(val metricId: Int) : UiEventLogger.UiEventEnum {
@@ -269,7 +287,11 @@
@UiEvent(doc = "User tapped on a media recommendation card")
MEDIA_RECOMMENDATION_CARD_TAP(1045),
@UiEvent(doc = "User opened the broadcast dialog from a media control")
- MEDIA_OPEN_BROADCAST_DIALOG(1079);
+ MEDIA_OPEN_BROADCAST_DIALOG(1079),
+ @UiEvent(doc = "The media carousel contains one media player card")
+ MEDIA_CAROUSEL_SINGLE_PLAYER(1244),
+ @UiEvent(doc = "The media carousel contains multiple media player cards")
+ MEDIA_CAROUSEL_MULTIPLE_PLAYERS(1245);
override fun getId() = metricId
}
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java
index ee59561..3dccae0 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java
@@ -16,7 +16,6 @@
package com.android.systemui.media.dialog;
-import android.annotation.DrawableRes;
import android.content.res.ColorStateList;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffColorFilter;
@@ -122,17 +121,19 @@
// Set different layout for each device
if (device.isMutingExpectedDevice()
&& !mController.isCurrentConnectedDeviceRemote()) {
- updateTitleIcon(R.drawable.media_output_icon_volume,
- mController.getColorItemContent());
+ if (!mController.isAdvancedLayoutSupported()) {
+ updateTitleIcon(R.drawable.media_output_icon_volume,
+ mController.getColorItemContent());
+ }
initMutingExpectedDevice();
mCurrentActivePosition = position;
- updateContainerClickListener(v -> onItemClick(v, device));
+ updateFullItemClickListener(v -> onItemClick(v, device));
setSingleLineLayout(getItemTitle(device));
} else if (device.getState() == MediaDeviceState.STATE_CONNECTING_FAILED) {
setUpDeviceIcon(device);
updateConnectionFailedStatusIcon();
mSubTitleText.setText(R.string.media_output_dialog_connect_failed);
- updateContainerClickListener(v -> onItemClick(v, device));
+ updateFullItemClickListener(v -> onItemClick(v, device));
setTwoLineLayout(device, false /* bFocused */, false /* showSeekBar */,
false /* showProgressBar */, true /* showSubtitle */,
true /* showStatus */);
@@ -146,8 +147,10 @@
&& isDeviceIncluded(mController.getSelectedMediaDevice(), device)) {
boolean isDeviceDeselectable = isDeviceIncluded(
mController.getDeselectableMediaDevice(), device);
- updateTitleIcon(R.drawable.media_output_icon_volume,
- mController.getColorItemContent());
+ if (!mController.isAdvancedLayoutSupported()) {
+ updateTitleIcon(R.drawable.media_output_icon_volume,
+ mController.getColorItemContent());
+ }
updateGroupableCheckBox(true, isDeviceDeselectable, device);
updateEndClickArea(device, isDeviceDeselectable);
setUpContentDescriptionForView(mContainerLayout, false, device);
@@ -161,8 +164,22 @@
&& !mController.isCurrentConnectedDeviceRemote()) {
// mark as disconnected and set special click listener
setUpDeviceIcon(device);
- updateContainerClickListener(v -> cancelMuteAwaitConnection());
+ updateFullItemClickListener(v -> cancelMuteAwaitConnection());
setSingleLineLayout(getItemTitle(device));
+ } else if (mController.isCurrentConnectedDeviceRemote()
+ && !mController.getSelectableMediaDevice().isEmpty()
+ && mController.isAdvancedLayoutSupported()) {
+ //If device is connected and there's other selectable devices, layout as
+ // one of selected devices.
+ boolean isDeviceDeselectable = isDeviceIncluded(
+ mController.getDeselectableMediaDevice(), device);
+ updateGroupableCheckBox(true, isDeviceDeselectable, device);
+ updateEndClickArea(device, isDeviceDeselectable);
+ setUpContentDescriptionForView(mContainerLayout, false, device);
+ setSingleLineLayout(getItemTitle(device), true /* showSeekBar */,
+ false /* showProgressBar */, true /* showCheckBox */,
+ true /* showEndTouchArea */);
+ initSeekbar(device, isCurrentSeekbarInvisible);
} else {
updateTitleIcon(R.drawable.media_output_icon_volume,
mController.getColorItemContent());
@@ -176,14 +193,19 @@
} else if (isDeviceIncluded(mController.getSelectableMediaDevice(), device)) {
setUpDeviceIcon(device);
updateGroupableCheckBox(false, true, device);
- updateContainerClickListener(v -> onGroupActionTriggered(true, device));
+ if (mController.isAdvancedLayoutSupported()) {
+ updateEndClickArea(device, true);
+ }
+ updateFullItemClickListener(mController.isAdvancedLayoutSupported()
+ ? v -> onItemClick(v, device)
+ : v -> onGroupActionTriggered(true, device));
setSingleLineLayout(getItemTitle(device), false /* showSeekBar */,
false /* showProgressBar */, true /* showCheckBox */,
true /* showEndTouchArea */);
} else {
setUpDeviceIcon(device);
setSingleLineLayout(getItemTitle(device));
- updateContainerClickListener(v -> onItemClick(v, device));
+ updateFullItemClickListener(v -> onItemClick(v, device));
}
}
}
@@ -214,6 +236,11 @@
isDeviceDeselectable ? (v) -> mCheckBox.performClick() : null);
mEndTouchArea.setImportantForAccessibility(
View.IMPORTANT_FOR_ACCESSIBILITY_YES);
+ if (mController.isAdvancedLayoutSupported()) {
+ mEndTouchArea.getBackground().setColorFilter(
+ new PorterDuffColorFilter(mController.getColorItemBackground(),
+ PorterDuff.Mode.SRC_IN));
+ }
setUpContentDescriptionForView(mEndTouchArea, true, device);
}
@@ -228,13 +255,9 @@
setCheckBoxColor(mCheckBox, mController.getColorItemContent());
}
- private void updateTitleIcon(@DrawableRes int id, int color) {
- mTitleIcon.setImageDrawable(mContext.getDrawable(id));
- mTitleIcon.setColorFilter(color);
- }
-
- private void updateContainerClickListener(View.OnClickListener listener) {
+ private void updateFullItemClickListener(View.OnClickListener listener) {
mContainerLayout.setOnClickListener(listener);
+ updateIconAreaClickListener(listener);
}
@Override
@@ -246,6 +269,11 @@
final Drawable addDrawable = mContext.getDrawable(R.drawable.ic_add);
mTitleIcon.setImageDrawable(addDrawable);
mTitleIcon.setColorFilter(mController.getColorItemContent());
+ if (mController.isAdvancedLayoutSupported()) {
+ mIconAreaLayout.getBackground().setColorFilter(
+ new PorterDuffColorFilter(mController.getColorItemBackground(),
+ PorterDuff.Mode.SRC_IN));
+ }
mContainerLayout.setOnClickListener(mController::launchBluetoothPairing);
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java
index 3f7b226..db62e51 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java
@@ -16,8 +16,11 @@
package com.android.systemui.media.dialog;
+import static com.android.systemui.media.dialog.MediaOutputSeekbar.VOLUME_PERCENTAGE_SCALE_SIZE;
+
import android.animation.Animator;
import android.animation.ValueAnimator;
+import android.annotation.DrawableRes;
import android.app.WallpaperColors;
import android.content.Context;
import android.graphics.PorterDuff;
@@ -80,8 +83,9 @@
public MediaDeviceBaseViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup,
int viewType) {
mContext = viewGroup.getContext();
- mHolderView = LayoutInflater.from(mContext).inflate(R.layout.media_output_list_item,
- viewGroup, false);
+ mHolderView = LayoutInflater.from(mContext).inflate(
+ mController.isAdvancedLayoutSupported() ? R.layout.media_output_list_item_advanced
+ : R.layout.media_output_list_item, viewGroup, false);
return null;
}
@@ -129,18 +133,20 @@
private static final int ANIM_DURATION = 500;
- final LinearLayout mContainerLayout;
+ final ViewGroup mContainerLayout;
final FrameLayout mItemLayout;
+ final FrameLayout mIconAreaLayout;
final TextView mTitleText;
final TextView mTwoLineTitleText;
final TextView mSubTitleText;
+ final TextView mVolumeValueText;
final ImageView mTitleIcon;
final ProgressBar mProgressBar;
final MediaOutputSeekbar mSeekBar;
final LinearLayout mTwoLineLayout;
final ImageView mStatusIcon;
final CheckBox mCheckBox;
- final LinearLayout mEndTouchArea;
+ final ViewGroup mEndTouchArea;
private String mDeviceId;
private ValueAnimator mCornerAnimator;
private ValueAnimator mVolumeAnimator;
@@ -159,6 +165,13 @@
mStatusIcon = view.requireViewById(R.id.media_output_item_status);
mCheckBox = view.requireViewById(R.id.check_box);
mEndTouchArea = view.requireViewById(R.id.end_action_area);
+ if (mController.isAdvancedLayoutSupported()) {
+ mVolumeValueText = view.requireViewById(R.id.volume_value);
+ mIconAreaLayout = view.requireViewById(R.id.icon_area);
+ } else {
+ mVolumeValueText = null;
+ mIconAreaLayout = null;
+ }
initAnimator();
}
@@ -170,9 +183,14 @@
mEndTouchArea.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO);
mContainerLayout.setOnClickListener(null);
mContainerLayout.setContentDescription(null);
+ mTitleIcon.setOnClickListener(null);
mTitleText.setTextColor(mController.getColorItemContent());
mSubTitleText.setTextColor(mController.getColorItemContent());
mTwoLineTitleText.setTextColor(mController.getColorItemContent());
+ if (mController.isAdvancedLayoutSupported()) {
+ mIconAreaLayout.setOnClickListener(null);
+ mVolumeValueText.setTextColor(mController.getColorItemContent());
+ }
mSeekBar.getProgressDrawable().setColorFilter(
new PorterDuffColorFilter(mController.getColorSeekbarProgress(),
PorterDuff.Mode.SRC_IN));
@@ -203,13 +221,28 @@
.findDrawableByLayerId(android.R.id.progress);
final GradientDrawable progressDrawable =
(GradientDrawable) clipDrawable.getDrawable();
- progressDrawable.setCornerRadius(mController.getActiveRadius());
+ if (mController.isAdvancedLayoutSupported()) {
+ progressDrawable.setCornerRadii(
+ new float[]{0, 0, mController.getActiveRadius(),
+ mController.getActiveRadius(),
+ mController.getActiveRadius(),
+ mController.getActiveRadius(), 0, 0});
+ } else {
+ progressDrawable.setCornerRadius(mController.getActiveRadius());
+ }
}
}
mItemLayout.getBackground().setColorFilter(new PorterDuffColorFilter(
isActive ? mController.getColorConnectedItemBackground()
: mController.getColorItemBackground(),
PorterDuff.Mode.SRC_IN));
+ if (mController.isAdvancedLayoutSupported()) {
+ mIconAreaLayout.getBackground().setColorFilter(new PorterDuffColorFilter(
+ showSeekBar ? mController.getColorSeekbarProgress()
+ : showProgressBar ? mController.getColorConnectedItemBackground()
+ : mController.getColorItemBackground(),
+ PorterDuff.Mode.SRC_IN));
+ }
mProgressBar.setVisibility(showProgressBar ? View.VISIBLE : View.GONE);
mSeekBar.setAlpha(1);
mSeekBar.setVisibility(showSeekBar ? View.VISIBLE : View.GONE);
@@ -220,6 +253,13 @@
mTitleText.setVisibility(View.VISIBLE);
mCheckBox.setVisibility(showCheckBox ? View.VISIBLE : View.GONE);
mEndTouchArea.setVisibility(showEndTouchArea ? View.VISIBLE : View.GONE);
+ if (mController.isAdvancedLayoutSupported()) {
+ ViewGroup.MarginLayoutParams params =
+ (ViewGroup.MarginLayoutParams) mItemLayout.getLayoutParams();
+ params.rightMargin = showEndTouchArea ? mController.getItemMarginEndSelectable()
+ : mController.getItemMarginEndDefault();
+ }
+ mTitleIcon.setColorFilter(mController.getColorItemContent());
}
void setTwoLineLayout(MediaDevice device, boolean bFocused, boolean showSeekBar,
@@ -263,42 +303,134 @@
final int currentVolume = device.getCurrentVolume();
if (mSeekBar.getVolume() != currentVolume) {
if (isCurrentSeekbarInvisible && !mIsInitVolumeFirstTime) {
+ if (mController.isAdvancedLayoutSupported()) {
+ updateTitleIcon(currentVolume == 0 ? R.drawable.media_output_icon_volume_off
+ : R.drawable.media_output_icon_volume,
+ mController.getColorItemContent());
+ }
animateCornerAndVolume(mSeekBar.getProgress(),
MediaOutputSeekbar.scaleVolumeToProgress(currentVolume));
} else {
if (!mVolumeAnimator.isStarted()) {
+ if (mController.isAdvancedLayoutSupported()) {
+ int percentage =
+ (int) ((double) currentVolume * VOLUME_PERCENTAGE_SCALE_SIZE
+ / (double) mSeekBar.getMax());
+ if (percentage == 0) {
+ updateMutedVolumeIcon();
+ } else {
+ updateUnmutedVolumeIcon();
+ }
+ }
mSeekBar.setVolume(currentVolume);
}
}
+ } else if (mController.isAdvancedLayoutSupported() && currentVolume == 0) {
+ mSeekBar.resetVolume();
+ updateMutedVolumeIcon();
}
if (mIsInitVolumeFirstTime) {
mIsInitVolumeFirstTime = false;
}
+ if (mController.isAdvancedLayoutSupported()) {
+ updateIconAreaClickListener((v) -> {
+ mSeekBar.resetVolume();
+ mController.adjustVolume(device, 0);
+ updateMutedVolumeIcon();
+ });
+ }
mSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
if (device == null || !fromUser) {
return;
}
- int currentVolume = MediaOutputSeekbar.scaleProgressToVolume(progress);
+ int progressToVolume = MediaOutputSeekbar.scaleProgressToVolume(progress);
int deviceVolume = device.getCurrentVolume();
- if (currentVolume != deviceVolume) {
- mController.adjustVolume(device, currentVolume);
+ if (mController.isAdvancedLayoutSupported()) {
+ int percentage =
+ (int) ((double) progressToVolume * VOLUME_PERCENTAGE_SCALE_SIZE
+ / (double) seekBar.getMax());
+ mVolumeValueText.setText(mContext.getResources().getString(
+ R.string.media_output_dialog_volume_percentage, percentage));
+ mVolumeValueText.setVisibility(View.VISIBLE);
+ }
+ if (progressToVolume != deviceVolume) {
+ mController.adjustVolume(device, progressToVolume);
+ if (mController.isAdvancedLayoutSupported() && deviceVolume == 0) {
+ updateUnmutedVolumeIcon();
+ }
}
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) {
+ if (mController.isAdvancedLayoutSupported()) {
+ mTitleIcon.setVisibility(View.INVISIBLE);
+ mVolumeValueText.setVisibility(View.VISIBLE);
+ }
mIsDragging = true;
}
@Override
public void onStopTrackingTouch(SeekBar seekBar) {
+ if (mController.isAdvancedLayoutSupported()) {
+ int currentVolume = MediaOutputSeekbar.scaleProgressToVolume(
+ seekBar.getProgress());
+ int percentage =
+ (int) ((double) currentVolume * VOLUME_PERCENTAGE_SCALE_SIZE
+ / (double) seekBar.getMax());
+ if (percentage == 0) {
+ seekBar.setProgress(0);
+ updateMutedVolumeIcon();
+ } else {
+ updateUnmutedVolumeIcon();
+ }
+ mTitleIcon.setVisibility(View.VISIBLE);
+ mVolumeValueText.setVisibility(View.GONE);
+ }
mIsDragging = false;
}
});
}
+ void updateMutedVolumeIcon() {
+ updateTitleIcon(R.drawable.media_output_icon_volume_off,
+ mController.getColorItemContent());
+ final GradientDrawable iconAreaBackgroundDrawable =
+ (GradientDrawable) mIconAreaLayout.getBackground();
+ iconAreaBackgroundDrawable.setCornerRadius(mController.getActiveRadius());
+ }
+
+ void updateUnmutedVolumeIcon() {
+ updateTitleIcon(R.drawable.media_output_icon_volume,
+ mController.getColorItemContent());
+ final GradientDrawable iconAreaBackgroundDrawable =
+ (GradientDrawable) mIconAreaLayout.getBackground();
+ iconAreaBackgroundDrawable.setCornerRadii(new float[]{
+ mController.getActiveRadius(),
+ mController.getActiveRadius(),
+ 0, 0, 0, 0, mController.getActiveRadius(), mController.getActiveRadius()
+ });
+ }
+
+ void updateTitleIcon(@DrawableRes int id, int color) {
+ mTitleIcon.setImageDrawable(mContext.getDrawable(id));
+ mTitleIcon.setColorFilter(color);
+ if (mController.isAdvancedLayoutSupported()) {
+ mIconAreaLayout.getBackground().setColorFilter(
+ new PorterDuffColorFilter(mController.getColorSeekbarProgress(),
+ PorterDuff.Mode.SRC_IN));
+ }
+ }
+
+ void updateIconAreaClickListener(View.OnClickListener listener) {
+ if (mController.isAdvancedLayoutSupported()) {
+ mIconAreaLayout.setOnClickListener(listener);
+ }
+ mTitleIcon.setOnClickListener(listener);
+ }
+
void initMutingExpectedDevice() {
disableSeekBar();
final Drawable backgroundDrawable = mContext.getDrawable(
@@ -316,11 +448,26 @@
final ClipDrawable clipDrawable =
(ClipDrawable) ((LayerDrawable) mSeekBar.getProgressDrawable())
.findDrawableByLayerId(android.R.id.progress);
- final GradientDrawable progressDrawable = (GradientDrawable) clipDrawable.getDrawable();
+ final GradientDrawable targetBackgroundDrawable =
+ (GradientDrawable) (mController.isAdvancedLayoutSupported()
+ ? mIconAreaLayout.getBackground()
+ : clipDrawable.getDrawable());
mCornerAnimator.addUpdateListener(animation -> {
float value = (float) animation.getAnimatedValue();
layoutBackgroundDrawable.setCornerRadius(value);
- progressDrawable.setCornerRadius(value);
+ if (mController.isAdvancedLayoutSupported()) {
+ if (toProgress == 0) {
+ targetBackgroundDrawable.setCornerRadius(value);
+ } else {
+ targetBackgroundDrawable.setCornerRadii(new float[]{
+ value,
+ value,
+ 0, 0, 0, 0, value, value
+ });
+ }
+ } else {
+ targetBackgroundDrawable.setCornerRadius(value);
+ }
});
mVolumeAnimator.setIntValues(fromProgress, toProgress);
mVolumeAnimator.start();
@@ -391,6 +538,7 @@
return;
}
mTitleIcon.setImageIcon(icon);
+ mTitleIcon.setColorFilter(mController.getColorItemContent());
});
});
}
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialogFactory.kt b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialogFactory.kt
index 2b5d6fd..cdd00f9 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialogFactory.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialogFactory.kt
@@ -26,6 +26,7 @@
import com.android.settingslib.bluetooth.LocalBluetoothManager
import com.android.systemui.animation.DialogLaunchAnimator
import com.android.systemui.broadcast.BroadcastSender
+import com.android.systemui.flags.FeatureFlags
import com.android.systemui.media.nearby.NearbyMediaDevicesManager
import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.statusbar.notification.collection.notifcollection.CommonNotifCollection
@@ -47,7 +48,8 @@
private val nearbyMediaDevicesManagerOptional: Optional<NearbyMediaDevicesManager>,
private val audioManager: AudioManager,
private val powerExemptionManager: PowerExemptionManager,
- private val keyGuardManager: KeyguardManager
+ private val keyGuardManager: KeyguardManager,
+ private val featureFlags: FeatureFlags
) {
var mediaOutputBroadcastDialog: MediaOutputBroadcastDialog? = null
@@ -59,7 +61,7 @@
val controller = MediaOutputController(context, packageName,
mediaSessionManager, lbm, starter, notifCollection,
dialogLaunchAnimator, nearbyMediaDevicesManagerOptional, audioManager,
- powerExemptionManager, keyGuardManager)
+ powerExemptionManager, keyGuardManager, featureFlags)
val dialog =
MediaOutputBroadcastDialog(context, aboveStatusBar, broadcastSender, controller)
mediaOutputBroadcastDialog = dialog
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java
index 19b401d..9b361e3 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java
@@ -73,6 +73,8 @@
import com.android.systemui.animation.ActivityLaunchAnimator;
import com.android.systemui.animation.DialogLaunchAnimator;
import com.android.systemui.broadcast.BroadcastSender;
+import com.android.systemui.flags.FeatureFlags;
+import com.android.systemui.flags.Flags;
import com.android.systemui.media.nearby.NearbyMediaDevicesManager;
import com.android.systemui.monet.ColorScheme;
import com.android.systemui.plugins.ActivityStarter;
@@ -145,8 +147,11 @@
private int mColorConnectedItemBackground;
private int mColorPositiveButtonText;
private int mColorDialogBackground;
+ private int mItemMarginEndDefault;
+ private int mItemMarginEndSelectable;
private float mInactiveRadius;
private float mActiveRadius;
+ private FeatureFlags mFeatureFlags;
public enum BroadcastNotifyDialog {
ACTION_FIRST_LAUNCH,
@@ -162,7 +167,8 @@
Optional<NearbyMediaDevicesManager> nearbyMediaDevicesManagerOptional,
AudioManager audioManager,
PowerExemptionManager powerExemptionManager,
- KeyguardManager keyGuardManager) {
+ KeyguardManager keyGuardManager,
+ FeatureFlags featureFlags) {
mContext = context;
mPackageName = packageName;
mMediaSessionManager = mediaSessionManager;
@@ -172,6 +178,7 @@
mAudioManager = audioManager;
mPowerExemptionManager = powerExemptionManager;
mKeyGuardManager = keyGuardManager;
+ mFeatureFlags = featureFlags;
InfoMediaManager imm = new InfoMediaManager(mContext, packageName, null, lbm);
mLocalMediaManager = new LocalMediaManager(mContext, lbm, imm, packageName);
mMetricLogger = new MediaOutputMetricLogger(mContext, mPackageName);
@@ -195,6 +202,10 @@
R.dimen.media_output_dialog_active_background_radius);
mColorDialogBackground = Utils.getColorStateListDefaultColor(mContext,
R.color.media_dialog_background);
+ mItemMarginEndDefault = (int) mContext.getResources().getDimension(
+ R.dimen.media_output_dialog_default_margin_end);
+ mItemMarginEndSelectable = (int) mContext.getResources().getDimension(
+ R.dimen.media_output_dialog_selectable_margin_end);
}
void start(@NonNull Callback cb) {
@@ -527,6 +538,14 @@
return mActiveRadius;
}
+ public int getItemMarginEndDefault() {
+ return mItemMarginEndDefault;
+ }
+
+ public int getItemMarginEndSelectable() {
+ return mItemMarginEndSelectable;
+ }
+
private void buildMediaDevices(List<MediaDevice> devices) {
synchronized (mMediaDevicesLock) {
attachRangeInfo(devices);
@@ -599,6 +618,10 @@
currentConnectedMediaDevice);
}
+ public boolean isAdvancedLayoutSupported() {
+ return mFeatureFlags.isEnabled(Flags.OUTPUT_SWITCHER_ADVANCED_LAYOUT);
+ }
+
List<MediaDevice> getGroupMediaDevices() {
final List<MediaDevice> selectedDevices = getSelectedMediaDevice();
final List<MediaDevice> selectableDevices = getSelectableMediaDevice();
@@ -792,7 +815,7 @@
MediaOutputController controller = new MediaOutputController(mContext, mPackageName,
mMediaSessionManager, mLocalBluetoothManager, mActivityStarter,
mNotifCollection, mDialogLaunchAnimator, Optional.of(mNearbyMediaDevicesManager),
- mAudioManager, mPowerExemptionManager, mKeyGuardManager);
+ mAudioManager, mPowerExemptionManager, mKeyGuardManager, mFeatureFlags);
MediaOutputBroadcastDialog dialog = new MediaOutputBroadcastDialog(mContext, true,
broadcastSender, controller);
mDialogLaunchAnimator.showFromView(dialog, mediaOutputDialog);
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialogFactory.kt b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialogFactory.kt
index 543efed..7dbf876 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialogFactory.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialogFactory.kt
@@ -31,6 +31,7 @@
import com.android.systemui.media.nearby.NearbyMediaDevicesManager
import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.statusbar.notification.collection.notifcollection.CommonNotifCollection
+import com.android.systemui.flags.FeatureFlags
import java.util.Optional
import javax.inject.Inject
@@ -49,7 +50,8 @@
private val nearbyMediaDevicesManagerOptional: Optional<NearbyMediaDevicesManager>,
private val audioManager: AudioManager,
private val powerExemptionManager: PowerExemptionManager,
- private val keyGuardManager: KeyguardManager
+ private val keyGuardManager: KeyguardManager,
+ private val featureFlags: FeatureFlags
) {
companion object {
private const val INTERACTION_JANK_TAG = "media_output"
@@ -65,7 +67,7 @@
context, packageName,
mediaSessionManager, lbm, starter, notifCollection,
dialogLaunchAnimator, nearbyMediaDevicesManagerOptional, audioManager,
- powerExemptionManager, keyGuardManager)
+ powerExemptionManager, keyGuardManager, featureFlags)
val dialog =
MediaOutputDialog(context, aboveStatusBar, broadcastSender, controller, uiEventLogger)
mediaOutputDialog = dialog
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputSeekbar.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputSeekbar.java
index 4ff79d6..253c3c7 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputSeekbar.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputSeekbar.java
@@ -26,6 +26,7 @@
*/
public class MediaOutputSeekbar extends SeekBar {
private static final int SCALE_SIZE = 1000;
+ public static final int VOLUME_PERCENTAGE_SCALE_SIZE = 100000;
public MediaOutputSeekbar(Context context, AttributeSet attrs) {
super(context, attrs);
diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/MediaTttCommandLineHelper.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/MediaTttCommandLineHelper.kt
index 647beb9..b10abb5 100644
--- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/MediaTttCommandLineHelper.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/MediaTttCommandLineHelper.kt
@@ -48,52 +48,66 @@
/** All commands for the sender device. */
inner class SenderCommand : Command {
override fun execute(pw: PrintWriter, args: List<String>) {
- val commandName = args[1]
+ if (args.size < 2) {
+ help(pw)
+ return
+ }
+
+ val senderArgs = processArgs(args)
+
@StatusBarManager.MediaTransferSenderState
val displayState: Int?
try {
- displayState = ChipStateSender.getSenderStateIdFromName(commandName)
+ displayState = ChipStateSender.getSenderStateIdFromName(senderArgs.commandName)
} catch (ex: IllegalArgumentException) {
- pw.println("Invalid command name $commandName")
+ pw.println("Invalid command name ${senderArgs.commandName}")
return
}
@SuppressLint("WrongConstant") // sysui allowed to call STATUS_BAR_SERVICE
val statusBarManager = context.getSystemService(Context.STATUS_BAR_SERVICE)
as StatusBarManager
- val routeInfo = MediaRoute2Info.Builder(if (args.size >= 4) args[3] else "id", args[0])
+ val routeInfo = MediaRoute2Info.Builder(senderArgs.id, senderArgs.deviceName)
.addFeature("feature")
- val useAppIcon = !(args.size >= 3 && args[2] == "useAppIcon=false")
- if (useAppIcon) {
+ if (senderArgs.useAppIcon) {
routeInfo.setClientPackageName(TEST_PACKAGE_NAME)
}
+ var undoExecutor: Executor? = null
+ var undoRunnable: Runnable? = null
+ if (isSucceededState(displayState) && senderArgs.showUndo) {
+ undoExecutor = mainExecutor
+ undoRunnable = Runnable { Log.i(CLI_TAG, "Undo triggered for $displayState") }
+ }
+
statusBarManager.updateMediaTapToTransferSenderDisplay(
displayState,
routeInfo.build(),
- getUndoExecutor(displayState),
- getUndoCallback(displayState)
+ undoExecutor,
+ undoRunnable,
)
}
- private fun getUndoExecutor(
- @StatusBarManager.MediaTransferSenderState displayState: Int
- ): Executor? {
- return if (isSucceededState(displayState)) {
- mainExecutor
- } else {
- null
- }
- }
+ private fun processArgs(args: List<String>): SenderArgs {
+ val senderArgs = SenderArgs(
+ deviceName = args[0],
+ commandName = args[1],
+ )
- private fun getUndoCallback(
- @StatusBarManager.MediaTransferSenderState displayState: Int
- ): Runnable? {
- return if (isSucceededState(displayState)) {
- Runnable { Log.i(CLI_TAG, "Undo triggered for $displayState") }
- } else {
- null
+ if (args.size == 2) {
+ return senderArgs
}
+
+ // Process any optional arguments
+ args.subList(2, args.size).forEach {
+ when {
+ it == "useAppIcon=false" -> senderArgs.useAppIcon = false
+ it == "showUndo=false" -> senderArgs.showUndo = false
+ it.substring(0, 3) == "id=" -> senderArgs.id = it.substring(3)
+ }
+ }
+
+ return senderArgs
}
private fun isSucceededState(
@@ -106,14 +120,31 @@
}
override fun help(pw: PrintWriter) {
- pw.println("Usage: adb shell cmd statusbar $SENDER_COMMAND " +
- "<deviceName> <chipState> useAppIcon=[true|false] <id>")
+ pw.println(
+ "Usage: adb shell cmd statusbar $SENDER_COMMAND " +
+ "<deviceName> <chipState> " +
+ "useAppIcon=[true|false] id=<id> showUndo=[true|false]"
+ )
+ pw.println("Note: useAppIcon, id, and showUndo are optional additional commands.")
}
}
+ private data class SenderArgs(
+ val deviceName: String,
+ val commandName: String,
+ var id: String = "id",
+ var useAppIcon: Boolean = true,
+ var showUndo: Boolean = true,
+ )
+
/** All commands for the receiver device. */
inner class ReceiverCommand : Command {
override fun execute(pw: PrintWriter, args: List<String>) {
+ if (args.isEmpty()) {
+ help(pw)
+ return
+ }
+
val commandName = args[0]
@StatusBarManager.MediaTransferReceiverState
val displayState: Int?
diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttUtils.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttUtils.kt
index 769494a..009595a 100644
--- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttUtils.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttUtils.kt
@@ -19,10 +19,12 @@
import android.content.Context
import android.content.pm.PackageManager
import android.graphics.drawable.Drawable
-import com.android.settingslib.Utils
+import androidx.annotation.AttrRes
+import androidx.annotation.DrawableRes
import com.android.systemui.R
import com.android.systemui.common.shared.model.ContentDescription
import com.android.systemui.common.shared.model.Icon
+import com.android.systemui.common.shared.model.TintedIcon
/** Utility methods for media tap-to-transfer. */
class MediaTttUtils {
@@ -34,23 +36,6 @@
const val WAKE_REASON_RECEIVER = "MEDIA_TRANSFER_ACTIVATED_RECEIVER"
/**
- * Returns the information needed to display the icon in [Icon] form.
- *
- * See [getIconInfoFromPackageName].
- */
- fun getIconFromPackageName(
- context: Context,
- appPackageName: String?,
- logger: MediaTttLogger,
- ): Icon {
- val iconInfo = getIconInfoFromPackageName(context, appPackageName, logger)
- return Icon.Loaded(
- iconInfo.drawable,
- ContentDescription.Loaded(iconInfo.contentDescription)
- )
- }
-
- /**
* Returns the information needed to display the icon.
*
* The information will either contain app name and icon of the app playing media, or a
@@ -65,18 +50,22 @@
logger: MediaTttLogger
): IconInfo {
if (appPackageName != null) {
+ val packageManager = context.packageManager
try {
val contentDescription =
- context.packageManager
- .getApplicationInfo(
- appPackageName,
- PackageManager.ApplicationInfoFlags.of(0)
- )
- .loadLabel(context.packageManager)
- .toString()
+ ContentDescription.Loaded(
+ packageManager
+ .getApplicationInfo(
+ appPackageName,
+ PackageManager.ApplicationInfoFlags.of(0)
+ )
+ .loadLabel(packageManager)
+ .toString()
+ )
return IconInfo(
contentDescription,
- drawable = context.packageManager.getApplicationIcon(appPackageName),
+ MediaTttIcon.Loaded(packageManager.getApplicationIcon(appPackageName)),
+ tintAttr = null,
isAppIcon = true
)
} catch (e: PackageManager.NameNotFoundException) {
@@ -84,25 +73,41 @@
}
}
return IconInfo(
- contentDescription =
- context.getString(R.string.media_output_dialog_unknown_launch_app_name),
- drawable =
- context.resources.getDrawable(R.drawable.ic_cast).apply {
- this.setTint(
- Utils.getColorAttrDefaultColor(context, android.R.attr.textColorPrimary)
- )
- },
+ ContentDescription.Resource(R.string.media_output_dialog_unknown_launch_app_name),
+ MediaTttIcon.Resource(R.drawable.ic_cast),
+ tintAttr = android.R.attr.textColorPrimary,
isAppIcon = false
)
}
}
}
+/** Stores all the information for an icon shown with media TTT. */
data class IconInfo(
- val contentDescription: String,
- val drawable: Drawable,
+ val contentDescription: ContentDescription,
+ val icon: MediaTttIcon,
+ @AttrRes val tintAttr: Int?,
/**
* True if [drawable] is the app's icon, and false if [drawable] is some generic default icon.
*/
val isAppIcon: Boolean
-)
+) {
+ /** Converts this into a [TintedIcon]. */
+ fun toTintedIcon(): TintedIcon {
+ val iconOutput =
+ when (icon) {
+ is MediaTttIcon.Loaded -> Icon.Loaded(icon.drawable, contentDescription)
+ is MediaTttIcon.Resource -> Icon.Resource(icon.res, contentDescription)
+ }
+ return TintedIcon(iconOutput, tintAttr)
+ }
+}
+
+/**
+ * Mimics [com.android.systemui.common.shared.model.Icon] but without the content description, since
+ * the content description may need to be overridden.
+ */
+sealed interface MediaTttIcon {
+ data class Loaded(val drawable: Drawable) : MediaTttIcon
+ data class Resource(@DrawableRes val res: Int) : MediaTttIcon
+}
diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiver.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiver.kt
index cc5e256..1c3a53c 100644
--- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiver.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiver.kt
@@ -33,9 +33,12 @@
import com.android.internal.widget.CachingIconView
import com.android.settingslib.Utils
import com.android.systemui.R
+import com.android.systemui.common.shared.model.ContentDescription
+import com.android.systemui.common.ui.binder.TintedIconViewBinder
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.media.taptotransfer.MediaTttFlags
+import com.android.systemui.media.taptotransfer.common.MediaTttIcon
import com.android.systemui.media.taptotransfer.common.MediaTttLogger
import com.android.systemui.media.taptotransfer.common.MediaTttUtils
import com.android.systemui.statusbar.CommandQueue
@@ -161,11 +164,23 @@
}
override fun updateView(newInfo: ChipReceiverInfo, currentView: ViewGroup) {
- val iconInfo = MediaTttUtils.getIconInfoFromPackageName(
+ var iconInfo = MediaTttUtils.getIconInfoFromPackageName(
context, newInfo.routeInfo.clientPackageName, logger
)
- val iconDrawable = newInfo.appIconDrawableOverride ?: iconInfo.drawable
- val iconContentDescription = newInfo.appNameOverride ?: iconInfo.contentDescription
+
+ if (newInfo.appNameOverride != null) {
+ iconInfo = iconInfo.copy(
+ contentDescription = ContentDescription.Loaded(newInfo.appNameOverride.toString())
+ )
+ }
+
+ if (newInfo.appIconDrawableOverride != null) {
+ iconInfo = iconInfo.copy(
+ icon = MediaTttIcon.Loaded(newInfo.appIconDrawableOverride),
+ isAppIcon = true,
+ )
+ }
+
val iconPadding =
if (iconInfo.isAppIcon) {
0
@@ -175,8 +190,7 @@
val iconView = currentView.getAppIconView()
iconView.setPadding(iconPadding, iconPadding, iconPadding, iconPadding)
- iconView.setImageDrawable(iconDrawable)
- iconView.contentDescription = iconContentDescription
+ TintedIconViewBinder.bind(iconInfo.toTintedIcon(), iconView)
}
override fun animateViewIn(view: ViewGroup) {
diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinator.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinator.kt
index e34b0cb..ec1984d 100644
--- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinator.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinator.kt
@@ -153,7 +153,9 @@
return ChipbarInfo(
// Display the app's icon as the start icon
- startIcon = MediaTttUtils.getIconFromPackageName(context, packageName, logger),
+ startIcon =
+ MediaTttUtils.getIconInfoFromPackageName(context, packageName, logger)
+ .toTintedIcon(),
text = chipStateSender.getChipTextString(context, otherDeviceName),
endItem =
when (chipStateSender.endItem) {
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarController.java b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarController.java
index 3fd1aa7..e2f55f0 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarController.java
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarController.java
@@ -145,7 +145,7 @@
boolean willApplyConfig = mConfigChanges.applyNewConfig(mContext.getResources());
boolean largeScreenChanged = mIsTablet != isOldConfigTablet;
// TODO(b/243765256): Disable this logging once b/243765256 is fixed.
- Log.d(DEBUG_MISSING_GESTURE_TAG, "NavbarController: newConfig=" + newConfig
+ Log.i(DEBUG_MISSING_GESTURE_TAG, "NavbarController: newConfig=" + newConfig
+ " mTaskbarDelegate initialized=" + mTaskbarDelegate.isInitialized()
+ " willApplyConfigToNavbars=" + willApplyConfig
+ " navBarCount=" + mNavigationBars.size());
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java
index 26d3902..f97385b 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java
@@ -977,7 +977,7 @@
}
// TODO(b/243765256): Disable this logging once b/243765256 is fixed.
- Log.d(DEBUG_MISSING_GESTURE_TAG, "Config changed: newConfig=" + newConfig
+ Log.i(DEBUG_MISSING_GESTURE_TAG, "Config changed: newConfig=" + newConfig
+ " lastReportedConfig=" + mLastReportedConfig);
mLastReportedConfig.updateFrom(newConfig);
updateDisplaySize();
diff --git a/packages/SystemUI/src/com/android/systemui/qs/FgsManagerController.kt b/packages/SystemUI/src/com/android/systemui/qs/FgsManagerController.kt
index 3c10778..930de13 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/FgsManagerController.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/FgsManagerController.kt
@@ -122,10 +122,6 @@
/** Remove a [OnDialogDismissedListener]. */
fun removeOnDialogDismissedListener(listener: OnDialogDismissedListener)
- /** Whether we should update the footer visibility. */
- // TODO(b/242040009): Remove this.
- fun shouldUpdateFooterVisibility(): Boolean
-
@VisibleForTesting
fun visibleButtonsCount(): Int
@@ -375,8 +371,6 @@
}
}
- override fun shouldUpdateFooterVisibility() = dialog == null
-
override fun showDialog(expandable: Expandable?) {
synchronized(lock) {
if (dialog == null) {
diff --git a/packages/SystemUI/src/com/android/systemui/qs/FooterActionsController.kt b/packages/SystemUI/src/com/android/systemui/qs/FooterActionsController.kt
index a9943e8..b52233f 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/FooterActionsController.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/FooterActionsController.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2021 The Android Open Source Project
+ * Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -16,261 +16,17 @@
package com.android.systemui.qs
-import android.content.Intent
-import android.content.res.Configuration
-import android.os.Handler
-import android.os.UserManager
-import android.provider.Settings
-import android.provider.Settings.Global.USER_SWITCHER_ENABLED
-import android.view.View
-import android.view.ViewGroup
-import androidx.annotation.VisibleForTesting
-import com.android.internal.jank.InteractionJankMonitor
-import com.android.internal.logging.MetricsLogger
-import com.android.internal.logging.UiEventLogger
-import com.android.internal.logging.nano.MetricsProto
-import com.android.keyguard.KeyguardUpdateMonitor
-import com.android.systemui.R
-import com.android.systemui.animation.ActivityLaunchAnimator
-import com.android.systemui.animation.Expandable
-import com.android.systemui.globalactions.GlobalActionsDialogLite
-import com.android.systemui.plugins.ActivityStarter
-import com.android.systemui.plugins.FalsingManager
-import com.android.systemui.qs.dagger.QSFlagsModule.PM_LITE_ENABLED
-import com.android.systemui.qs.dagger.QSScope
-import com.android.systemui.settings.UserTracker
-import com.android.systemui.statusbar.phone.MultiUserSwitchController
-import com.android.systemui.statusbar.policy.ConfigurationController
-import com.android.systemui.statusbar.policy.DeviceProvisionedController
-import com.android.systemui.statusbar.policy.UserInfoController
-import com.android.systemui.statusbar.policy.UserInfoController.OnUserInfoChangedListener
-import com.android.systemui.util.LargeScreenUtils
-import com.android.systemui.util.ViewController
-import com.android.systemui.util.settings.GlobalSettings
+import com.android.systemui.dagger.SysUISingleton
import javax.inject.Inject
-import javax.inject.Named
-import javax.inject.Provider
-/**
- * Manages [FooterActionsView] behaviour, both when it's placed in QS or QQS (split shade).
- * Main difference between QS and QQS behaviour is condition when buttons should be visible,
- * determined by [buttonsVisibleState]
- */
-@QSScope
-// TODO(b/242040009): Remove this file.
-internal class FooterActionsController @Inject constructor(
- view: FooterActionsView,
- multiUserSwitchControllerFactory: MultiUserSwitchController.Factory,
- private val activityStarter: ActivityStarter,
- private val userManager: UserManager,
- private val userTracker: UserTracker,
- private val userInfoController: UserInfoController,
- private val deviceProvisionedController: DeviceProvisionedController,
- private val securityFooterController: QSSecurityFooter,
- private val fgsManagerFooterController: QSFgsManagerFooter,
- private val falsingManager: FalsingManager,
- private val metricsLogger: MetricsLogger,
- private val globalActionsDialogProvider: Provider<GlobalActionsDialogLite>,
- private val uiEventLogger: UiEventLogger,
- @Named(PM_LITE_ENABLED) private val showPMLiteButton: Boolean,
- private val globalSetting: GlobalSettings,
- private val handler: Handler,
- private val configurationController: ConfigurationController,
-) : ViewController<FooterActionsView>(view) {
-
- private var globalActionsDialog: GlobalActionsDialogLite? = null
-
- private var lastExpansion = -1f
- private var listening: Boolean = false
- private var inSplitShade = false
-
- private val singleShadeAnimator by lazy {
- // In single shade, the actions footer should only appear at the end of the expansion,
- // so that it doesn't overlap with the notifications panel.
- TouchAnimator.Builder().addFloat(mView, "alpha", 0f, 1f).setStartDelay(0.9f).build()
- }
-
- private val splitShadeAnimator by lazy {
- // The Actions footer view has its own background which is the same color as the qs panel's
- // background.
- // We don't want it to fade in at the same time as the rest of the panel, otherwise it is
- // more opaque than the rest of the panel's background. Only applies to split shade.
- val alphaAnimator = TouchAnimator.Builder().addFloat(mView, "alpha", 0f, 1f).build()
- val bgAlphaAnimator =
- TouchAnimator.Builder()
- .addFloat(mView, "backgroundAlpha", 0f, 1f)
- .setStartDelay(0.9f)
- .build()
- // In split shade, we want the actions footer to fade in exactly at the same time as the
- // rest of the shade, as there is no overlap.
- TouchAnimator.Builder()
- .addFloat(alphaAnimator, "position", 0f, 1f)
- .addFloat(bgAlphaAnimator, "position", 0f, 1f)
- .build()
- }
-
- private val animators: TouchAnimator
- get() = if (inSplitShade) splitShadeAnimator else singleShadeAnimator
-
- var visible = true
- set(value) {
- field = value
- updateVisibility()
- }
-
- private val settingsButtonContainer: View = view.findViewById(R.id.settings_button_container)
- private val securityFootersContainer: ViewGroup? =
- view.findViewById(R.id.security_footers_container)
- private val powerMenuLite: View = view.findViewById(R.id.pm_lite)
- private val multiUserSwitchController = multiUserSwitchControllerFactory.create(view)
-
- @VisibleForTesting
- internal val securityFootersSeparator = View(context).apply { visibility = View.GONE }
-
- private val onUserInfoChangedListener = OnUserInfoChangedListener { _, picture, _ ->
- val isGuestUser: Boolean = userManager.isGuestUser(KeyguardUpdateMonitor.getCurrentUser())
- mView.onUserInfoChanged(picture, isGuestUser)
- }
-
- private val multiUserSetting =
- object : SettingObserver(
- globalSetting, handler, USER_SWITCHER_ENABLED, userTracker.userId) {
- override fun handleValueChanged(value: Int, observedChange: Boolean) {
- if (observedChange) {
- updateView()
- }
- }
- }
-
- private val onClickListener = View.OnClickListener { v ->
- // Don't do anything if the tap looks suspicious.
- if (!visible || falsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) {
- return@OnClickListener
- }
- if (v === settingsButtonContainer) {
- if (!deviceProvisionedController.isCurrentUserSetup) {
- // If user isn't setup just unlock the device and dump them back at SUW.
- activityStarter.postQSRunnableDismissingKeyguard {}
- return@OnClickListener
- }
- metricsLogger.action(MetricsProto.MetricsEvent.ACTION_QS_EXPANDED_SETTINGS_LAUNCH)
- startSettingsActivity()
- } else if (v === powerMenuLite) {
- uiEventLogger.log(GlobalActionsDialogLite.GlobalActionsEvent.GA_OPEN_QS)
- globalActionsDialog?.showOrHideDialog(false, true, Expandable.fromView(powerMenuLite))
- }
- }
-
- private val configurationListener =
- object : ConfigurationController.ConfigurationListener {
- override fun onConfigChanged(newConfig: Configuration?) {
- updateResources()
- }
- }
-
- private fun updateResources() {
- inSplitShade = LargeScreenUtils.shouldUseSplitNotificationShade(resources)
- }
-
- override fun onInit() {
- multiUserSwitchController.init()
- securityFooterController.init()
- fgsManagerFooterController.init()
- }
-
- private fun updateVisibility() {
- val previousVisibility = mView.visibility
- mView.visibility = if (visible) View.VISIBLE else View.INVISIBLE
- if (previousVisibility != mView.visibility) updateView()
- }
-
- private fun startSettingsActivity() {
- val animationController = settingsButtonContainer?.let {
- ActivityLaunchAnimator.Controller.fromView(
- it,
- InteractionJankMonitor.CUJ_SHADE_APP_LAUNCH_FROM_SETTINGS_BUTTON)
- }
- activityStarter.startActivity(Intent(Settings.ACTION_SETTINGS),
- true /* dismissShade */, animationController)
- }
-
- @VisibleForTesting
- public override fun onViewAttached() {
- globalActionsDialog = globalActionsDialogProvider.get()
- if (showPMLiteButton) {
- powerMenuLite.visibility = View.VISIBLE
- powerMenuLite.setOnClickListener(onClickListener)
- } else {
- powerMenuLite.visibility = View.GONE
- }
- settingsButtonContainer.setOnClickListener(onClickListener)
- multiUserSetting.isListening = true
-
- val securityFooter = securityFooterController.view
- securityFootersContainer?.addView(securityFooter)
- val separatorWidth = resources.getDimensionPixelSize(R.dimen.qs_footer_action_inset)
- securityFootersContainer?.addView(securityFootersSeparator, separatorWidth, 1)
-
- val fgsFooter = fgsManagerFooterController.view
- securityFootersContainer?.addView(fgsFooter)
-
- val visibilityListener =
- VisibilityChangedDispatcher.OnVisibilityChangedListener { visibility ->
- if (securityFooter.visibility == View.VISIBLE &&
- fgsFooter.visibility == View.VISIBLE) {
- securityFootersSeparator.visibility = View.VISIBLE
- } else {
- securityFootersSeparator.visibility = View.GONE
- }
- fgsManagerFooterController
- .setCollapsed(securityFooter.visibility == View.VISIBLE)
- }
- securityFooterController.setOnVisibilityChangedListener(visibilityListener)
- fgsManagerFooterController.setOnVisibilityChangedListener(visibilityListener)
-
- configurationController.addCallback(configurationListener)
-
- updateResources()
- updateView()
- }
-
- private fun updateView() {
- mView.updateEverything(multiUserSwitchController.isMultiUserEnabled)
- }
-
- override fun onViewDetached() {
- globalActionsDialog?.destroy()
- globalActionsDialog = null
- setListening(false)
- multiUserSetting.isListening = false
- configurationController.removeCallback(configurationListener)
- }
-
- fun setListening(listening: Boolean) {
- if (this.listening == listening) {
- return
- }
- this.listening = listening
- if (this.listening) {
- userInfoController.addCallback(onUserInfoChangedListener)
- updateView()
- } else {
- userInfoController.removeCallback(onUserInfoChangedListener)
- }
-
- fgsManagerFooterController.setListening(listening)
- securityFooterController.setListening(listening)
- }
-
- fun disable(state2: Int) {
- mView.disable(state2, multiUserSwitchController.isMultiUserEnabled)
- }
-
- fun setExpansion(headerExpansionFraction: Float) {
- animators.setPosition(headerExpansionFraction)
- }
-
- fun setKeyguardShowing(showing: Boolean) {
- setExpansion(lastExpansion)
+/** Controller for the footer actions. This manages the initialization of its dependencies. */
+@SysUISingleton
+class FooterActionsController
+@Inject
+constructor(
+ private val fgsManagerController: FgsManagerController,
+) {
+ fun init() {
+ fgsManagerController.init()
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/FooterActionsView.kt b/packages/SystemUI/src/com/android/systemui/qs/FooterActionsView.kt
deleted file mode 100644
index d602b0b..0000000
--- a/packages/SystemUI/src/com/android/systemui/qs/FooterActionsView.kt
+++ /dev/null
@@ -1,135 +0,0 @@
-/*
- * Copyright (C) 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.systemui.qs
-
-import android.app.StatusBarManager
-import android.content.Context
-import android.graphics.PorterDuff
-import android.graphics.drawable.Drawable
-import android.graphics.drawable.RippleDrawable
-import android.os.UserManager
-import android.util.AttributeSet
-import android.util.Log
-import android.view.MotionEvent
-import android.view.View
-import android.widget.ImageView
-import android.widget.LinearLayout
-import androidx.annotation.Keep
-import com.android.settingslib.Utils
-import com.android.settingslib.drawable.UserIconDrawable
-import com.android.systemui.R
-import com.android.systemui.statusbar.phone.MultiUserSwitch
-
-/**
- * Quick Settings bottom buttons placed in footer (aka utility bar) - always visible in expanded QS,
- * in split shade mode visible also in collapsed state. May contain up to 5 buttons: settings,
- * edit tiles, power off and conditionally: user switch and tuner
- */
-// TODO(b/242040009): Remove this file.
-class FooterActionsView(context: Context?, attrs: AttributeSet?) : LinearLayout(context, attrs) {
- private lateinit var settingsContainer: View
- private lateinit var multiUserSwitch: MultiUserSwitch
- private lateinit var multiUserAvatar: ImageView
-
- private var qsDisabled = false
- private var expansionAmount = 0f
-
- /**
- * Sets the alpha of the background of this view.
- *
- * Used from a [TouchAnimator] in the controller.
- */
- var backgroundAlpha: Float = 1f
- @Keep
- set(value) {
- field = value
- background?.alpha = (value * 255).toInt()
- }
- @Keep get
-
- override fun onFinishInflate() {
- super.onFinishInflate()
- settingsContainer = findViewById(R.id.settings_button_container)
- multiUserSwitch = findViewById(R.id.multi_user_switch)
- multiUserAvatar = multiUserSwitch.findViewById(R.id.multi_user_avatar)
-
- // RenderThread is doing more harm than good when touching the header (to expand quick
- // settings), so disable it for this view
- if (settingsContainer.background is RippleDrawable) {
- (settingsContainer.background as RippleDrawable).setForceSoftware(true)
- }
- importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_YES
- }
-
- fun disable(
- state2: Int,
- multiUserEnabled: Boolean
- ) {
- val disabled = state2 and StatusBarManager.DISABLE2_QUICK_SETTINGS != 0
- if (disabled == qsDisabled) return
- qsDisabled = disabled
- updateEverything(multiUserEnabled)
- }
-
- fun updateEverything(
- multiUserEnabled: Boolean
- ) {
- post {
- updateVisibilities(multiUserEnabled)
- updateClickabilities()
- isClickable = false
- }
- }
-
- private fun updateClickabilities() {
- multiUserSwitch.isClickable = multiUserSwitch.visibility == VISIBLE
- settingsContainer.isClickable = settingsContainer.visibility == VISIBLE
- }
-
- private fun updateVisibilities(
- multiUserEnabled: Boolean
- ) {
- settingsContainer.visibility = if (qsDisabled) GONE else VISIBLE
- multiUserSwitch.visibility = if (multiUserEnabled) VISIBLE else GONE
- val isDemo = UserManager.isDeviceInDemoMode(context)
- settingsContainer.visibility = if (isDemo) INVISIBLE else VISIBLE
- }
-
- fun onUserInfoChanged(picture: Drawable?, isGuestUser: Boolean) {
- var pictureToSet = picture
- if (picture != null && isGuestUser && picture !is UserIconDrawable) {
- pictureToSet = picture.constantState.newDrawable(resources).mutate()
- pictureToSet.setColorFilter(
- Utils.getColorAttrDefaultColor(mContext, android.R.attr.colorForeground),
- PorterDuff.Mode.SRC_IN)
- }
- multiUserAvatar.setImageDrawable(pictureToSet)
- }
-
- override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
- if (VERBOSE) Log.d(TAG, "FooterActionsView onInterceptTouchEvent ${ev?.string}")
- return super.onInterceptTouchEvent(ev)
- }
-
- override fun onTouchEvent(event: MotionEvent?): Boolean {
- if (VERBOSE) Log.d(TAG, "FooterActionsView onTouchEvent ${event?.string}")
- return super.onTouchEvent(event)
- }
-}
-private const val TAG = "FooterActionsView"
-private val VERBOSE = Log.isLoggable(TAG, Log.VERBOSE)
-private val MotionEvent.string
- get() = "($id): ($x,$y)"
diff --git a/packages/SystemUI/src/com/android/systemui/qs/NewFooterActionsController.kt b/packages/SystemUI/src/com/android/systemui/qs/NewFooterActionsController.kt
deleted file mode 100644
index 7c67d9f..0000000
--- a/packages/SystemUI/src/com/android/systemui/qs/NewFooterActionsController.kt
+++ /dev/null
@@ -1,33 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.qs
-
-import com.android.systemui.dagger.SysUISingleton
-import javax.inject.Inject
-
-/** Controller for the footer actions. This manages the initialization of its dependencies. */
-@SysUISingleton
-class NewFooterActionsController
-@Inject
-// TODO(b/242040009): Rename this to FooterActionsController.
-constructor(
- private val fgsManagerController: FgsManagerController,
-) {
- fun init() {
- fgsManagerController.init()
- }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSContainerImpl.java b/packages/SystemUI/src/com/android/systemui/qs/QSContainerImpl.java
index dc9dcc2..0c242d9 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSContainerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSContainerImpl.java
@@ -215,7 +215,7 @@
// Some views are always full width or have dependent padding
continue;
}
- if (!(view instanceof FooterActionsView)) {
+ if (view.getId() != R.id.qs_footer_actions) {
// Only padding for FooterActionsView, no margin. That way, the background goes
// all the way to the edge.
LayoutParams lp = (LayoutParams) view.getLayoutParams();
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSFgsManagerFooter.java b/packages/SystemUI/src/com/android/systemui/qs/QSFgsManagerFooter.java
deleted file mode 100644
index b1b9dd7..0000000
--- a/packages/SystemUI/src/com/android/systemui/qs/QSFgsManagerFooter.java
+++ /dev/null
@@ -1,178 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.qs;
-
-import static com.android.systemui.qs.dagger.QSFragmentModule.QS_FGS_MANAGER_FOOTER_VIEW;
-import static com.android.systemui.util.PluralMessageFormaterKt.icuMessageFormat;
-
-import android.content.Context;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.ImageView;
-import android.widget.LinearLayout;
-import android.widget.TextView;
-
-import androidx.annotation.Nullable;
-
-import com.android.systemui.R;
-import com.android.systemui.animation.Expandable;
-import com.android.systemui.dagger.qualifiers.Background;
-import com.android.systemui.dagger.qualifiers.Main;
-import com.android.systemui.qs.dagger.QSScope;
-
-import java.util.concurrent.Executor;
-
-import javax.inject.Inject;
-import javax.inject.Named;
-
-/**
- * Footer entry point for the foreground service manager
- */
-// TODO(b/242040009): Remove this file.
-@QSScope
-public class QSFgsManagerFooter implements View.OnClickListener,
- FgsManagerController.OnDialogDismissedListener,
- FgsManagerController.OnNumberOfPackagesChangedListener,
- VisibilityChangedDispatcher {
-
- private final View mRootView;
- private final TextView mFooterText;
- private final Context mContext;
- private final Executor mMainExecutor;
- private final Executor mExecutor;
-
- private final FgsManagerController mFgsManagerController;
-
- private boolean mIsInitialized = false;
- private int mNumPackages;
-
- private final View mTextContainer;
- private final View mNumberContainer;
- private final TextView mNumberView;
- private final ImageView mDotView;
- private final ImageView mCollapsedDotView;
-
- @Nullable
- private VisibilityChangedDispatcher.OnVisibilityChangedListener mVisibilityChangedListener;
-
- @Inject
- QSFgsManagerFooter(@Named(QS_FGS_MANAGER_FOOTER_VIEW) View rootView,
- @Main Executor mainExecutor, @Background Executor executor,
- FgsManagerController fgsManagerController) {
- mRootView = rootView;
- mFooterText = mRootView.findViewById(R.id.footer_text);
- mTextContainer = mRootView.findViewById(R.id.fgs_text_container);
- mNumberContainer = mRootView.findViewById(R.id.fgs_number_container);
- mNumberView = mRootView.findViewById(R.id.fgs_number);
- mDotView = mRootView.findViewById(R.id.fgs_new);
- mCollapsedDotView = mRootView.findViewById(R.id.fgs_collapsed_new);
- mContext = rootView.getContext();
- mMainExecutor = mainExecutor;
- mExecutor = executor;
- mFgsManagerController = fgsManagerController;
- }
-
- /**
- * Whether to show the footer in collapsed mode (just a number) or not (text).
- * @param collapsed
- */
- public void setCollapsed(boolean collapsed) {
- mTextContainer.setVisibility(collapsed ? View.GONE : View.VISIBLE);
- mNumberContainer.setVisibility(collapsed ? View.VISIBLE : View.GONE);
- LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) mRootView.getLayoutParams();
- lp.width = collapsed ? ViewGroup.LayoutParams.WRAP_CONTENT : 0;
- lp.weight = collapsed ? 0f : 1f;
- mRootView.setLayoutParams(lp);
- }
-
- public void init() {
- if (mIsInitialized) {
- return;
- }
-
- mFgsManagerController.init();
-
- mRootView.setOnClickListener(this);
-
- mIsInitialized = true;
- }
-
- public void setListening(boolean listening) {
- if (listening) {
- mFgsManagerController.addOnDialogDismissedListener(this);
- mFgsManagerController.addOnNumberOfPackagesChangedListener(this);
- mNumPackages = mFgsManagerController.getNumRunningPackages();
- refreshState();
- } else {
- mFgsManagerController.removeOnDialogDismissedListener(this);
- mFgsManagerController.removeOnNumberOfPackagesChangedListener(this);
- }
- }
-
- @Override
- public void setOnVisibilityChangedListener(
- @Nullable OnVisibilityChangedListener onVisibilityChangedListener) {
- mVisibilityChangedListener = onVisibilityChangedListener;
- }
-
- @Override
- public void onClick(View view) {
- mFgsManagerController.showDialog(Expandable.fromView(view));
- }
-
- public void refreshState() {
- mExecutor.execute(this::handleRefreshState);
- }
-
- public View getView() {
- return mRootView;
- }
-
- public void handleRefreshState() {
- mMainExecutor.execute(() -> {
- CharSequence text = icuMessageFormat(mContext.getResources(),
- R.string.fgs_manager_footer_label, mNumPackages);
- mFooterText.setText(text);
- mNumberView.setText(Integer.toString(mNumPackages));
- mNumberView.setContentDescription(text);
- if (mFgsManagerController.shouldUpdateFooterVisibility()) {
- mRootView.setVisibility(mNumPackages > 0
- && mFgsManagerController.isAvailable().getValue() ? View.VISIBLE
- : View.GONE);
- int dotVis = mFgsManagerController.getShowFooterDot().getValue()
- && mFgsManagerController.getNewChangesSinceDialogWasDismissed()
- ? View.VISIBLE : View.GONE;
- mDotView.setVisibility(dotVis);
- mCollapsedDotView.setVisibility(dotVis);
- if (mVisibilityChangedListener != null) {
- mVisibilityChangedListener.onVisibilityChanged(mRootView.getVisibility());
- }
- }
- });
- }
-
- @Override
- public void onDialogDismissed() {
- refreshState();
- }
-
- @Override
- public void onNumberOfPackagesChanged(int numPackages) {
- mNumPackages = numPackages;
- refreshState();
- }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java b/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java
index c0533ba..893574a 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java
@@ -33,6 +33,7 @@
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
+import android.widget.LinearLayout;
import androidx.annotation.FloatRange;
import androidx.annotation.Nullable;
@@ -48,7 +49,6 @@
import com.android.systemui.animation.ShadeInterpolation;
import com.android.systemui.dump.DumpManager;
import com.android.systemui.flags.FeatureFlags;
-import com.android.systemui.flags.Flags;
import com.android.systemui.media.controls.ui.MediaHost;
import com.android.systemui.plugins.FalsingManager;
import com.android.systemui.plugins.qs.QS;
@@ -114,7 +114,7 @@
private final QSFragmentDisableFlagsLogger mQsFragmentDisableFlagsLogger;
private final QSTileHost mHost;
private final FeatureFlags mFeatureFlags;
- private final NewFooterActionsController mNewFooterActionsController;
+ private final FooterActionsController mFooterActionsController;
private final FooterActionsViewModel.Factory mFooterActionsViewModelFactory;
private final ListeningAndVisibilityLifecycleOwner mListeningAndVisibilityLifecycleOwner;
private boolean mShowCollapsedOnKeyguard;
@@ -132,9 +132,6 @@
private QSPanelController mQSPanelController;
private QuickQSPanelController mQuickQSPanelController;
private QSCustomizerController mQSCustomizerController;
- @Nullable
- private FooterActionsController mQSFooterActionController;
- @Nullable
private FooterActionsViewModel mQSFooterActionsViewModel;
@Nullable
private ScrollListener mScrollListener;
@@ -185,7 +182,7 @@
QSFragmentComponent.Factory qsComponentFactory,
QSFragmentDisableFlagsLogger qsFragmentDisableFlagsLogger,
FalsingManager falsingManager, DumpManager dumpManager, FeatureFlags featureFlags,
- NewFooterActionsController newFooterActionsController,
+ FooterActionsController footerActionsController,
FooterActionsViewModel.Factory footerActionsViewModelFactory) {
mRemoteInputQuickSettingsDisabler = remoteInputQsDisabler;
mQsMediaHost = qsMediaHost;
@@ -199,7 +196,7 @@
mStatusBarStateController = statusBarStateController;
mDumpManager = dumpManager;
mFeatureFlags = featureFlags;
- mNewFooterActionsController = newFooterActionsController;
+ mFooterActionsController = footerActionsController;
mFooterActionsViewModelFactory = footerActionsViewModelFactory;
mListeningAndVisibilityLifecycleOwner = new ListeningAndVisibilityLifecycleOwner();
}
@@ -226,18 +223,12 @@
mQSPanelController.init();
mQuickQSPanelController.init();
- if (mFeatureFlags.isEnabled(Flags.NEW_FOOTER_ACTIONS)) {
- mQSFooterActionsViewModel = mFooterActionsViewModelFactory.create(/* lifecycleOwner */
- this);
- FooterActionsView footerActionsView = view.findViewById(R.id.qs_footer_actions);
- FooterActionsViewBinder.bind(footerActionsView, mQSFooterActionsViewModel,
- mListeningAndVisibilityLifecycleOwner);
-
- mNewFooterActionsController.init();
- } else {
- mQSFooterActionController = qsFragmentComponent.getQSFooterActionController();
- mQSFooterActionController.init();
- }
+ mQSFooterActionsViewModel = mFooterActionsViewModelFactory.create(/* lifecycleOwner */
+ this);
+ LinearLayout footerActionsView = view.findViewById(R.id.qs_footer_actions);
+ FooterActionsViewBinder.bind(footerActionsView, mQSFooterActionsViewModel,
+ mListeningAndVisibilityLifecycleOwner);
+ mFooterActionsController.init();
mQSPanelScrollView = view.findViewById(R.id.expanded_qs_scroll_view);
mQSPanelScrollView.addOnLayoutChangeListener(
@@ -436,9 +427,6 @@
mContainer.disable(state1, state2, animate);
mHeader.disable(state1, state2, animate);
mFooter.disable(state1, state2, animate);
- if (mQSFooterActionController != null) {
- mQSFooterActionController.disable(state2);
- }
updateQsState();
}
@@ -457,11 +445,7 @@
boolean footerVisible = qsPanelVisible && (mQsExpanded || !keyguardShowing
|| mHeaderAnimating || mShowCollapsedOnKeyguard);
mFooter.setVisibility(footerVisible ? View.VISIBLE : View.INVISIBLE);
- if (mQSFooterActionController != null) {
- mQSFooterActionController.setVisible(footerVisible);
- } else {
- mQSFooterActionsViewModel.onVisibilityChangeRequested(footerVisible);
- }
+ mQSFooterActionsViewModel.onVisibilityChangeRequested(footerVisible);
mFooter.setExpanded((keyguardShowing && !mHeaderAnimating && !mShowCollapsedOnKeyguard)
|| (mQsExpanded && !mStackScrollerOverscrolling));
mQSPanelController.setVisibility(qsPanelVisible ? View.VISIBLE : View.INVISIBLE);
@@ -534,9 +518,6 @@
}
mFooter.setKeyguardShowing(keyguardShowing);
- if (mQSFooterActionController != null) {
- mQSFooterActionController.setKeyguardShowing(keyguardShowing);
- }
updateQsState();
}
@@ -552,9 +533,6 @@
if (DEBUG) Log.d(TAG, "setListening " + listening);
mListening = listening;
mQSContainerImplController.setListening(listening && mQsVisible);
- if (mQSFooterActionController != null) {
- mQSFooterActionController.setListening(listening && mQsVisible);
- }
mListeningAndVisibilityLifecycleOwner.updateState();
updateQsPanelControllerListening();
}
@@ -665,12 +643,8 @@
mFooter.setExpansion(onKeyguardAndExpanded ? 1 : expansion);
float footerActionsExpansion =
onKeyguardAndExpanded ? 1 : mInSplitShade ? alphaProgress : expansion;
- if (mQSFooterActionController != null) {
- mQSFooterActionController.setExpansion(footerActionsExpansion);
- } else {
- mQSFooterActionsViewModel.onQuickSettingsExpansionChanged(footerActionsExpansion,
- mInSplitShade);
- }
+ mQSFooterActionsViewModel.onQuickSettingsExpansionChanged(footerActionsExpansion,
+ mInSplitShade);
mQSPanelController.setRevealExpansion(expansion);
mQSPanelController.getTileLayout().setExpansion(expansion, proposedTranslation);
mQuickQSPanelController.getTileLayout().setExpansion(expansion, proposedTranslation);
@@ -835,11 +809,7 @@
boolean customizing = isCustomizing();
mQSPanelScrollView.setVisibility(!customizing ? View.VISIBLE : View.INVISIBLE);
mFooter.setVisibility(!customizing ? View.VISIBLE : View.INVISIBLE);
- if (mQSFooterActionController != null) {
- mQSFooterActionController.setVisible(!customizing);
- } else {
- mQSFooterActionsViewModel.onVisibilityChangeRequested(!customizing);
- }
+ mQSFooterActionsViewModel.onVisibilityChangeRequested(!customizing);
mHeader.setVisibility(!customizing ? View.VISIBLE : View.INVISIBLE);
// Let the panel know the position changed and it needs to update where notifications
// and whatnot are.
@@ -927,6 +897,11 @@
updateShowCollapsedOnKeyguard();
}
+ @VisibleForTesting
+ public ListeningAndVisibilityLifecycleOwner getListeningAndVisibilityLifecycleOwner() {
+ return mListeningAndVisibilityLifecycleOwner;
+ }
+
@Override
public void dump(PrintWriter pw, String[] args) {
IndentingPrintWriter indentingPw = new IndentingPrintWriter(pw, /* singleIndent= */ " ");
@@ -994,7 +969,8 @@
* - STARTED when mListening == true && mQsVisible == false.
* - RESUMED when mListening == true && mQsVisible == true.
*/
- private class ListeningAndVisibilityLifecycleOwner implements LifecycleOwner {
+ @VisibleForTesting
+ class ListeningAndVisibilityLifecycleOwner implements LifecycleOwner {
private final LifecycleRegistry mLifecycleRegistry = new LifecycleRegistry(this);
private boolean mDestroyed = false;
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSSecurityFooter.java b/packages/SystemUI/src/com/android/systemui/qs/QSSecurityFooter.java
deleted file mode 100644
index 6c1e956..0000000
--- a/packages/SystemUI/src/com/android/systemui/qs/QSSecurityFooter.java
+++ /dev/null
@@ -1,262 +0,0 @@
-/*
- * Copyright (C) 2014 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.systemui.qs;
-
-import static com.android.systemui.qs.dagger.QSFragmentModule.QS_SECURITY_FOOTER_VIEW;
-
-import android.app.admin.DevicePolicyEventLogger;
-import android.app.admin.DevicePolicyManager;
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.content.res.Resources;
-import android.os.Handler;
-import android.os.Looper;
-import android.os.Message;
-import android.os.UserHandle;
-import android.util.Log;
-import android.view.View;
-import android.view.View.OnClickListener;
-import android.widget.ImageView;
-import android.widget.TextView;
-
-import androidx.annotation.Nullable;
-
-import com.android.internal.util.FrameworkStatsLog;
-import com.android.systemui.FontSizeUtils;
-import com.android.systemui.R;
-import com.android.systemui.animation.Expandable;
-import com.android.systemui.broadcast.BroadcastDispatcher;
-import com.android.systemui.common.shared.model.Icon;
-import com.android.systemui.dagger.qualifiers.Background;
-import com.android.systemui.dagger.qualifiers.Main;
-import com.android.systemui.qs.dagger.QSScope;
-import com.android.systemui.qs.footer.domain.model.SecurityButtonConfig;
-import com.android.systemui.security.data.model.SecurityModel;
-import com.android.systemui.statusbar.policy.SecurityController;
-import com.android.systemui.util.ViewController;
-
-import javax.inject.Inject;
-import javax.inject.Named;
-
-/** ViewController for the footer actions. */
-// TODO(b/242040009): Remove this class.
-@QSScope
-public class QSSecurityFooter extends ViewController<View>
- implements OnClickListener, VisibilityChangedDispatcher {
- protected static final String TAG = "QSSecurityFooter";
-
- private final TextView mFooterText;
- private final ImageView mPrimaryFooterIcon;
- private Context mContext;
- private final Callback mCallback = new Callback();
- private final SecurityController mSecurityController;
- private final Handler mMainHandler;
- private final BroadcastDispatcher mBroadcastDispatcher;
- private final QSSecurityFooterUtils mQSSecurityFooterUtils;
-
- protected H mHandler;
-
- private boolean mIsVisible;
- private boolean mIsClickable;
- @Nullable
- private CharSequence mFooterTextContent = null;
- private Icon mFooterIcon;
-
- @Nullable
- private VisibilityChangedDispatcher.OnVisibilityChangedListener mVisibilityChangedListener;
-
- private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
- @Override
- public void onReceive(Context context, Intent intent) {
- if (intent.getAction().equals(
- DevicePolicyManager.ACTION_SHOW_DEVICE_MONITORING_DIALOG)) {
- showDeviceMonitoringDialog();
- }
- }
- };
-
- @Inject
- QSSecurityFooter(@Named(QS_SECURITY_FOOTER_VIEW) View rootView,
- @Main Handler mainHandler, SecurityController securityController,
- @Background Looper bgLooper, BroadcastDispatcher broadcastDispatcher,
- QSSecurityFooterUtils qSSecurityFooterUtils) {
- super(rootView);
- mFooterText = mView.findViewById(R.id.footer_text);
- mPrimaryFooterIcon = mView.findViewById(R.id.primary_footer_icon);
- mFooterIcon = new Icon.Resource(
- R.drawable.ic_info_outline, /* contentDescription= */ null);
- mContext = rootView.getContext();
- mSecurityController = securityController;
- mMainHandler = mainHandler;
- mHandler = new H(bgLooper);
- mBroadcastDispatcher = broadcastDispatcher;
- mQSSecurityFooterUtils = qSSecurityFooterUtils;
- }
-
- @Override
- protected void onViewAttached() {
- // Use background handler, as it's the same thread that handleClick is called on.
- mBroadcastDispatcher.registerReceiverWithHandler(mReceiver,
- new IntentFilter(DevicePolicyManager.ACTION_SHOW_DEVICE_MONITORING_DIALOG),
- mHandler, UserHandle.ALL);
- mView.setOnClickListener(this);
- }
-
- @Override
- protected void onViewDetached() {
- mBroadcastDispatcher.unregisterReceiver(mReceiver);
- mView.setOnClickListener(null);
- }
-
- public void setListening(boolean listening) {
- if (listening) {
- mSecurityController.addCallback(mCallback);
- refreshState();
- } else {
- mSecurityController.removeCallback(mCallback);
- }
- }
-
- @Override
- public void setOnVisibilityChangedListener(
- @Nullable OnVisibilityChangedListener onVisibilityChangedListener) {
- mVisibilityChangedListener = onVisibilityChangedListener;
- }
-
- public void onConfigurationChanged() {
- FontSizeUtils.updateFontSize(mFooterText, R.dimen.qs_tile_text_size);
- Resources r = mContext.getResources();
-
- int padding = r.getDimensionPixelSize(R.dimen.qs_footer_padding);
- mView.setPaddingRelative(padding, 0, padding, 0);
- mView.setBackground(mContext.getDrawable(R.drawable.qs_security_footer_background));
- }
-
- public View getView() {
- return mView;
- }
-
- public boolean hasFooter() {
- return mView.getVisibility() != View.GONE;
- }
-
- @Override
- public void onClick(View v) {
- if (!hasFooter()) return;
- mHandler.sendEmptyMessage(H.CLICK);
- }
-
- private void handleClick() {
- showDeviceMonitoringDialog();
- DevicePolicyEventLogger
- .createEvent(FrameworkStatsLog.DEVICE_POLICY_EVENT__EVENT_ID__DO_USER_INFO_CLICKED)
- .write();
- }
-
- // TODO(b/242040009): Remove this.
- public void showDeviceMonitoringDialog() {
- mQSSecurityFooterUtils.showDeviceMonitoringDialog(mContext, Expandable.fromView(mView));
- }
-
- public void refreshState() {
- mHandler.sendEmptyMessage(H.REFRESH_STATE);
- }
-
- private void handleRefreshState() {
- SecurityModel securityModel = SecurityModel.create(mSecurityController);
- SecurityButtonConfig buttonConfig = mQSSecurityFooterUtils.getButtonConfig(securityModel);
-
- if (buttonConfig == null) {
- mIsVisible = false;
- } else {
- mIsVisible = true;
- mIsClickable = buttonConfig.isClickable();
- mFooterTextContent = buttonConfig.getText();
- mFooterIcon = buttonConfig.getIcon();
- }
-
- // Update the UI.
- mMainHandler.post(mUpdatePrimaryIcon);
- mMainHandler.post(mUpdateDisplayState);
- }
-
- private final Runnable mUpdatePrimaryIcon = new Runnable() {
- @Override
- public void run() {
- if (mFooterIcon instanceof Icon.Loaded) {
- mPrimaryFooterIcon.setImageDrawable(((Icon.Loaded) mFooterIcon).getDrawable());
- } else if (mFooterIcon instanceof Icon.Resource) {
- mPrimaryFooterIcon.setImageResource(((Icon.Resource) mFooterIcon).getRes());
- }
- }
- };
-
- private final Runnable mUpdateDisplayState = new Runnable() {
- @Override
- public void run() {
- if (mFooterTextContent != null) {
- mFooterText.setText(mFooterTextContent);
- }
- mView.setVisibility(mIsVisible ? View.VISIBLE : View.GONE);
- if (mVisibilityChangedListener != null) {
- mVisibilityChangedListener.onVisibilityChanged(mView.getVisibility());
- }
-
- if (mIsVisible && mIsClickable) {
- mView.setClickable(true);
- mView.findViewById(R.id.footer_icon).setVisibility(View.VISIBLE);
- } else {
- mView.setClickable(false);
- mView.findViewById(R.id.footer_icon).setVisibility(View.GONE);
- }
- }
- };
-
- private class Callback implements SecurityController.SecurityControllerCallback {
- @Override
- public void onStateChanged() {
- refreshState();
- }
- }
-
- private class H extends Handler {
- private static final int CLICK = 0;
- private static final int REFRESH_STATE = 1;
-
- private H(Looper looper) {
- super(looper);
- }
-
- @Override
- public void handleMessage(Message msg) {
- String name = null;
- try {
- if (msg.what == REFRESH_STATE) {
- name = "handleRefreshState";
- handleRefreshState();
- } else if (msg.what == CLICK) {
- name = "handleClick";
- handleClick();
- }
- } catch (Throwable t) {
- final String error = "Error in " + name;
- Log.w(TAG, error, t);
- }
- }
- }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSTileHost.java b/packages/SystemUI/src/com/android/systemui/qs/QSTileHost.java
index 6240c10..cad296b 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSTileHost.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSTileHost.java
@@ -608,7 +608,7 @@
if (TextUtils.isEmpty(tileList)) {
tileList = res.getString(R.string.quick_settings_tiles);
- if (DEBUG) Log.d(TAG, "Loaded tile specs from config: " + tileList);
+ if (DEBUG) Log.d(TAG, "Loaded tile specs from default config: " + tileList);
} else {
if (DEBUG) Log.d(TAG, "Loaded tile specs from setting: " + tileList);
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/dagger/QSFragmentModule.java b/packages/SystemUI/src/com/android/systemui/qs/dagger/QSFragmentModule.java
index aa505fb..01eb636 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/dagger/QSFragmentModule.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/dagger/QSFragmentModule.java
@@ -28,7 +28,6 @@
import com.android.systemui.dagger.qualifiers.RootView;
import com.android.systemui.plugins.qs.QS;
import com.android.systemui.privacy.OngoingPrivacyChip;
-import com.android.systemui.qs.FooterActionsView;
import com.android.systemui.qs.QSContainerImpl;
import com.android.systemui.qs.QSFooter;
import com.android.systemui.qs.QSFooterView;
@@ -51,8 +50,6 @@
*/
@Module
public interface QSFragmentModule {
- String QS_FGS_MANAGER_FOOTER_VIEW = "qs_fgs_manager_footer";
- String QS_SECURITY_FOOTER_VIEW = "qs_security_footer";
String QS_USING_MEDIA_PLAYER = "qs_using_media_player";
String QS_USING_COLLAPSED_LANDSCAPE_MEDIA = "qs_using_collapsed_landscape_media";
@@ -119,16 +116,6 @@
return view.findViewById(R.id.qs_footer);
}
- /**
- * Provides a {@link FooterActionsView}.
- *
- * This will replace a ViewStub either in {@link QSFooterView} or in {@link QSContainerImpl}.
- */
- @Provides
- static FooterActionsView providesQSFooterActionsView(@RootView View view) {
- return view.findViewById(R.id.qs_footer_actions);
- }
-
/** */
@Provides
@QSScope
@@ -146,18 +133,6 @@
/** */
@Provides
- @QSScope
- @Named(QS_SECURITY_FOOTER_VIEW)
- static View providesQSSecurityFooterView(
- @QSThemedContext LayoutInflater layoutInflater,
- FooterActionsView footerActionsView
- ) {
- return layoutInflater.inflate(R.layout.quick_settings_security_footer, footerActionsView,
- false);
- }
-
- /** */
- @Provides
@Named(QS_USING_MEDIA_PLAYER)
static boolean providesQSUsingMediaPlayer(Context context) {
return useQsMediaPlayer(context);
@@ -183,15 +158,4 @@
static StatusIconContainer providesStatusIconContainer(QuickStatusBarHeader qsHeader) {
return qsHeader.findViewById(R.id.statusIcons);
}
-
- /** */
- @Provides
- @QSScope
- @Named(QS_FGS_MANAGER_FOOTER_VIEW)
- static View providesQSFgsManagerFooterView(
- @QSThemedContext LayoutInflater layoutInflater,
- FooterActionsView footerActionsView
- ) {
- return layoutInflater.inflate(R.layout.fgs_footer, footerActionsView, false);
- }
}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/qs/footer/ui/binder/FooterActionsViewBinder.kt b/packages/SystemUI/src/com/android/systemui/qs/footer/ui/binder/FooterActionsViewBinder.kt
index 3e39c8e..6db3c99 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/footer/ui/binder/FooterActionsViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/footer/ui/binder/FooterActionsViewBinder.kt
@@ -35,35 +35,31 @@
import com.android.systemui.common.ui.binder.IconViewBinder
import com.android.systemui.lifecycle.repeatWhenAttached
import com.android.systemui.people.ui.view.PeopleViewBinder.bind
-import com.android.systemui.qs.FooterActionsView
import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsButtonViewModel
import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsForegroundServicesButtonViewModel
import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsSecurityButtonViewModel
import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel
+import kotlin.math.roundToInt
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
/** A ViewBinder for [FooterActionsViewBinder]. */
object FooterActionsViewBinder {
- /**
- * Create a [FooterActionsView] that can later be [bound][bind] to a [FooterActionsViewModel].
- */
+ /** Create a view that can later be [bound][bind] to a [FooterActionsViewModel]. */
@JvmStatic
- fun create(context: Context): FooterActionsView {
+ fun create(context: Context): LinearLayout {
return LayoutInflater.from(context).inflate(R.layout.footer_actions, /* root= */ null)
- as FooterActionsView
+ as LinearLayout
}
/** Bind [view] to [viewModel]. */
@JvmStatic
fun bind(
- view: FooterActionsView,
+ view: LinearLayout,
viewModel: FooterActionsViewModel,
qsVisibilityLifecycleOwner: LifecycleOwner,
) {
- // Remove all children of the FooterActionsView that are used by the old implementation.
- // TODO(b/242040009): Clean up the XML once the old implementation is removed.
- view.removeAllViews()
+ view.importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_YES
// Add the views used by this new implementation.
val context = view.context
@@ -117,7 +113,11 @@
}
launch { viewModel.alpha.collect { view.alpha = it } }
- launch { viewModel.backgroundAlpha.collect { view.backgroundAlpha = it } }
+ launch {
+ viewModel.backgroundAlpha.collect {
+ view.background?.alpha = (it * 255).roundToInt()
+ }
+ }
}
// Listen for model changes only when QS are visible.
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/InternetTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/InternetTile.java
index 9542a02..28dd986 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/InternetTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/InternetTile.java
@@ -333,7 +333,14 @@
mCellularInfo.mAirplaneModeEnabled = icon.visible;
mWifiInfo.mAirplaneModeEnabled = icon.visible;
if (!mSignalCallback.mEthernetInfo.mConnected) {
- if (mWifiInfo.mEnabled && (mWifiInfo.mWifiSignalIconId > 0)
+ // Always use mWifiInfo to refresh the Internet Tile if airplane mode is enabled,
+ // because Internet Tile will show different information depending on whether WiFi
+ // is enabled or not.
+ if (mWifiInfo.mAirplaneModeEnabled) {
+ refreshState(mWifiInfo);
+ // If airplane mode is disabled, we will use mWifiInfo to refresh the Internet Tile
+ // if WiFi is currently connected to avoid any icon flickering.
+ } else if (mWifiInfo.mEnabled && (mWifiInfo.mWifiSignalIconId > 0)
&& (mWifiInfo.mSsid != null)) {
refreshState(mWifiInfo);
} else {
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/WorkModeTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/WorkModeTile.java
index 7130294..a6c7781 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/WorkModeTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/WorkModeTile.java
@@ -27,6 +27,7 @@
import android.view.View;
import android.widget.Switch;
+import androidx.annotation.MainThread;
import androidx.annotation.Nullable;
import com.android.internal.logging.MetricsLogger;
@@ -91,11 +92,13 @@
}
@Override
+ @MainThread
public void onManagedProfileChanged() {
refreshState(mProfileController.isWorkModeEnabled());
}
@Override
+ @MainThread
public void onManagedProfileRemoved() {
mHost.removeTile(getTileSpec());
mHost.unmarkTileAsAutoAdded(getTileSpec());
diff --git a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java
index 547b496..00d129a 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java
@@ -565,13 +565,25 @@
statusBarWinController.registerCallback(mStatusBarWindowCallback);
mScreenshotHelper = new ScreenshotHelper(context);
- // Listen for tracing state changes
commandQueue.addCallback(new CommandQueue.Callbacks() {
+
+ // Listen for tracing state changes
@Override
public void onTracingStateChanged(boolean enabled) {
mSysUiState.setFlag(SYSUI_STATE_TRACING_ENABLED, enabled)
.commitUpdate(mContext.getDisplayId());
}
+
+ @Override
+ public void enterStageSplitFromRunningApp(boolean leftOrTop) {
+ if (mOverviewProxy != null) {
+ try {
+ mOverviewProxy.enterStageSplitFromRunningApp(leftOrTop);
+ } catch (RemoteException e) {
+ Log.w(TAG_OPS, "Unable to enter stage split from the current running app");
+ }
+ }
+ }
});
mCommandQueue = commandQueue;
diff --git a/packages/SystemUI/src/com/android/systemui/security/data/model/SecurityModel.kt b/packages/SystemUI/src/com/android/systemui/security/data/model/SecurityModel.kt
index 50af260..1cf5a8f 100644
--- a/packages/SystemUI/src/com/android/systemui/security/data/model/SecurityModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/security/data/model/SecurityModel.kt
@@ -17,6 +17,7 @@
package com.android.systemui.security.data.model
import android.graphics.drawable.Drawable
+import androidx.annotation.VisibleForTesting
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.statusbar.policy.SecurityController
import kotlinx.coroutines.CoroutineDispatcher
@@ -55,8 +56,8 @@
* Important: This method should be called from a background thread as this will do a lot of
* binder calls.
*/
- // TODO(b/242040009): Remove this.
@JvmStatic
+ @VisibleForTesting
fun create(securityController: SecurityController): SecurityModel {
val deviceAdminInfo =
if (securityController.isParentalControlsEnabled) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java b/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java
index 6babc78..590a04a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java
@@ -165,6 +165,7 @@
private static final int MSG_TILE_SERVICE_REQUEST_LISTENING_STATE = 68 << MSG_SHIFT;
private static final int MSG_SHOW_REAR_DISPLAY_DIALOG = 69 << MSG_SHIFT;
private static final int MSG_GO_TO_FULLSCREEN_FROM_SPLIT = 70 << MSG_SHIFT;
+ private static final int MSG_ENTER_STAGE_SPLIT_FROM_RUNNING_APP = 71 << MSG_SHIFT;
public static final int FLAG_EXCLUDE_NONE = 0;
public static final int FLAG_EXCLUDE_SEARCH_PANEL = 1 << 0;
@@ -484,6 +485,11 @@
* @see IStatusBar#goToFullscreenFromSplit
*/
default void goToFullscreenFromSplit() {}
+
+ /**
+ * @see IStatusBar#enterStageSplitFromRunningApp
+ */
+ default void enterStageSplitFromRunningApp(boolean leftOrTop) {}
}
public CommandQueue(Context context) {
@@ -1245,6 +1251,14 @@
}
@Override
+ public void enterStageSplitFromRunningApp(boolean leftOrTop) {
+ synchronized (mLock) {
+ mHandler.obtainMessage(MSG_ENTER_STAGE_SPLIT_FROM_RUNNING_APP,
+ leftOrTop).sendToTarget();
+ }
+ }
+
+ @Override
public void requestAddTile(
@NonNull ComponentName componentName,
@NonNull CharSequence appName,
@@ -1755,6 +1769,11 @@
mCallbacks.get(i).goToFullscreenFromSplit();
}
break;
+ case MSG_ENTER_STAGE_SPLIT_FROM_RUNNING_APP:
+ for (int i = 0; i < mCallbacks.size(); i++) {
+ mCallbacks.get(i).enterStageSplitFromRunningApp((Boolean) msg.obj);
+ }
+ break;
}
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/NetworkControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/NetworkControllerImpl.java
index 37d83f8..336356e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/NetworkControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/NetworkControllerImpl.java
@@ -78,6 +78,7 @@
import com.android.systemui.plugins.log.LogLevel;
import com.android.systemui.qs.tiles.dialog.InternetDialogFactory;
import com.android.systemui.settings.UserTracker;
+import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags;
import com.android.systemui.statusbar.policy.ConfigurationController;
import com.android.systemui.statusbar.policy.DataSaverController;
import com.android.systemui.statusbar.policy.DataSaverControllerImpl;
@@ -192,6 +193,7 @@
private final Executor mBgExecutor;
// Handler that all callbacks are made on.
private final CallbackHandler mCallbackHandler;
+ private final StatusBarPipelineFlags mStatusBarPipelineFlags;
private int mEmergencySource;
private boolean mIsEmergency;
@@ -242,6 +244,7 @@
TelephonyListenerManager telephonyListenerManager,
@Nullable WifiManager wifiManager,
AccessPointControllerImpl accessPointController,
+ StatusBarPipelineFlags statusBarPipelineFlags,
DemoModeController demoModeController,
CarrierConfigTracker carrierConfigTracker,
WifiStatusTrackerFactory trackerFactory,
@@ -260,6 +263,7 @@
bgExecutor,
callbackHandler,
accessPointController,
+ statusBarPipelineFlags,
new DataUsageController(context),
new SubscriptionDefaults(),
deviceProvisionedController,
@@ -287,6 +291,7 @@
Executor bgExecutor,
CallbackHandler callbackHandler,
AccessPointControllerImpl accessPointController,
+ StatusBarPipelineFlags statusBarPipelineFlags,
DataUsageController dataUsageController,
SubscriptionDefaults defaultsHandler,
DeviceProvisionedController deviceProvisionedController,
@@ -308,6 +313,7 @@
mBgLooper = bgLooper;
mBgExecutor = bgExecutor;
mCallbackHandler = callbackHandler;
+ mStatusBarPipelineFlags = statusBarPipelineFlags;
mDataSaverController = new DataSaverControllerImpl(context);
mBroadcastDispatcher = broadcastDispatcher;
mMobileFactory = mobileFactory;
@@ -1333,7 +1339,7 @@
mWifiSignalController.notifyListeners();
}
String sims = args.getString("sims");
- if (sims != null) {
+ if (sims != null && !mStatusBarPipelineFlags.useNewMobileIcons()) {
int num = MathUtils.constrain(Integer.parseInt(sims), 1, 8);
List<SubscriptionInfo> subs = new ArrayList<>();
if (num != mMobileSignalControllers.size()) {
@@ -1356,7 +1362,7 @@
mCallbackHandler.setNoSims(mHasNoSubs, mSimDetected);
}
String mobile = args.getString("mobile");
- if (mobile != null) {
+ if (mobile != null && !mStatusBarPipelineFlags.useNewMobileIcons()) {
boolean show = mobile.equals("show");
String datatype = args.getString("datatype");
String slotString = args.getString("slot");
@@ -1441,7 +1447,7 @@
controller.notifyListeners();
}
String carrierNetworkChange = args.getString("carriernetworkchange");
- if (carrierNetworkChange != null) {
+ if (carrierNetworkChange != null && !mStatusBarPipelineFlags.useNewMobileIcons()) {
boolean show = carrierNetworkChange.equals("show");
for (int i = 0; i < mMobileSignalControllers.size(); i++) {
MobileSignalController controller = mMobileSignalControllers.valueAt(i);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt
index 6bd9502..8bb2d46 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt
@@ -102,6 +102,8 @@
private var showSensitiveContentForManagedUser = false
private var managedUserHandle: UserHandle? = null
+ // TODO(b/202758428): refactor so that we can test color updates via region samping, similar to
+ // how we test color updates when theme changes (See testThemeChangeUpdatesTextColor).
private val updateFun: UpdateColorCallback = { updateTextColorFromRegionSampler() }
// TODO: Move logic into SmartspaceView
@@ -109,16 +111,19 @@
override fun onViewAttachedToWindow(v: View) {
smartspaceViews.add(v as SmartspaceView)
- var regionSampler = RegionSampler(
- v,
- uiExecutor,
- bgExecutor,
- regionSamplingEnabled,
- updateFun
- )
- initializeTextColors(regionSampler)
- regionSampler.startRegionSampler()
- regionSamplers.put(v, regionSampler)
+ if (regionSamplingEnabled) {
+ var regionSampler = RegionSampler(
+ v,
+ uiExecutor,
+ bgExecutor,
+ regionSamplingEnabled,
+ updateFun
+ )
+ initializeTextColors(regionSampler)
+ regionSampler.startRegionSampler()
+ regionSamplers.put(v, regionSampler)
+ }
+
connectSession()
updateTextColorFromWallpaper()
@@ -128,9 +133,11 @@
override fun onViewDetachedFromWindow(v: View) {
smartspaceViews.remove(v as SmartspaceView)
- var regionSampler = regionSamplers.getValue(v)
- regionSampler.stopRegionSampler()
- regionSamplers.remove(v)
+ if (regionSamplingEnabled) {
+ var regionSampler = regionSamplers.getValue(v)
+ regionSampler.stopRegionSampler()
+ regionSamplers.remove(v)
+ }
if (smartspaceViews.isEmpty()) {
disconnect()
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DemoStatusIcons.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DemoStatusIcons.java
index 1169d3f..c7be219 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DemoStatusIcons.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DemoStatusIcons.java
@@ -40,6 +40,8 @@
import com.android.systemui.statusbar.connectivity.ui.MobileContextProvider;
import com.android.systemui.statusbar.phone.StatusBarSignalPolicy.MobileIconState;
import com.android.systemui.statusbar.phone.StatusBarSignalPolicy.WifiIconState;
+import com.android.systemui.statusbar.pipeline.mobile.ui.view.ModernStatusBarMobileView;
+import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconsViewModel;
import java.util.ArrayList;
import java.util.List;
@@ -50,20 +52,25 @@
private final LinearLayout mStatusIcons;
private final ArrayList<StatusBarMobileView> mMobileViews = new ArrayList<>();
+ private final ArrayList<ModernStatusBarMobileView> mModernMobileViews = new ArrayList<>();
private final int mIconSize;
private StatusBarWifiView mWifiView;
private boolean mDemoMode;
private int mColor;
+ private final MobileIconsViewModel mMobileIconsViewModel;
+
public DemoStatusIcons(
LinearLayout statusIcons,
+ MobileIconsViewModel mobileIconsViewModel,
int iconSize
) {
super(statusIcons.getContext());
mStatusIcons = statusIcons;
mIconSize = iconSize;
mColor = DarkIconDispatcher.DEFAULT_ICON_TINT;
+ mMobileIconsViewModel = mobileIconsViewModel;
if (statusIcons instanceof StatusIconContainer) {
setShouldRestrictIcons(((StatusIconContainer) statusIcons).isRestrictingIcons());
@@ -71,7 +78,7 @@
setShouldRestrictIcons(false);
}
setLayoutParams(mStatusIcons.getLayoutParams());
- setPadding(mStatusIcons.getPaddingLeft(),mStatusIcons.getPaddingTop(),
+ setPadding(mStatusIcons.getPaddingLeft(), mStatusIcons.getPaddingTop(),
mStatusIcons.getPaddingRight(), mStatusIcons.getPaddingBottom());
setOrientation(mStatusIcons.getOrientation());
setGravity(Gravity.CENTER_VERTICAL); // no LL.getGravity()
@@ -115,6 +122,8 @@
public void onDemoModeFinished() {
mDemoMode = false;
mStatusIcons.setVisibility(View.VISIBLE);
+ mModernMobileViews.clear();
+ mMobileViews.clear();
setVisibility(View.GONE);
}
@@ -269,6 +278,24 @@
}
/**
+ * Add a {@link ModernStatusBarMobileView}
+ * @param mobileContext possibly mcc/mnc overridden mobile context
+ * @param subId the subscriptionId for this mobile view
+ */
+ public void addModernMobileView(Context mobileContext, int subId) {
+ Log.d(TAG, "addModernMobileView (subId=" + subId + ")");
+ ModernStatusBarMobileView view = ModernStatusBarMobileView.constructAndBind(
+ mobileContext,
+ "mobile",
+ mMobileIconsViewModel.viewModelForSub(subId)
+ );
+
+ // mobile always goes at the end
+ mModernMobileViews.add(view);
+ addView(view, getChildCount(), createLayoutParams());
+ }
+
+ /**
* Apply an update to a mobile icon view for the given {@link MobileIconState}. For
* compatibility with {@link MobileContextProvider}, we have to recreate the view every time we
* update it, since the context (and thus the {@link Configuration}) may have changed
@@ -292,12 +319,19 @@
if (view.getSlot().equals("wifi")) {
removeView(mWifiView);
mWifiView = null;
- } else {
+ } else if (view instanceof StatusBarMobileView) {
StatusBarMobileView mobileView = matchingMobileView(view);
if (mobileView != null) {
removeView(mobileView);
mMobileViews.remove(mobileView);
}
+ } else if (view instanceof ModernStatusBarMobileView) {
+ ModernStatusBarMobileView mobileView = matchingModernMobileView(
+ (ModernStatusBarMobileView) view);
+ if (mobileView != null) {
+ removeView(mobileView);
+ mModernMobileViews.remove(mobileView);
+ }
}
}
@@ -316,6 +350,16 @@
return null;
}
+ private ModernStatusBarMobileView matchingModernMobileView(ModernStatusBarMobileView other) {
+ for (ModernStatusBarMobileView v : mModernMobileViews) {
+ if (v.getSubId() == other.getSubId()) {
+ return v;
+ }
+ }
+
+ return null;
+ }
+
private LayoutParams createLayoutParams() {
return new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, mIconSize);
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ManagedProfileController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ManagedProfileController.java
index 4969a1c..6811bf6 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ManagedProfileController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ManagedProfileController.java
@@ -14,6 +14,8 @@
package com.android.systemui.statusbar.phone;
+import androidx.annotation.MainThread;
+
import com.android.systemui.statusbar.phone.ManagedProfileController.Callback;
import com.android.systemui.statusbar.policy.CallbackController;
@@ -25,8 +27,20 @@
boolean isWorkModeEnabled();
- public interface Callback {
+ /**
+ * Callback to get updates about work profile status.
+ */
+ interface Callback {
+ /**
+ * Called when managed profile change is detected. This always runs on the main thread.
+ */
+ @MainThread
void onManagedProfileChanged();
+
+ /**
+ * Called when managed profile removal is detected. This always runs on the main thread.
+ */
+ @MainThread
void onManagedProfileRemoved();
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ManagedProfileControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ManagedProfileControllerImpl.java
index 4beb87d..abdf827 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ManagedProfileControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ManagedProfileControllerImpl.java
@@ -33,31 +33,28 @@
import javax.inject.Inject;
-/**
- */
@SysUISingleton
public class ManagedProfileControllerImpl implements ManagedProfileController {
private final List<Callback> mCallbacks = new ArrayList<>();
-
+ private final UserTrackerCallback mUserTrackerCallback = new UserTrackerCallback();
private final Context mContext;
private final Executor mMainExecutor;
private final UserManager mUserManager;
private final UserTracker mUserTracker;
private final LinkedList<UserInfo> mProfiles;
+
private boolean mListening;
private int mCurrentUser;
- /**
- */
@Inject
public ManagedProfileControllerImpl(Context context, @Main Executor mainExecutor,
- UserTracker userTracker) {
+ UserTracker userTracker, UserManager userManager) {
mContext = context;
mMainExecutor = mainExecutor;
- mUserManager = UserManager.get(mContext);
+ mUserManager = userManager;
mUserTracker = userTracker;
- mProfiles = new LinkedList<UserInfo>();
+ mProfiles = new LinkedList<>();
}
@Override
@@ -100,16 +97,22 @@
}
}
if (mProfiles.size() == 0 && hadProfile && (user == mCurrentUser)) {
- for (Callback callback : mCallbacks) {
- callback.onManagedProfileRemoved();
- }
+ mMainExecutor.execute(this::notifyManagedProfileRemoved);
}
mCurrentUser = user;
}
}
+ private void notifyManagedProfileRemoved() {
+ for (Callback callback : mCallbacks) {
+ callback.onManagedProfileRemoved();
+ }
+ }
+
public boolean hasActiveProfile() {
- if (!mListening) reloadManagedProfiles();
+ if (!mListening || mUserTracker.getUserId() != mCurrentUser) {
+ reloadManagedProfiles();
+ }
synchronized (mProfiles) {
return mProfiles.size() > 0;
}
@@ -134,28 +137,28 @@
mListening = listening;
if (listening) {
reloadManagedProfiles();
- mUserTracker.addCallback(mUserChangedCallback, mMainExecutor);
+ mUserTracker.addCallback(mUserTrackerCallback, mMainExecutor);
} else {
- mUserTracker.removeCallback(mUserChangedCallback);
+ mUserTracker.removeCallback(mUserTrackerCallback);
}
}
- private final UserTracker.Callback mUserChangedCallback =
- new UserTracker.Callback() {
- @Override
- public void onUserChanged(int newUser, @NonNull Context userContext) {
- reloadManagedProfiles();
- for (Callback callback : mCallbacks) {
- callback.onManagedProfileChanged();
- }
- }
+ private final class UserTrackerCallback implements UserTracker.Callback {
- @Override
- public void onProfilesChanged(@NonNull List<UserInfo> profiles) {
- reloadManagedProfiles();
- for (Callback callback : mCallbacks) {
- callback.onManagedProfileChanged();
- }
- }
- };
+ @Override
+ public void onUserChanged(int newUser, @NonNull Context userContext) {
+ reloadManagedProfiles();
+ for (Callback callback : mCallbacks) {
+ callback.onManagedProfileChanged();
+ }
+ }
+
+ @Override
+ public void onProfilesChanged(@NonNull List<UserInfo> profiles) {
+ reloadManagedProfiles();
+ for (Callback callback : mCallbacks) {
+ callback.onManagedProfileChanged();
+ }
+ }
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/MultiUserSwitchController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/MultiUserSwitchController.java
deleted file mode 100644
index 5e2a7c8..0000000
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/MultiUserSwitchController.java
+++ /dev/null
@@ -1,171 +0,0 @@
-/*
- * Copyright (C) 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.statusbar.phone;
-
-import static com.android.systemui.DejankUtils.whitelistIpcs;
-
-import android.content.Intent;
-import android.os.UserHandle;
-import android.os.UserManager;
-import android.view.View;
-import android.view.ViewGroup;
-
-import com.android.systemui.R;
-import com.android.systemui.animation.ActivityLaunchAnimator;
-import com.android.systemui.animation.Expandable;
-import com.android.systemui.flags.FeatureFlags;
-import com.android.systemui.flags.Flags;
-import com.android.systemui.plugins.ActivityStarter;
-import com.android.systemui.plugins.FalsingManager;
-import com.android.systemui.qs.FooterActionsView;
-import com.android.systemui.qs.dagger.QSScope;
-import com.android.systemui.qs.user.UserSwitchDialogController;
-import com.android.systemui.statusbar.policy.BaseUserSwitcherAdapter;
-import com.android.systemui.statusbar.policy.UserSwitcherController;
-import com.android.systemui.user.UserSwitcherActivity;
-import com.android.systemui.util.ViewController;
-
-import javax.inject.Inject;
-
-/** View Controller for {@link MultiUserSwitch}. */
-// TODO(b/242040009): Remove this file.
-public class MultiUserSwitchController extends ViewController<MultiUserSwitch> {
- private final UserManager mUserManager;
- private final UserSwitcherController mUserSwitcherController;
- private final FalsingManager mFalsingManager;
- private final UserSwitchDialogController mUserSwitchDialogController;
- private final ActivityStarter mActivityStarter;
- private final FeatureFlags mFeatureFlags;
-
- private BaseUserSwitcherAdapter mUserListener;
-
- private final View.OnClickListener mOnClickListener = new View.OnClickListener() {
- @Override
- public void onClick(View v) {
- if (mFalsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) {
- return;
- }
-
- if (mFeatureFlags.isEnabled(Flags.FULL_SCREEN_USER_SWITCHER)) {
- Intent intent = new Intent(v.getContext(), UserSwitcherActivity.class);
- intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK);
-
- mActivityStarter.startActivity(intent, true /* dismissShade */,
- ActivityLaunchAnimator.Controller.fromView(v, null),
- true /* showOverlockscreenwhenlocked */, UserHandle.SYSTEM);
- } else {
- mUserSwitchDialogController.showDialog(v.getContext(), Expandable.fromView(v));
- }
- }
- };
-
- @QSScope
- public static class Factory {
- private final UserManager mUserManager;
- private final UserSwitcherController mUserSwitcherController;
- private final FalsingManager mFalsingManager;
- private final UserSwitchDialogController mUserSwitchDialogController;
- private final ActivityStarter mActivityStarter;
- private final FeatureFlags mFeatureFlags;
-
- @Inject
- public Factory(UserManager userManager, UserSwitcherController userSwitcherController,
- FalsingManager falsingManager,
- UserSwitchDialogController userSwitchDialogController, FeatureFlags featureFlags,
- ActivityStarter activityStarter) {
- mUserManager = userManager;
- mUserSwitcherController = userSwitcherController;
- mFalsingManager = falsingManager;
- mUserSwitchDialogController = userSwitchDialogController;
- mActivityStarter = activityStarter;
- mFeatureFlags = featureFlags;
- }
-
- public MultiUserSwitchController create(FooterActionsView view) {
- return new MultiUserSwitchController(view.findViewById(R.id.multi_user_switch),
- mUserManager, mUserSwitcherController,
- mFalsingManager, mUserSwitchDialogController, mFeatureFlags,
- mActivityStarter);
- }
- }
-
- private MultiUserSwitchController(MultiUserSwitch view, UserManager userManager,
- UserSwitcherController userSwitcherController,
- FalsingManager falsingManager, UserSwitchDialogController userSwitchDialogController,
- FeatureFlags featureFlags, ActivityStarter activityStarter) {
- super(view);
- mUserManager = userManager;
- mUserSwitcherController = userSwitcherController;
- mFalsingManager = falsingManager;
- mUserSwitchDialogController = userSwitchDialogController;
- mFeatureFlags = featureFlags;
- mActivityStarter = activityStarter;
- }
-
- @Override
- protected void onInit() {
- registerListener();
- mView.refreshContentDescription(getCurrentUser());
- }
-
- @Override
- protected void onViewAttached() {
- mView.setOnClickListener(mOnClickListener);
- }
-
- @Override
- protected void onViewDetached() {
- mView.setOnClickListener(null);
- }
-
- private void registerListener() {
- if (mUserManager.isUserSwitcherEnabled() && mUserListener == null) {
-
- final UserSwitcherController controller = mUserSwitcherController;
- if (controller != null) {
- mUserListener = new BaseUserSwitcherAdapter(controller) {
- @Override
- public void notifyDataSetChanged() {
- mView.refreshContentDescription(getCurrentUser());
- }
-
- @Override
- public View getView(int position, View convertView, ViewGroup parent) {
- return null;
- }
- };
- mView.refreshContentDescription(getCurrentUser());
- }
- }
- }
-
- private String getCurrentUser() {
- // TODO(b/138661450)
- if (whitelistIpcs(() -> mUserManager.isUserSwitcherEnabled())) {
- return mUserSwitcherController.getCurrentUserName();
- }
-
- return null;
- }
-
- /** Returns true if view should be made visible. */
- public boolean isMultiUserEnabled() {
- // TODO(b/138661450) Move IPC calls to background
- return whitelistIpcs(() -> mUserManager.isUserSwitcherEnabled(
- getResources().getBoolean(R.bool.qs_show_user_switcher_for_single_user)));
- }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconController.java
index 0a0ded2..df3ab49 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconController.java
@@ -536,8 +536,7 @@
mGroup.addView(view, index, onCreateLayoutParams());
if (mIsInDemoMode) {
- // TODO (b/249790009): demo mode should be handled at the data layer in the
- // new pipeline
+ mDemoStatusIcons.addModernMobileView(mContext, subId);
}
return view;
@@ -565,11 +564,13 @@
private ModernStatusBarMobileView onCreateModernStatusBarMobileView(
String slot, int subId) {
+ Context mobileContext = mMobileContextProvider.getMobileContextForSub(subId, mContext);
return ModernStatusBarMobileView
.constructAndBind(
- mContext,
+ mobileContext,
slot,
- mMobileIconsViewModel.viewModelForSub(subId));
+ mMobileIconsViewModel.viewModelForSub(subId)
+ );
}
protected LinearLayout.LayoutParams onCreateLayoutParams() {
@@ -704,7 +705,7 @@
}
protected DemoStatusIcons createDemoStatusIcons() {
- return new DemoStatusIcons((LinearLayout) mGroup, mIconSize);
+ return new DemoStatusIcons((LinearLayout) mGroup, mMobileIconsViewModel, mIconSize);
}
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconControllerImpl.java
index 674e574..9fbe6cb 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconControllerImpl.java
@@ -276,6 +276,11 @@
String slotName = mContext.getString(com.android.internal.R.string.status_bar_mobile);
Slot mobileSlot = mStatusBarIconList.getSlot(slotName);
+ // Because of the way we cache the icon holders, we need to remove everything any time
+ // we get a new set of subscriptions. This might change in the future, but is required
+ // to support demo mode for now
+ removeAllIconsForSlot(slotName);
+
Collections.reverse(subIds);
for (Integer subId : subIds) {
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 fb67f1a..c350c78 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,7 @@
package com.android.systemui.statusbar.pipeline.dagger
+import com.android.systemui.CoreStartable
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.log.table.TableLogBuffer
import com.android.systemui.log.table.TableLogBufferFactory
@@ -24,11 +25,12 @@
import com.android.systemui.statusbar.pipeline.airplane.ui.viewmodel.AirplaneModeViewModel
import com.android.systemui.statusbar.pipeline.airplane.ui.viewmodel.AirplaneModeViewModelImpl
import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionsRepository
-import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionsRepositoryImpl
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileRepositorySwitcher
import com.android.systemui.statusbar.pipeline.mobile.data.repository.UserSetupRepository
import com.android.systemui.statusbar.pipeline.mobile.data.repository.UserSetupRepositoryImpl
import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractor
import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractorImpl
+import com.android.systemui.statusbar.pipeline.mobile.ui.MobileUiAdapter
import com.android.systemui.statusbar.pipeline.mobile.util.MobileMappingsProxy
import com.android.systemui.statusbar.pipeline.mobile.util.MobileMappingsProxyImpl
import com.android.systemui.statusbar.pipeline.shared.data.repository.ConnectivityRepository
@@ -40,6 +42,8 @@
import dagger.Binds
import dagger.Module
import dagger.Provides
+import dagger.multibindings.ClassKey
+import dagger.multibindings.IntoMap
@Module
abstract class StatusBarPipelineModule {
@@ -52,26 +56,28 @@
@Binds
abstract fun connectivityRepository(impl: ConnectivityRepositoryImpl): ConnectivityRepository
- @Binds
- abstract fun wifiRepository(impl: WifiRepositoryImpl): WifiRepository
+ @Binds abstract fun wifiRepository(impl: WifiRepositoryImpl): WifiRepository
@Binds
abstract fun wifiInteractor(impl: WifiInteractorImpl): WifiInteractor
@Binds
abstract fun mobileConnectionsRepository(
- impl: MobileConnectionsRepositoryImpl
+ impl: MobileRepositorySwitcher
): MobileConnectionsRepository
- @Binds
- abstract fun userSetupRepository(impl: UserSetupRepositoryImpl): UserSetupRepository
+ @Binds abstract fun userSetupRepository(impl: UserSetupRepositoryImpl): UserSetupRepository
- @Binds
- abstract fun mobileMappingsProxy(impl: MobileMappingsProxyImpl): MobileMappingsProxy
+ @Binds abstract fun mobileMappingsProxy(impl: MobileMappingsProxyImpl): MobileMappingsProxy
@Binds
abstract fun mobileIconsInteractor(impl: MobileIconsInteractorImpl): MobileIconsInteractor
+ @Binds
+ @IntoMap
+ @ClassKey(MobileUiAdapter::class)
+ abstract fun bindFeature(impl: MobileUiAdapter): CoreStartable
+
@Module
companion object {
@JvmStatic
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/DataConnectionState.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/DataConnectionState.kt
index da87f73..5479b92 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/DataConnectionState.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/DataConnectionState.kt
@@ -20,6 +20,7 @@
import android.telephony.TelephonyManager.DATA_CONNECTING
import android.telephony.TelephonyManager.DATA_DISCONNECTED
import android.telephony.TelephonyManager.DATA_DISCONNECTING
+import android.telephony.TelephonyManager.DATA_UNKNOWN
import android.telephony.TelephonyManager.DataState
/** Internal enum representation of the telephony data connection states */
@@ -28,6 +29,7 @@
Connecting(DATA_CONNECTING),
Disconnected(DATA_DISCONNECTED),
Disconnecting(DATA_DISCONNECTING),
+ Unknown(DATA_UNKNOWN),
}
fun @receiver:DataState Int.toDataConnectionType(): DataConnectionState =
@@ -36,5 +38,6 @@
DATA_CONNECTING -> DataConnectionState.Connecting
DATA_DISCONNECTED -> DataConnectionState.Disconnected
DATA_DISCONNECTING -> DataConnectionState.Disconnecting
- else -> throw IllegalArgumentException("unknown data state received")
+ DATA_UNKNOWN -> DataConnectionState.Unknown
+ else -> throw IllegalArgumentException("unknown data state received $this")
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/MobileSubscriptionModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/MobileConnectionModel.kt
similarity index 93%
rename from packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/MobileSubscriptionModel.kt
rename to packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/MobileConnectionModel.kt
index 6341a11..1d00c33 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/MobileSubscriptionModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/MobileConnectionModel.kt
@@ -27,7 +27,6 @@
import android.telephony.TelephonyCallback.SignalStrengthsListener
import android.telephony.TelephonyDisplayInfo
import android.telephony.TelephonyManager
-import android.telephony.TelephonyManager.NETWORK_TYPE_UNKNOWN
import com.android.systemui.statusbar.pipeline.mobile.data.model.DataConnectionState.Disconnected
/**
@@ -39,7 +38,7 @@
* any new field that needs to be tracked should be copied into this data class rather than
* threading complex system objects through the pipeline.
*/
-data class MobileSubscriptionModel(
+data class MobileConnectionModel(
/** From [ServiceStateListener.onServiceStateChanged] */
val isEmergencyOnly: Boolean = false,
@@ -65,5 +64,5 @@
* [resolvedNetworkType] is the [TelephonyDisplayInfo.getOverrideNetworkType] if it exists or
* [TelephonyDisplayInfo.getNetworkType]. This is used to look up the proper network type icon
*/
- val resolvedNetworkType: ResolvedNetworkType = DefaultNetworkType(NETWORK_TYPE_UNKNOWN),
+ val resolvedNetworkType: ResolvedNetworkType = ResolvedNetworkType.UnknownNetworkType,
)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/ResolvedNetworkType.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/ResolvedNetworkType.kt
index f385806..dd93541 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/ResolvedNetworkType.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/ResolvedNetworkType.kt
@@ -17,6 +17,7 @@
package com.android.systemui.statusbar.pipeline.mobile.data.model
import android.telephony.Annotation.NetworkType
+import android.telephony.TelephonyManager.NETWORK_TYPE_UNKNOWN
import com.android.systemui.statusbar.pipeline.mobile.util.MobileMappingsProxy
/**
@@ -26,8 +27,20 @@
*/
sealed interface ResolvedNetworkType {
@NetworkType val type: Int
+ val lookupKey: String
+
+ object UnknownNetworkType : ResolvedNetworkType {
+ override val type: Int = NETWORK_TYPE_UNKNOWN
+ override val lookupKey: String = "unknown"
+ }
+
+ data class DefaultNetworkType(
+ @NetworkType override val type: Int,
+ override val lookupKey: String,
+ ) : ResolvedNetworkType
+
+ data class OverrideNetworkType(
+ @NetworkType override val type: Int,
+ override val lookupKey: String,
+ ) : ResolvedNetworkType
}
-
-data class DefaultNetworkType(@NetworkType override val type: Int) : ResolvedNetworkType
-
-data class OverrideNetworkType(@NetworkType override val type: Int) : ResolvedNetworkType
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/SubscriptionModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/SubscriptionModel.kt
new file mode 100644
index 0000000..2f34516
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/SubscriptionModel.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.statusbar.pipeline.mobile.data.model
+
+/**
+ * SystemUI representation of [SubscriptionInfo]. Currently we only use two fields on the
+ * subscriptions themselves: subscriptionId and isOpportunistic. Any new fields that we need can be
+ * added below and provided in the repository classes
+ */
+data class SubscriptionModel(
+ val subscriptionId: Int,
+ /**
+ * True if the subscription that this model represents has [SubscriptionInfo.isOpportunistic].
+ * Opportunistic networks are networks with limited coverage, and we use this bit to determine
+ * filtering in certain cases. See [MobileIconsInteractor] for the filtering logic
+ */
+ val isOpportunistic: Boolean = false,
+)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepository.kt
index 581842b..2621f997 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepository.kt
@@ -16,44 +16,13 @@
package com.android.systemui.statusbar.pipeline.mobile.data.repository
-import android.content.Context
-import android.database.ContentObserver
-import android.provider.Settings.Global
-import android.telephony.CellSignalStrength
-import android.telephony.CellSignalStrengthCdma
-import android.telephony.ServiceState
-import android.telephony.SignalStrength
import android.telephony.SubscriptionInfo
import android.telephony.SubscriptionManager
import android.telephony.TelephonyCallback
-import android.telephony.TelephonyDisplayInfo
-import android.telephony.TelephonyDisplayInfo.OVERRIDE_NETWORK_TYPE_NONE
import android.telephony.TelephonyManager
-import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
-import com.android.systemui.dagger.qualifiers.Application
-import com.android.systemui.dagger.qualifiers.Background
-import com.android.systemui.statusbar.pipeline.mobile.data.model.DefaultNetworkType
-import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileSubscriptionModel
-import com.android.systemui.statusbar.pipeline.mobile.data.model.OverrideNetworkType
-import com.android.systemui.statusbar.pipeline.mobile.data.model.toDataConnectionType
-import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
-import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger.Companion.logOutputChange
-import com.android.systemui.util.settings.GlobalSettings
-import java.lang.IllegalStateException
-import javax.inject.Inject
-import kotlinx.coroutines.CoroutineDispatcher
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.asExecutor
-import kotlinx.coroutines.channels.awaitClose
+import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel
import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.MutableSharedFlow
-import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.flow.mapLatest
-import kotlinx.coroutines.flow.merge
-import kotlinx.coroutines.flow.onEach
-import kotlinx.coroutines.flow.stateIn
/**
* Every mobile line of service can be identified via a [SubscriptionInfo] object. We set up a
@@ -67,11 +36,13 @@
* eventually becomes a single icon in the status bar.
*/
interface MobileConnectionRepository {
+ /** The subscriptionId that this connection represents */
+ val subId: Int
/**
* A flow that aggregates all necessary callbacks from [TelephonyCallback] into a single
* listener + model.
*/
- val subscriptionModelFlow: Flow<MobileSubscriptionModel>
+ val connectionInfo: Flow<MobileConnectionModel>
/** Observable tracking [TelephonyManager.isDataConnectionAllowed] */
val dataEnabled: StateFlow<Boolean>
/**
@@ -80,183 +51,3 @@
*/
val isDefaultDataSubscription: StateFlow<Boolean>
}
-
-@Suppress("EXPERIMENTAL_IS_NOT_ENABLED")
-@OptIn(ExperimentalCoroutinesApi::class)
-class MobileConnectionRepositoryImpl(
- private val context: Context,
- private val subId: Int,
- private val telephonyManager: TelephonyManager,
- private val globalSettings: GlobalSettings,
- defaultDataSubId: StateFlow<Int>,
- globalMobileDataSettingChangedEvent: Flow<Unit>,
- bgDispatcher: CoroutineDispatcher,
- logger: ConnectivityPipelineLogger,
- scope: CoroutineScope,
-) : MobileConnectionRepository {
- init {
- if (telephonyManager.subscriptionId != subId) {
- throw IllegalStateException(
- "TelephonyManager should be created with subId($subId). " +
- "Found ${telephonyManager.subscriptionId} instead."
- )
- }
- }
-
- private val telephonyCallbackEvent = MutableSharedFlow<Unit>(extraBufferCapacity = 1)
-
- override val subscriptionModelFlow: StateFlow<MobileSubscriptionModel> = run {
- var state = MobileSubscriptionModel()
- conflatedCallbackFlow {
- // TODO (b/240569788): log all of these into the connectivity logger
- val callback =
- object :
- TelephonyCallback(),
- TelephonyCallback.ServiceStateListener,
- TelephonyCallback.SignalStrengthsListener,
- TelephonyCallback.DataConnectionStateListener,
- TelephonyCallback.DataActivityListener,
- TelephonyCallback.CarrierNetworkListener,
- TelephonyCallback.DisplayInfoListener {
- override fun onServiceStateChanged(serviceState: ServiceState) {
- state = state.copy(isEmergencyOnly = serviceState.isEmergencyOnly)
- trySend(state)
- }
-
- override fun onSignalStrengthsChanged(signalStrength: SignalStrength) {
- val cdmaLevel =
- signalStrength
- .getCellSignalStrengths(CellSignalStrengthCdma::class.java)
- .let { strengths ->
- if (!strengths.isEmpty()) {
- strengths[0].level
- } else {
- CellSignalStrength.SIGNAL_STRENGTH_NONE_OR_UNKNOWN
- }
- }
-
- val primaryLevel = signalStrength.level
-
- state =
- state.copy(
- cdmaLevel = cdmaLevel,
- primaryLevel = primaryLevel,
- isGsm = signalStrength.isGsm,
- )
- trySend(state)
- }
-
- override fun onDataConnectionStateChanged(
- dataState: Int,
- networkType: Int
- ) {
- state =
- state.copy(dataConnectionState = dataState.toDataConnectionType())
- trySend(state)
- }
-
- override fun onDataActivity(direction: Int) {
- state = state.copy(dataActivityDirection = direction)
- trySend(state)
- }
-
- override fun onCarrierNetworkChange(active: Boolean) {
- state = state.copy(carrierNetworkChangeActive = active)
- trySend(state)
- }
-
- override fun onDisplayInfoChanged(
- telephonyDisplayInfo: TelephonyDisplayInfo
- ) {
- val networkType =
- if (
- telephonyDisplayInfo.overrideNetworkType ==
- OVERRIDE_NETWORK_TYPE_NONE
- ) {
- DefaultNetworkType(telephonyDisplayInfo.networkType)
- } else {
- OverrideNetworkType(telephonyDisplayInfo.overrideNetworkType)
- }
- state = state.copy(resolvedNetworkType = networkType)
- trySend(state)
- }
- }
- telephonyManager.registerTelephonyCallback(bgDispatcher.asExecutor(), callback)
- awaitClose { telephonyManager.unregisterTelephonyCallback(callback) }
- }
- .onEach { telephonyCallbackEvent.tryEmit(Unit) }
- .logOutputChange(logger, "MobileSubscriptionModel")
- .stateIn(scope, SharingStarted.WhileSubscribed(), state)
- }
-
- /** Produces whenever the mobile data setting changes for this subId */
- private val localMobileDataSettingChangedEvent: Flow<Unit> = conflatedCallbackFlow {
- val observer =
- object : ContentObserver(null) {
- override fun onChange(selfChange: Boolean) {
- trySend(Unit)
- }
- }
-
- globalSettings.registerContentObserver(
- globalSettings.getUriFor("${Global.MOBILE_DATA}$subId"),
- /* notifyForDescendants */ true,
- observer
- )
-
- awaitClose { context.contentResolver.unregisterContentObserver(observer) }
- }
-
- /**
- * There are a few cases where we will need to poll [TelephonyManager] so we can update some
- * internal state where callbacks aren't provided. Any of those events should be merged into
- * this flow, which can be used to trigger the polling.
- */
- private val telephonyPollingEvent: Flow<Unit> =
- merge(
- telephonyCallbackEvent,
- localMobileDataSettingChangedEvent,
- globalMobileDataSettingChangedEvent,
- )
-
- override val dataEnabled: StateFlow<Boolean> =
- telephonyPollingEvent
- .mapLatest { dataConnectionAllowed() }
- .stateIn(scope, SharingStarted.WhileSubscribed(), dataConnectionAllowed())
-
- private fun dataConnectionAllowed(): Boolean = telephonyManager.isDataConnectionAllowed
-
- override val isDefaultDataSubscription: StateFlow<Boolean> =
- defaultDataSubId
- .mapLatest { it == subId }
- .stateIn(scope, SharingStarted.WhileSubscribed(), defaultDataSubId.value == subId)
-
- class Factory
- @Inject
- constructor(
- private val context: Context,
- private val telephonyManager: TelephonyManager,
- private val logger: ConnectivityPipelineLogger,
- private val globalSettings: GlobalSettings,
- @Background private val bgDispatcher: CoroutineDispatcher,
- @Application private val scope: CoroutineScope,
- ) {
- fun build(
- subId: Int,
- defaultDataSubId: StateFlow<Int>,
- globalMobileDataSettingChangedEvent: Flow<Unit>,
- ): MobileConnectionRepository {
- return MobileConnectionRepositoryImpl(
- context,
- subId,
- telephonyManager.createForSubscriptionId(subId),
- globalSettings,
- defaultDataSubId,
- globalMobileDataSettingChangedEvent,
- bgDispatcher,
- logger,
- scope,
- )
- }
- }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepository.kt
index c3c1f14..aea85eb 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepository.kt
@@ -16,53 +16,13 @@
package com.android.systemui.statusbar.pipeline.mobile.data.repository
-import android.annotation.SuppressLint
-import android.content.Context
-import android.content.IntentFilter
-import android.database.ContentObserver
-import android.net.ConnectivityManager
-import android.net.ConnectivityManager.NetworkCallback
-import android.net.Network
-import android.net.NetworkCapabilities
-import android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED
-import android.net.NetworkCapabilities.TRANSPORT_CELLULAR
import android.provider.Settings
-import android.provider.Settings.Global.MOBILE_DATA
-import android.telephony.CarrierConfigManager
-import android.telephony.SubscriptionInfo
import android.telephony.SubscriptionManager
-import android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID
-import android.telephony.TelephonyCallback
-import android.telephony.TelephonyCallback.ActiveDataSubscriptionIdListener
-import android.telephony.TelephonyManager
-import androidx.annotation.VisibleForTesting
-import com.android.internal.telephony.PhoneConstants
-import com.android.settingslib.mobile.MobileMappings
-import com.android.settingslib.mobile.MobileMappings.Config
-import com.android.systemui.broadcast.BroadcastDispatcher
-import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
-import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.dagger.qualifiers.Application
-import com.android.systemui.dagger.qualifiers.Background
+import com.android.settingslib.SignalIcon.MobileIconGroup
import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectivityModel
-import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
-import com.android.systemui.util.settings.GlobalSettings
-import javax.inject.Inject
-import kotlinx.coroutines.CoroutineDispatcher
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.asExecutor
-import kotlinx.coroutines.channels.awaitClose
+import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel
import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.MutableSharedFlow
-import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.flow.distinctUntilChanged
-import kotlinx.coroutines.flow.mapLatest
-import kotlinx.coroutines.flow.merge
-import kotlinx.coroutines.flow.onEach
-import kotlinx.coroutines.flow.stateIn
-import kotlinx.coroutines.withContext
/**
* Repo for monitoring the complete active subscription info list, to be consumed and filtered based
@@ -70,14 +30,11 @@
*/
interface MobileConnectionsRepository {
/** Observable list of current mobile subscriptions */
- val subscriptionsFlow: Flow<List<SubscriptionInfo>>
+ val subscriptions: StateFlow<List<SubscriptionModel>>
/** Observable for the subscriptionId of the current mobile data connection */
val activeMobileDataSubscriptionId: StateFlow<Int>
- /** Observable for [MobileMappings.Config] tracking the defaults */
- val defaultDataSubRatConfig: StateFlow<Config>
-
/** Tracks [SubscriptionManager.getDefaultDataSubscriptionId] */
val defaultDataSubId: StateFlow<Int>
@@ -89,203 +46,10 @@
/** Observe changes to the [Settings.Global.MOBILE_DATA] setting */
val globalMobileDataSettingChangedEvent: Flow<Unit>
-}
-@Suppress("EXPERIMENTAL_IS_NOT_ENABLED")
-@OptIn(ExperimentalCoroutinesApi::class)
-@SysUISingleton
-class MobileConnectionsRepositoryImpl
-@Inject
-constructor(
- private val connectivityManager: ConnectivityManager,
- private val subscriptionManager: SubscriptionManager,
- private val telephonyManager: TelephonyManager,
- private val logger: ConnectivityPipelineLogger,
- broadcastDispatcher: BroadcastDispatcher,
- private val globalSettings: GlobalSettings,
- private val context: Context,
- @Background private val bgDispatcher: CoroutineDispatcher,
- @Application private val scope: CoroutineScope,
- private val mobileConnectionRepositoryFactory: MobileConnectionRepositoryImpl.Factory
-) : MobileConnectionsRepository {
- private val subIdRepositoryCache: MutableMap<Int, MobileConnectionRepository> = mutableMapOf()
+ /** The icon mapping from network type to [MobileIconGroup] for the default subscription */
+ val defaultMobileIconMapping: Flow<Map<String, MobileIconGroup>>
- /**
- * State flow that emits the set of mobile data subscriptions, each represented by its own
- * [SubscriptionInfo]. We probably only need the [SubscriptionInfo.getSubscriptionId] of each
- * info object, but for now we keep track of the infos themselves.
- */
- override val subscriptionsFlow: StateFlow<List<SubscriptionInfo>> =
- conflatedCallbackFlow {
- val callback =
- object : SubscriptionManager.OnSubscriptionsChangedListener() {
- override fun onSubscriptionsChanged() {
- trySend(Unit)
- }
- }
-
- subscriptionManager.addOnSubscriptionsChangedListener(
- bgDispatcher.asExecutor(),
- callback,
- )
-
- awaitClose { subscriptionManager.removeOnSubscriptionsChangedListener(callback) }
- }
- .mapLatest { fetchSubscriptionsList() }
- .onEach { infos -> dropUnusedReposFromCache(infos) }
- .stateIn(scope, started = SharingStarted.WhileSubscribed(), listOf())
-
- /** StateFlow that keeps track of the current active mobile data subscription */
- override val activeMobileDataSubscriptionId: StateFlow<Int> =
- conflatedCallbackFlow {
- val callback =
- object : TelephonyCallback(), ActiveDataSubscriptionIdListener {
- override fun onActiveDataSubscriptionIdChanged(subId: Int) {
- trySend(subId)
- }
- }
-
- telephonyManager.registerTelephonyCallback(bgDispatcher.asExecutor(), callback)
- awaitClose { telephonyManager.unregisterTelephonyCallback(callback) }
- }
- .stateIn(scope, started = SharingStarted.WhileSubscribed(), INVALID_SUBSCRIPTION_ID)
-
- private val defaultDataSubIdChangeEvent: MutableSharedFlow<Unit> =
- MutableSharedFlow(extraBufferCapacity = 1)
-
- override val defaultDataSubId: StateFlow<Int> =
- broadcastDispatcher
- .broadcastFlow(
- IntentFilter(TelephonyManager.ACTION_DEFAULT_DATA_SUBSCRIPTION_CHANGED)
- ) { intent, _ ->
- intent.getIntExtra(PhoneConstants.SUBSCRIPTION_KEY, INVALID_SUBSCRIPTION_ID)
- }
- .distinctUntilChanged()
- .onEach { defaultDataSubIdChangeEvent.tryEmit(Unit) }
- .stateIn(
- scope,
- SharingStarted.WhileSubscribed(),
- SubscriptionManager.getDefaultDataSubscriptionId()
- )
-
- private val carrierConfigChangedEvent =
- broadcastDispatcher.broadcastFlow(
- IntentFilter(CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED)
- )
-
- /**
- * [Config] is an object that tracks relevant configuration flags for a given subscription ID.
- * In the case of [MobileMappings], it's hard-coded to check the default data subscription's
- * config, so this will apply to every icon that we care about.
- *
- * Relevant bits in the config are things like
- * [CarrierConfigManager.KEY_SHOW_4G_FOR_LTE_DATA_ICON_BOOL]
- *
- * This flow will produce whenever the default data subscription or the carrier config changes.
- */
- override val defaultDataSubRatConfig: StateFlow<Config> =
- merge(defaultDataSubIdChangeEvent, carrierConfigChangedEvent)
- .mapLatest { Config.readConfig(context) }
- .stateIn(
- scope,
- SharingStarted.WhileSubscribed(),
- initialValue = Config.readConfig(context)
- )
-
- override fun getRepoForSubId(subId: Int): MobileConnectionRepository {
- if (!isValidSubId(subId)) {
- throw IllegalArgumentException(
- "subscriptionId $subId is not in the list of valid subscriptions"
- )
- }
-
- return subIdRepositoryCache[subId]
- ?: createRepositoryForSubId(subId).also { subIdRepositoryCache[subId] = it }
- }
-
- /**
- * In single-SIM devices, the [MOBILE_DATA] setting is phone-wide. For multi-SIM, the individual
- * connection repositories also observe the URI for [MOBILE_DATA] + subId.
- */
- override val globalMobileDataSettingChangedEvent: Flow<Unit> = conflatedCallbackFlow {
- val observer =
- object : ContentObserver(null) {
- override fun onChange(selfChange: Boolean) {
- trySend(Unit)
- }
- }
-
- globalSettings.registerContentObserver(
- globalSettings.getUriFor(MOBILE_DATA),
- true,
- observer
- )
-
- awaitClose { context.contentResolver.unregisterContentObserver(observer) }
- }
-
- @SuppressLint("MissingPermission")
- override val defaultMobileNetworkConnectivity: StateFlow<MobileConnectivityModel> =
- conflatedCallbackFlow {
- val callback =
- object : NetworkCallback(FLAG_INCLUDE_LOCATION_INFO) {
- override fun onLost(network: Network) {
- // Send a disconnected model when lost. Maybe should create a sealed
- // type or null here?
- trySend(MobileConnectivityModel())
- }
-
- override fun onCapabilitiesChanged(
- network: Network,
- caps: NetworkCapabilities
- ) {
- trySend(
- MobileConnectivityModel(
- isConnected = caps.hasTransport(TRANSPORT_CELLULAR),
- isValidated = caps.hasCapability(NET_CAPABILITY_VALIDATED),
- )
- )
- }
- }
-
- connectivityManager.registerDefaultNetworkCallback(callback)
-
- awaitClose { connectivityManager.unregisterNetworkCallback(callback) }
- }
- .stateIn(scope, SharingStarted.WhileSubscribed(), MobileConnectivityModel())
-
- private fun isValidSubId(subId: Int): Boolean {
- subscriptionsFlow.value.forEach {
- if (it.subscriptionId == subId) {
- return true
- }
- }
-
- return false
- }
-
- @VisibleForTesting fun getSubIdRepoCache() = subIdRepositoryCache
-
- private fun createRepositoryForSubId(subId: Int): MobileConnectionRepository {
- return mobileConnectionRepositoryFactory.build(
- subId,
- defaultDataSubId,
- globalMobileDataSettingChangedEvent,
- )
- }
-
- private fun dropUnusedReposFromCache(newInfos: List<SubscriptionInfo>) {
- // Remove any connection repository from the cache that isn't in the new set of IDs. They
- // will get garbage collected once their subscribers go away
- val currentValidSubscriptionIds = newInfos.map { it.subscriptionId }
-
- subIdRepositoryCache.keys.forEach {
- if (!currentValidSubscriptionIds.contains(it)) {
- subIdRepositoryCache.remove(it)
- }
- }
- }
-
- private suspend fun fetchSubscriptionsList(): List<SubscriptionInfo> =
- withContext(bgDispatcher) { subscriptionManager.completeActiveSubscriptionInfoList }
+ /** Fallback [MobileIconGroup] in the case where there is no icon in the mapping */
+ val defaultMobileIconGroup: Flow<MobileIconGroup>
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileRepositorySwitcher.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileRepositorySwitcher.kt
new file mode 100644
index 0000000..d8e0e81
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileRepositorySwitcher.kt
@@ -0,0 +1,155 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.mobile.data.repository
+
+import android.os.Bundle
+import androidx.annotation.VisibleForTesting
+import com.android.settingslib.SignalIcon
+import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.demomode.DemoMode
+import com.android.systemui.demomode.DemoModeController
+import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectivityModel
+import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.DemoMobileConnectionsRepository
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.prod.MobileConnectionsRepositoryImpl
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.mapLatest
+import kotlinx.coroutines.flow.stateIn
+
+/**
+ * A provider for the [MobileConnectionsRepository] interface that can choose between the Demo and
+ * Prod concrete implementations at runtime. It works by defining a base flow, [activeRepo], which
+ * switches based on the latest information from [DemoModeController], and switches every flow in
+ * the interface to point to the currently-active provider. This allows us to put the demo mode
+ * interface in its own repository, completely separate from the real version, while still using all
+ * of the prod implementations for the rest of the pipeline (interactors and onward). Looks
+ * something like this:
+ *
+ * ```
+ * RealRepository
+ * ā
+ * āāāāŗRepositorySwitcherāāāŗRealInteractorāāāŗRealViewModel
+ * ā
+ * DemoRepository
+ * ```
+ *
+ * NOTE: because the UI layer for mobile icons relies on a nested-repository structure, it is likely
+ * that we will have to drain the subscription list whenever demo mode changes. Otherwise if a real
+ * subscription list [1] is replaced with a demo subscription list [1], the view models will not see
+ * a change (due to `distinctUntilChanged`) and will not refresh their data providers to the demo
+ * implementation.
+ */
+@Suppress("EXPERIMENTAL_IS_NOT_ENABLED")
+@OptIn(ExperimentalCoroutinesApi::class)
+class MobileRepositorySwitcher
+@Inject
+constructor(
+ @Application scope: CoroutineScope,
+ val realRepository: MobileConnectionsRepositoryImpl,
+ val demoMobileConnectionsRepository: DemoMobileConnectionsRepository,
+ demoModeController: DemoModeController,
+) : MobileConnectionsRepository {
+
+ val isDemoMode: StateFlow<Boolean> =
+ conflatedCallbackFlow {
+ val callback =
+ object : DemoMode {
+ override fun dispatchDemoCommand(command: String?, args: Bundle?) {
+ // Nothing, we just care about on/off
+ }
+
+ override fun onDemoModeStarted() {
+ demoMobileConnectionsRepository.startProcessingCommands()
+ trySend(true)
+ }
+
+ override fun onDemoModeFinished() {
+ demoMobileConnectionsRepository.stopProcessingCommands()
+ trySend(false)
+ }
+ }
+
+ demoModeController.addCallback(callback)
+ awaitClose { demoModeController.removeCallback(callback) }
+ }
+ .stateIn(scope, SharingStarted.WhileSubscribed(), demoModeController.isInDemoMode)
+
+ // Convenient definition flow for the currently active repo (based on demo mode or not)
+ @VisibleForTesting
+ internal val activeRepo: StateFlow<MobileConnectionsRepository> =
+ isDemoMode
+ .mapLatest { demoMode ->
+ if (demoMode) {
+ demoMobileConnectionsRepository
+ } else {
+ realRepository
+ }
+ }
+ .stateIn(scope, SharingStarted.WhileSubscribed(), realRepository)
+
+ override val subscriptions: StateFlow<List<SubscriptionModel>> =
+ activeRepo
+ .flatMapLatest { it.subscriptions }
+ .stateIn(scope, SharingStarted.WhileSubscribed(), realRepository.subscriptions.value)
+
+ override val activeMobileDataSubscriptionId: StateFlow<Int> =
+ activeRepo
+ .flatMapLatest { it.activeMobileDataSubscriptionId }
+ .stateIn(
+ scope,
+ SharingStarted.WhileSubscribed(),
+ realRepository.activeMobileDataSubscriptionId.value
+ )
+
+ override val defaultMobileIconMapping: Flow<Map<String, SignalIcon.MobileIconGroup>> =
+ activeRepo.flatMapLatest { it.defaultMobileIconMapping }
+
+ override val defaultMobileIconGroup: Flow<SignalIcon.MobileIconGroup> =
+ activeRepo.flatMapLatest { it.defaultMobileIconGroup }
+
+ override val defaultDataSubId: StateFlow<Int> =
+ activeRepo
+ .flatMapLatest { it.defaultDataSubId }
+ .stateIn(scope, SharingStarted.WhileSubscribed(), realRepository.defaultDataSubId.value)
+
+ override val defaultMobileNetworkConnectivity: StateFlow<MobileConnectivityModel> =
+ activeRepo
+ .flatMapLatest { it.defaultMobileNetworkConnectivity }
+ .stateIn(
+ scope,
+ SharingStarted.WhileSubscribed(),
+ realRepository.defaultMobileNetworkConnectivity.value
+ )
+
+ override val globalMobileDataSettingChangedEvent: Flow<Unit> =
+ activeRepo.flatMapLatest { it.globalMobileDataSettingChangedEvent }
+
+ override fun getRepoForSubId(subId: Int): MobileConnectionRepository {
+ if (isDemoMode.value) {
+ return demoMobileConnectionsRepository.getRepoForSubId(subId)
+ }
+ return realRepository.getRepoForSubId(subId)
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionsRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionsRepository.kt
new file mode 100644
index 0000000..1e7fae7
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionsRepository.kt
@@ -0,0 +1,262 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.mobile.data.repository.demo
+
+import android.content.Context
+import android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID
+import android.util.Log
+import com.android.settingslib.SignalIcon
+import com.android.settingslib.mobile.MobileMappings
+import com.android.settingslib.mobile.TelephonyIcons
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.statusbar.pipeline.mobile.data.model.DataConnectionState
+import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel
+import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectivityModel
+import com.android.systemui.statusbar.pipeline.mobile.data.model.ResolvedNetworkType
+import com.android.systemui.statusbar.pipeline.mobile.data.model.ResolvedNetworkType.DefaultNetworkType
+import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionsRepository
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.model.FakeNetworkEventModel
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.model.FakeNetworkEventModel.Mobile
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.model.FakeNetworkEventModel.MobileDisabled
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.filterNotNull
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.mapLatest
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.launch
+
+/** This repository vends out data based on demo mode commands */
+@OptIn(ExperimentalCoroutinesApi::class)
+class DemoMobileConnectionsRepository
+@Inject
+constructor(
+ private val dataSource: DemoModeMobileConnectionDataSource,
+ @Application private val scope: CoroutineScope,
+ context: Context,
+) : MobileConnectionsRepository {
+
+ private var demoCommandJob: Job? = null
+
+ private var connectionRepoCache = mutableMapOf<Int, DemoMobileConnectionRepository>()
+ private val subscriptionInfoCache = mutableMapOf<Int, SubscriptionModel>()
+ val demoModeFinishedEvent = MutableSharedFlow<Unit>(extraBufferCapacity = 1)
+
+ private val _subscriptions = MutableStateFlow<List<SubscriptionModel>>(listOf())
+ override val subscriptions =
+ _subscriptions
+ .onEach { infos -> dropUnusedReposFromCache(infos) }
+ .stateIn(scope, SharingStarted.WhileSubscribed(), _subscriptions.value)
+
+ private fun dropUnusedReposFromCache(newInfos: List<SubscriptionModel>) {
+ // Remove any connection repository from the cache that isn't in the new set of IDs. They
+ // will get garbage collected once their subscribers go away
+ val currentValidSubscriptionIds = newInfos.map { it.subscriptionId }
+
+ connectionRepoCache =
+ connectionRepoCache
+ .filter { currentValidSubscriptionIds.contains(it.key) }
+ .toMutableMap()
+ }
+
+ private fun maybeCreateSubscription(subId: Int) {
+ if (!subscriptionInfoCache.containsKey(subId)) {
+ SubscriptionModel(subscriptionId = subId, isOpportunistic = false).also {
+ subscriptionInfoCache[subId] = it
+ }
+
+ _subscriptions.value = subscriptionInfoCache.values.toList()
+ }
+ }
+
+ // TODO(b/261029387): add a command for this value
+ override val activeMobileDataSubscriptionId =
+ subscriptions
+ .mapLatest { infos ->
+ // For now, active is just the first in the list
+ infos.firstOrNull()?.subscriptionId ?: INVALID_SUBSCRIPTION_ID
+ }
+ .stateIn(
+ scope,
+ SharingStarted.WhileSubscribed(),
+ subscriptions.value.firstOrNull()?.subscriptionId ?: INVALID_SUBSCRIPTION_ID
+ )
+
+ /** Demo mode doesn't currently support modifications to the mobile mappings */
+ val defaultDataSubRatConfig = MutableStateFlow(MobileMappings.Config.readConfig(context))
+
+ override val defaultMobileIconGroup = flowOf(TelephonyIcons.THREE_G)
+
+ override val defaultMobileIconMapping = MutableStateFlow(TelephonyIcons.ICON_NAME_TO_ICON)
+
+ /**
+ * In order to maintain compatibility with the old demo mode shell command API, reverse the
+ * [MobileMappings] lookup from (NetworkType: String -> Icon: MobileIconGroup), so that we can
+ * parse the string from the command line into a preferred icon group, and send _a_ valid
+ * network type for that icon through the pipeline.
+ *
+ * Note: collisions don't matter here, because the data source (the command line) only cares
+ * about the resulting icon, not the underlying network type.
+ */
+ private val mobileMappingsReverseLookup: StateFlow<Map<SignalIcon.MobileIconGroup, String>> =
+ defaultMobileIconMapping
+ .mapLatest { networkToIconMap -> networkToIconMap.reverse() }
+ .stateIn(
+ scope,
+ SharingStarted.WhileSubscribed(),
+ defaultMobileIconMapping.value.reverse()
+ )
+
+ private fun <K, V> Map<K, V>.reverse() = entries.associateBy({ it.value }) { it.key }
+
+ // TODO(b/261029387): add a command for this value
+ override val defaultDataSubId =
+ activeMobileDataSubscriptionId.stateIn(
+ scope,
+ SharingStarted.WhileSubscribed(),
+ INVALID_SUBSCRIPTION_ID
+ )
+
+ // TODO(b/261029387): not yet supported
+ override val defaultMobileNetworkConnectivity = MutableStateFlow(MobileConnectivityModel())
+
+ override fun getRepoForSubId(subId: Int): DemoMobileConnectionRepository {
+ return connectionRepoCache[subId]
+ ?: DemoMobileConnectionRepository(subId).also { connectionRepoCache[subId] = it }
+ }
+
+ override val globalMobileDataSettingChangedEvent = MutableStateFlow(Unit)
+
+ fun startProcessingCommands() {
+ demoCommandJob =
+ scope.launch {
+ dataSource.mobileEvents.filterNotNull().collect { event -> processEvent(event) }
+ }
+ }
+
+ fun stopProcessingCommands() {
+ demoCommandJob?.cancel()
+ _subscriptions.value = listOf()
+ connectionRepoCache.clear()
+ subscriptionInfoCache.clear()
+ }
+
+ private fun processEvent(event: FakeNetworkEventModel) {
+ when (event) {
+ is Mobile -> {
+ processEnabledMobileState(event)
+ }
+ is MobileDisabled -> {
+ processDisabledMobileState(event)
+ }
+ }
+ }
+
+ private fun processEnabledMobileState(state: Mobile) {
+ // get or create the connection repo, and set its values
+ val subId = state.subId ?: DEFAULT_SUB_ID
+ maybeCreateSubscription(subId)
+
+ val connection = getRepoForSubId(subId)
+ // This is always true here, because we split out disabled states at the data-source level
+ connection.dataEnabled.value = true
+ connection.isDefaultDataSubscription.value = state.dataType != null
+
+ connection.connectionInfo.value = state.toMobileConnectionModel()
+ }
+
+ private fun processDisabledMobileState(state: MobileDisabled) {
+ if (_subscriptions.value.isEmpty()) {
+ // Nothing to do here
+ return
+ }
+
+ val subId =
+ state.subId
+ ?: run {
+ // For sake of usability, we can allow for no subId arg if there is only one
+ // subscription
+ if (_subscriptions.value.size > 1) {
+ Log.d(
+ TAG,
+ "processDisabledMobileState: Unable to infer subscription to " +
+ "disable. Specify subId using '-e slot <subId>'" +
+ "Known subIds: [${subIdsString()}]"
+ )
+ return
+ }
+
+ // Use the only existing subscription as our arg, since there is only one
+ _subscriptions.value[0].subscriptionId
+ }
+
+ removeSubscription(subId)
+ }
+
+ private fun removeSubscription(subId: Int) {
+ val currentSubscriptions = _subscriptions.value
+ subscriptionInfoCache.remove(subId)
+ _subscriptions.value = currentSubscriptions.filter { it.subscriptionId != subId }
+ }
+
+ private fun subIdsString(): String =
+ _subscriptions.value.joinToString(",") { it.subscriptionId.toString() }
+
+ private fun Mobile.toMobileConnectionModel(): MobileConnectionModel {
+ return MobileConnectionModel(
+ isEmergencyOnly = false, // TODO(b/261029387): not yet supported
+ isGsm = false, // TODO(b/261029387): not yet supported
+ cdmaLevel = level ?: 0,
+ primaryLevel = level ?: 0,
+ dataConnectionState =
+ DataConnectionState.Connected, // TODO(b/261029387): not yet supported
+ dataActivityDirection = activity,
+ carrierNetworkChangeActive = carrierNetworkChange,
+ resolvedNetworkType = dataType.toResolvedNetworkType()
+ )
+ }
+
+ private fun SignalIcon.MobileIconGroup?.toResolvedNetworkType(): ResolvedNetworkType {
+ val key = mobileMappingsReverseLookup.value[this] ?: "dis"
+ return DefaultNetworkType(DEMO_NET_TYPE, key)
+ }
+
+ companion object {
+ private const val TAG = "DemoMobileConnectionsRepo"
+
+ private const val DEFAULT_SUB_ID = 1
+
+ private const val DEMO_NET_TYPE = 1234
+ }
+}
+
+class DemoMobileConnectionRepository(override val subId: Int) : MobileConnectionRepository {
+ override val connectionInfo = MutableStateFlow(MobileConnectionModel())
+
+ override val dataEnabled = MutableStateFlow(true)
+
+ override val isDefaultDataSubscription = MutableStateFlow(true)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoModeMobileConnectionDataSource.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoModeMobileConnectionDataSource.kt
new file mode 100644
index 0000000..da55787
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoModeMobileConnectionDataSource.kt
@@ -0,0 +1,141 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.mobile.data.repository.demo
+
+import android.os.Bundle
+import android.telephony.Annotation.DataActivityType
+import android.telephony.TelephonyManager.DATA_ACTIVITY_IN
+import android.telephony.TelephonyManager.DATA_ACTIVITY_INOUT
+import android.telephony.TelephonyManager.DATA_ACTIVITY_NONE
+import android.telephony.TelephonyManager.DATA_ACTIVITY_OUT
+import com.android.settingslib.SignalIcon.MobileIconGroup
+import com.android.settingslib.mobile.TelephonyIcons
+import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.demomode.DemoMode
+import com.android.systemui.demomode.DemoMode.COMMAND_NETWORK
+import com.android.systemui.demomode.DemoModeController
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.model.FakeNetworkEventModel
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.model.FakeNetworkEventModel.Mobile
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.model.FakeNetworkEventModel.MobileDisabled
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.shareIn
+
+/**
+ * Data source that can map from demo mode commands to inputs into the
+ * [DemoMobileConnectionsRepository]'s flows
+ */
+@SysUISingleton
+class DemoModeMobileConnectionDataSource
+@Inject
+constructor(
+ demoModeController: DemoModeController,
+ @Application scope: CoroutineScope,
+) {
+ private val demoCommandStream: Flow<Bundle> = conflatedCallbackFlow {
+ val callback =
+ object : DemoMode {
+ override fun demoCommands(): List<String> = listOf(COMMAND_NETWORK)
+
+ override fun dispatchDemoCommand(command: String, args: Bundle) {
+ trySend(args)
+ }
+
+ override fun onDemoModeFinished() {
+ // Handled elsewhere
+ }
+
+ override fun onDemoModeStarted() {
+ // Handled elsewhere
+ }
+ }
+
+ demoModeController.addCallback(callback)
+ awaitClose { demoModeController.removeCallback(callback) }
+ }
+
+ // If the args contains "mobile", then all of the args are relevant. It's just the way demo mode
+ // commands work and it's a little silly
+ private val _mobileCommands = demoCommandStream.map { args -> args.toMobileEvent() }
+ val mobileEvents = _mobileCommands.shareIn(scope, SharingStarted.WhileSubscribed())
+
+ private fun Bundle.toMobileEvent(): FakeNetworkEventModel? {
+ val mobile = getString("mobile") ?: return null
+ return if (mobile == "show") {
+ activeMobileEvent()
+ } else {
+ MobileDisabled(subId = getString("slot")?.toInt())
+ }
+ }
+
+ /** Parse a valid mobile command string into a network event */
+ private fun Bundle.activeMobileEvent(): Mobile {
+ // There are many key/value pairs supported by mobile demo mode. Bear with me here
+ val level = getString("level")?.toInt()
+ val dataType = getString("datatype")?.toDataType()
+ val slot = getString("slot")?.toInt()
+ val carrierId = getString("carrierid")?.toInt()
+ val inflateStrength = getString("inflate")?.toBoolean()
+ val activity = getString("activity")?.toActivity()
+ val carrierNetworkChange = getString("carriernetworkchange") == "show"
+
+ return Mobile(
+ level = level,
+ dataType = dataType,
+ subId = slot,
+ carrierId = carrierId,
+ inflateStrength = inflateStrength,
+ activity = activity,
+ carrierNetworkChange = carrierNetworkChange,
+ )
+ }
+}
+
+private fun String.toDataType(): MobileIconGroup =
+ when (this) {
+ "1x" -> TelephonyIcons.ONE_X
+ "3g" -> TelephonyIcons.THREE_G
+ "4g" -> TelephonyIcons.FOUR_G
+ "4g+" -> TelephonyIcons.FOUR_G_PLUS
+ "5g" -> TelephonyIcons.NR_5G
+ "5ge" -> TelephonyIcons.LTE_CA_5G_E
+ "5g+" -> TelephonyIcons.NR_5G_PLUS
+ "e" -> TelephonyIcons.E
+ "g" -> TelephonyIcons.G
+ "h" -> TelephonyIcons.H
+ "h+" -> TelephonyIcons.H_PLUS
+ "lte" -> TelephonyIcons.LTE
+ "lte+" -> TelephonyIcons.LTE_PLUS
+ "dis" -> TelephonyIcons.DATA_DISABLED
+ "not" -> TelephonyIcons.NOT_DEFAULT_DATA
+ else -> TelephonyIcons.UNKNOWN
+ }
+
+@DataActivityType
+private fun String.toActivity(): Int =
+ when (this) {
+ "inout" -> DATA_ACTIVITY_INOUT
+ "in" -> DATA_ACTIVITY_IN
+ "out" -> DATA_ACTIVITY_OUT
+ else -> DATA_ACTIVITY_NONE
+ }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/model/FakeNetworkEventModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/model/FakeNetworkEventModel.kt
new file mode 100644
index 0000000..3f3acaf
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/model/FakeNetworkEventModel.kt
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.model
+
+import android.telephony.Annotation.DataActivityType
+import com.android.settingslib.SignalIcon
+
+/**
+ * Model for the demo commands, ported from [NetworkControllerImpl]
+ *
+ * Nullable fields represent optional command line arguments
+ */
+sealed interface FakeNetworkEventModel {
+ data class Mobile(
+ val level: Int?,
+ val dataType: SignalIcon.MobileIconGroup?,
+ // Null means the default (chosen by the repository)
+ val subId: Int?,
+ val carrierId: Int?,
+ val inflateStrength: Boolean?,
+ @DataActivityType val activity: Int?,
+ val carrierNetworkChange: Boolean,
+ ) : FakeNetworkEventModel
+
+ data class MobileDisabled(
+ // Null means the default (chosen by the repository)
+ val subId: Int?
+ ) : FakeNetworkEventModel
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryImpl.kt
new file mode 100644
index 0000000..15505fd
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryImpl.kt
@@ -0,0 +1,253 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.mobile.data.repository.prod
+
+import android.content.Context
+import android.database.ContentObserver
+import android.provider.Settings.Global
+import android.telephony.CellSignalStrength
+import android.telephony.CellSignalStrengthCdma
+import android.telephony.ServiceState
+import android.telephony.SignalStrength
+import android.telephony.TelephonyCallback
+import android.telephony.TelephonyDisplayInfo
+import android.telephony.TelephonyDisplayInfo.OVERRIDE_NETWORK_TYPE_NONE
+import android.telephony.TelephonyManager
+import android.telephony.TelephonyManager.NETWORK_TYPE_UNKNOWN
+import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel
+import com.android.systemui.statusbar.pipeline.mobile.data.model.ResolvedNetworkType.DefaultNetworkType
+import com.android.systemui.statusbar.pipeline.mobile.data.model.ResolvedNetworkType.OverrideNetworkType
+import com.android.systemui.statusbar.pipeline.mobile.data.model.ResolvedNetworkType.UnknownNetworkType
+import com.android.systemui.statusbar.pipeline.mobile.data.model.toDataConnectionType
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository
+import com.android.systemui.statusbar.pipeline.mobile.util.MobileMappingsProxy
+import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
+import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger.Companion.logOutputChange
+import com.android.systemui.util.settings.GlobalSettings
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.asExecutor
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.mapLatest
+import kotlinx.coroutines.flow.merge
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.flow.stateIn
+
+@Suppress("EXPERIMENTAL_IS_NOT_ENABLED")
+@OptIn(ExperimentalCoroutinesApi::class)
+class MobileConnectionRepositoryImpl(
+ private val context: Context,
+ override val subId: Int,
+ private val telephonyManager: TelephonyManager,
+ private val globalSettings: GlobalSettings,
+ defaultDataSubId: StateFlow<Int>,
+ globalMobileDataSettingChangedEvent: Flow<Unit>,
+ mobileMappingsProxy: MobileMappingsProxy,
+ bgDispatcher: CoroutineDispatcher,
+ logger: ConnectivityPipelineLogger,
+ scope: CoroutineScope,
+) : MobileConnectionRepository {
+ init {
+ if (telephonyManager.subscriptionId != subId) {
+ throw IllegalStateException(
+ "TelephonyManager should be created with subId($subId). " +
+ "Found ${telephonyManager.subscriptionId} instead."
+ )
+ }
+ }
+
+ private val telephonyCallbackEvent = MutableSharedFlow<Unit>(extraBufferCapacity = 1)
+
+ override val connectionInfo: StateFlow<MobileConnectionModel> = run {
+ var state = MobileConnectionModel()
+ conflatedCallbackFlow {
+ // TODO (b/240569788): log all of these into the connectivity logger
+ val callback =
+ object :
+ TelephonyCallback(),
+ TelephonyCallback.ServiceStateListener,
+ TelephonyCallback.SignalStrengthsListener,
+ TelephonyCallback.DataConnectionStateListener,
+ TelephonyCallback.DataActivityListener,
+ TelephonyCallback.CarrierNetworkListener,
+ TelephonyCallback.DisplayInfoListener {
+ override fun onServiceStateChanged(serviceState: ServiceState) {
+ state = state.copy(isEmergencyOnly = serviceState.isEmergencyOnly)
+ trySend(state)
+ }
+
+ override fun onSignalStrengthsChanged(signalStrength: SignalStrength) {
+ val cdmaLevel =
+ signalStrength
+ .getCellSignalStrengths(CellSignalStrengthCdma::class.java)
+ .let { strengths ->
+ if (!strengths.isEmpty()) {
+ strengths[0].level
+ } else {
+ CellSignalStrength.SIGNAL_STRENGTH_NONE_OR_UNKNOWN
+ }
+ }
+
+ val primaryLevel = signalStrength.level
+
+ state =
+ state.copy(
+ cdmaLevel = cdmaLevel,
+ primaryLevel = primaryLevel,
+ isGsm = signalStrength.isGsm,
+ )
+ trySend(state)
+ }
+
+ override fun onDataConnectionStateChanged(
+ dataState: Int,
+ networkType: Int
+ ) {
+ state =
+ state.copy(dataConnectionState = dataState.toDataConnectionType())
+ trySend(state)
+ }
+
+ override fun onDataActivity(direction: Int) {
+ state = state.copy(dataActivityDirection = direction)
+ trySend(state)
+ }
+
+ override fun onCarrierNetworkChange(active: Boolean) {
+ state = state.copy(carrierNetworkChangeActive = active)
+ trySend(state)
+ }
+
+ override fun onDisplayInfoChanged(
+ telephonyDisplayInfo: TelephonyDisplayInfo
+ ) {
+
+ val networkType =
+ if (telephonyDisplayInfo.networkType == NETWORK_TYPE_UNKNOWN) {
+ UnknownNetworkType
+ } else if (
+ telephonyDisplayInfo.overrideNetworkType ==
+ OVERRIDE_NETWORK_TYPE_NONE
+ ) {
+ DefaultNetworkType(
+ telephonyDisplayInfo.networkType,
+ mobileMappingsProxy.toIconKey(
+ telephonyDisplayInfo.networkType
+ )
+ )
+ } else {
+ OverrideNetworkType(
+ telephonyDisplayInfo.overrideNetworkType,
+ mobileMappingsProxy.toIconKeyOverride(
+ telephonyDisplayInfo.overrideNetworkType
+ )
+ )
+ }
+ state = state.copy(resolvedNetworkType = networkType)
+ trySend(state)
+ }
+ }
+ telephonyManager.registerTelephonyCallback(bgDispatcher.asExecutor(), callback)
+ awaitClose { telephonyManager.unregisterTelephonyCallback(callback) }
+ }
+ .onEach { telephonyCallbackEvent.tryEmit(Unit) }
+ .logOutputChange(logger, "MobileSubscriptionModel")
+ .stateIn(scope, SharingStarted.WhileSubscribed(), state)
+ }
+
+ /** Produces whenever the mobile data setting changes for this subId */
+ private val localMobileDataSettingChangedEvent: Flow<Unit> = conflatedCallbackFlow {
+ val observer =
+ object : ContentObserver(null) {
+ override fun onChange(selfChange: Boolean) {
+ trySend(Unit)
+ }
+ }
+
+ globalSettings.registerContentObserver(
+ globalSettings.getUriFor("${Global.MOBILE_DATA}$subId"),
+ /* notifyForDescendants */ true,
+ observer
+ )
+
+ awaitClose { context.contentResolver.unregisterContentObserver(observer) }
+ }
+
+ /**
+ * There are a few cases where we will need to poll [TelephonyManager] so we can update some
+ * internal state where callbacks aren't provided. Any of those events should be merged into
+ * this flow, which can be used to trigger the polling.
+ */
+ private val telephonyPollingEvent: Flow<Unit> =
+ merge(
+ telephonyCallbackEvent,
+ localMobileDataSettingChangedEvent,
+ globalMobileDataSettingChangedEvent,
+ )
+
+ override val dataEnabled: StateFlow<Boolean> =
+ telephonyPollingEvent
+ .mapLatest { dataConnectionAllowed() }
+ .stateIn(scope, SharingStarted.WhileSubscribed(), dataConnectionAllowed())
+
+ private fun dataConnectionAllowed(): Boolean = telephonyManager.isDataConnectionAllowed
+
+ override val isDefaultDataSubscription: StateFlow<Boolean> =
+ defaultDataSubId
+ .mapLatest { it == subId }
+ .stateIn(scope, SharingStarted.WhileSubscribed(), defaultDataSubId.value == subId)
+
+ class Factory
+ @Inject
+ constructor(
+ private val context: Context,
+ private val telephonyManager: TelephonyManager,
+ private val logger: ConnectivityPipelineLogger,
+ private val globalSettings: GlobalSettings,
+ private val mobileMappingsProxy: MobileMappingsProxy,
+ @Background private val bgDispatcher: CoroutineDispatcher,
+ @Application private val scope: CoroutineScope,
+ ) {
+ fun build(
+ subId: Int,
+ defaultDataSubId: StateFlow<Int>,
+ globalMobileDataSettingChangedEvent: Flow<Unit>,
+ ): MobileConnectionRepository {
+ return MobileConnectionRepositoryImpl(
+ context,
+ subId,
+ telephonyManager.createForSubscriptionId(subId),
+ globalSettings,
+ defaultDataSubId,
+ globalMobileDataSettingChangedEvent,
+ mobileMappingsProxy,
+ bgDispatcher,
+ logger,
+ scope,
+ )
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryImpl.kt
new file mode 100644
index 0000000..f27a9c9
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryImpl.kt
@@ -0,0 +1,281 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.mobile.data.repository.prod
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.content.IntentFilter
+import android.database.ContentObserver
+import android.net.ConnectivityManager
+import android.net.ConnectivityManager.NetworkCallback
+import android.net.Network
+import android.net.NetworkCapabilities
+import android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED
+import android.net.NetworkCapabilities.TRANSPORT_CELLULAR
+import android.provider.Settings.Global.MOBILE_DATA
+import android.telephony.CarrierConfigManager
+import android.telephony.SubscriptionInfo
+import android.telephony.SubscriptionManager
+import android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID
+import android.telephony.TelephonyCallback
+import android.telephony.TelephonyCallback.ActiveDataSubscriptionIdListener
+import android.telephony.TelephonyManager
+import androidx.annotation.VisibleForTesting
+import com.android.internal.telephony.PhoneConstants
+import com.android.settingslib.SignalIcon.MobileIconGroup
+import com.android.settingslib.mobile.MobileMappings
+import com.android.settingslib.mobile.MobileMappings.Config
+import com.android.systemui.broadcast.BroadcastDispatcher
+import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectivityModel
+import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionsRepository
+import com.android.systemui.statusbar.pipeline.mobile.util.MobileMappingsProxy
+import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
+import com.android.systemui.util.settings.GlobalSettings
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.asExecutor
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.mapLatest
+import kotlinx.coroutines.flow.merge
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.withContext
+
+@Suppress("EXPERIMENTAL_IS_NOT_ENABLED")
+@OptIn(ExperimentalCoroutinesApi::class)
+@SysUISingleton
+class MobileConnectionsRepositoryImpl
+@Inject
+constructor(
+ private val connectivityManager: ConnectivityManager,
+ private val subscriptionManager: SubscriptionManager,
+ private val telephonyManager: TelephonyManager,
+ private val logger: ConnectivityPipelineLogger,
+ mobileMappingsProxy: MobileMappingsProxy,
+ broadcastDispatcher: BroadcastDispatcher,
+ private val globalSettings: GlobalSettings,
+ private val context: Context,
+ @Background private val bgDispatcher: CoroutineDispatcher,
+ @Application private val scope: CoroutineScope,
+ private val mobileConnectionRepositoryFactory: MobileConnectionRepositoryImpl.Factory
+) : MobileConnectionsRepository {
+ private var subIdRepositoryCache: MutableMap<Int, MobileConnectionRepository> = mutableMapOf()
+
+ /**
+ * State flow that emits the set of mobile data subscriptions, each represented by its own
+ * [SubscriptionInfo]. We probably only need the [SubscriptionInfo.getSubscriptionId] of each
+ * info object, but for now we keep track of the infos themselves.
+ */
+ override val subscriptions: StateFlow<List<SubscriptionModel>> =
+ conflatedCallbackFlow {
+ val callback =
+ object : SubscriptionManager.OnSubscriptionsChangedListener() {
+ override fun onSubscriptionsChanged() {
+ trySend(Unit)
+ }
+ }
+
+ subscriptionManager.addOnSubscriptionsChangedListener(
+ bgDispatcher.asExecutor(),
+ callback,
+ )
+
+ awaitClose { subscriptionManager.removeOnSubscriptionsChangedListener(callback) }
+ }
+ .mapLatest { fetchSubscriptionsList().map { it.toSubscriptionModel() } }
+ .onEach { infos -> dropUnusedReposFromCache(infos) }
+ .stateIn(scope, started = SharingStarted.WhileSubscribed(), listOf())
+
+ /** StateFlow that keeps track of the current active mobile data subscription */
+ override val activeMobileDataSubscriptionId: StateFlow<Int> =
+ conflatedCallbackFlow {
+ val callback =
+ object : TelephonyCallback(), ActiveDataSubscriptionIdListener {
+ override fun onActiveDataSubscriptionIdChanged(subId: Int) {
+ trySend(subId)
+ }
+ }
+
+ telephonyManager.registerTelephonyCallback(bgDispatcher.asExecutor(), callback)
+ awaitClose { telephonyManager.unregisterTelephonyCallback(callback) }
+ }
+ .stateIn(scope, started = SharingStarted.WhileSubscribed(), INVALID_SUBSCRIPTION_ID)
+
+ private val defaultDataSubIdChangeEvent: MutableSharedFlow<Unit> =
+ MutableSharedFlow(extraBufferCapacity = 1)
+
+ override val defaultDataSubId: StateFlow<Int> =
+ broadcastDispatcher
+ .broadcastFlow(
+ IntentFilter(TelephonyManager.ACTION_DEFAULT_DATA_SUBSCRIPTION_CHANGED)
+ ) { intent, _ ->
+ intent.getIntExtra(PhoneConstants.SUBSCRIPTION_KEY, INVALID_SUBSCRIPTION_ID)
+ }
+ .distinctUntilChanged()
+ .onEach { defaultDataSubIdChangeEvent.tryEmit(Unit) }
+ .stateIn(
+ scope,
+ SharingStarted.WhileSubscribed(),
+ SubscriptionManager.getDefaultDataSubscriptionId()
+ )
+
+ private val carrierConfigChangedEvent =
+ broadcastDispatcher.broadcastFlow(
+ IntentFilter(CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED)
+ )
+
+ /**
+ * [Config] is an object that tracks relevant configuration flags for a given subscription ID.
+ * In the case of [MobileMappings], it's hard-coded to check the default data subscription's
+ * config, so this will apply to every icon that we care about.
+ *
+ * Relevant bits in the config are things like
+ * [CarrierConfigManager.KEY_SHOW_4G_FOR_LTE_DATA_ICON_BOOL]
+ *
+ * This flow will produce whenever the default data subscription or the carrier config changes.
+ */
+ private val defaultDataSubRatConfig: StateFlow<Config> =
+ merge(defaultDataSubIdChangeEvent, carrierConfigChangedEvent)
+ .mapLatest { Config.readConfig(context) }
+ .stateIn(
+ scope,
+ SharingStarted.WhileSubscribed(),
+ initialValue = Config.readConfig(context)
+ )
+
+ override val defaultMobileIconMapping: Flow<Map<String, MobileIconGroup>> =
+ defaultDataSubRatConfig.map { mobileMappingsProxy.mapIconSets(it) }
+
+ override val defaultMobileIconGroup: Flow<MobileIconGroup> =
+ defaultDataSubRatConfig.map { mobileMappingsProxy.getDefaultIcons(it) }
+
+ override fun getRepoForSubId(subId: Int): MobileConnectionRepository {
+ if (!isValidSubId(subId)) {
+ throw IllegalArgumentException(
+ "subscriptionId $subId is not in the list of valid subscriptions"
+ )
+ }
+
+ return subIdRepositoryCache[subId]
+ ?: createRepositoryForSubId(subId).also { subIdRepositoryCache[subId] = it }
+ }
+
+ /**
+ * In single-SIM devices, the [MOBILE_DATA] setting is phone-wide. For multi-SIM, the individual
+ * connection repositories also observe the URI for [MOBILE_DATA] + subId.
+ */
+ override val globalMobileDataSettingChangedEvent: Flow<Unit> = conflatedCallbackFlow {
+ val observer =
+ object : ContentObserver(null) {
+ override fun onChange(selfChange: Boolean) {
+ trySend(Unit)
+ }
+ }
+
+ globalSettings.registerContentObserver(
+ globalSettings.getUriFor(MOBILE_DATA),
+ true,
+ observer
+ )
+
+ awaitClose { context.contentResolver.unregisterContentObserver(observer) }
+ }
+
+ @SuppressLint("MissingPermission")
+ override val defaultMobileNetworkConnectivity: StateFlow<MobileConnectivityModel> =
+ conflatedCallbackFlow {
+ val callback =
+ object : NetworkCallback(FLAG_INCLUDE_LOCATION_INFO) {
+ override fun onLost(network: Network) {
+ // Send a disconnected model when lost. Maybe should create a sealed
+ // type or null here?
+ trySend(MobileConnectivityModel())
+ }
+
+ override fun onCapabilitiesChanged(
+ network: Network,
+ caps: NetworkCapabilities
+ ) {
+ trySend(
+ MobileConnectivityModel(
+ isConnected = caps.hasTransport(TRANSPORT_CELLULAR),
+ isValidated = caps.hasCapability(NET_CAPABILITY_VALIDATED),
+ )
+ )
+ }
+ }
+
+ connectivityManager.registerDefaultNetworkCallback(callback)
+
+ awaitClose { connectivityManager.unregisterNetworkCallback(callback) }
+ }
+ .stateIn(scope, SharingStarted.WhileSubscribed(), MobileConnectivityModel())
+
+ private fun isValidSubId(subId: Int): Boolean {
+ subscriptions.value.forEach {
+ if (it.subscriptionId == subId) {
+ return true
+ }
+ }
+
+ return false
+ }
+
+ @VisibleForTesting fun getSubIdRepoCache() = subIdRepositoryCache
+
+ private fun createRepositoryForSubId(subId: Int): MobileConnectionRepository {
+ return mobileConnectionRepositoryFactory.build(
+ subId,
+ defaultDataSubId,
+ globalMobileDataSettingChangedEvent,
+ )
+ }
+
+ private fun dropUnusedReposFromCache(newInfos: List<SubscriptionModel>) {
+ // Remove any connection repository from the cache that isn't in the new set of IDs. They
+ // will get garbage collected once their subscribers go away
+ val currentValidSubscriptionIds = newInfos.map { it.subscriptionId }
+
+ subIdRepositoryCache =
+ subIdRepositoryCache
+ .filter { currentValidSubscriptionIds.contains(it.key) }
+ .toMutableMap()
+ }
+
+ private suspend fun fetchSubscriptionsList(): List<SubscriptionInfo> =
+ withContext(bgDispatcher) { subscriptionManager.completeActiveSubscriptionInfoList }
+
+ private fun SubscriptionInfo.toSubscriptionModel(): SubscriptionModel =
+ SubscriptionModel(
+ subscriptionId = subscriptionId,
+ isOpportunistic = isOpportunistic,
+ )
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt
index 0da84f0..8e1197c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt
@@ -20,10 +20,7 @@
import com.android.settingslib.SignalIcon.MobileIconGroup
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.statusbar.pipeline.mobile.data.model.DataConnectionState.Connected
-import com.android.systemui.statusbar.pipeline.mobile.data.model.DefaultNetworkType
-import com.android.systemui.statusbar.pipeline.mobile.data.model.OverrideNetworkType
import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository
-import com.android.systemui.statusbar.pipeline.mobile.util.MobileMappingsProxy
import com.android.systemui.util.CarrierConfigTracker
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -70,10 +67,9 @@
defaultMobileIconMapping: StateFlow<Map<String, MobileIconGroup>>,
defaultMobileIconGroup: StateFlow<MobileIconGroup>,
override val isDefaultConnectionFailed: StateFlow<Boolean>,
- mobileMappingsProxy: MobileMappingsProxy,
connectionRepository: MobileConnectionRepository,
) : MobileIconInteractor {
- private val mobileStatusInfo = connectionRepository.subscriptionModelFlow
+ private val connectionInfo = connectionRepository.connectionInfo
override val isDataEnabled: StateFlow<Boolean> = connectionRepository.dataEnabled
@@ -82,33 +78,27 @@
/** Observable for the current RAT indicator icon ([MobileIconGroup]) */
override val networkTypeIconGroup: StateFlow<MobileIconGroup> =
combine(
- mobileStatusInfo,
+ connectionInfo,
defaultMobileIconMapping,
defaultMobileIconGroup,
) { info, mapping, defaultGroup ->
- val lookupKey =
- when (val resolved = info.resolvedNetworkType) {
- is DefaultNetworkType -> mobileMappingsProxy.toIconKey(resolved.type)
- is OverrideNetworkType ->
- mobileMappingsProxy.toIconKeyOverride(resolved.type)
- }
- mapping[lookupKey] ?: defaultGroup
+ mapping[info.resolvedNetworkType.lookupKey] ?: defaultGroup
}
.stateIn(scope, SharingStarted.WhileSubscribed(), defaultMobileIconGroup.value)
override val isEmergencyOnly: StateFlow<Boolean> =
- mobileStatusInfo
+ connectionInfo
.mapLatest { it.isEmergencyOnly }
.stateIn(scope, SharingStarted.WhileSubscribed(), false)
override val level: StateFlow<Int> =
- mobileStatusInfo
- .mapLatest { mobileModel ->
+ connectionInfo
+ .mapLatest { connection ->
// TODO: incorporate [MobileMappings.Config.alwaysShowCdmaRssi]
- if (mobileModel.isGsm) {
- mobileModel.primaryLevel
+ if (connection.isGsm) {
+ connection.primaryLevel
} else {
- mobileModel.cdmaLevel
+ connection.cdmaLevel
}
}
.stateIn(scope, SharingStarted.WhileSubscribed(), 0)
@@ -120,7 +110,7 @@
override val numberOfLevels: StateFlow<Int> = MutableStateFlow(4)
override val isDataConnected: StateFlow<Boolean> =
- mobileStatusInfo
- .mapLatest { subscriptionModel -> subscriptionModel.dataConnectionState == Connected }
+ connectionInfo
+ .mapLatest { connection -> connection.dataConnectionState == Connected }
.stateIn(scope, SharingStarted.WhileSubscribed(), false)
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractor.kt
index a4175c3..6f8fb2e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractor.kt
@@ -17,17 +17,16 @@
package com.android.systemui.statusbar.pipeline.mobile.domain.interactor
import android.telephony.CarrierConfigManager
-import android.telephony.SubscriptionInfo
import android.telephony.SubscriptionManager
import android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID
import com.android.settingslib.SignalIcon.MobileIconGroup
import com.android.settingslib.mobile.TelephonyIcons
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel
import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository
import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionsRepository
import com.android.systemui.statusbar.pipeline.mobile.data.repository.UserSetupRepository
-import com.android.systemui.statusbar.pipeline.mobile.util.MobileMappingsProxy
import com.android.systemui.util.CarrierConfigTracker
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
@@ -53,7 +52,7 @@
*/
interface MobileIconsInteractor {
/** List of subscriptions, potentially filtered for CBRS */
- val filteredSubscriptions: Flow<List<SubscriptionInfo>>
+ val filteredSubscriptions: Flow<List<SubscriptionModel>>
/** True if the active mobile data subscription has data enabled */
val activeDataConnectionHasDataEnabled: StateFlow<Boolean>
/** The icon mapping from network type to [MobileIconGroup] for the default subscription */
@@ -79,7 +78,6 @@
constructor(
private val mobileConnectionsRepo: MobileConnectionsRepository,
private val carrierConfigTracker: CarrierConfigTracker,
- private val mobileMappingsProxy: MobileMappingsProxy,
userSetupRepo: UserSetupRepository,
@Application private val scope: CoroutineScope,
) : MobileIconsInteractor {
@@ -102,8 +100,8 @@
.flatMapLatest { it?.dataEnabled ?: flowOf(false) }
.stateIn(scope, SharingStarted.WhileSubscribed(), false)
- private val unfilteredSubscriptions: Flow<List<SubscriptionInfo>> =
- mobileConnectionsRepo.subscriptionsFlow
+ private val unfilteredSubscriptions: Flow<List<SubscriptionModel>> =
+ mobileConnectionsRepo.subscriptions
/**
* Generally, SystemUI wants to show iconography for each subscription that is listed by
@@ -118,7 +116,7 @@
* [CarrierConfigManager.KEY_ALWAYS_SHOW_PRIMARY_SIGNAL_BAR_IN_OPPORTUNISTIC_NETWORK_BOOLEAN],
* and by checking which subscription is opportunistic, or which one is active.
*/
- override val filteredSubscriptions: Flow<List<SubscriptionInfo>> =
+ override val filteredSubscriptions: Flow<List<SubscriptionModel>> =
combine(unfilteredSubscriptions, activeMobileDataSubscriptionId) { unfilteredSubs, activeId
->
// Based on the old logic,
@@ -154,15 +152,19 @@
* subscription Id. This mapping is the same for every subscription.
*/
override val defaultMobileIconMapping: StateFlow<Map<String, MobileIconGroup>> =
- mobileConnectionsRepo.defaultDataSubRatConfig
- .mapLatest { mobileMappingsProxy.mapIconSets(it) }
- .stateIn(scope, SharingStarted.WhileSubscribed(), initialValue = mapOf())
+ mobileConnectionsRepo.defaultMobileIconMapping.stateIn(
+ scope,
+ SharingStarted.WhileSubscribed(),
+ initialValue = mapOf()
+ )
/** If there is no mapping in [defaultMobileIconMapping], then use this default icon group */
override val defaultMobileIconGroup: StateFlow<MobileIconGroup> =
- mobileConnectionsRepo.defaultDataSubRatConfig
- .mapLatest { mobileMappingsProxy.getDefaultIcons(it) }
- .stateIn(scope, SharingStarted.WhileSubscribed(), initialValue = TelephonyIcons.G)
+ mobileConnectionsRepo.defaultMobileIconGroup.stateIn(
+ scope,
+ SharingStarted.WhileSubscribed(),
+ initialValue = TelephonyIcons.G
+ )
/**
* We want to show an error state when cellular has actually failed to validate, but not if some
@@ -189,7 +191,6 @@
defaultMobileIconMapping,
defaultMobileIconGroup,
isDefaultConnectionFailed,
- mobileMappingsProxy,
mobileConnectionsRepo.getRepoForSubId(subId),
)
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/MobileUiAdapter.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/MobileUiAdapter.kt
index c7e0ce1..62fa723 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/MobileUiAdapter.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/MobileUiAdapter.kt
@@ -16,6 +16,7 @@
package com.android.systemui.statusbar.pipeline.mobile.ui
+import com.android.systemui.CoreStartable
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.statusbar.phone.StatusBarIconController
@@ -29,9 +30,10 @@
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.mapLatest
-import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.launch
/**
* This class is intended to provide a context to collect on the
@@ -50,12 +52,12 @@
interactor: MobileIconsInteractor,
private val iconController: StatusBarIconController,
private val iconsViewModelFactory: MobileIconsViewModel.Factory,
- @Application scope: CoroutineScope,
+ @Application private val scope: CoroutineScope,
private val statusBarPipelineFlags: StatusBarPipelineFlags,
-) {
+) : CoreStartable {
private val mobileSubIds: Flow<List<Int>> =
- interactor.filteredSubscriptions.mapLatest { infos ->
- infos.map { subscriptionInfo -> subscriptionInfo.subscriptionId }
+ interactor.filteredSubscriptions.mapLatest { subscriptions ->
+ subscriptions.map { subscriptionModel -> subscriptionModel.subscriptionId }
}
/**
@@ -66,18 +68,19 @@
* NOTE: this should go away as the view presenter learns more about this data pipeline
*/
private val mobileSubIdsState: StateFlow<List<Int>> =
- mobileSubIds
- .onEach {
- // Only notify the icon controller if we want to *render* the new icons.
- // Note that this flow may still run if
- // [statusBarPipelineFlags.runNewMobileIconsBackend] is true because we may want to
- // get the logging data without rendering.
- if (statusBarPipelineFlags.useNewMobileIcons()) {
- // Notify the icon controller here so that it knows to add icons
- iconController.setNewMobileIconSubIds(it)
- }
+ mobileSubIds.stateIn(scope, SharingStarted.WhileSubscribed(), listOf())
+
+ override fun start() {
+ // Only notify the icon controller if we want to *render* the new icons.
+ // Note that this flow may still run if
+ // [statusBarPipelineFlags.runNewMobileIconsBackend] is true because we may want to
+ // get the logging data without rendering.
+ if (statusBarPipelineFlags.useNewMobileIcons()) {
+ scope.launch {
+ mobileSubIds.collectLatest { iconController.setNewMobileIconSubIds(it) }
}
- .stateIn(scope, SharingStarted.WhileSubscribed(), listOf())
+ }
+ }
/**
* Create a MobileIconsViewModel for a given [IconManager], and bind it to to the manager's
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/view/ModernStatusBarMobileView.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/view/ModernStatusBarMobileView.kt
index ec4fa9c..0ab7bcd 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/view/ModernStatusBarMobileView.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/view/ModernStatusBarMobileView.kt
@@ -32,6 +32,8 @@
attrs: AttributeSet?,
) : BaseStatusBarFrameLayout(context, attrs) {
+ var subId: Int = -1
+
private lateinit var slot: String
override fun getSlot() = slot
@@ -76,6 +78,7 @@
as ModernStatusBarMobileView)
.also {
it.slot = slot
+ it.subId = viewModel.subscriptionId
MobileIconBinder.bind(it, viewModel)
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModel.kt
index 24c1db9..2349cb7 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModel.kt
@@ -23,7 +23,7 @@
import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
import javax.inject.Inject
import kotlinx.coroutines.InternalCoroutinesApi
-import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.StateFlow
/**
* View model for describing the system's current mobile cellular connections. The result is a list
@@ -33,7 +33,7 @@
class MobileIconsViewModel
@Inject
constructor(
- val subscriptionIdsFlow: Flow<List<Int>>,
+ val subscriptionIdsFlow: StateFlow<List<Int>>,
private val interactor: MobileIconsInteractor,
private val logger: ConnectivityPipelineLogger,
) {
@@ -51,7 +51,7 @@
private val interactor: MobileIconsInteractor,
private val logger: ConnectivityPipelineLogger,
) {
- fun create(subscriptionIdsFlow: Flow<List<Int>>): MobileIconsViewModel {
+ fun create(subscriptionIdsFlow: StateFlow<List<Int>>): MobileIconsViewModel {
return MobileIconsViewModel(
subscriptionIdsFlow,
interactor,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/DeviceControlsControllerImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/DeviceControlsControllerImpl.kt
index 6c66f0b..341eb3b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/DeviceControlsControllerImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/DeviceControlsControllerImpl.kt
@@ -68,7 +68,6 @@
internal const val PREFS_CONTROLS_SEEDING_COMPLETED = "SeedingCompleted"
const val PREFS_CONTROLS_FILE = "controls_prefs"
- internal const val PREFS_SETTINGS_DIALOG_ATTEMPTS = "show_settings_attempts"
private const val SEEDING_MAX = 2
}
diff --git a/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinator.kt b/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinator.kt
index fb17b69..4d91e35 100644
--- a/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinator.kt
+++ b/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinator.kt
@@ -34,8 +34,8 @@
import com.android.systemui.classifier.FalsingCollector
import com.android.systemui.common.shared.model.ContentDescription.Companion.loadContentDescription
import com.android.systemui.common.shared.model.Text.Companion.loadText
-import com.android.systemui.common.ui.binder.IconViewBinder
import com.android.systemui.common.ui.binder.TextViewBinder
+import com.android.systemui.common.ui.binder.TintedIconViewBinder
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.plugins.FalsingManager
@@ -121,7 +121,7 @@
// ---- Start icon ----
val iconView = currentView.requireViewById<CachingIconView>(R.id.start_icon)
- IconViewBinder.bind(newInfo.startIcon, iconView)
+ TintedIconViewBinder.bind(newInfo.startIcon, iconView)
// ---- Text ----
val textView = currentView.requireViewById<TextView>(R.id.text)
@@ -156,11 +156,14 @@
}
// ---- Overall accessibility ----
- currentView.requireViewById<ViewGroup>(
- R.id.chipbar_inner
- ).contentDescription =
- "${newInfo.startIcon.contentDescription.loadContentDescription(context)} " +
- "${newInfo.text.loadText(context)}"
+ val iconDesc = newInfo.startIcon.icon.contentDescription
+ val loadedIconDesc = if (iconDesc != null) {
+ "${iconDesc.loadContentDescription(context)} "
+ } else {
+ ""
+ }
+ currentView.requireViewById<ViewGroup>(R.id.chipbar_inner).contentDescription =
+ "$loadedIconDesc${newInfo.text.loadText(context)}"
// ---- Haptics ----
newInfo.vibrationEffect?.let {
diff --git a/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarInfo.kt b/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarInfo.kt
index b92e0ec..a3eef80 100644
--- a/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarInfo.kt
+++ b/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarInfo.kt
@@ -18,8 +18,9 @@
import android.os.VibrationEffect
import android.view.View
-import com.android.systemui.common.shared.model.Icon
+import androidx.annotation.AttrRes
import com.android.systemui.common.shared.model.Text
+import com.android.systemui.common.shared.model.TintedIcon
import com.android.systemui.temporarydisplay.TemporaryViewInfo
/**
@@ -33,7 +34,7 @@
* @property vibrationEffect an optional vibration effect when the chipbar is displayed
*/
data class ChipbarInfo(
- val startIcon: Icon,
+ val startIcon: TintedIcon,
val text: Text,
val endItem: ChipbarEndItem?,
val vibrationEffect: VibrationEffect? = null,
@@ -41,7 +42,11 @@
override val wakeReason: String,
override val timeoutMs: Int,
override val id: String,
-) : TemporaryViewInfo()
+) : TemporaryViewInfo() {
+ companion object {
+ @AttrRes const val DEFAULT_ICON_TINT_ATTR = android.R.attr.textColorPrimary
+ }
+}
/** The possible items to display at the end of the chipbar. */
sealed class ChipbarEndItem {
diff --git a/packages/SystemUI/src/com/android/systemui/unfold/UnfoldLightRevealOverlayAnimation.kt b/packages/SystemUI/src/com/android/systemui/unfold/UnfoldLightRevealOverlayAnimation.kt
index 162c915..b2ec27c 100644
--- a/packages/SystemUI/src/com/android/systemui/unfold/UnfoldLightRevealOverlayAnimation.kt
+++ b/packages/SystemUI/src/com/android/systemui/unfold/UnfoldLightRevealOverlayAnimation.kt
@@ -40,6 +40,8 @@
import com.android.systemui.statusbar.LightRevealEffect
import com.android.systemui.statusbar.LightRevealScrim
import com.android.systemui.statusbar.LinearLightRevealEffect
+import com.android.systemui.unfold.UnfoldLightRevealOverlayAnimation.AddOverlayReason.FOLD
+import com.android.systemui.unfold.UnfoldLightRevealOverlayAnimation.AddOverlayReason.UNFOLD
import com.android.systemui.unfold.UnfoldTransitionProgressProvider.TransitionProgressListener
import com.android.systemui.unfold.updates.RotationChangeProvider
import com.android.systemui.unfold.util.ScaleAwareTransitionProgressProvider.Companion.areAnimationsEnabled
@@ -125,7 +127,7 @@
try {
// Add the view only if we are unfolding and this is the first screen on
if (!isFolded && !isUnfoldHandled && contentResolver.areAnimationsEnabled()) {
- executeInBackground { addView(onOverlayReady) }
+ executeInBackground { addOverlay(onOverlayReady, reason = UNFOLD) }
isUnfoldHandled = true
} else {
// No unfold transition, immediately report that overlay is ready
@@ -137,7 +139,7 @@
}
}
- private fun addView(onOverlayReady: Runnable? = null) {
+ private fun addOverlay(onOverlayReady: Runnable? = null, reason: AddOverlayReason) {
if (!::wwm.isInitialized) {
// Surface overlay is not created yet on the first SysUI launch
onOverlayReady?.run()
@@ -152,7 +154,10 @@
LightRevealScrim(context, null).apply {
revealEffect = createLightRevealEffect()
isScrimOpaqueChangedListener = Consumer {}
- revealAmount = 0f
+ revealAmount = when (reason) {
+ FOLD -> TRANSPARENT
+ UNFOLD -> BLACK
+ }
}
val params = getLayoutParams()
@@ -228,7 +233,7 @@
}
private fun getUnfoldedDisplayInfo(): DisplayInfo =
- displayManager.displays
+ displayManager.getDisplays(DisplayManager.DISPLAY_CATEGORY_ALL_INCLUDING_DISABLED)
.asSequence()
.map { DisplayInfo().apply { it.getDisplayInfo(this) } }
.filter { it.type == Display.TYPE_INTERNAL }
@@ -247,7 +252,7 @@
override fun onTransitionStarted() {
// Add view for folding case (when unfolding the view is added earlier)
if (scrimView == null) {
- executeInBackground { addView() }
+ executeInBackground { addOverlay(reason = FOLD) }
}
// Disable input dispatching during transition.
InputManager.getInstance().cancelCurrentTouch()
@@ -294,11 +299,17 @@
}
)
+ private enum class AddOverlayReason { FOLD, UNFOLD }
+
private companion object {
- private const val ROTATION_ANIMATION_OVERLAY_Z_INDEX = Integer.MAX_VALUE
+ const val ROTATION_ANIMATION_OVERLAY_Z_INDEX = Integer.MAX_VALUE
// Put the unfold overlay below the rotation animation screenshot to hide the moment
// when it is rotated but the rotation of the other windows hasn't happen yet
- private const val UNFOLD_OVERLAY_LAYER_Z_INDEX = ROTATION_ANIMATION_OVERLAY_Z_INDEX - 1
+ const val UNFOLD_OVERLAY_LAYER_Z_INDEX = ROTATION_ANIMATION_OVERLAY_Z_INDEX - 1
+
+ // constants for revealAmount.
+ const val TRANSPARENT = 1f
+ const val BLACK = 0f
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/util/DeviceConfigProxy.java b/packages/SystemUI/src/com/android/systemui/util/DeviceConfigProxy.java
index 0f3eddf..bff6132d 100644
--- a/packages/SystemUI/src/com/android/systemui/util/DeviceConfigProxy.java
+++ b/packages/SystemUI/src/com/android/systemui/util/DeviceConfigProxy.java
@@ -50,13 +50,6 @@
}
/**
- * Wrapped version of {@link DeviceConfig#enforceReadPermission}.
- */
- public void enforceReadPermission(String namespace) {
- DeviceConfig.enforceReadPermission(namespace);
- }
-
- /**
* Wrapped version of {@link DeviceConfig#getBoolean}.
*/
public boolean getBoolean(
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardAbsKeyInputViewControllerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardAbsKeyInputViewControllerTest.java
index 8bbaf3d..4903d31 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardAbsKeyInputViewControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardAbsKeyInputViewControllerTest.java
@@ -19,6 +19,7 @@
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyZeroInteractions;
@@ -87,6 +88,7 @@
when(mAbsKeyInputView.isAttachedToWindow()).thenReturn(true);
when(mAbsKeyInputView.requireViewById(R.id.bouncer_message_area))
.thenReturn(mKeyguardMessageArea);
+ when(mAbsKeyInputView.getResources()).thenReturn(getContext().getResources());
mKeyguardAbsKeyInputViewController = new KeyguardAbsKeyInputViewController(mAbsKeyInputView,
mKeyguardUpdateMonitor, mSecurityMode, mLockPatternUtils, mKeyguardSecurityCallback,
mKeyguardMessageAreaControllerFactory, mLatencyTracker, mFalsingCollector,
@@ -125,4 +127,22 @@
verifyZeroInteractions(mKeyguardSecurityCallback);
verifyZeroInteractions(mKeyguardMessageAreaController);
}
+
+ @Test
+ public void onPromptReasonNone_doesNotSetMessage() {
+ mKeyguardAbsKeyInputViewController.showPromptReason(0);
+ verify(mKeyguardMessageAreaController, never()).setMessage(
+ getContext().getResources().getString(R.string.kg_prompt_reason_restart_password),
+ false);
+ }
+
+ @Test
+ public void onPromptReason_setsMessage() {
+ when(mAbsKeyInputView.getPromptReasonStringRes(1)).thenReturn(
+ R.string.kg_prompt_reason_restart_password);
+ mKeyguardAbsKeyInputViewController.showPromptReason(1);
+ verify(mKeyguardMessageAreaController).setMessage(
+ getContext().getResources().getString(R.string.kg_prompt_reason_restart_password),
+ false);
+ }
}
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPasswordViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPasswordViewControllerTest.kt
index d20be56..d912793 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPasswordViewControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPasswordViewControllerTest.kt
@@ -30,64 +30,54 @@
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.anyBoolean
+import org.mockito.ArgumentMatchers.anyString
import org.mockito.Mock
import org.mockito.Mockito
-import org.mockito.Mockito.`when`
import org.mockito.Mockito.never
import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when`
import org.mockito.MockitoAnnotations
@SmallTest
@RunWith(AndroidTestingRunner::class)
@TestableLooper.RunWithLooper
class KeyguardPasswordViewControllerTest : SysuiTestCase() {
- @Mock
- private lateinit var keyguardPasswordView: KeyguardPasswordView
- @Mock
- private lateinit var passwordEntry: EditText
- @Mock
- lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor
- @Mock
- lateinit var securityMode: KeyguardSecurityModel.SecurityMode
- @Mock
- lateinit var lockPatternUtils: LockPatternUtils
- @Mock
- lateinit var keyguardSecurityCallback: KeyguardSecurityCallback
- @Mock
- lateinit var messageAreaControllerFactory: KeyguardMessageAreaController.Factory
- @Mock
- lateinit var latencyTracker: LatencyTracker
- @Mock
- lateinit var inputMethodManager: InputMethodManager
- @Mock
- lateinit var emergencyButtonController: EmergencyButtonController
- @Mock
- lateinit var mainExecutor: DelayableExecutor
- @Mock
- lateinit var falsingCollector: FalsingCollector
- @Mock
- lateinit var keyguardViewController: KeyguardViewController
- @Mock
- private lateinit var mKeyguardMessageArea: BouncerKeyguardMessageArea
- @Mock
- private lateinit var mKeyguardMessageAreaController:
- KeyguardMessageAreaController<BouncerKeyguardMessageArea>
+ @Mock private lateinit var keyguardPasswordView: KeyguardPasswordView
+ @Mock private lateinit var passwordEntry: EditText
+ @Mock lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor
+ @Mock lateinit var securityMode: KeyguardSecurityModel.SecurityMode
+ @Mock lateinit var lockPatternUtils: LockPatternUtils
+ @Mock lateinit var keyguardSecurityCallback: KeyguardSecurityCallback
+ @Mock lateinit var messageAreaControllerFactory: KeyguardMessageAreaController.Factory
+ @Mock lateinit var latencyTracker: LatencyTracker
+ @Mock lateinit var inputMethodManager: InputMethodManager
+ @Mock lateinit var emergencyButtonController: EmergencyButtonController
+ @Mock lateinit var mainExecutor: DelayableExecutor
+ @Mock lateinit var falsingCollector: FalsingCollector
+ @Mock lateinit var keyguardViewController: KeyguardViewController
+ @Mock private lateinit var mKeyguardMessageArea: BouncerKeyguardMessageArea
+ @Mock
+ private lateinit var mKeyguardMessageAreaController:
+ KeyguardMessageAreaController<BouncerKeyguardMessageArea>
- private lateinit var keyguardPasswordViewController: KeyguardPasswordViewController
+ private lateinit var keyguardPasswordViewController: KeyguardPasswordViewController
- @Before
- fun setup() {
- MockitoAnnotations.initMocks(this)
- Mockito.`when`(
- keyguardPasswordView
- .requireViewById<BouncerKeyguardMessageArea>(R.id.bouncer_message_area)
- ).thenReturn(mKeyguardMessageArea)
- Mockito.`when`(messageAreaControllerFactory.create(mKeyguardMessageArea))
- .thenReturn(mKeyguardMessageAreaController)
- Mockito.`when`(keyguardPasswordView.passwordTextViewId).thenReturn(R.id.passwordEntry)
- Mockito.`when`(keyguardPasswordView.findViewById<EditText>(R.id.passwordEntry)
- ).thenReturn(passwordEntry)
- keyguardPasswordViewController = KeyguardPasswordViewController(
+ @Before
+ fun setup() {
+ MockitoAnnotations.initMocks(this)
+ Mockito.`when`(
+ keyguardPasswordView.requireViewById<BouncerKeyguardMessageArea>(
+ R.id.bouncer_message_area))
+ .thenReturn(mKeyguardMessageArea)
+ Mockito.`when`(messageAreaControllerFactory.create(mKeyguardMessageArea))
+ .thenReturn(mKeyguardMessageAreaController)
+ Mockito.`when`(keyguardPasswordView.passwordTextViewId).thenReturn(R.id.passwordEntry)
+ Mockito.`when`(keyguardPasswordView.findViewById<EditText>(R.id.passwordEntry))
+ .thenReturn(passwordEntry)
+ `when`(keyguardPasswordView.resources).thenReturn(context.resources)
+ keyguardPasswordViewController =
+ KeyguardPasswordViewController(
keyguardPasswordView,
keyguardUpdateMonitor,
securityMode,
@@ -100,51 +90,48 @@
mainExecutor,
mContext.resources,
falsingCollector,
- keyguardViewController
- )
- }
+ keyguardViewController)
+ }
- @Test
- fun testFocusWhenBouncerIsShown() {
- Mockito.`when`(keyguardViewController.isBouncerShowing).thenReturn(true)
- Mockito.`when`(keyguardPasswordView.isShown).thenReturn(true)
- keyguardPasswordViewController.onResume(KeyguardSecurityView.VIEW_REVEALED)
- keyguardPasswordView.post {
- verify(keyguardPasswordView).requestFocus()
- verify(keyguardPasswordView).showKeyboard()
- }
+ @Test
+ fun testFocusWhenBouncerIsShown() {
+ Mockito.`when`(keyguardViewController.isBouncerShowing).thenReturn(true)
+ Mockito.`when`(keyguardPasswordView.isShown).thenReturn(true)
+ keyguardPasswordViewController.onResume(KeyguardSecurityView.VIEW_REVEALED)
+ keyguardPasswordView.post {
+ verify(keyguardPasswordView).requestFocus()
+ verify(keyguardPasswordView).showKeyboard()
}
+ }
- @Test
- fun testDoNotFocusWhenBouncerIsHidden() {
- Mockito.`when`(keyguardViewController.isBouncerShowing).thenReturn(false)
- Mockito.`when`(keyguardPasswordView.isShown).thenReturn(true)
- keyguardPasswordViewController.onResume(KeyguardSecurityView.VIEW_REVEALED)
- verify(keyguardPasswordView, never()).requestFocus()
- }
+ @Test
+ fun testDoNotFocusWhenBouncerIsHidden() {
+ Mockito.`when`(keyguardViewController.isBouncerShowing).thenReturn(false)
+ Mockito.`when`(keyguardPasswordView.isShown).thenReturn(true)
+ keyguardPasswordViewController.onResume(KeyguardSecurityView.VIEW_REVEALED)
+ verify(keyguardPasswordView, never()).requestFocus()
+ }
- @Test
- fun testHideKeyboardWhenOnPause() {
- keyguardPasswordViewController.onPause()
- keyguardPasswordView.post {
- verify(keyguardPasswordView).clearFocus()
- verify(keyguardPasswordView).hideKeyboard()
- }
+ @Test
+ fun testHideKeyboardWhenOnPause() {
+ keyguardPasswordViewController.onPause()
+ keyguardPasswordView.post {
+ verify(keyguardPasswordView).clearFocus()
+ verify(keyguardPasswordView).hideKeyboard()
}
+ }
- @Test
- fun startAppearAnimation() {
- keyguardPasswordViewController.startAppearAnimation()
- verify(mKeyguardMessageAreaController).setMessage(R.string.keyguard_enter_your_password)
- }
+ @Test
+ fun startAppearAnimation() {
+ keyguardPasswordViewController.startAppearAnimation()
+ verify(mKeyguardMessageAreaController)
+ .setMessage(context.resources.getString(R.string.keyguard_enter_your_password), false)
+ }
- @Test
- fun startAppearAnimation_withExistingMessage() {
- `when`(mKeyguardMessageAreaController.message).thenReturn("Unlock to continue.")
- keyguardPasswordViewController.startAppearAnimation()
- verify(
- mKeyguardMessageAreaController,
- never()
- ).setMessage(R.string.keyguard_enter_your_password)
- }
+ @Test
+ fun startAppearAnimation_withExistingMessage() {
+ `when`(mKeyguardMessageAreaController.message).thenReturn("Unlock to continue.")
+ keyguardPasswordViewController.startAppearAnimation()
+ verify(mKeyguardMessageAreaController, never()).setMessage(anyString(), anyBoolean())
+ }
}
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPatternViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPatternViewControllerTest.kt
index b3d1c8f..85dbdb8 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPatternViewControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPatternViewControllerTest.kt
@@ -30,97 +30,93 @@
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.anyBoolean
+import org.mockito.ArgumentMatchers.anyString
import org.mockito.Mock
+import org.mockito.Mockito.never
import org.mockito.Mockito.verify
import org.mockito.Mockito.`when`
-import org.mockito.Mockito.never
import org.mockito.MockitoAnnotations
@SmallTest
@RunWith(AndroidTestingRunner::class)
@TestableLooper.RunWithLooper
class KeyguardPatternViewControllerTest : SysuiTestCase() {
- @Mock
- private lateinit var mKeyguardPatternView: KeyguardPatternView
+ @Mock private lateinit var mKeyguardPatternView: KeyguardPatternView
- @Mock
- private lateinit var mKeyguardUpdateMonitor: KeyguardUpdateMonitor
+ @Mock private lateinit var mKeyguardUpdateMonitor: KeyguardUpdateMonitor
- @Mock
- private lateinit var mSecurityMode: KeyguardSecurityModel.SecurityMode
+ @Mock private lateinit var mSecurityMode: KeyguardSecurityModel.SecurityMode
- @Mock
- private lateinit var mLockPatternUtils: LockPatternUtils
+ @Mock private lateinit var mLockPatternUtils: LockPatternUtils
- @Mock
- private lateinit var mKeyguardSecurityCallback: KeyguardSecurityCallback
+ @Mock private lateinit var mKeyguardSecurityCallback: KeyguardSecurityCallback
- @Mock
- private lateinit var mLatencyTracker: LatencyTracker
- private var mFalsingCollector: FalsingCollector = FalsingCollectorFake()
+ @Mock private lateinit var mLatencyTracker: LatencyTracker
+ private var mFalsingCollector: FalsingCollector = FalsingCollectorFake()
- @Mock
- private lateinit var mEmergencyButtonController: EmergencyButtonController
+ @Mock private lateinit var mEmergencyButtonController: EmergencyButtonController
- @Mock
- private lateinit
- var mKeyguardMessageAreaControllerFactory: KeyguardMessageAreaController.Factory
+ @Mock
+ private lateinit var mKeyguardMessageAreaControllerFactory: KeyguardMessageAreaController.Factory
- @Mock
- private lateinit var mKeyguardMessageArea: BouncerKeyguardMessageArea
+ @Mock private lateinit var mKeyguardMessageArea: BouncerKeyguardMessageArea
- @Mock
- private lateinit var mKeyguardMessageAreaController:
- KeyguardMessageAreaController<BouncerKeyguardMessageArea>
+ @Mock
+ private lateinit var mKeyguardMessageAreaController:
+ KeyguardMessageAreaController<BouncerKeyguardMessageArea>
- @Mock
- private lateinit var mLockPatternView: LockPatternView
+ @Mock private lateinit var mLockPatternView: LockPatternView
- @Mock
- private lateinit var mPostureController: DevicePostureController
+ @Mock private lateinit var mPostureController: DevicePostureController
- private lateinit var mKeyguardPatternViewController: KeyguardPatternViewController
+ private lateinit var mKeyguardPatternViewController: KeyguardPatternViewController
- @Before
- fun setup() {
- MockitoAnnotations.initMocks(this)
- `when`(mKeyguardPatternView.isAttachedToWindow).thenReturn(true)
- `when`(mKeyguardPatternView
- .requireViewById<BouncerKeyguardMessageArea>(R.id.bouncer_message_area))
- .thenReturn(mKeyguardMessageArea)
- `when`(mKeyguardPatternView.findViewById<LockPatternView>(R.id.lockPatternView))
- .thenReturn(mLockPatternView)
- `when`(mKeyguardMessageAreaControllerFactory.create(mKeyguardMessageArea))
- .thenReturn(mKeyguardMessageAreaController)
- mKeyguardPatternViewController = KeyguardPatternViewController(
+ @Before
+ fun setup() {
+ MockitoAnnotations.initMocks(this)
+ `when`(mKeyguardPatternView.isAttachedToWindow).thenReturn(true)
+ `when`(
+ mKeyguardPatternView.requireViewById<BouncerKeyguardMessageArea>(
+ R.id.bouncer_message_area))
+ .thenReturn(mKeyguardMessageArea)
+ `when`(mKeyguardPatternView.findViewById<LockPatternView>(R.id.lockPatternView))
+ .thenReturn(mLockPatternView)
+ `when`(mKeyguardMessageAreaControllerFactory.create(mKeyguardMessageArea))
+ .thenReturn(mKeyguardMessageAreaController)
+ `when`(mKeyguardPatternView.resources).thenReturn(context.resources)
+ mKeyguardPatternViewController =
+ KeyguardPatternViewController(
mKeyguardPatternView,
- mKeyguardUpdateMonitor, mSecurityMode, mLockPatternUtils, mKeyguardSecurityCallback,
- mLatencyTracker, mFalsingCollector, mEmergencyButtonController,
- mKeyguardMessageAreaControllerFactory, mPostureController
- )
- }
+ mKeyguardUpdateMonitor,
+ mSecurityMode,
+ mLockPatternUtils,
+ mKeyguardSecurityCallback,
+ mLatencyTracker,
+ mFalsingCollector,
+ mEmergencyButtonController,
+ mKeyguardMessageAreaControllerFactory,
+ mPostureController)
+ }
- @Test
- fun onPause_resetsText() {
- mKeyguardPatternViewController.init()
- mKeyguardPatternViewController.onPause()
- verify(mKeyguardMessageAreaController).setMessage(R.string.keyguard_enter_your_pattern)
- }
+ @Test
+ fun onPause_resetsText() {
+ mKeyguardPatternViewController.init()
+ mKeyguardPatternViewController.onPause()
+ verify(mKeyguardMessageAreaController).setMessage(R.string.keyguard_enter_your_pattern)
+ }
+ @Test
+ fun startAppearAnimation() {
+ mKeyguardPatternViewController.startAppearAnimation()
+ verify(mKeyguardMessageAreaController)
+ .setMessage(context.resources.getString(R.string.keyguard_enter_your_pattern), false)
+ }
- @Test
- fun startAppearAnimation() {
- mKeyguardPatternViewController.startAppearAnimation()
- verify(mKeyguardMessageAreaController).setMessage(R.string.keyguard_enter_your_pattern)
- }
-
- @Test
- fun startAppearAnimation_withExistingMessage() {
- `when`(mKeyguardMessageAreaController.message).thenReturn("Unlock to continue.")
- mKeyguardPatternViewController.startAppearAnimation()
- verify(
- mKeyguardMessageAreaController,
- never()
- ).setMessage(R.string.keyguard_enter_your_password)
- }
+ @Test
+ fun startAppearAnimation_withExistingMessage() {
+ `when`(mKeyguardMessageAreaController.message).thenReturn("Unlock to continue.")
+ mKeyguardPatternViewController.startAppearAnimation()
+ verify(mKeyguardMessageAreaController, never()).setMessage(anyString(), anyBoolean())
+ }
}
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPinViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPinViewControllerTest.kt
index 8bcfe6f..cdb7bbb 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPinViewControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPinViewControllerTest.kt
@@ -31,10 +31,13 @@
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.anyBoolean
+import org.mockito.ArgumentMatchers.anyString
import org.mockito.Mock
import org.mockito.Mockito
import org.mockito.Mockito.any
import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when`
import org.mockito.MockitoAnnotations
@SmallTest
@@ -79,6 +82,7 @@
keyguardMessageAreaControllerFactory.create(any(KeyguardMessageArea::class.java))
)
.thenReturn(keyguardMessageAreaController)
+ `when`(keyguardPinView.resources).thenReturn(context.resources)
pinViewController =
KeyguardPinViewController(
keyguardPinView,
@@ -98,14 +102,14 @@
@Test
fun startAppearAnimation() {
pinViewController.startAppearAnimation()
- verify(keyguardMessageAreaController).setMessage(R.string.keyguard_enter_your_pin)
+ verify(keyguardMessageAreaController)
+ .setMessage(context.resources.getString(R.string.keyguard_enter_your_pin), false)
}
@Test
fun startAppearAnimation_withExistingMessage() {
Mockito.`when`(keyguardMessageAreaController.message).thenReturn("Unlock to continue.")
pinViewController.startAppearAnimation()
- verify(keyguardMessageAreaController, Mockito.never())
- .setMessage(R.string.keyguard_enter_your_password)
+ verify(keyguardMessageAreaController, Mockito.never()).setMessage(anyString(), anyBoolean())
}
}
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java
index 002da55..bdd29aa 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java
@@ -31,6 +31,7 @@
import static com.android.keyguard.FaceAuthApiRequestReason.NOTIFICATION_PANEL_CLICKED;
import static com.android.keyguard.KeyguardUpdateMonitor.BIOMETRIC_STATE_CANCELLING_RESTARTING;
import static com.android.keyguard.KeyguardUpdateMonitor.DEFAULT_CANCEL_SIGNAL_TIMEOUT;
+import static com.android.keyguard.KeyguardUpdateMonitor.HAL_POWER_PRESS_TIMEOUT;
import static com.android.keyguard.KeyguardUpdateMonitor.getCurrentUser;
import static com.google.common.truth.Truth.assertThat;
@@ -855,12 +856,21 @@
}
@Test
- public void testFingerprintPowerPressed_restartsFingerprintListeningStateImmediately() {
+ public void testFingerprintPowerPressed_restartsFingerprintListeningStateWithDelay() {
mKeyguardUpdateMonitor.mFingerprintAuthenticationCallback
.onAuthenticationError(FingerprintManager.BIOMETRIC_ERROR_POWER_PRESSED, "");
- verify(mFingerprintManager).authenticate(any(), any(), any(), any(), anyInt(), anyInt(),
- anyInt());
+ // THEN doesn't authenticate immediately
+ verify(mFingerprintManager, never()).authenticate(any(),
+ any(), any(), any(), anyInt(), anyInt(), anyInt());
+
+ // WHEN all messages (with delays) are processed
+ mTestableLooper.moveTimeForward(HAL_POWER_PRESS_TIMEOUT);
+ mTestableLooper.processAllMessages();
+
+ // THEN fingerprint manager attempts to authenticate again
+ verify(mFingerprintManager).authenticate(any(),
+ any(), any(), any(), anyInt(), anyInt(), anyInt());
}
@Test
diff --git a/packages/SystemUI/animation/tests/com/android/systemui/animation/InterpolatorsAndroidXTest.kt b/packages/SystemUI/tests/src/com/android/systemui/animation/InterpolatorsAndroidXTest.kt
similarity index 94%
rename from packages/SystemUI/animation/tests/com/android/systemui/animation/InterpolatorsAndroidXTest.kt
rename to packages/SystemUI/tests/src/com/android/systemui/animation/InterpolatorsAndroidXTest.kt
index 389eed0..2c680be 100644
--- a/packages/SystemUI/animation/tests/com/android/systemui/animation/InterpolatorsAndroidXTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/animation/InterpolatorsAndroidXTest.kt
@@ -17,6 +17,7 @@
package com.android.systemui.animation
import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
import java.lang.reflect.Modifier
import junit.framework.Assert.assertEquals
import org.junit.Test
@@ -25,7 +26,7 @@
@SmallTest
@RunWith(JUnit4::class)
-class InterpolatorsAndroidXTest {
+class InterpolatorsAndroidXTest : SysuiTestCase() {
@Test
fun testInterpolatorsAndInterpolatorsAndroidXPublicMethodsAreEqual() {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java
index b267a5c..a94f342 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java
@@ -110,6 +110,8 @@
import java.util.List;
import java.util.Optional;
+import javax.inject.Provider;
+
@SmallTest
@RunWith(AndroidTestingRunner.class)
@RunWithLooper(setAsMainLooper = true)
@@ -261,6 +263,7 @@
initUdfpsController(true /* hasAlternateTouchProvider */);
}
+
private void initUdfpsController(boolean hasAlternateTouchProvider) {
initUdfpsController(mOpticalProps, hasAlternateTouchProvider);
}
@@ -270,8 +273,10 @@
reset(mFingerprintManager);
reset(mScreenLifecycle);
- final Optional<AlternateUdfpsTouchProvider> alternateTouchProvider =
- hasAlternateTouchProvider ? Optional.of(mAlternateTouchProvider) : Optional.empty();
+ final Optional<Provider<AlternateUdfpsTouchProvider>> alternateTouchProvider =
+ hasAlternateTouchProvider ? Optional.of(
+ (Provider<AlternateUdfpsTouchProvider>) () -> mAlternateTouchProvider)
+ : Optional.empty();
mUdfpsController = new UdfpsController(mContext, new FakeExecution(), mLayoutInflater,
mFingerprintManager, mWindowManager, mStatusBarStateController, mFgExecutor,
@@ -1140,7 +1145,7 @@
}
@Test
- public void onTouch_withNewTouchDetection_shouldCallOldFingerprintManagerPath()
+ public void onTouch_withNewTouchDetection_shouldCallNewFingerprintManagerPath()
throws RemoteException {
final NormalizedTouchData touchData = new NormalizedTouchData(0, 0f, 0f, 0f, 0f, 0f, 0L,
0L);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlActionCoordinatorImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlActionCoordinatorImplTest.kt
index 1d00d6b..16fb50c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlActionCoordinatorImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlActionCoordinatorImplTest.kt
@@ -16,21 +16,19 @@
package com.android.systemui.controls.ui
-import android.content.Context
-import android.content.SharedPreferences
import android.test.suitebuilder.annotation.SmallTest
import android.testing.AndroidTestingRunner
import com.android.systemui.SysuiTestCase
import com.android.systemui.broadcast.BroadcastSender
import com.android.systemui.controls.ControlsMetricsLogger
-import com.android.systemui.controls.FakeControlsSettingsRepository
+import com.android.systemui.controls.settings.ControlsSettingsDialogManager
+import com.android.systemui.controls.settings.FakeControlsSettingsRepository
+import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.flags.Flags
import com.android.systemui.plugins.ActivityStarter
-import com.android.systemui.settings.UserContextProvider
import com.android.systemui.statusbar.VibratorHelper
-import com.android.systemui.statusbar.policy.DeviceControlsControllerImpl
import com.android.systemui.statusbar.policy.KeyguardStateController
import com.android.systemui.util.concurrency.DelayableExecutor
-import com.android.systemui.util.settings.SecureSettings
import com.android.wm.shell.TaskViewFactory
import org.junit.Before
import org.junit.Test
@@ -40,8 +38,8 @@
import org.mockito.Mockito
import org.mockito.Mockito.`when`
import org.mockito.Mockito.anyBoolean
+import org.mockito.Mockito.doNothing
import org.mockito.Mockito.doReturn
-import org.mockito.Mockito.mock
import org.mockito.Mockito.never
import org.mockito.Mockito.reset
import org.mockito.Mockito.spy
@@ -71,9 +69,9 @@
@Mock
private lateinit var metricsLogger: ControlsMetricsLogger
@Mock
- private lateinit var secureSettings: SecureSettings
+ private lateinit var featureFlags: FeatureFlags
@Mock
- private lateinit var userContextProvider: UserContextProvider
+ private lateinit var controlsSettingsDialogManager: ControlsSettingsDialogManager
companion object {
fun <T> any(): T = Mockito.any<T>()
@@ -103,23 +101,16 @@
taskViewFactory,
metricsLogger,
vibratorHelper,
- secureSettings,
- userContextProvider,
- controlsSettingsRepository
+ controlsSettingsRepository,
+ controlsSettingsDialogManager,
+ featureFlags
))
-
- val userContext = mock(Context::class.java)
- val pref = mock(SharedPreferences::class.java)
- `when`(userContextProvider.userContext).thenReturn(userContext)
- `when`(userContext.getSharedPreferences(
- DeviceControlsControllerImpl.PREFS_CONTROLS_FILE, Context.MODE_PRIVATE))
- .thenReturn(pref)
- // Just return 2 so we don't test any Dialog logic which requires a launched activity.
- `when`(pref.getInt(DeviceControlsControllerImpl.PREFS_SETTINGS_DIALOG_ATTEMPTS, 0))
- .thenReturn(2)
+ coordinator.activityContext = mContext
`when`(cvh.cws.ci.controlId).thenReturn(ID)
`when`(cvh.cws.control?.isAuthRequired()).thenReturn(true)
+ `when`(featureFlags.isEnabled(Flags.USE_APP_PANELS)).thenReturn(false)
+
action = spy(coordinator.Action(ID, {}, false, true))
doReturn(action).`when`(coordinator).createAction(any(), any(), anyBoolean(), anyBoolean())
}
@@ -160,15 +151,31 @@
doReturn(action).`when`(coordinator).createAction(any(), any(), anyBoolean(), anyBoolean())
`when`(keyguardStateController.isShowing()).thenReturn(true)
- `when`(keyguardStateController.isUnlocked()).thenReturn(false)
coordinator.toggle(cvh, "", true)
verify(coordinator).bouncerOrRun(action)
+ verify(controlsSettingsDialogManager).maybeShowDialog(any(), any())
verify(action).invoke()
}
@Test
+ fun testToggleWhenLockedDoesNotTriggerDialog_featureFlagEnabled() {
+ `when`(featureFlags.isEnabled(Flags.USE_APP_PANELS)).thenReturn(true)
+ action = spy(coordinator.Action(ID, {}, false, false))
+ doReturn(action).`when`(coordinator).createAction(any(), any(), anyBoolean(), anyBoolean())
+
+ `when`(keyguardStateController.isShowing()).thenReturn(true)
+ `when`(keyguardStateController.isUnlocked()).thenReturn(false)
+ doNothing().`when`(controlsSettingsDialogManager).maybeShowDialog(any(), any())
+
+ coordinator.toggle(cvh, "", true)
+
+ verify(coordinator).bouncerOrRun(action)
+ verify(controlsSettingsDialogManager, never()).maybeShowDialog(any(), any())
+ }
+
+ @Test
fun testToggleDoesNotRunsWhenLockedAndAuthRequired() {
action = spy(coordinator.Action(ID, {}, false, true))
doReturn(action).`when`(coordinator).createAction(any(), any(), anyBoolean(), anyBoolean())
diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/dagger/ControlsComponentTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/dagger/ControlsComponentTest.kt
index 48fc46b..9144b13 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/controls/dagger/ControlsComponentTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/controls/dagger/ControlsComponentTest.kt
@@ -22,7 +22,7 @@
import com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_NOT_REQUIRED
import com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_BOOT
import com.android.systemui.SysuiTestCase
-import com.android.systemui.controls.FakeControlsSettingsRepository
+import com.android.systemui.controls.settings.FakeControlsSettingsRepository
import com.android.systemui.controls.controller.ControlsController
import com.android.systemui.controls.controller.ControlsTileResourceConfiguration
import com.android.systemui.controls.management.ControlsListingController
diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/settings/ControlsSettingsDialogManagerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/settings/ControlsSettingsDialogManagerImplTest.kt
new file mode 100644
index 0000000..0c9986d
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/controls/settings/ControlsSettingsDialogManagerImplTest.kt
@@ -0,0 +1,328 @@
+/*
+ * 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.controls.settings
+
+import android.content.DialogInterface
+import android.content.SharedPreferences
+import android.database.ContentObserver
+import android.provider.Settings.Secure.LOCKSCREEN_ALLOW_TRIVIAL_CONTROLS
+import android.provider.Settings.Secure.LOCKSCREEN_SHOW_CONTROLS
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.controls.settings.ControlsSettingsDialogManager.Companion.PREFS_SETTINGS_DIALOG_ATTEMPTS
+import com.android.systemui.plugins.ActivityStarter
+import com.android.systemui.settings.UserFileManager
+import com.android.systemui.settings.UserTracker
+import com.android.systemui.statusbar.policy.DeviceControlsControllerImpl
+import com.android.systemui.util.FakeSharedPreferences
+import com.android.systemui.util.TestableAlertDialog
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.eq
+import com.android.systemui.util.mockito.nullable
+import com.android.systemui.util.settings.FakeSettings
+import com.google.common.truth.Truth.assertThat
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.anyBoolean
+import org.mockito.Mock
+import org.mockito.Mockito.anyInt
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when`
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+@TestableLooper.RunWithLooper
+class ControlsSettingsDialogManagerImplTest : SysuiTestCase() {
+
+ companion object {
+ private const val SETTING_SHOW = LOCKSCREEN_SHOW_CONTROLS
+ private const val SETTING_ACTION = LOCKSCREEN_ALLOW_TRIVIAL_CONTROLS
+ private const val MAX_NUMBER_ATTEMPTS_CONTROLS_DIALOG = 2
+ }
+
+ @Mock private lateinit var userFileManager: UserFileManager
+ @Mock private lateinit var userTracker: UserTracker
+ @Mock private lateinit var activityStarter: ActivityStarter
+ @Mock private lateinit var completedRunnable: () -> Unit
+
+ private lateinit var controlsSettingsRepository: FakeControlsSettingsRepository
+ private lateinit var sharedPreferences: FakeSharedPreferences
+ private lateinit var secureSettings: FakeSettings
+
+ private lateinit var underTest: ControlsSettingsDialogManagerImpl
+
+ private var dialog: TestableAlertDialog? = null
+
+ @Before
+ fun setUp() {
+ MockitoAnnotations.initMocks(this)
+
+ controlsSettingsRepository = FakeControlsSettingsRepository()
+ sharedPreferences = FakeSharedPreferences()
+ secureSettings = FakeSettings()
+
+ `when`(userTracker.userId).thenReturn(0)
+ secureSettings.userId = userTracker.userId
+ `when`(
+ userFileManager.getSharedPreferences(
+ eq(DeviceControlsControllerImpl.PREFS_CONTROLS_FILE),
+ anyInt(),
+ anyInt()
+ )
+ )
+ .thenReturn(sharedPreferences)
+
+ `when`(activityStarter.dismissKeyguardThenExecute(any(), nullable(), anyBoolean()))
+ .thenAnswer { (it.arguments[0] as ActivityStarter.OnDismissAction).onDismiss() }
+
+ attachRepositoryToSettings()
+ underTest =
+ ControlsSettingsDialogManagerImpl(
+ secureSettings,
+ userFileManager,
+ controlsSettingsRepository,
+ userTracker,
+ activityStarter
+ ) { context, _ -> TestableAlertDialog(context).also { dialog = it } }
+ }
+
+ @After
+ fun tearDown() {
+ underTest.closeDialog()
+ }
+
+ @Test
+ fun dialogNotShownIfPrefsAtMaximum() {
+ sharedPreferences.putAttempts(MAX_NUMBER_ATTEMPTS_CONTROLS_DIALOG)
+
+ underTest.maybeShowDialog(context, completedRunnable)
+
+ assertThat(dialog?.isShowing ?: false).isFalse()
+ verify(completedRunnable).invoke()
+ }
+
+ @Test
+ fun dialogNotShownIfSettingsAreTrue() {
+ sharedPreferences.putAttempts(0)
+ secureSettings.putBool(SETTING_SHOW, true)
+ secureSettings.putBool(SETTING_ACTION, true)
+
+ underTest.maybeShowDialog(context, completedRunnable)
+
+ assertThat(dialog?.isShowing ?: false).isFalse()
+ verify(completedRunnable).invoke()
+ }
+
+ @Test
+ fun dialogShownIfAllowTrivialControlsFalse() {
+ sharedPreferences.putAttempts(0)
+ secureSettings.putBool(SETTING_SHOW, true)
+ secureSettings.putBool(SETTING_ACTION, false)
+
+ underTest.maybeShowDialog(context, completedRunnable)
+
+ assertThat(dialog?.isShowing ?: false).isTrue()
+ }
+
+ @Test
+ fun dialogDispossedAfterClosing() {
+ sharedPreferences.putAttempts(0)
+ secureSettings.putBool(SETTING_SHOW, true)
+ secureSettings.putBool(SETTING_ACTION, false)
+
+ underTest.maybeShowDialog(context, completedRunnable)
+ underTest.closeDialog()
+
+ assertThat(dialog?.isShowing ?: false).isFalse()
+ }
+
+ @Test
+ fun dialogNeutralButtonDoesntChangeSetting() {
+ sharedPreferences.putAttempts(0)
+ secureSettings.putBool(SETTING_SHOW, true)
+ secureSettings.putBool(SETTING_ACTION, false)
+
+ underTest.maybeShowDialog(context, completedRunnable)
+ clickButton(DialogInterface.BUTTON_NEUTRAL)
+
+ assertThat(secureSettings.getBool(SETTING_ACTION, false)).isFalse()
+ }
+
+ @Test
+ fun dialogNeutralButtonPutsMaxAttempts() {
+ sharedPreferences.putAttempts(0)
+ secureSettings.putBool(SETTING_SHOW, true)
+ secureSettings.putBool(SETTING_ACTION, false)
+
+ underTest.maybeShowDialog(context, completedRunnable)
+ clickButton(DialogInterface.BUTTON_NEUTRAL)
+
+ assertThat(sharedPreferences.getInt(PREFS_SETTINGS_DIALOG_ATTEMPTS, 0))
+ .isEqualTo(MAX_NUMBER_ATTEMPTS_CONTROLS_DIALOG)
+ }
+
+ @Test
+ fun dialogNeutralButtonCallsOnComplete() {
+ sharedPreferences.putAttempts(0)
+ secureSettings.putBool(SETTING_SHOW, true)
+ secureSettings.putBool(SETTING_ACTION, false)
+
+ underTest.maybeShowDialog(context, completedRunnable)
+ clickButton(DialogInterface.BUTTON_NEUTRAL)
+
+ verify(completedRunnable).invoke()
+ }
+
+ @Test
+ fun dialogPositiveButtonChangesSetting() {
+ sharedPreferences.putAttempts(0)
+ secureSettings.putBool(SETTING_SHOW, true)
+ secureSettings.putBool(SETTING_ACTION, false)
+
+ underTest.maybeShowDialog(context, completedRunnable)
+ clickButton(DialogInterface.BUTTON_POSITIVE)
+
+ assertThat(secureSettings.getBool(SETTING_ACTION, false)).isTrue()
+ }
+
+ @Test
+ fun dialogPositiveButtonPutsMaxAttempts() {
+ sharedPreferences.putAttempts(0)
+ secureSettings.putBool(SETTING_SHOW, true)
+ secureSettings.putBool(SETTING_ACTION, false)
+
+ underTest.maybeShowDialog(context, completedRunnable)
+ clickButton(DialogInterface.BUTTON_POSITIVE)
+
+ assertThat(sharedPreferences.getInt(PREFS_SETTINGS_DIALOG_ATTEMPTS, 0))
+ .isEqualTo(MAX_NUMBER_ATTEMPTS_CONTROLS_DIALOG)
+ }
+
+ @Test
+ fun dialogPositiveButtonCallsOnComplete() {
+ sharedPreferences.putAttempts(0)
+ secureSettings.putBool(SETTING_SHOW, true)
+ secureSettings.putBool(SETTING_ACTION, false)
+
+ underTest.maybeShowDialog(context, completedRunnable)
+ clickButton(DialogInterface.BUTTON_POSITIVE)
+
+ verify(completedRunnable).invoke()
+ }
+
+ @Test
+ fun dialogCancelDoesntChangeSetting() {
+ sharedPreferences.putAttempts(0)
+ secureSettings.putBool(SETTING_SHOW, true)
+ secureSettings.putBool(SETTING_ACTION, false)
+
+ underTest.maybeShowDialog(context, completedRunnable)
+ dialog?.cancel()
+
+ assertThat(secureSettings.getBool(SETTING_ACTION, false)).isFalse()
+ }
+
+ @Test
+ fun dialogCancelPutsOneExtraAttempt() {
+ val attempts = 0
+ sharedPreferences.putAttempts(attempts)
+ secureSettings.putBool(SETTING_SHOW, true)
+ secureSettings.putBool(SETTING_ACTION, false)
+
+ underTest.maybeShowDialog(context, completedRunnable)
+ dialog?.cancel()
+
+ assertThat(sharedPreferences.getInt(PREFS_SETTINGS_DIALOG_ATTEMPTS, 0))
+ .isEqualTo(attempts + 1)
+ }
+
+ @Test
+ fun dialogCancelCallsOnComplete() {
+ sharedPreferences.putAttempts(0)
+ secureSettings.putBool(SETTING_SHOW, true)
+ secureSettings.putBool(SETTING_ACTION, false)
+
+ underTest.maybeShowDialog(context, completedRunnable)
+ dialog?.cancel()
+
+ verify(completedRunnable).invoke()
+ }
+
+ @Test
+ fun closeDialogDoesNotCallOnComplete() {
+ sharedPreferences.putAttempts(0)
+ secureSettings.putBool(SETTING_SHOW, true)
+ secureSettings.putBool(SETTING_ACTION, false)
+
+ underTest.maybeShowDialog(context, completedRunnable)
+ underTest.closeDialog()
+
+ verify(completedRunnable, never()).invoke()
+ }
+
+ @Test
+ fun dialogPositiveWithBothSettingsFalseTogglesBothSettings() {
+ sharedPreferences.putAttempts(0)
+ secureSettings.putBool(SETTING_SHOW, false)
+ secureSettings.putBool(SETTING_ACTION, false)
+
+ underTest.maybeShowDialog(context, completedRunnable)
+ clickButton(DialogInterface.BUTTON_POSITIVE)
+
+ assertThat(secureSettings.getBool(SETTING_SHOW)).isTrue()
+ assertThat(secureSettings.getBool(SETTING_ACTION)).isTrue()
+ }
+
+ private fun clickButton(which: Int) {
+ dialog?.clickButton(which)
+ }
+
+ private fun attachRepositoryToSettings() {
+ secureSettings.registerContentObserver(
+ SETTING_SHOW,
+ object : ContentObserver(null) {
+ override fun onChange(selfChange: Boolean) {
+ controlsSettingsRepository.setCanShowControlsInLockscreen(
+ secureSettings.getBool(SETTING_SHOW, false)
+ )
+ }
+ }
+ )
+
+ secureSettings.registerContentObserver(
+ SETTING_ACTION,
+ object : ContentObserver(null) {
+ override fun onChange(selfChange: Boolean) {
+ controlsSettingsRepository.setAllowActionOnTrivialControlsInLockscreen(
+ secureSettings.getBool(SETTING_ACTION, false)
+ )
+ }
+ }
+ )
+ }
+
+ private fun SharedPreferences.putAttempts(value: Int) {
+ edit().putInt(PREFS_SETTINGS_DIALOG_ATTEMPTS, value).commit()
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/ControlsSettingsRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/settings/ControlsSettingsRepositoryImplTest.kt
similarity index 98%
rename from packages/SystemUI/tests/src/com/android/systemui/controls/ControlsSettingsRepositoryImplTest.kt
rename to packages/SystemUI/tests/src/com/android/systemui/controls/settings/ControlsSettingsRepositoryImplTest.kt
index 4b88b44..b904ac1 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/controls/ControlsSettingsRepositoryImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/controls/settings/ControlsSettingsRepositoryImplTest.kt
@@ -15,7 +15,7 @@
*
*/
-package com.android.systemui.controls
+package com.android.systemui.controls.settings
import android.content.pm.UserInfo
import android.provider.Settings
diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/FakeControlsSettingsRepository.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/settings/FakeControlsSettingsRepository.kt
similarity index 96%
rename from packages/SystemUI/tests/src/com/android/systemui/controls/FakeControlsSettingsRepository.kt
rename to packages/SystemUI/tests/src/com/android/systemui/controls/settings/FakeControlsSettingsRepository.kt
index 8a1bed2..b6628db 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/controls/FakeControlsSettingsRepository.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/controls/settings/FakeControlsSettingsRepository.kt
@@ -15,7 +15,7 @@
*
*/
-package com.android.systemui.controls
+package com.android.systemui.controls.settings
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/ui/ControlsUiControllerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/ui/ControlsUiControllerImplTest.kt
index e679b13..779788a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/controls/ui/ControlsUiControllerImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/controls/ui/ControlsUiControllerImplTest.kt
@@ -16,18 +16,29 @@
package com.android.systemui.controls.ui
+import android.app.PendingIntent
import android.content.ComponentName
import android.content.Context
+import android.content.pm.ApplicationInfo
+import android.content.pm.ServiceInfo
+import android.os.UserHandle
+import android.service.controls.ControlsProviderService
import android.testing.AndroidTestingRunner
import android.testing.TestableLooper
+import android.util.AttributeSet
+import android.view.LayoutInflater
+import android.view.View
import android.widget.FrameLayout
import androidx.test.filters.SmallTest
+import com.android.systemui.R
import com.android.systemui.SysuiTestCase
import com.android.systemui.controls.ControlsMetricsLogger
+import com.android.systemui.controls.ControlsServiceInfo
import com.android.systemui.controls.CustomIconCache
import com.android.systemui.controls.controller.ControlsController
import com.android.systemui.controls.controller.StructureInfo
import com.android.systemui.controls.management.ControlsListingController
+import com.android.systemui.controls.settings.FakeControlsSettingsRepository
import com.android.systemui.dump.DumpManager
import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.settings.UserFileManager
@@ -38,19 +49,26 @@
import com.android.systemui.util.FakeSharedPreferences
import com.android.systemui.util.concurrency.FakeExecutor
import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.argumentCaptor
+import com.android.systemui.util.mockito.capture
+import com.android.systemui.util.mockito.eq
+import com.android.systemui.util.mockito.mock
import com.android.systemui.util.time.FakeSystemClock
+import com.android.wm.shell.TaskView
import com.android.wm.shell.TaskViewFactory
import com.google.common.truth.Truth.assertThat
import dagger.Lazy
import java.util.Optional
+import java.util.function.Consumer
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mock
import org.mockito.Mockito.anyInt
import org.mockito.Mockito.anyString
-import org.mockito.Mockito.mock
+import org.mockito.Mockito.clearInvocations
import org.mockito.Mockito.never
+import org.mockito.Mockito.spy
import org.mockito.Mockito.verify
import org.mockito.Mockito.`when`
import org.mockito.MockitoAnnotations
@@ -70,9 +88,9 @@
@Mock lateinit var userFileManager: UserFileManager
@Mock lateinit var userTracker: UserTracker
@Mock lateinit var taskViewFactory: TaskViewFactory
- @Mock lateinit var activityContext: Context
@Mock lateinit var dumpManager: DumpManager
val sharedPreferences = FakeSharedPreferences()
+ lateinit var controlsSettingsRepository: FakeControlsSettingsRepository
var uiExecutor = FakeExecutor(FakeSystemClock())
var bgExecutor = FakeExecutor(FakeSystemClock())
@@ -83,6 +101,17 @@
fun setup() {
MockitoAnnotations.initMocks(this)
+ controlsSettingsRepository = FakeControlsSettingsRepository()
+
+ // This way, it won't be cloned every time `LayoutInflater.fromContext` is called, but we
+ // need to clone it once so we don't modify the original one.
+ mContext.addMockSystemService(
+ Context.LAYOUT_INFLATER_SERVICE,
+ mContext.baseContext
+ .getSystemService(LayoutInflater::class.java)!!
+ .cloneInContext(mContext)
+ )
+
parent = FrameLayout(mContext)
underTest =
@@ -100,6 +129,7 @@
userFileManager,
userTracker,
Optional.of(taskViewFactory),
+ controlsSettingsRepository,
dumpManager
)
`when`(
@@ -113,11 +143,12 @@
`when`(userFileManager.getSharedPreferences(anyString(), anyInt(), anyInt()))
.thenReturn(sharedPreferences)
`when`(userTracker.userId).thenReturn(0)
+ `when`(userTracker.userHandle).thenReturn(UserHandle.of(0))
}
@Test
fun testGetPreferredStructure() {
- val structureInfo = mock(StructureInfo::class.java)
+ val structureInfo = mock<StructureInfo>()
underTest.getPreferredSelectedItem(listOf(structureInfo))
verify(userFileManager)
.getSharedPreferences(
@@ -189,14 +220,195 @@
@Test
fun testPanelDoesNotRefreshControls() {
val panel = SelectedItem.PanelItem("App name", ComponentName("pkg", "cls"))
+ setUpPanel(panel)
+
+ underTest.show(parent, {}, context)
+ verify(controlsController, never()).refreshStatus(any(), any())
+ }
+
+ @Test
+ fun testPanelCallsTaskViewFactoryCreate() {
+ mockLayoutInflater()
+ val panel = SelectedItem.PanelItem("App name", ComponentName("pkg", "cls"))
+ val serviceInfo = setUpPanel(panel)
+
+ underTest.show(parent, {}, context)
+
+ val captor = argumentCaptor<ControlsListingController.ControlsListingCallback>()
+
+ verify(controlsListingController).addCallback(capture(captor))
+
+ captor.value.onServicesUpdated(listOf(serviceInfo))
+ FakeExecutor.exhaustExecutors(uiExecutor, bgExecutor)
+
+ verify(taskViewFactory).create(eq(context), eq(uiExecutor), any())
+ }
+
+ @Test
+ fun testPanelControllerStartActivityWithCorrectArguments() {
+ mockLayoutInflater()
+ controlsSettingsRepository.setAllowActionOnTrivialControlsInLockscreen(true)
+
+ val panel = SelectedItem.PanelItem("App name", ComponentName("pkg", "cls"))
+ val serviceInfo = setUpPanel(panel)
+
+ underTest.show(parent, {}, context)
+
+ val captor = argumentCaptor<ControlsListingController.ControlsListingCallback>()
+
+ verify(controlsListingController).addCallback(capture(captor))
+
+ captor.value.onServicesUpdated(listOf(serviceInfo))
+ FakeExecutor.exhaustExecutors(uiExecutor, bgExecutor)
+
+ val pendingIntent = verifyPanelCreatedAndStartTaskView()
+
+ with(pendingIntent) {
+ assertThat(isActivity).isTrue()
+ assertThat(intent.component).isEqualTo(serviceInfo.panelActivity)
+ assertThat(
+ intent.getBooleanExtra(
+ ControlsProviderService.EXTRA_LOCKSCREEN_ALLOW_TRIVIAL_CONTROLS,
+ false
+ )
+ )
+ .isTrue()
+ }
+ }
+
+ @Test
+ fun testPendingIntentExtrasAreModified() {
+ mockLayoutInflater()
+ controlsSettingsRepository.setAllowActionOnTrivialControlsInLockscreen(true)
+
+ val panel = SelectedItem.PanelItem("App name", ComponentName("pkg", "cls"))
+ val serviceInfo = setUpPanel(panel)
+
+ underTest.show(parent, {}, context)
+
+ val captor = argumentCaptor<ControlsListingController.ControlsListingCallback>()
+
+ verify(controlsListingController).addCallback(capture(captor))
+
+ captor.value.onServicesUpdated(listOf(serviceInfo))
+ FakeExecutor.exhaustExecutors(uiExecutor, bgExecutor)
+
+ val pendingIntent = verifyPanelCreatedAndStartTaskView()
+ assertThat(
+ pendingIntent.intent.getBooleanExtra(
+ ControlsProviderService.EXTRA_LOCKSCREEN_ALLOW_TRIVIAL_CONTROLS,
+ false
+ )
+ )
+ .isTrue()
+
+ underTest.hide()
+
+ clearInvocations(controlsListingController, taskViewFactory)
+ controlsSettingsRepository.setAllowActionOnTrivialControlsInLockscreen(false)
+ underTest.show(parent, {}, context)
+
+ verify(controlsListingController).addCallback(capture(captor))
+ captor.value.onServicesUpdated(listOf(serviceInfo))
+ FakeExecutor.exhaustExecutors(uiExecutor, bgExecutor)
+
+ val newPendingIntent = verifyPanelCreatedAndStartTaskView()
+ assertThat(
+ newPendingIntent.intent.getBooleanExtra(
+ ControlsProviderService.EXTRA_LOCKSCREEN_ALLOW_TRIVIAL_CONTROLS,
+ false
+ )
+ )
+ .isFalse()
+ }
+
+ private fun setUpPanel(panel: SelectedItem.PanelItem): ControlsServiceInfo {
+ val activity = ComponentName("pkg", "activity")
sharedPreferences
.edit()
.putString("controls_component", panel.componentName.flattenToString())
.putString("controls_structure", panel.appName.toString())
.putBoolean("controls_is_panel", true)
.commit()
+ return ControlsServiceInfo(panel.componentName, panel.appName, activity)
+ }
- underTest.show(parent, {}, activityContext)
- verify(controlsController, never()).refreshStatus(any(), any())
+ private fun verifyPanelCreatedAndStartTaskView(): PendingIntent {
+ val taskViewConsumerCaptor = argumentCaptor<Consumer<TaskView>>()
+ verify(taskViewFactory).create(eq(context), eq(uiExecutor), capture(taskViewConsumerCaptor))
+
+ val taskView: TaskView = mock {
+ `when`(this.post(any())).thenAnswer {
+ uiExecutor.execute(it.arguments[0] as Runnable)
+ true
+ }
+ }
+ // calls PanelTaskViewController#launchTaskView
+ taskViewConsumerCaptor.value.accept(taskView)
+ val listenerCaptor = argumentCaptor<TaskView.Listener>()
+ verify(taskView).setListener(any(), capture(listenerCaptor))
+ listenerCaptor.value.onInitialized()
+ FakeExecutor.exhaustExecutors(uiExecutor, bgExecutor)
+
+ val pendingIntentCaptor = argumentCaptor<PendingIntent>()
+ verify(taskView).startActivity(capture(pendingIntentCaptor), any(), any(), any())
+ return pendingIntentCaptor.value
+ }
+
+ private fun ControlsServiceInfo(
+ componentName: ComponentName,
+ label: CharSequence,
+ panelComponentName: ComponentName? = null
+ ): ControlsServiceInfo {
+ val serviceInfo =
+ ServiceInfo().apply {
+ applicationInfo = ApplicationInfo()
+ packageName = componentName.packageName
+ name = componentName.className
+ }
+ return spy(ControlsServiceInfo(mContext, serviceInfo)).apply {
+ `when`(loadLabel()).thenReturn(label)
+ `when`(loadIcon()).thenReturn(mock())
+ `when`(panelActivity).thenReturn(panelComponentName)
+ }
+ }
+
+ private fun mockLayoutInflater() {
+ LayoutInflater.from(context)
+ .setPrivateFactory(
+ object : LayoutInflater.Factory2 {
+ override fun onCreateView(
+ view: View?,
+ name: String,
+ context: Context,
+ attrs: AttributeSet
+ ): View? {
+ return onCreateView(name, context, attrs)
+ }
+
+ override fun onCreateView(
+ name: String,
+ context: Context,
+ attrs: AttributeSet
+ ): View? {
+ if (FrameLayout::class.java.simpleName.equals(name)) {
+ val mock: FrameLayout = mock {
+ `when`(this.context).thenReturn(context)
+ `when`(this.id).thenReturn(R.id.controls_panel)
+ `when`(this.requireViewById<View>(any())).thenCallRealMethod()
+ `when`(this.findViewById<View>(R.id.controls_panel))
+ .thenReturn(this)
+ `when`(this.post(any())).thenAnswer {
+ uiExecutor.execute(it.arguments[0] as Runnable)
+ true
+ }
+ }
+ return mock
+ } else {
+ return null
+ }
+ }
+ }
+ )
}
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/flags/FeatureFlagsDebugRestarterTest.kt b/packages/SystemUI/tests/src/com/android/systemui/flags/FeatureFlagsDebugRestarterTest.kt
index 1e7b1f2..ed16721 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/flags/FeatureFlagsDebugRestarterTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/flags/FeatureFlagsDebugRestarterTest.kt
@@ -48,22 +48,22 @@
@Test
fun testRestart_ImmediateWhenAsleep() {
whenever(wakefulnessLifecycle.wakefulness).thenReturn(WAKEFULNESS_ASLEEP)
- restarter.restart()
- verify(systemExitRestarter).restart()
+ restarter.restartSystemUI()
+ verify(systemExitRestarter).restartSystemUI()
}
@Test
fun testRestart_WaitsForSceenOff() {
whenever(wakefulnessLifecycle.wakefulness).thenReturn(WAKEFULNESS_AWAKE)
- restarter.restart()
- verify(systemExitRestarter, never()).restart()
+ restarter.restartSystemUI()
+ verify(systemExitRestarter, never()).restartSystemUI()
val captor = ArgumentCaptor.forClass(WakefulnessLifecycle.Observer::class.java)
verify(wakefulnessLifecycle).addObserver(captor.capture())
captor.value.onFinishedGoingToSleep()
- verify(systemExitRestarter).restart()
+ verify(systemExitRestarter).restartSystemUI()
}
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/flags/FeatureFlagsReleaseRestarterTest.kt b/packages/SystemUI/tests/src/com/android/systemui/flags/FeatureFlagsReleaseRestarterTest.kt
index 68ca48d..7d807e2 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/flags/FeatureFlagsReleaseRestarterTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/flags/FeatureFlagsReleaseRestarterTest.kt
@@ -63,7 +63,7 @@
whenever(batteryController.isPluggedIn).thenReturn(true)
assertThat(executor.numPending()).isEqualTo(0)
- restarter.restart()
+ restarter.restartSystemUI()
assertThat(executor.numPending()).isEqualTo(1)
}
@@ -72,11 +72,11 @@
whenever(wakefulnessLifecycle.wakefulness).thenReturn(WAKEFULNESS_ASLEEP)
whenever(batteryController.isPluggedIn).thenReturn(true)
- restarter.restart()
- verify(systemExitRestarter, never()).restart()
+ restarter.restartSystemUI()
+ verify(systemExitRestarter, never()).restartSystemUI()
executor.advanceClockToLast()
executor.runAllReady()
- verify(systemExitRestarter).restart()
+ verify(systemExitRestarter).restartSystemUI()
}
@Test
@@ -85,7 +85,7 @@
whenever(batteryController.isPluggedIn).thenReturn(true)
assertThat(executor.numPending()).isEqualTo(0)
- restarter.restart()
+ restarter.restartSystemUI()
assertThat(executor.numPending()).isEqualTo(0)
}
@@ -95,7 +95,7 @@
whenever(batteryController.isPluggedIn).thenReturn(false)
assertThat(executor.numPending()).isEqualTo(0)
- restarter.restart()
+ restarter.restartSystemUI()
assertThat(executor.numPending()).isEqualTo(0)
}
@@ -105,8 +105,8 @@
whenever(batteryController.isPluggedIn).thenReturn(true)
assertThat(executor.numPending()).isEqualTo(0)
- restarter.restart()
- restarter.restart()
+ restarter.restartSystemUI()
+ restarter.restartSystemUI()
assertThat(executor.numPending()).isEqualTo(1)
}
@@ -115,7 +115,7 @@
whenever(wakefulnessLifecycle.wakefulness).thenReturn(WAKEFULNESS_AWAKE)
whenever(batteryController.isPluggedIn).thenReturn(true)
assertThat(executor.numPending()).isEqualTo(0)
- restarter.restart()
+ restarter.restartSystemUI()
val captor = ArgumentCaptor.forClass(WakefulnessLifecycle.Observer::class.java)
verify(wakefulnessLifecycle).addObserver(captor.capture())
@@ -131,7 +131,7 @@
whenever(wakefulnessLifecycle.wakefulness).thenReturn(WAKEFULNESS_ASLEEP)
whenever(batteryController.isPluggedIn).thenReturn(false)
assertThat(executor.numPending()).isEqualTo(0)
- restarter.restart()
+ restarter.restartSystemUI()
val captor =
ArgumentCaptor.forClass(BatteryController.BatteryStateChangeCallback::class.java)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/log/SessionTrackerTest.java b/packages/SystemUI/tests/src/com/android/systemui/log/SessionTrackerTest.java
index dc5522e..aa54a1c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/log/SessionTrackerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/log/SessionTrackerTest.java
@@ -23,8 +23,10 @@
import static junit.framework.Assert.assertNotNull;
import static junit.framework.Assert.assertNull;
+import static org.junit.Assert.assertNotEquals;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -171,6 +173,34 @@
}
@Test
+ public void testKeyguardSessionOnDeviceStartsSleepingTwiceInARow_startsNewKeyguardSession()
+ throws RemoteException {
+ // GIVEN session tracker started w/o any sessions
+ mSessionTracker.start();
+ captureKeyguardUpdateMonitorCallback();
+
+ // WHEN device starts going to sleep
+ mKeyguardUpdateMonitorCallback.onStartedGoingToSleep(0);
+
+ // THEN the keyguard session has a session id
+ final InstanceId firstSessionId = mSessionTracker.getSessionId(SESSION_KEYGUARD);
+ assertNotNull(firstSessionId);
+
+ // WHEN device starts going to sleep a second time
+ mKeyguardUpdateMonitorCallback.onStartedGoingToSleep(0);
+
+ // THEN there's a new keyguard session with a unique session id
+ final InstanceId secondSessionId = mSessionTracker.getSessionId(SESSION_KEYGUARD);
+ assertNotNull(secondSessionId);
+ assertNotEquals(firstSessionId, secondSessionId);
+
+ // THEN session start event gets sent to status bar service twice (once per going to
+ // sleep signal)
+ verify(mStatusBarService, times(2)).onSessionStarted(
+ eq(SESSION_KEYGUARD), any(InstanceId.class));
+ }
+
+ @Test
public void testKeyguardSessionOnKeyguardShowingChange() throws RemoteException {
// GIVEN session tracker started w/o any sessions
mSessionTracker.start();
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputAdapterTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputAdapterTest.java
index 5f64336..5c0f0fe 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputAdapterTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputAdapterTest.java
@@ -38,6 +38,8 @@
import com.android.systemui.R;
import com.android.systemui.SysuiTestCase;
+import com.google.common.collect.ImmutableList;
+
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -73,11 +75,6 @@
@Before
public void setUp() {
- mMediaOutputAdapter = new MediaOutputAdapter(mMediaOutputController);
- mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter
- .onCreateViewHolder(new LinearLayout(mContext), 0);
- mSpyMediaOutputSeekbar = spy(mViewHolder.mSeekBar);
-
when(mMediaOutputController.getMediaDevices()).thenReturn(mMediaDevices);
when(mMediaOutputController.hasAdjustVolumeUserRestriction()).thenReturn(false);
when(mMediaOutputController.isAnyDeviceTransferring()).thenReturn(false);
@@ -85,6 +82,7 @@
when(mMediaOutputController.getDeviceIconCompat(mMediaDevice2)).thenReturn(mIconCompat);
when(mMediaOutputController.getCurrentConnectedMediaDevice()).thenReturn(mMediaDevice1);
when(mMediaOutputController.isActiveRemoteDevice(mMediaDevice1)).thenReturn(true);
+ when(mMediaOutputController.isAdvancedLayoutSupported()).thenReturn(false);
when(mIconCompat.toIcon(mContext)).thenReturn(mIcon);
when(mMediaDevice1.getName()).thenReturn(TEST_DEVICE_NAME_1);
when(mMediaDevice1.getId()).thenReturn(TEST_DEVICE_ID_1);
@@ -96,6 +94,11 @@
LocalMediaManager.MediaDeviceState.STATE_DISCONNECTED);
mMediaDevices.add(mMediaDevice1);
mMediaDevices.add(mMediaDevice2);
+
+ mMediaOutputAdapter = new MediaOutputAdapter(mMediaOutputController);
+ mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter
+ .onCreateViewHolder(new LinearLayout(mContext), 0);
+ mSpyMediaOutputSeekbar = spy(mViewHolder.mSeekBar);
}
@Test
@@ -169,6 +172,63 @@
}
@Test
+ public void advanced_onBindViewHolder_bindNonRemoteConnectedDevice_verifyView() {
+ when(mMediaOutputController.isAdvancedLayoutSupported()).thenReturn(true);
+ when(mMediaOutputController.isActiveRemoteDevice(mMediaDevice1)).thenReturn(false);
+ mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter
+ .onCreateViewHolder(new LinearLayout(mContext), 0);
+ mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0);
+
+ assertThat(mViewHolder.mTitleText.getVisibility()).isEqualTo(View.VISIBLE);
+ assertThat(mViewHolder.mTitleText.getText().toString()).isEqualTo(TEST_DEVICE_NAME_1);
+ assertThat(mViewHolder.mSubTitleText.getVisibility()).isEqualTo(View.GONE);
+ assertThat(mViewHolder.mProgressBar.getVisibility()).isEqualTo(View.GONE);
+ assertThat(mViewHolder.mCheckBox.getVisibility()).isEqualTo(View.GONE);
+ assertThat(mViewHolder.mTwoLineLayout.getVisibility()).isEqualTo(View.GONE);
+ assertThat(mViewHolder.mSeekBar.getVisibility()).isEqualTo(View.VISIBLE);
+ }
+
+ @Test
+ public void advanced_onBindViewHolder_bindConnectedRemoteDevice_verifyView() {
+ when(mMediaOutputController.isAdvancedLayoutSupported()).thenReturn(true);
+ when(mMediaOutputController.getSelectableMediaDevice()).thenReturn(
+ ImmutableList.of(mMediaDevice2));
+ when(mMediaOutputController.isCurrentConnectedDeviceRemote()).thenReturn(true);
+ mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter
+ .onCreateViewHolder(new LinearLayout(mContext), 0);
+ mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0);
+
+ assertThat(mViewHolder.mTitleText.getVisibility()).isEqualTo(View.VISIBLE);
+ assertThat(mViewHolder.mTitleText.getText().toString()).isEqualTo(TEST_DEVICE_NAME_1);
+ assertThat(mViewHolder.mSubTitleText.getVisibility()).isEqualTo(View.GONE);
+ assertThat(mViewHolder.mProgressBar.getVisibility()).isEqualTo(View.GONE);
+ assertThat(mViewHolder.mCheckBox.getVisibility()).isEqualTo(View.VISIBLE);
+ assertThat(mViewHolder.mTwoLineLayout.getVisibility()).isEqualTo(View.GONE);
+ assertThat(mViewHolder.mSeekBar.getVisibility()).isEqualTo(View.VISIBLE);
+ assertThat(mViewHolder.mEndTouchArea.getVisibility()).isEqualTo(View.VISIBLE);
+ }
+
+ @Test
+ public void advanced_onBindViewHolder_bindSingleConnectedRemoteDevice_verifyView() {
+ when(mMediaOutputController.isAdvancedLayoutSupported()).thenReturn(true);
+ when(mMediaOutputController.getSelectableMediaDevice()).thenReturn(
+ ImmutableList.of());
+ when(mMediaOutputController.isCurrentConnectedDeviceRemote()).thenReturn(true);
+ mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter
+ .onCreateViewHolder(new LinearLayout(mContext), 0);
+ mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0);
+
+ assertThat(mViewHolder.mTitleText.getVisibility()).isEqualTo(View.VISIBLE);
+ assertThat(mViewHolder.mTitleText.getText().toString()).isEqualTo(TEST_DEVICE_NAME_1);
+ assertThat(mViewHolder.mSubTitleText.getVisibility()).isEqualTo(View.GONE);
+ assertThat(mViewHolder.mProgressBar.getVisibility()).isEqualTo(View.GONE);
+ assertThat(mViewHolder.mCheckBox.getVisibility()).isEqualTo(View.GONE);
+ assertThat(mViewHolder.mTwoLineLayout.getVisibility()).isEqualTo(View.GONE);
+ assertThat(mViewHolder.mSeekBar.getVisibility()).isEqualTo(View.VISIBLE);
+ assertThat(mViewHolder.mEndTouchArea.getVisibility()).isEqualTo(View.GONE);
+ }
+
+ @Test
public void onBindViewHolder_bindConnectedDeviceWithMutingExpectedDeviceExist_verifyView() {
when(mMediaOutputController.hasMutingExpectedDevice()).thenReturn(true);
when(mMediaOutputController.isCurrentConnectedDeviceRemote()).thenReturn(false);
@@ -352,6 +412,38 @@
}
@Test
+ public void advanced_onGroupActionTriggered_clicksEndAreaOfSelectableDevice_triggerGrouping() {
+ when(mMediaOutputController.isAdvancedLayoutSupported()).thenReturn(true);
+ List<MediaDevice> selectableDevices = new ArrayList<>();
+ selectableDevices.add(mMediaDevice2);
+ when(mMediaOutputController.getSelectableMediaDevice()).thenReturn(selectableDevices);
+ mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter
+ .onCreateViewHolder(new LinearLayout(mContext), 0);
+ mMediaOutputAdapter.onBindViewHolder(mViewHolder, 1);
+
+ mViewHolder.mEndTouchArea.performClick();
+
+ verify(mMediaOutputController).addDeviceToPlayMedia(mMediaDevice2);
+ }
+
+ @Test
+ public void advanced_onGroupActionTriggered_clickSelectedRemoteDevice_triggerUngrouping() {
+ when(mMediaOutputController.isAdvancedLayoutSupported()).thenReturn(true);
+ when(mMediaOutputController.getSelectableMediaDevice()).thenReturn(
+ ImmutableList.of(mMediaDevice2));
+ when(mMediaOutputController.getDeselectableMediaDevice()).thenReturn(
+ ImmutableList.of(mMediaDevice1));
+ when(mMediaOutputController.isCurrentConnectedDeviceRemote()).thenReturn(true);
+ mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter
+ .onCreateViewHolder(new LinearLayout(mContext), 0);
+ mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0);
+
+ mViewHolder.mEndTouchArea.performClick();
+
+ verify(mMediaOutputController).removeDeviceFromPlayMedia(mMediaDevice1);
+ }
+
+ @Test
public void onItemClick_onGroupActionTriggered_verifySeekbarDisabled() {
when(mMediaOutputController.getSelectedMediaDevice()).thenReturn(mMediaDevices);
List<MediaDevice> selectableDevices = new ArrayList<>();
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBaseDialogTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBaseDialogTest.java
index 9be201e..094d69a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBaseDialogTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBaseDialogTest.java
@@ -51,6 +51,7 @@
import com.android.systemui.SysuiTestCase;
import com.android.systemui.animation.DialogLaunchAnimator;
import com.android.systemui.broadcast.BroadcastSender;
+import com.android.systemui.flags.FeatureFlags;
import com.android.systemui.media.nearby.NearbyMediaDevicesManager;
import com.android.systemui.plugins.ActivityStarter;
import com.android.systemui.statusbar.notification.collection.notifcollection.CommonNotifCollection;
@@ -89,6 +90,7 @@
private final AudioManager mAudioManager = mock(AudioManager.class);
private PowerExemptionManager mPowerExemptionManager = mock(PowerExemptionManager.class);
private KeyguardManager mKeyguardManager = mock(KeyguardManager.class);
+ private FeatureFlags mFlags = mock(FeatureFlags.class);
private List<MediaController> mMediaControllers = new ArrayList<>();
private MediaOutputBaseDialogImpl mMediaOutputBaseDialogImpl;
@@ -121,7 +123,7 @@
mMediaSessionManager, mLocalBluetoothManager, mStarter,
mNotifCollection, mDialogLaunchAnimator,
Optional.of(mNearbyMediaDevicesManager), mAudioManager, mPowerExemptionManager,
- mKeyguardManager);
+ mKeyguardManager, mFlags);
mMediaOutputBaseDialogImpl = new MediaOutputBaseDialogImpl(mContext, mBroadcastSender,
mMediaOutputController);
mMediaOutputBaseDialogImpl.onCreate(new Bundle());
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputControllerTest.java
index cb31fde..c544c0e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputControllerTest.java
@@ -62,6 +62,7 @@
import com.android.systemui.SysuiTestCase;
import com.android.systemui.animation.ActivityLaunchAnimator;
import com.android.systemui.animation.DialogLaunchAnimator;
+import com.android.systemui.flags.FeatureFlags;
import com.android.systemui.media.nearby.NearbyMediaDevicesManager;
import com.android.systemui.plugins.ActivityStarter;
import com.android.systemui.statusbar.notification.collection.NotificationEntry;
@@ -110,6 +111,7 @@
private PowerExemptionManager mPowerExemptionManager = mock(PowerExemptionManager.class);
private CommonNotifCollection mNotifCollection = mock(CommonNotifCollection.class);
private final DialogLaunchAnimator mDialogLaunchAnimator = mock(DialogLaunchAnimator.class);
+ private FeatureFlags mFlags = mock(FeatureFlags.class);
private final ActivityLaunchAnimator.Controller mActivityLaunchAnimatorController = mock(
ActivityLaunchAnimator.Controller.class);
private final NearbyMediaDevicesManager mNearbyMediaDevicesManager = mock(
@@ -141,7 +143,7 @@
mMediaSessionManager, mLocalBluetoothManager, mStarter,
mNotifCollection, mDialogLaunchAnimator,
Optional.of(mNearbyMediaDevicesManager), mAudioManager, mPowerExemptionManager,
- mKeyguardManager);
+ mKeyguardManager, mFlags);
mLocalMediaManager = spy(mMediaOutputController.mLocalMediaManager);
mMediaOutputController.mLocalMediaManager = mLocalMediaManager;
MediaDescription.Builder builder = new MediaDescription.Builder();
@@ -194,7 +196,7 @@
mMediaSessionManager, mLocalBluetoothManager, mStarter,
mNotifCollection, mDialogLaunchAnimator,
Optional.of(mNearbyMediaDevicesManager), mAudioManager, mPowerExemptionManager,
- mKeyguardManager);
+ mKeyguardManager, mFlags);
mMediaOutputController.start(mCb);
@@ -224,7 +226,7 @@
mMediaSessionManager, mLocalBluetoothManager, mStarter,
mNotifCollection, mDialogLaunchAnimator,
Optional.of(mNearbyMediaDevicesManager), mAudioManager, mPowerExemptionManager,
- mKeyguardManager);
+ mKeyguardManager, mFlags);
mMediaOutputController.start(mCb);
@@ -318,7 +320,7 @@
mMediaSessionManager, mLocalBluetoothManager, mStarter,
mNotifCollection, mDialogLaunchAnimator,
Optional.of(mNearbyMediaDevicesManager), mAudioManager, mPowerExemptionManager,
- mKeyguardManager);
+ mKeyguardManager, mFlags);
testMediaOutputController.start(mCb);
reset(mCb);
@@ -341,7 +343,7 @@
mMediaSessionManager, mLocalBluetoothManager, mStarter,
mNotifCollection, mDialogLaunchAnimator,
Optional.of(mNearbyMediaDevicesManager), mAudioManager, mPowerExemptionManager,
- mKeyguardManager);
+ mKeyguardManager, mFlags);
testMediaOutputController.start(mCb);
reset(mCb);
@@ -377,7 +379,7 @@
mMediaSessionManager, mLocalBluetoothManager, mStarter,
mNotifCollection, mDialogLaunchAnimator,
Optional.of(mNearbyMediaDevicesManager), mAudioManager, mPowerExemptionManager,
- mKeyguardManager);
+ mKeyguardManager, mFlags);
LocalMediaManager testLocalMediaManager = spy(testMediaOutputController.mLocalMediaManager);
testMediaOutputController.mLocalMediaManager = testLocalMediaManager;
@@ -394,7 +396,7 @@
mMediaSessionManager, mLocalBluetoothManager, mStarter,
mNotifCollection, mDialogLaunchAnimator,
Optional.of(mNearbyMediaDevicesManager), mAudioManager, mPowerExemptionManager,
- mKeyguardManager);
+ mKeyguardManager, mFlags);
LocalMediaManager testLocalMediaManager = spy(testMediaOutputController.mLocalMediaManager);
testMediaOutputController.mLocalMediaManager = testLocalMediaManager;
@@ -671,7 +673,7 @@
mMediaSessionManager, mLocalBluetoothManager, mStarter,
mNotifCollection, mDialogLaunchAnimator,
Optional.of(mNearbyMediaDevicesManager), mAudioManager, mPowerExemptionManager,
- mKeyguardManager);
+ mKeyguardManager, mFlags);
assertThat(mMediaOutputController.getNotificationIcon()).isNull();
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputDialogTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputDialogTest.java
index bae3569..31866a8 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputDialogTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputDialogTest.java
@@ -50,6 +50,7 @@
import com.android.systemui.SysuiTestCase;
import com.android.systemui.animation.DialogLaunchAnimator;
import com.android.systemui.broadcast.BroadcastSender;
+import com.android.systemui.flags.FeatureFlags;
import com.android.systemui.media.nearby.NearbyMediaDevicesManager;
import com.android.systemui.plugins.ActivityStarter;
import com.android.systemui.statusbar.notification.collection.notifcollection.CommonNotifCollection;
@@ -93,6 +94,7 @@
private final AudioManager mAudioManager = mock(AudioManager.class);
private PowerExemptionManager mPowerExemptionManager = mock(PowerExemptionManager.class);
private KeyguardManager mKeyguardManager = mock(KeyguardManager.class);
+ private FeatureFlags mFlags = mock(FeatureFlags.class);
private List<MediaController> mMediaControllers = new ArrayList<>();
private MediaOutputDialog mMediaOutputDialog;
@@ -115,7 +117,7 @@
mMediaSessionManager, mLocalBluetoothManager, mStarter,
mNotifCollection, mDialogLaunchAnimator,
Optional.of(mNearbyMediaDevicesManager), mAudioManager, mPowerExemptionManager,
- mKeyguardManager);
+ mKeyguardManager, mFlags);
mMediaOutputController.mLocalMediaManager = mLocalMediaManager;
mMediaOutputDialog = new MediaOutputDialog(mContext, false, mBroadcastSender,
mMediaOutputController, mUiEventLogger);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/common/MediaTttUtilsTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/common/MediaTttUtilsTest.kt
index 6a4c0f6..cce3e36 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/common/MediaTttUtilsTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/common/MediaTttUtilsTest.kt
@@ -65,41 +65,14 @@
}
@Test
- fun getIconFromPackageName_nullPackageName_returnsDefault() {
- val icon = MediaTttUtils.getIconFromPackageName(context, appPackageName = null, logger)
-
- val expectedDesc =
- ContentDescription.Resource(R.string.media_output_dialog_unknown_launch_app_name)
- .loadContentDescription(context)
- assertThat(icon.contentDescription.loadContentDescription(context)).isEqualTo(expectedDesc)
- }
-
- @Test
- fun getIconFromPackageName_invalidPackageName_returnsDefault() {
- val icon = MediaTttUtils.getIconFromPackageName(context, "fakePackageName", logger)
-
- val expectedDesc =
- ContentDescription.Resource(R.string.media_output_dialog_unknown_launch_app_name)
- .loadContentDescription(context)
- assertThat(icon.contentDescription.loadContentDescription(context)).isEqualTo(expectedDesc)
- }
-
- @Test
- fun getIconFromPackageName_validPackageName_returnsAppInfo() {
- val icon = MediaTttUtils.getIconFromPackageName(context, PACKAGE_NAME, logger)
-
- assertThat(icon)
- .isEqualTo(Icon.Loaded(appIconFromPackageName, ContentDescription.Loaded(APP_NAME)))
- }
-
- @Test
fun getIconInfoFromPackageName_nullPackageName_returnsDefault() {
val iconInfo =
MediaTttUtils.getIconInfoFromPackageName(context, appPackageName = null, logger)
assertThat(iconInfo.isAppIcon).isFalse()
- assertThat(iconInfo.contentDescription)
+ assertThat(iconInfo.contentDescription.loadContentDescription(context))
.isEqualTo(context.getString(R.string.media_output_dialog_unknown_launch_app_name))
+ assertThat(iconInfo.icon).isEqualTo(MediaTttIcon.Resource(R.drawable.ic_cast))
}
@Test
@@ -107,8 +80,9 @@
val iconInfo = MediaTttUtils.getIconInfoFromPackageName(context, "fakePackageName", logger)
assertThat(iconInfo.isAppIcon).isFalse()
- assertThat(iconInfo.contentDescription)
+ assertThat(iconInfo.contentDescription.loadContentDescription(context))
.isEqualTo(context.getString(R.string.media_output_dialog_unknown_launch_app_name))
+ assertThat(iconInfo.icon).isEqualTo(MediaTttIcon.Resource(R.drawable.ic_cast))
}
@Test
@@ -116,8 +90,48 @@
val iconInfo = MediaTttUtils.getIconInfoFromPackageName(context, PACKAGE_NAME, logger)
assertThat(iconInfo.isAppIcon).isTrue()
- assertThat(iconInfo.drawable).isEqualTo(appIconFromPackageName)
- assertThat(iconInfo.contentDescription).isEqualTo(APP_NAME)
+ assertThat(iconInfo.icon).isEqualTo(MediaTttIcon.Loaded(appIconFromPackageName))
+ assertThat(iconInfo.contentDescription.loadContentDescription(context)).isEqualTo(APP_NAME)
+ }
+
+ @Test
+ fun iconInfo_toTintedIcon_loaded() {
+ val contentDescription = ContentDescription.Loaded("test")
+ val drawable = context.getDrawable(R.drawable.ic_cake)!!
+ val tintAttr = android.R.attr.textColorTertiary
+
+ val iconInfo =
+ IconInfo(
+ contentDescription,
+ MediaTttIcon.Loaded(drawable),
+ tintAttr,
+ isAppIcon = false,
+ )
+
+ val tinted = iconInfo.toTintedIcon()
+
+ assertThat(tinted.icon).isEqualTo(Icon.Loaded(drawable, contentDescription))
+ assertThat(tinted.tintAttr).isEqualTo(tintAttr)
+ }
+
+ @Test
+ fun iconInfo_toTintedIcon_resource() {
+ val contentDescription = ContentDescription.Loaded("test")
+ val drawableRes = R.drawable.ic_cake
+ val tintAttr = android.R.attr.textColorTertiary
+
+ val iconInfo =
+ IconInfo(
+ contentDescription,
+ MediaTttIcon.Resource(drawableRes),
+ tintAttr,
+ isAppIcon = false
+ )
+
+ val tinted = iconInfo.toTintedIcon()
+
+ assertThat(tinted.icon).isEqualTo(Icon.Resource(drawableRes, contentDescription))
+ assertThat(tinted.tintAttr).isEqualTo(tintAttr)
}
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/FooterActionsControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/FooterActionsControllerTest.kt
deleted file mode 100644
index 2ba8782..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/FooterActionsControllerTest.kt
+++ /dev/null
@@ -1,440 +0,0 @@
-package com.android.systemui.qs
-
-import android.content.Intent
-import android.os.Handler
-import android.os.UserManager
-import android.provider.Settings
-import android.testing.AndroidTestingRunner
-import android.testing.TestableLooper
-import android.testing.ViewUtils
-import android.view.LayoutInflater
-import android.view.View
-import android.view.ViewGroup
-import androidx.test.filters.SmallTest
-import com.android.internal.logging.MetricsLogger
-import com.android.internal.logging.UiEventLogger
-import com.android.internal.logging.testing.FakeMetricsLogger
-import com.android.systemui.R
-import com.android.systemui.animation.ActivityLaunchAnimator
-import com.android.systemui.classifier.FalsingManagerFake
-import com.android.systemui.globalactions.GlobalActionsDialogLite
-import com.android.systemui.plugins.ActivityStarter
-import com.android.systemui.settings.UserTracker
-import com.android.systemui.statusbar.phone.MultiUserSwitchController
-import com.android.systemui.statusbar.policy.DeviceProvisionedController
-import com.android.systemui.statusbar.policy.FakeConfigurationController
-import com.android.systemui.statusbar.policy.UserInfoController
-import com.android.systemui.util.mockito.capture
-import com.android.systemui.util.settings.FakeSettings
-import com.android.systemui.utils.leaks.LeakCheckedTest
-import com.google.common.truth.Expect
-import com.google.common.truth.Truth.assertThat
-import javax.inject.Provider
-import org.junit.After
-import org.junit.Before
-import org.junit.Rule
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.mockito.ArgumentCaptor
-import org.mockito.ArgumentMatchers.any
-import org.mockito.ArgumentMatchers.anyBoolean
-import org.mockito.Captor
-import org.mockito.Mock
-import org.mockito.Mockito
-import org.mockito.Mockito.atLeastOnce
-import org.mockito.Mockito.clearInvocations
-import org.mockito.Mockito.never
-import org.mockito.Mockito.reset
-import org.mockito.Mockito.verify
-import org.mockito.Mockito.`when` as whenever
-import org.mockito.MockitoAnnotations
-
-@SmallTest
-@TestableLooper.RunWithLooper(setAsMainLooper = true)
-@RunWith(AndroidTestingRunner::class)
-class FooterActionsControllerTest : LeakCheckedTest() {
-
- @get:Rule var expect: Expect = Expect.create()
-
- @Mock private lateinit var userManager: UserManager
- @Mock private lateinit var userTracker: UserTracker
- @Mock private lateinit var activityStarter: ActivityStarter
- @Mock private lateinit var deviceProvisionedController: DeviceProvisionedController
- @Mock private lateinit var userInfoController: UserInfoController
- @Mock private lateinit var multiUserSwitchControllerFactory: MultiUserSwitchController.Factory
- @Mock private lateinit var multiUserSwitchController: MultiUserSwitchController
- @Mock private lateinit var globalActionsDialogProvider: Provider<GlobalActionsDialogLite>
- @Mock private lateinit var globalActionsDialog: GlobalActionsDialogLite
- @Mock private lateinit var uiEventLogger: UiEventLogger
- @Mock private lateinit var securityFooterController: QSSecurityFooter
- @Mock private lateinit var fgsManagerController: QSFgsManagerFooter
- @Captor
- private lateinit var visibilityChangedCaptor:
- ArgumentCaptor<VisibilityChangedDispatcher.OnVisibilityChangedListener>
-
- private lateinit var controller: FooterActionsController
-
- private val configurationController = FakeConfigurationController()
- private val metricsLogger: MetricsLogger = FakeMetricsLogger()
- private val falsingManager: FalsingManagerFake = FalsingManagerFake()
- private lateinit var view: FooterActionsView
- private lateinit var testableLooper: TestableLooper
- private lateinit var fakeSettings: FakeSettings
- private lateinit var securityFooter: View
- private lateinit var fgsFooter: View
-
- @Before
- fun setUp() {
- // We want to make sure testable resources are always used
- context.ensureTestableResources()
-
- MockitoAnnotations.initMocks(this)
- testableLooper = TestableLooper.get(this)
- fakeSettings = FakeSettings()
-
- whenever(multiUserSwitchControllerFactory.create(any()))
- .thenReturn(multiUserSwitchController)
- whenever(globalActionsDialogProvider.get()).thenReturn(globalActionsDialog)
-
- securityFooter = View(mContext)
- fgsFooter = View(mContext)
-
- whenever(securityFooterController.view).thenReturn(securityFooter)
- whenever(fgsManagerController.view).thenReturn(fgsFooter)
-
- view = inflateView()
-
- controller = constructFooterActionsController(view)
- controller.init()
- ViewUtils.attachView(view)
- // View looper is the testable looper associated with the test
- testableLooper.processAllMessages()
- }
-
- @After
- fun tearDown() {
- if (view.isAttachedToWindow) {
- ViewUtils.detachView(view)
- }
- }
-
- @Test
- fun testInitializesControllers() {
- verify(multiUserSwitchController).init()
- verify(fgsManagerController).init()
- verify(securityFooterController).init()
- }
-
- @Test
- fun testLogPowerMenuClick() {
- controller.visible = true
- falsingManager.setFalseTap(false)
-
- view.findViewById<View>(R.id.pm_lite).performClick()
- // Verify clicks are logged
- verify(uiEventLogger, Mockito.times(1))
- .log(GlobalActionsDialogLite.GlobalActionsEvent.GA_OPEN_QS)
- }
-
- @Test
- fun testSettings() {
- val captor = ArgumentCaptor.forClass(Intent::class.java)
- whenever(deviceProvisionedController.isCurrentUserSetup).thenReturn(true)
- view.findViewById<View>(R.id.settings_button_container).performClick()
-
- verify(activityStarter)
- .startActivity(capture(captor), anyBoolean(), any<ActivityLaunchAnimator.Controller>())
-
- assertThat(captor.value.action).isEqualTo(Settings.ACTION_SETTINGS)
- }
-
- @Test
- fun testSettings_UserNotSetup() {
- whenever(deviceProvisionedController.isCurrentUserSetup).thenReturn(false)
- view.findViewById<View>(R.id.settings_button_container).performClick()
- // Verify Settings wasn't launched.
- verify(activityStarter, never())
- .startActivity(any(), anyBoolean(), any<ActivityLaunchAnimator.Controller>())
- }
-
- @Test
- fun testMultiUserSwitchUpdatedWhenExpansionStarts() {
- // When expansion starts, listening is set to true
- val multiUserSwitch = view.requireViewById<View>(R.id.multi_user_switch)
-
- assertThat(multiUserSwitch.visibility).isNotEqualTo(View.VISIBLE)
-
- whenever(multiUserSwitchController.isMultiUserEnabled).thenReturn(true)
-
- controller.setListening(true)
- testableLooper.processAllMessages()
-
- assertThat(multiUserSwitch.visibility).isEqualTo(View.VISIBLE)
- }
-
- @Test
- fun testMultiUserSwitchUpdatedWhenSettingChanged() {
- // Always listening to setting while View is attached
- testableLooper.processAllMessages()
-
- val multiUserSwitch = view.requireViewById<View>(R.id.multi_user_switch)
- assertThat(multiUserSwitch.visibility).isNotEqualTo(View.VISIBLE)
-
- // The setting is only used as an indicator for whether the view should refresh. The actual
- // value of the setting is ignored; isMultiUserEnabled is the source of truth
- whenever(multiUserSwitchController.isMultiUserEnabled).thenReturn(true)
-
- // Changing the value of USER_SWITCHER_ENABLED should cause the view to update
- fakeSettings.putIntForUser(Settings.Global.USER_SWITCHER_ENABLED, 1, userTracker.userId)
- testableLooper.processAllMessages()
-
- assertThat(multiUserSwitch.visibility).isEqualTo(View.VISIBLE)
- }
-
- @Test
- fun testMultiUserSettingNotListenedAfterDetach() {
- testableLooper.processAllMessages()
-
- val multiUserSwitch = view.requireViewById<View>(R.id.multi_user_switch)
- assertThat(multiUserSwitch.visibility).isNotEqualTo(View.VISIBLE)
-
- ViewUtils.detachView(view)
-
- // The setting is only used as an indicator for whether the view should refresh. The actual
- // value of the setting is ignored; isMultiUserEnabled is the source of truth
- whenever(multiUserSwitchController.isMultiUserEnabled).thenReturn(true)
-
- // Changing the value of USER_SWITCHER_ENABLED should cause the view to update
- fakeSettings.putIntForUser(Settings.Global.USER_SWITCHER_ENABLED, 1, userTracker.userId)
- testableLooper.processAllMessages()
-
- assertThat(multiUserSwitch.visibility).isNotEqualTo(View.VISIBLE)
- }
-
- @Test
- fun testCleanUpGAD() {
- reset(globalActionsDialogProvider)
- // We are creating a new controller, so detach the views from it
- (securityFooter.parent as ViewGroup).removeView(securityFooter)
- (fgsFooter.parent as ViewGroup).removeView(fgsFooter)
-
- whenever(globalActionsDialogProvider.get()).thenReturn(globalActionsDialog)
- val view = inflateView()
- controller = constructFooterActionsController(view)
- controller.init()
- verify(globalActionsDialogProvider, never()).get()
-
- // GAD is constructed during attachment
- ViewUtils.attachView(view)
- testableLooper.processAllMessages()
- verify(globalActionsDialogProvider).get()
-
- ViewUtils.detachView(view)
- testableLooper.processAllMessages()
- verify(globalActionsDialog).destroy()
- }
-
- @Test
- fun testSeparatorVisibility_noneVisible_gone() {
- verify(securityFooterController)
- .setOnVisibilityChangedListener(capture(visibilityChangedCaptor))
- val listener = visibilityChangedCaptor.value
- val separator = controller.securityFootersSeparator
-
- setVisibilities(securityFooterVisible = false, fgsFooterVisible = false, listener)
- assertThat(separator.visibility).isEqualTo(View.GONE)
- }
-
- @Test
- fun testSeparatorVisibility_onlySecurityFooterVisible_gone() {
- verify(securityFooterController)
- .setOnVisibilityChangedListener(capture(visibilityChangedCaptor))
- val listener = visibilityChangedCaptor.value
- val separator = controller.securityFootersSeparator
-
- setVisibilities(securityFooterVisible = true, fgsFooterVisible = false, listener)
- assertThat(separator.visibility).isEqualTo(View.GONE)
- }
-
- @Test
- fun testSeparatorVisibility_onlyFgsFooterVisible_gone() {
- verify(securityFooterController)
- .setOnVisibilityChangedListener(capture(visibilityChangedCaptor))
- val listener = visibilityChangedCaptor.value
- val separator = controller.securityFootersSeparator
-
- setVisibilities(securityFooterVisible = false, fgsFooterVisible = true, listener)
- assertThat(separator.visibility).isEqualTo(View.GONE)
- }
-
- @Test
- fun testSeparatorVisibility_bothVisible_visible() {
- verify(securityFooterController)
- .setOnVisibilityChangedListener(capture(visibilityChangedCaptor))
- val listener = visibilityChangedCaptor.value
- val separator = controller.securityFootersSeparator
-
- setVisibilities(securityFooterVisible = true, fgsFooterVisible = true, listener)
- assertThat(separator.visibility).isEqualTo(View.VISIBLE)
- }
-
- @Test
- fun testFgsFooterCollapsed() {
- verify(securityFooterController)
- .setOnVisibilityChangedListener(capture(visibilityChangedCaptor))
- val listener = visibilityChangedCaptor.value
-
- val booleanCaptor = ArgumentCaptor.forClass(Boolean::class.java)
-
- clearInvocations(fgsManagerController)
- setVisibilities(securityFooterVisible = false, fgsFooterVisible = true, listener)
- verify(fgsManagerController, atLeastOnce()).setCollapsed(capture(booleanCaptor))
- assertThat(booleanCaptor.allValues.last()).isFalse()
-
- clearInvocations(fgsManagerController)
- setVisibilities(securityFooterVisible = true, fgsFooterVisible = true, listener)
- verify(fgsManagerController, atLeastOnce()).setCollapsed(capture(booleanCaptor))
- assertThat(booleanCaptor.allValues.last()).isTrue()
- }
-
- @Test
- fun setExpansion_inSplitShade_alphaFollowsExpansion() {
- enableSplitShade()
-
- controller.setExpansion(0f)
- expect.that(view.alpha).isEqualTo(0f)
-
- controller.setExpansion(0.25f)
- expect.that(view.alpha).isEqualTo(0.25f)
-
- controller.setExpansion(0.5f)
- expect.that(view.alpha).isEqualTo(0.5f)
-
- controller.setExpansion(0.75f)
- expect.that(view.alpha).isEqualTo(0.75f)
-
- controller.setExpansion(1f)
- expect.that(view.alpha).isEqualTo(1f)
- }
-
- @Test
- fun setExpansion_inSplitShade_backgroundAlphaFollowsExpansion_with_0_9_delay() {
- enableSplitShade()
-
- controller.setExpansion(0f)
- expect.that(view.backgroundAlphaFraction).isEqualTo(0f)
-
- controller.setExpansion(0.5f)
- expect.that(view.backgroundAlphaFraction).isEqualTo(0f)
-
- controller.setExpansion(0.9f)
- expect.that(view.backgroundAlphaFraction).isEqualTo(0f)
-
- controller.setExpansion(0.91f)
- expect.that(view.backgroundAlphaFraction).isWithin(FLOAT_TOLERANCE).of(0.1f)
-
- controller.setExpansion(0.95f)
- expect.that(view.backgroundAlphaFraction).isWithin(FLOAT_TOLERANCE).of(0.5f)
-
- controller.setExpansion(1f)
- expect.that(view.backgroundAlphaFraction).isEqualTo(1f)
- }
-
- @Test
- fun setExpansion_inSingleShade_alphaFollowsExpansion_with_0_9_delay() {
- disableSplitShade()
-
- controller.setExpansion(0f)
- expect.that(view.alpha).isEqualTo(0f)
-
- controller.setExpansion(0.5f)
- expect.that(view.alpha).isEqualTo(0f)
-
- controller.setExpansion(0.9f)
- expect.that(view.alpha).isEqualTo(0f)
-
- controller.setExpansion(0.91f)
- expect.that(view.alpha).isWithin(FLOAT_TOLERANCE).of(0.1f)
-
- controller.setExpansion(0.95f)
- expect.that(view.alpha).isWithin(FLOAT_TOLERANCE).of(0.5f)
-
- controller.setExpansion(1f)
- expect.that(view.alpha).isEqualTo(1f)
- }
-
- @Test
- fun setExpansion_inSingleShade_backgroundAlphaAlways1() {
- disableSplitShade()
-
- controller.setExpansion(0f)
- expect.that(view.backgroundAlphaFraction).isEqualTo(1f)
-
- controller.setExpansion(0.5f)
- expect.that(view.backgroundAlphaFraction).isEqualTo(1f)
-
- controller.setExpansion(1f)
- expect.that(view.backgroundAlphaFraction).isEqualTo(1f)
- }
-
- private fun setVisibilities(
- securityFooterVisible: Boolean,
- fgsFooterVisible: Boolean,
- listener: VisibilityChangedDispatcher.OnVisibilityChangedListener
- ) {
- securityFooter.visibility = if (securityFooterVisible) View.VISIBLE else View.GONE
- listener.onVisibilityChanged(securityFooter.visibility)
- fgsFooter.visibility = if (fgsFooterVisible) View.VISIBLE else View.GONE
- listener.onVisibilityChanged(fgsFooter.visibility)
- }
-
- private fun inflateView(): FooterActionsView {
- return LayoutInflater.from(context).inflate(R.layout.footer_actions, null)
- as FooterActionsView
- }
-
- private fun constructFooterActionsController(view: FooterActionsView): FooterActionsController {
- return FooterActionsController(
- view,
- multiUserSwitchControllerFactory,
- activityStarter,
- userManager,
- userTracker,
- userInfoController,
- deviceProvisionedController,
- securityFooterController,
- fgsManagerController,
- falsingManager,
- metricsLogger,
- globalActionsDialogProvider,
- uiEventLogger,
- showPMLiteButton = true,
- fakeSettings,
- Handler(testableLooper.looper),
- configurationController)
- }
-
- private fun enableSplitShade() {
- setSplitShadeEnabled(true)
- }
-
- private fun disableSplitShade() {
- setSplitShadeEnabled(false)
- }
-
- private fun setSplitShadeEnabled(enabled: Boolean) {
- overrideResource(R.bool.config_use_split_notification_shade, enabled)
- configurationController.notifyConfigurationChanged()
- }
-}
-
-private const val FLOAT_TOLERANCE = 0.01f
-
-private val View.backgroundAlphaFraction: Float?
- get() {
- return if (background != null) {
- background.alpha / 255f
- } else {
- null
- }
- }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QSFragmentTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/QSFragmentTest.java
index aedb935..ffe918d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/QSFragmentTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QSFragmentTest.java
@@ -25,6 +25,7 @@
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.nullable;
import static org.mockito.Mockito.clearInvocations;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
@@ -32,6 +33,7 @@
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
+import android.annotation.Nullable;
import android.app.Fragment;
import android.content.Context;
import android.graphics.Rect;
@@ -42,6 +44,7 @@
import android.view.View;
import android.view.ViewGroup;
+import androidx.lifecycle.Lifecycle;
import androidx.test.filters.SmallTest;
import com.android.keyguard.BouncerPanelExpansionCalculator;
@@ -50,12 +53,12 @@
import com.android.systemui.animation.ShadeInterpolation;
import com.android.systemui.dump.DumpManager;
import com.android.systemui.flags.FakeFeatureFlags;
-import com.android.systemui.flags.Flags;
import com.android.systemui.media.controls.ui.MediaHost;
import com.android.systemui.plugins.FalsingManager;
import com.android.systemui.qs.customize.QSCustomizerController;
import com.android.systemui.qs.dagger.QSFragmentComponent;
import com.android.systemui.qs.external.TileServiceRequestController;
+import com.android.systemui.qs.footer.ui.binder.FooterActionsViewBinder;
import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel;
import com.android.systemui.statusbar.CommandQueue;
import com.android.systemui.statusbar.StatusBarState;
@@ -99,6 +102,8 @@
@Mock private QSAnimator mQSAnimator;
@Mock private SysuiStatusBarStateController mStatusBarStateController;
@Mock private QSSquishinessController mSquishinessController;
+ @Mock private FooterActionsViewModel mFooterActionsViewModel;
+ @Mock private FooterActionsViewModel.Factory mFooterActionsViewModelFactory;
private View mQsFragmentView;
public QSFragmentTest() {
@@ -245,7 +250,8 @@
fragment.setQsExpansion(expansion, panelExpansionFraction, proposedTranslation,
squishinessFraction);
- verify(mQSFooterActionController).setExpansion(panelExpansionFraction);
+ verify(mFooterActionsViewModel).onQuickSettingsExpansionChanged(
+ panelExpansionFraction, /* isInSplitShade= */ true);
}
@Test
@@ -262,7 +268,8 @@
fragment.setQsExpansion(expansion, panelExpansionFraction, proposedTranslation,
squishinessFraction);
- verify(mQSFooterActionController).setExpansion(expansion);
+ verify(mFooterActionsViewModel).onQuickSettingsExpansionChanged(
+ expansion, /* isInSplitShade= */ false);
}
@Test
@@ -379,6 +386,13 @@
assertThat(mQsFragmentView.getTranslationY()).isEqualTo(0);
}
+ private Lifecycle.State getListeningAndVisibilityLifecycleState() {
+ return getFragment()
+ .getListeningAndVisibilityLifecycleOwner()
+ .getLifecycle()
+ .getCurrentState();
+ }
+
@Test
public void setListeningFalse_notVisible() {
QSFragment fragment = resumeAndGetFragment();
@@ -387,7 +401,7 @@
fragment.setListening(false);
verify(mQSContainerImplController).setListening(false);
- verify(mQSFooterActionController).setListening(false);
+ assertThat(getListeningAndVisibilityLifecycleState()).isEqualTo(Lifecycle.State.CREATED);
verify(mQSPanelController).setListening(eq(false), anyBoolean());
}
@@ -399,7 +413,7 @@
fragment.setListening(true);
verify(mQSContainerImplController).setListening(false);
- verify(mQSFooterActionController).setListening(false);
+ assertThat(getListeningAndVisibilityLifecycleState()).isEqualTo(Lifecycle.State.STARTED);
verify(mQSPanelController).setListening(eq(false), anyBoolean());
}
@@ -411,7 +425,7 @@
fragment.setListening(false);
verify(mQSContainerImplController).setListening(false);
- verify(mQSFooterActionController).setListening(false);
+ assertThat(getListeningAndVisibilityLifecycleState()).isEqualTo(Lifecycle.State.CREATED);
verify(mQSPanelController).setListening(eq(false), anyBoolean());
}
@@ -423,7 +437,7 @@
fragment.setListening(true);
verify(mQSContainerImplController).setListening(true);
- verify(mQSFooterActionController).setListening(true);
+ assertThat(getListeningAndVisibilityLifecycleState()).isEqualTo(Lifecycle.State.RESUMED);
verify(mQSPanelController).setListening(eq(true), anyBoolean());
}
@@ -480,7 +494,6 @@
setUpOther();
FakeFeatureFlags featureFlags = new FakeFeatureFlags();
- featureFlags.set(Flags.NEW_FOOTER_ACTIONS, false);
return new QSFragment(
new RemoteInputQuickSettingsDisabler(
context, commandQueue, mock(ConfigurationController.class)),
@@ -495,8 +508,8 @@
mFalsingManager,
mock(DumpManager.class),
featureFlags,
- mock(NewFooterActionsController.class),
- mock(FooterActionsViewModel.Factory.class));
+ mock(FooterActionsController.class),
+ mFooterActionsViewModelFactory);
}
private void setUpOther() {
@@ -505,6 +518,7 @@
when(mQSContainerImplController.getView()).thenReturn(mContainer);
when(mQSPanelController.getTileLayout()).thenReturn(mQQsTileLayout);
when(mQuickQSPanelController.getTileLayout()).thenReturn(mQsTileLayout);
+ when(mFooterActionsViewModelFactory.create(any())).thenReturn(mFooterActionsViewModel);
}
private void setUpMedia() {
@@ -519,15 +533,40 @@
.thenReturn(mQSPanelScrollView);
when(mQsFragmentView.findViewById(R.id.header)).thenReturn(mHeader);
when(mQsFragmentView.findViewById(android.R.id.edit)).thenReturn(new View(mContext));
+ when(mQsFragmentView.findViewById(R.id.qs_footer_actions)).thenAnswer(
+ invocation -> FooterActionsViewBinder.create(mContext));
}
private void setUpInflater() {
+ LayoutInflater realInflater = LayoutInflater.from(mContext);
+
when(mLayoutInflater.cloneInContext(any(Context.class))).thenReturn(mLayoutInflater);
- when(mLayoutInflater.inflate(anyInt(), any(ViewGroup.class), anyBoolean()))
- .thenReturn(mQsFragmentView);
+ when(mLayoutInflater.inflate(anyInt(), nullable(ViewGroup.class), anyBoolean()))
+ .thenAnswer((invocation) -> inflate(realInflater, (int) invocation.getArgument(0),
+ (ViewGroup) invocation.getArgument(1),
+ (boolean) invocation.getArgument(2)));
+ when(mLayoutInflater.inflate(anyInt(), nullable(ViewGroup.class)))
+ .thenAnswer((invocation) -> inflate(realInflater, (int) invocation.getArgument(0),
+ (ViewGroup) invocation.getArgument(1)));
mContext.addMockSystemService(Context.LAYOUT_INFLATER_SERVICE, mLayoutInflater);
}
+ private View inflate(LayoutInflater realInflater, int layoutRes, @Nullable ViewGroup root) {
+ return inflate(realInflater, layoutRes, root, root != null);
+ }
+
+ private View inflate(LayoutInflater realInflater, int layoutRes, @Nullable ViewGroup root,
+ boolean attachToRoot) {
+ if (layoutRes == R.layout.footer_actions
+ || layoutRes == R.layout.footer_actions_text_button
+ || layoutRes == R.layout.footer_actions_number_button
+ || layoutRes == R.layout.footer_actions_icon_button) {
+ return realInflater.inflate(layoutRes, root, attachToRoot);
+ }
+
+ return mQsFragmentView;
+ }
+
private void setupQsComponent() {
when(mQsComponentFactory.create(any(QSFragment.class))).thenReturn(mQsFragmentComponent);
when(mQsFragmentComponent.getQSPanelController()).thenReturn(mQSPanelController);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QSSecurityFooterTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/QSSecurityFooterTest.java
index 906c20b..c656d6d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/QSSecurityFooterTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QSSecurityFooterTest.java
@@ -17,23 +17,24 @@
import static android.app.admin.DevicePolicyManager.DEVICE_OWNER_TYPE_DEFAULT;
import static android.app.admin.DevicePolicyManager.DEVICE_OWNER_TYPE_FINANCED;
+import static com.google.common.truth.Truth.assertThat;
+
import static junit.framework.Assert.assertEquals;
import static junit.framework.Assert.assertNotNull;
+import static junit.framework.Assert.assertNull;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.Matchers.any;
import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
+import android.annotation.IdRes;
import android.app.AlertDialog;
import android.app.admin.DevicePolicyManager;
-import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.DialogInterface;
-import android.content.Intent;
import android.content.pm.UserInfo;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.VectorDrawable;
@@ -42,27 +43,27 @@
import android.provider.Settings;
import android.test.suitebuilder.annotation.SmallTest;
import android.testing.AndroidTestingRunner;
-import android.testing.LayoutInflaterBuilder;
-import android.testing.TestableImageView;
import android.testing.TestableLooper;
import android.testing.TestableLooper.RunWithLooper;
-import android.testing.ViewUtils;
import android.text.SpannableStringBuilder;
import android.view.LayoutInflater;
import android.view.View;
-import android.view.ViewGroup;
-import android.widget.FrameLayout;
import android.widget.TextView;
+import androidx.annotation.Nullable;
+
import com.android.systemui.R;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.animation.DialogLaunchAnimator;
+import com.android.systemui.animation.Expandable;
import com.android.systemui.broadcast.BroadcastDispatcher;
+import com.android.systemui.common.shared.model.Icon;
import com.android.systemui.plugins.ActivityStarter;
+import com.android.systemui.qs.footer.domain.model.SecurityButtonConfig;
+import com.android.systemui.security.data.model.SecurityModel;
import com.android.systemui.settings.UserTracker;
import com.android.systemui.statusbar.policy.SecurityController;
-import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -71,8 +72,6 @@
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
-import java.util.concurrent.atomic.AtomicInteger;
-
/*
* Compile and run the whole SystemUI test suite:
runtest --path frameworks/base/packages/SystemUI/tests
@@ -96,11 +95,6 @@
new ComponentName("TestDPC", "Test");
private static final int DEFAULT_ICON_ID = R.drawable.ic_info_outline;
- private ViewGroup mRootView;
- private ViewGroup mSecurityFooterView;
- private TextView mFooterText;
- private TestableImageView mPrimaryFooterIcon;
- private QSSecurityFooter mFooter;
private QSSecurityFooterUtils mFooterUtils;
@Mock
private SecurityController mSecurityController;
@@ -122,58 +116,53 @@
Looper looper = mTestableLooper.getLooper();
Handler mainHandler = new Handler(looper);
when(mUserTracker.getUserInfo()).thenReturn(mock(UserInfo.class));
- mSecurityFooterView = (ViewGroup) new LayoutInflaterBuilder(mContext)
- .replace("ImageView", TestableImageView.class)
- .build().inflate(R.layout.quick_settings_security_footer, null, false);
mFooterUtils = new QSSecurityFooterUtils(getContext(),
getContext().getSystemService(DevicePolicyManager.class), mUserTracker,
mainHandler, mActivityStarter, mSecurityController, looper, mDialogLaunchAnimator);
- mFooter = new QSSecurityFooter(mSecurityFooterView, mainHandler, mSecurityController,
- looper, mBroadcastDispatcher, mFooterUtils);
- mFooterText = mSecurityFooterView.findViewById(R.id.footer_text);
- mPrimaryFooterIcon = mSecurityFooterView.findViewById(R.id.primary_footer_icon);
when(mSecurityController.getDeviceOwnerComponentOnAnyUser())
.thenReturn(DEVICE_OWNER_COMPONENT);
when(mSecurityController.getDeviceOwnerType(DEVICE_OWNER_COMPONENT))
.thenReturn(DEVICE_OWNER_TYPE_DEFAULT);
-
- // mSecurityFooterView must have a ViewGroup parent so that
- // DialogLaunchAnimator.Controller.fromView() does not return null.
- mRootView = new FrameLayout(mContext);
- mRootView.addView(mSecurityFooterView);
- ViewUtils.attachView(mRootView);
-
- mFooter.init();
}
- @After
- public void tearDown() {
- ViewUtils.detachView(mRootView);
+ @Nullable
+ private SecurityButtonConfig getButtonConfig() {
+ SecurityModel securityModel = SecurityModel.create(mSecurityController);
+ return mFooterUtils.getButtonConfig(securityModel);
+ }
+
+ private void assertIsDefaultIcon(Icon icon) {
+ assertIsIconResource(icon, DEFAULT_ICON_ID);
+ }
+
+ private void assertIsIconResource(Icon icon, @IdRes int res) {
+ assertThat(icon).isInstanceOf(Icon.Resource.class);
+ assertEquals(res, ((Icon.Resource) icon).getRes());
+ }
+
+ private void assertIsIconDrawable(Icon icon, Drawable drawable) {
+ assertThat(icon).isInstanceOf(Icon.Loaded.class);
+ assertEquals(drawable, ((Icon.Loaded) icon).getDrawable());
}
@Test
public void testUnmanaged() {
when(mSecurityController.isDeviceManaged()).thenReturn(false);
when(mSecurityController.isProfileOwnerOfOrganizationOwnedDevice()).thenReturn(false);
- mFooter.refreshState();
-
- TestableLooper.get(this).processAllMessages();
- assertEquals(View.GONE, mSecurityFooterView.getVisibility());
+ assertNull(getButtonConfig());
}
@Test
public void testManagedNoOwnerName() {
when(mSecurityController.isDeviceManaged()).thenReturn(true);
when(mSecurityController.getDeviceOwnerOrganizationName()).thenReturn(null);
- mFooter.refreshState();
- TestableLooper.get(this).processAllMessages();
+ SecurityButtonConfig buttonConfig = getButtonConfig();
+ assertNotNull(buttonConfig);
assertEquals(mContext.getString(R.string.quick_settings_disclosure_management),
- mFooterText.getText());
- assertEquals(View.VISIBLE, mSecurityFooterView.getVisibility());
- assertEquals(View.VISIBLE, mPrimaryFooterIcon.getVisibility());
- assertEquals(DEFAULT_ICON_ID, mPrimaryFooterIcon.getLastImageResource());
+ buttonConfig.getText());
+ assertIsDefaultIcon(buttonConfig.getIcon());
}
@Test
@@ -181,15 +170,13 @@
when(mSecurityController.isDeviceManaged()).thenReturn(true);
when(mSecurityController.getDeviceOwnerOrganizationName())
.thenReturn(MANAGING_ORGANIZATION);
- mFooter.refreshState();
- TestableLooper.get(this).processAllMessages();
+ SecurityButtonConfig buttonConfig = getButtonConfig();
+ assertNotNull(buttonConfig);
assertEquals(mContext.getString(R.string.quick_settings_disclosure_named_management,
- MANAGING_ORGANIZATION),
- mFooterText.getText());
- assertEquals(View.VISIBLE, mSecurityFooterView.getVisibility());
- assertEquals(View.VISIBLE, mPrimaryFooterIcon.getVisibility());
- assertEquals(DEFAULT_ICON_ID, mPrimaryFooterIcon.getLastImageResource());
+ MANAGING_ORGANIZATION),
+ buttonConfig.getText());
+ assertIsDefaultIcon(buttonConfig.getIcon());
}
@Test
@@ -200,15 +187,13 @@
when(mSecurityController.getDeviceOwnerType(DEVICE_OWNER_COMPONENT))
.thenReturn(DEVICE_OWNER_TYPE_FINANCED);
- mFooter.refreshState();
-
- TestableLooper.get(this).processAllMessages();
+ SecurityButtonConfig buttonConfig = getButtonConfig();
+ assertNotNull(buttonConfig);
assertEquals(mContext.getString(
- R.string.quick_settings_financed_disclosure_named_management,
- MANAGING_ORGANIZATION), mFooterText.getText());
- assertEquals(View.VISIBLE, mSecurityFooterView.getVisibility());
- assertEquals(View.VISIBLE, mPrimaryFooterIcon.getVisibility());
- assertEquals(DEFAULT_ICON_ID, mPrimaryFooterIcon.getLastImageResource());
+ R.string.quick_settings_financed_disclosure_named_management,
+ MANAGING_ORGANIZATION),
+ buttonConfig.getText());
+ assertIsDefaultIcon(buttonConfig.getIcon());
}
@Test
@@ -220,21 +205,16 @@
when(mUserTracker.getUserInfo()).thenReturn(mockUserInfo);
Settings.Global.putInt(mContext.getContentResolver(), Settings.Global.DEVICE_DEMO_MODE, 1);
- mFooter.refreshState();
-
- TestableLooper.get(this).processAllMessages();
- assertEquals(View.GONE, mSecurityFooterView.getVisibility());
+ assertNull(getButtonConfig());
}
@Test
public void testUntappableView_profileOwnerOfOrgOwnedDevice() {
when(mSecurityController.isProfileOwnerOfOrganizationOwnedDevice()).thenReturn(true);
- mFooter.refreshState();
-
- TestableLooper.get(this).processAllMessages();
- assertFalse(mSecurityFooterView.isClickable());
- assertEquals(View.GONE, mSecurityFooterView.findViewById(R.id.footer_icon).getVisibility());
+ SecurityButtonConfig buttonConfig = getButtonConfig();
+ assertNotNull(buttonConfig);
+ assertFalse(buttonConfig.isClickable());
}
@Test
@@ -244,12 +224,9 @@
when(mSecurityController.isWorkProfileOn()).thenReturn(true);
when(mSecurityController.hasWorkProfile()).thenReturn(true);
- mFooter.refreshState();
-
- TestableLooper.get(this).processAllMessages();
- assertTrue(mSecurityFooterView.isClickable());
- assertEquals(View.VISIBLE,
- mSecurityFooterView.findViewById(R.id.footer_icon).getVisibility());
+ SecurityButtonConfig buttonConfig = getButtonConfig();
+ assertNotNull(buttonConfig);
+ assertTrue(buttonConfig.isClickable());
}
@Test
@@ -258,35 +235,31 @@
when(mSecurityController.isNetworkLoggingEnabled()).thenReturn(true);
when(mSecurityController.isWorkProfileOn()).thenReturn(false);
- mFooter.refreshState();
-
- TestableLooper.get(this).processAllMessages();
- assertFalse(mSecurityFooterView.isClickable());
- assertEquals(View.GONE, mSecurityFooterView.findViewById(R.id.footer_icon).getVisibility());
+ SecurityButtonConfig buttonConfig = getButtonConfig();
+ assertNotNull(buttonConfig);
+ assertFalse(buttonConfig.isClickable());
}
@Test
public void testNetworkLoggingEnabled_deviceOwner() {
when(mSecurityController.isDeviceManaged()).thenReturn(true);
when(mSecurityController.isNetworkLoggingEnabled()).thenReturn(true);
- mFooter.refreshState();
- TestableLooper.get(this).processAllMessages();
+ SecurityButtonConfig buttonConfig = getButtonConfig();
+ assertNotNull(buttonConfig);
assertEquals(mContext.getString(R.string.quick_settings_disclosure_management_monitoring),
- mFooterText.getText());
- assertEquals(View.VISIBLE, mPrimaryFooterIcon.getVisibility());
- assertEquals(DEFAULT_ICON_ID, mPrimaryFooterIcon.getLastImageResource());
+ buttonConfig.getText());
+ assertIsDefaultIcon(buttonConfig.getIcon());
// Same situation, but with organization name set
when(mSecurityController.getDeviceOwnerOrganizationName())
.thenReturn(MANAGING_ORGANIZATION);
- mFooter.refreshState();
-
- TestableLooper.get(this).processAllMessages();
+ buttonConfig = getButtonConfig();
+ assertNotNull(buttonConfig);
assertEquals(mContext.getString(
- R.string.quick_settings_disclosure_named_management_monitoring,
- MANAGING_ORGANIZATION),
- mFooterText.getText());
+ R.string.quick_settings_disclosure_named_management_monitoring,
+ MANAGING_ORGANIZATION),
+ buttonConfig.getText());
}
@Test
@@ -294,12 +267,12 @@
when(mSecurityController.hasWorkProfile()).thenReturn(true);
when(mSecurityController.isNetworkLoggingEnabled()).thenReturn(true);
when(mSecurityController.isWorkProfileOn()).thenReturn(true);
- mFooter.refreshState();
- TestableLooper.get(this).processAllMessages();
+ SecurityButtonConfig buttonConfig = getButtonConfig();
+ assertNotNull(buttonConfig);
assertEquals(mContext.getString(
- R.string.quick_settings_disclosure_managed_profile_network_activity),
- mFooterText.getText());
+ R.string.quick_settings_disclosure_managed_profile_network_activity),
+ buttonConfig.getText());
}
@Test
@@ -307,21 +280,19 @@
when(mSecurityController.hasWorkProfile()).thenReturn(true);
when(mSecurityController.isNetworkLoggingEnabled()).thenReturn(true);
when(mSecurityController.isWorkProfileOn()).thenReturn(false);
- mFooter.refreshState();
- TestableLooper.get(this).processAllMessages();
- assertEquals("", mFooterText.getText());
+ assertNull(getButtonConfig());
}
@Test
public void testManagedCACertsInstalled() {
when(mSecurityController.isDeviceManaged()).thenReturn(true);
when(mSecurityController.hasCACertInCurrentUser()).thenReturn(true);
- mFooter.refreshState();
- TestableLooper.get(this).processAllMessages();
+ SecurityButtonConfig buttonConfig = getButtonConfig();
+ assertNotNull(buttonConfig);
assertEquals(mContext.getString(R.string.quick_settings_disclosure_management_monitoring),
- mFooterText.getText());
+ buttonConfig.getText());
}
@Test
@@ -329,25 +300,23 @@
when(mSecurityController.isDeviceManaged()).thenReturn(true);
when(mSecurityController.isVpnEnabled()).thenReturn(true);
when(mSecurityController.getPrimaryVpnName()).thenReturn(VPN_PACKAGE);
- mFooter.refreshState();
- TestableLooper.get(this).processAllMessages();
+ SecurityButtonConfig buttonConfig = getButtonConfig();
+ assertNotNull(buttonConfig);
assertEquals(mContext.getString(R.string.quick_settings_disclosure_management_named_vpn,
- VPN_PACKAGE),
- mFooterText.getText());
- assertEquals(View.VISIBLE, mPrimaryFooterIcon.getVisibility());
- assertEquals(R.drawable.stat_sys_vpn_ic, mPrimaryFooterIcon.getLastImageResource());
+ VPN_PACKAGE),
+ buttonConfig.getText());
+ assertIsIconResource(buttonConfig.getIcon(), R.drawable.stat_sys_vpn_ic);
// Same situation, but with organization name set
when(mSecurityController.getDeviceOwnerOrganizationName())
.thenReturn(MANAGING_ORGANIZATION);
- mFooter.refreshState();
-
- TestableLooper.get(this).processAllMessages();
+ buttonConfig = getButtonConfig();
+ assertNotNull(buttonConfig);
assertEquals(mContext.getString(
- R.string.quick_settings_disclosure_named_management_named_vpn,
- MANAGING_ORGANIZATION, VPN_PACKAGE),
- mFooterText.getText());
+ R.string.quick_settings_disclosure_named_management_named_vpn,
+ MANAGING_ORGANIZATION, VPN_PACKAGE),
+ buttonConfig.getText());
}
@Test
@@ -356,23 +325,21 @@
when(mSecurityController.isVpnEnabled()).thenReturn(true);
when(mSecurityController.getPrimaryVpnName()).thenReturn(VPN_PACKAGE);
when(mSecurityController.getWorkProfileVpnName()).thenReturn(VPN_PACKAGE_2);
- mFooter.refreshState();
- TestableLooper.get(this).processAllMessages();
+ SecurityButtonConfig buttonConfig = getButtonConfig();
+ assertNotNull(buttonConfig);
assertEquals(mContext.getString(R.string.quick_settings_disclosure_management_vpns),
- mFooterText.getText());
- assertEquals(View.VISIBLE, mPrimaryFooterIcon.getVisibility());
- assertEquals(R.drawable.stat_sys_vpn_ic, mPrimaryFooterIcon.getLastImageResource());
+ buttonConfig.getText());
+ assertIsIconResource(buttonConfig.getIcon(), R.drawable.stat_sys_vpn_ic);
// Same situation, but with organization name set
when(mSecurityController.getDeviceOwnerOrganizationName())
.thenReturn(MANAGING_ORGANIZATION);
- mFooter.refreshState();
-
- TestableLooper.get(this).processAllMessages();
+ buttonConfig = getButtonConfig();
+ assertNotNull(buttonConfig);
assertEquals(mContext.getString(R.string.quick_settings_disclosure_named_management_vpns,
MANAGING_ORGANIZATION),
- mFooterText.getText());
+ buttonConfig.getText());
}
@Test
@@ -381,13 +348,12 @@
when(mSecurityController.isNetworkLoggingEnabled()).thenReturn(true);
when(mSecurityController.isVpnEnabled()).thenReturn(true);
when(mSecurityController.getPrimaryVpnName()).thenReturn("VPN Test App");
- mFooter.refreshState();
- TestableLooper.get(this).processAllMessages();
- assertEquals(View.VISIBLE, mPrimaryFooterIcon.getVisibility());
- assertEquals(R.drawable.stat_sys_vpn_ic, mPrimaryFooterIcon.getLastImageResource());
+ SecurityButtonConfig buttonConfig = getButtonConfig();
+ assertNotNull(buttonConfig);
+ assertIsIconResource(buttonConfig.getIcon(), R.drawable.stat_sys_vpn_ic);
assertEquals(mContext.getString(R.string.quick_settings_disclosure_management_monitoring),
- mFooterText.getText());
+ buttonConfig.getText());
}
@Test
@@ -395,24 +361,23 @@
when(mSecurityController.isDeviceManaged()).thenReturn(false);
when(mSecurityController.hasCACertInWorkProfile()).thenReturn(true);
when(mSecurityController.isWorkProfileOn()).thenReturn(true);
- mFooter.refreshState();
- TestableLooper.get(this).processAllMessages();
- assertEquals(DEFAULT_ICON_ID, mPrimaryFooterIcon.getLastImageResource());
+ SecurityButtonConfig buttonConfig = getButtonConfig();
+ assertNotNull(buttonConfig);
+ assertIsDefaultIcon(buttonConfig.getIcon());
assertEquals(mContext.getString(
R.string.quick_settings_disclosure_managed_profile_monitoring),
- mFooterText.getText());
+ buttonConfig.getText());
// Same situation, but with organization name set
when(mSecurityController.getWorkProfileOrganizationName())
.thenReturn(MANAGING_ORGANIZATION);
- mFooter.refreshState();
-
- TestableLooper.get(this).processAllMessages();
+ buttonConfig = getButtonConfig();
+ assertNotNull(buttonConfig);
assertEquals(mContext.getString(
R.string.quick_settings_disclosure_named_managed_profile_monitoring,
MANAGING_ORGANIZATION),
- mFooterText.getText());
+ buttonConfig.getText());
}
@Test
@@ -420,22 +385,20 @@
when(mSecurityController.isDeviceManaged()).thenReturn(false);
when(mSecurityController.hasCACertInWorkProfile()).thenReturn(true);
when(mSecurityController.isWorkProfileOn()).thenReturn(false);
- mFooter.refreshState();
- TestableLooper.get(this).processAllMessages();
- assertEquals("", mFooterText.getText());
+ assertNull(getButtonConfig());
}
@Test
public void testCACertsInstalled() {
when(mSecurityController.isDeviceManaged()).thenReturn(false);
when(mSecurityController.hasCACertInCurrentUser()).thenReturn(true);
- mFooter.refreshState();
- TestableLooper.get(this).processAllMessages();
- assertEquals(DEFAULT_ICON_ID, mPrimaryFooterIcon.getLastImageResource());
+ SecurityButtonConfig buttonConfig = getButtonConfig();
+ assertNotNull(buttonConfig);
+ assertIsDefaultIcon(buttonConfig.getIcon());
assertEquals(mContext.getString(R.string.quick_settings_disclosure_monitoring),
- mFooterText.getText());
+ buttonConfig.getText());
}
@Test
@@ -443,12 +406,12 @@
when(mSecurityController.isVpnEnabled()).thenReturn(true);
when(mSecurityController.getPrimaryVpnName()).thenReturn(VPN_PACKAGE);
when(mSecurityController.getWorkProfileVpnName()).thenReturn(VPN_PACKAGE_2);
- mFooter.refreshState();
- TestableLooper.get(this).processAllMessages();
- assertEquals(R.drawable.stat_sys_vpn_ic, mPrimaryFooterIcon.getLastImageResource());
+ SecurityButtonConfig buttonConfig = getButtonConfig();
+ assertNotNull(buttonConfig);
+ assertIsIconResource(buttonConfig.getIcon(), R.drawable.stat_sys_vpn_ic);
assertEquals(mContext.getString(R.string.quick_settings_disclosure_vpns),
- mFooterText.getText());
+ buttonConfig.getText());
}
@Test
@@ -456,14 +419,14 @@
when(mSecurityController.isVpnEnabled()).thenReturn(true);
when(mSecurityController.getWorkProfileVpnName()).thenReturn(VPN_PACKAGE_2);
when(mSecurityController.isWorkProfileOn()).thenReturn(true);
- mFooter.refreshState();
- TestableLooper.get(this).processAllMessages();
- assertEquals(R.drawable.stat_sys_vpn_ic, mPrimaryFooterIcon.getLastImageResource());
+ SecurityButtonConfig buttonConfig = getButtonConfig();
+ assertNotNull(buttonConfig);
+ assertIsIconResource(buttonConfig.getIcon(), R.drawable.stat_sys_vpn_ic);
assertEquals(mContext.getString(
R.string.quick_settings_disclosure_managed_profile_named_vpn,
VPN_PACKAGE_2),
- mFooterText.getText());
+ buttonConfig.getText());
}
@Test
@@ -471,22 +434,19 @@
when(mSecurityController.isVpnEnabled()).thenReturn(true);
when(mSecurityController.getWorkProfileVpnName()).thenReturn(VPN_PACKAGE_2);
when(mSecurityController.isWorkProfileOn()).thenReturn(false);
- mFooter.refreshState();
- TestableLooper.get(this).processAllMessages();
- assertEquals("", mFooterText.getText());
+ assertNull(getButtonConfig());
}
@Test
public void testProfileOwnerOfOrganizationOwnedDeviceNoName() {
when(mSecurityController.isProfileOwnerOfOrganizationOwnedDevice()).thenReturn(true);
- mFooter.refreshState();
- TestableLooper.get(this).processAllMessages();
-
+ SecurityButtonConfig buttonConfig = getButtonConfig();
+ assertNotNull(buttonConfig);
assertEquals(mContext.getString(
R.string.quick_settings_disclosure_management),
- mFooterText.getText());
+ buttonConfig.getText());
}
@Test
@@ -495,35 +455,33 @@
when(mSecurityController.getWorkProfileOrganizationName())
.thenReturn(MANAGING_ORGANIZATION);
- mFooter.refreshState();
- TestableLooper.get(this).processAllMessages();
-
+ SecurityButtonConfig buttonConfig = getButtonConfig();
+ assertNotNull(buttonConfig);
assertEquals(mContext.getString(
R.string.quick_settings_disclosure_named_management,
MANAGING_ORGANIZATION),
- mFooterText.getText());
+ buttonConfig.getText());
}
@Test
public void testVpnEnabled() {
when(mSecurityController.isVpnEnabled()).thenReturn(true);
when(mSecurityController.getPrimaryVpnName()).thenReturn(VPN_PACKAGE);
- mFooter.refreshState();
- TestableLooper.get(this).processAllMessages();
- assertEquals(R.drawable.stat_sys_vpn_ic, mPrimaryFooterIcon.getLastImageResource());
+ SecurityButtonConfig buttonConfig = getButtonConfig();
+ assertNotNull(buttonConfig);
+ assertIsIconResource(buttonConfig.getIcon(), R.drawable.stat_sys_vpn_ic);
assertEquals(mContext.getString(R.string.quick_settings_disclosure_named_vpn,
VPN_PACKAGE),
- mFooterText.getText());
+ buttonConfig.getText());
when(mSecurityController.hasWorkProfile()).thenReturn(true);
- mFooter.refreshState();
-
- TestableLooper.get(this).processAllMessages();
+ buttonConfig = getButtonConfig();
+ assertNotNull(buttonConfig);
assertEquals(mContext.getString(
R.string.quick_settings_disclosure_personal_profile_named_vpn,
VPN_PACKAGE),
- mFooterText.getText());
+ buttonConfig.getText());
}
@Test
@@ -687,19 +645,6 @@
}
@Test
- public void testNoClickWhenGone() {
- mFooter.refreshState();
-
- TestableLooper.get(this).processAllMessages();
-
- assertFalse(mFooter.hasFooter());
- mFooter.onClick(mFooter.getView());
-
- // Proxy for dialog being created
- verify(mDialogLaunchAnimator, never()).showFromView(any(), any());
- }
-
- @Test
public void testParentalControls() {
// Make sure the security footer is visible, so that the images are updated.
when(mSecurityController.isProfileOwnerOfOrganizationOwnedDevice()).thenReturn(true);
@@ -707,29 +652,26 @@
// We use the default icon when there is no admin icon.
when(mSecurityController.getIcon(any())).thenReturn(null);
- mFooter.refreshState();
- TestableLooper.get(this).processAllMessages();
+ SecurityButtonConfig buttonConfig = getButtonConfig();
assertEquals(mContext.getString(R.string.quick_settings_disclosure_parental_controls),
- mFooterText.getText());
- assertEquals(DEFAULT_ICON_ID, mPrimaryFooterIcon.getLastImageResource());
+ buttonConfig.getText());
+ assertIsDefaultIcon(buttonConfig.getIcon());
Drawable testDrawable = new VectorDrawable();
when(mSecurityController.getIcon(any())).thenReturn(testDrawable);
assertNotNull(mSecurityController.getIcon(null));
- mFooter.refreshState();
- TestableLooper.get(this).processAllMessages();
-
+ buttonConfig = getButtonConfig();
+ assertNotNull(buttonConfig);
assertEquals(mContext.getString(R.string.quick_settings_disclosure_parental_controls),
- mFooterText.getText());
- assertEquals(View.VISIBLE, mPrimaryFooterIcon.getVisibility());
- assertEquals(testDrawable, mPrimaryFooterIcon.getDrawable());
+ buttonConfig.getText());
+ assertIsIconDrawable(buttonConfig.getIcon(), testDrawable);
// Ensure the primary icon is back to default after parental controls are gone
when(mSecurityController.isParentalControlsEnabled()).thenReturn(false);
- mFooter.refreshState();
- TestableLooper.get(this).processAllMessages();
- assertEquals(DEFAULT_ICON_ID, mPrimaryFooterIcon.getLastImageResource());
+ buttonConfig = getButtonConfig();
+ assertNotNull(buttonConfig);
+ assertIsDefaultIcon(buttonConfig.getIcon());
}
@Test
@@ -743,16 +685,6 @@
}
@Test
- public void testDialogUsesDialogLauncher() {
- when(mSecurityController.isDeviceManaged()).thenReturn(true);
- mFooter.onClick(mSecurityFooterView);
-
- mTestableLooper.processAllMessages();
-
- verify(mDialogLaunchAnimator).show(any(), any());
- }
-
- @Test
public void testCreateDialogViewForFinancedDevice() {
when(mSecurityController.isDeviceManaged()).thenReturn(true);
when(mSecurityController.getDeviceOwnerOrganizationName())
@@ -782,7 +714,10 @@
when(mSecurityController.getDeviceOwnerType(DEVICE_OWNER_COMPONENT))
.thenReturn(DEVICE_OWNER_TYPE_FINANCED);
- mFooter.showDeviceMonitoringDialog();
+ Expandable expandable = mock(Expandable.class);
+ when(expandable.dialogLaunchController(any())).thenReturn(
+ mock(DialogLaunchAnimator.Controller.class));
+ mFooterUtils.showDeviceMonitoringDialog(getContext(), expandable);
ArgumentCaptor<AlertDialog> dialogCaptor = ArgumentCaptor.forClass(AlertDialog.class);
mTestableLooper.processAllMessages();
@@ -797,47 +732,6 @@
dialog.dismiss();
}
- @Test
- public void testVisibilityListener() {
- final AtomicInteger lastVisibility = new AtomicInteger(-1);
- VisibilityChangedDispatcher.OnVisibilityChangedListener listener = lastVisibility::set;
-
- mFooter.setOnVisibilityChangedListener(listener);
-
- when(mSecurityController.isDeviceManaged()).thenReturn(true);
- mFooter.refreshState();
- mTestableLooper.processAllMessages();
- assertEquals(View.VISIBLE, lastVisibility.get());
-
- when(mSecurityController.isDeviceManaged()).thenReturn(false);
- mFooter.refreshState();
- mTestableLooper.processAllMessages();
- assertEquals(View.GONE, lastVisibility.get());
- }
-
- @Test
- public void testBroadcastShowsDialog() {
- // Setup dialog content
- when(mSecurityController.isDeviceManaged()).thenReturn(true);
- when(mSecurityController.getDeviceOwnerOrganizationName())
- .thenReturn(MANAGING_ORGANIZATION);
- when(mSecurityController.getDeviceOwnerType(DEVICE_OWNER_COMPONENT))
- .thenReturn(DEVICE_OWNER_TYPE_FINANCED);
-
- ArgumentCaptor<BroadcastReceiver> captor = ArgumentCaptor.forClass(BroadcastReceiver.class);
- verify(mBroadcastDispatcher).registerReceiverWithHandler(captor.capture(), any(), any(),
- any());
-
- // Pretend view is not attached anymore.
- mRootView.removeView(mSecurityFooterView);
- captor.getValue().onReceive(mContext,
- new Intent(DevicePolicyManager.ACTION_SHOW_DEVICE_MONITORING_DIALOG));
- mTestableLooper.processAllMessages();
-
- assertTrue(mFooterUtils.getDialog().isShowing());
- mFooterUtils.getDialog().dismiss();
- }
-
private CharSequence addLink(CharSequence description) {
final SpannableStringBuilder message = new SpannableStringBuilder();
message.append(description);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsViewModelTest.kt
index 47afa70..01411c9 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsViewModelTest.kt
@@ -385,4 +385,86 @@
underTest.onVisibilityChangeRequested(visible = true)
assertThat(underTest.isVisible.value).isTrue()
}
+
+ @Test
+ fun alpha_inSplitShade_followsExpansion() {
+ val underTest = utils.footerActionsViewModel()
+
+ underTest.onQuickSettingsExpansionChanged(0f, isInSplitShade = true)
+ assertThat(underTest.alpha.value).isEqualTo(0f)
+
+ underTest.onQuickSettingsExpansionChanged(0.25f, isInSplitShade = true)
+ assertThat(underTest.alpha.value).isEqualTo(0.25f)
+
+ underTest.onQuickSettingsExpansionChanged(0.5f, isInSplitShade = true)
+ assertThat(underTest.alpha.value).isEqualTo(0.5f)
+
+ underTest.onQuickSettingsExpansionChanged(0.75f, isInSplitShade = true)
+ assertThat(underTest.alpha.value).isEqualTo(0.75f)
+
+ underTest.onQuickSettingsExpansionChanged(1f, isInSplitShade = true)
+ assertThat(underTest.alpha.value).isEqualTo(1f)
+ }
+
+ @Test
+ fun backgroundAlpha_inSplitShade_followsExpansion_with_0_99_delay() {
+ val underTest = utils.footerActionsViewModel()
+ val floatTolerance = 0.01f
+
+ underTest.onQuickSettingsExpansionChanged(0f, isInSplitShade = true)
+ assertThat(underTest.backgroundAlpha.value).isEqualTo(0f)
+
+ underTest.onQuickSettingsExpansionChanged(0.5f, isInSplitShade = true)
+ assertThat(underTest.backgroundAlpha.value).isEqualTo(0f)
+
+ underTest.onQuickSettingsExpansionChanged(0.9f, isInSplitShade = true)
+ assertThat(underTest.backgroundAlpha.value).isEqualTo(0f)
+
+ underTest.onQuickSettingsExpansionChanged(0.991f, isInSplitShade = true)
+ assertThat(underTest.backgroundAlpha.value).isWithin(floatTolerance).of(0.1f)
+
+ underTest.onQuickSettingsExpansionChanged(0.995f, isInSplitShade = true)
+ assertThat(underTest.backgroundAlpha.value).isWithin(floatTolerance).of(0.5f)
+
+ underTest.onQuickSettingsExpansionChanged(1f, isInSplitShade = true)
+ assertThat(underTest.backgroundAlpha.value).isEqualTo(1f)
+ }
+
+ @Test
+ fun alpha_inSingleShade_followsExpansion_with_0_9_delay() {
+ val underTest = utils.footerActionsViewModel()
+ val floatTolerance = 0.01f
+
+ underTest.onQuickSettingsExpansionChanged(0f, isInSplitShade = false)
+ assertThat(underTest.alpha.value).isEqualTo(0f)
+
+ underTest.onQuickSettingsExpansionChanged(0.5f, isInSplitShade = false)
+ assertThat(underTest.alpha.value).isEqualTo(0f)
+
+ underTest.onQuickSettingsExpansionChanged(0.9f, isInSplitShade = false)
+ assertThat(underTest.alpha.value).isEqualTo(0f)
+
+ underTest.onQuickSettingsExpansionChanged(0.91f, isInSplitShade = false)
+ assertThat(underTest.alpha.value).isWithin(floatTolerance).of(0.1f)
+
+ underTest.onQuickSettingsExpansionChanged(0.95f, isInSplitShade = false)
+ assertThat(underTest.alpha.value).isWithin(floatTolerance).of(0.5f)
+
+ underTest.onQuickSettingsExpansionChanged(1f, isInSplitShade = false)
+ assertThat(underTest.alpha.value).isEqualTo(1f)
+ }
+
+ @Test
+ fun backgroundAlpha_inSingleShade_always1() {
+ val underTest = utils.footerActionsViewModel()
+
+ underTest.onQuickSettingsExpansionChanged(0f, isInSplitShade = false)
+ assertThat(underTest.backgroundAlpha.value).isEqualTo(1f)
+
+ underTest.onQuickSettingsExpansionChanged(0.5f, isInSplitShade = false)
+ assertThat(underTest.backgroundAlpha.value).isEqualTo(1f)
+
+ underTest.onQuickSettingsExpansionChanged(1f, isInSplitShade = false)
+ assertThat(underTest.backgroundAlpha.value).isEqualTo(1f)
+ }
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/InternetTileTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/InternetTileTest.java
index d91baa5..80c39cf 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/InternetTileTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/InternetTileTest.java
@@ -23,6 +23,7 @@
import android.os.Handler;
+import android.service.quicksettings.Tile;
import android.testing.AndroidTestingRunner;
import android.testing.TestableLooper;
@@ -38,6 +39,7 @@
import com.android.systemui.qs.logging.QSLogger;
import com.android.systemui.qs.tiles.dialog.InternetDialogFactory;
import com.android.systemui.statusbar.connectivity.AccessPointController;
+import com.android.systemui.statusbar.connectivity.IconState;
import com.android.systemui.statusbar.connectivity.NetworkController;
import org.junit.Before;
@@ -113,4 +115,24 @@
.isNotEqualTo(mContext.getString(R.string.quick_settings_networks_available));
assertThat(mTile.getLastTileState()).isEqualTo(-1);
}
+
+ @Test
+ public void setIsAirplaneMode_APM_enabled_wifi_disabled() {
+ IconState state = new IconState(true, 0, "");
+ mTile.mSignalCallback.setIsAirplaneMode(state);
+ mTestableLooper.processAllMessages();
+ assertThat(mTile.getState().state).isEqualTo(Tile.STATE_INACTIVE);
+ assertThat(mTile.getState().secondaryLabel)
+ .isEqualTo(mContext.getString(R.string.status_bar_airplane));
+ }
+
+ @Test
+ public void setIsAirplaneMode_APM_enabled_wifi_enabled() {
+ IconState state = new IconState(false, 0, "");
+ mTile.mSignalCallback.setIsAirplaneMode(state);
+ mTestableLooper.processAllMessages();
+ assertThat(mTile.getState().state).isEqualTo(Tile.STATE_ACTIVE);
+ assertThat(mTile.getState().secondaryLabel)
+ .isNotEqualTo(mContext.getString(R.string.status_bar_airplane));
+ }
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/util/condition/ConditionMonitorTest.java b/packages/SystemUI/tests/src/com/android/systemui/shared/condition/ConditionMonitorTest.java
similarity index 92%
rename from packages/SystemUI/tests/src/com/android/systemui/util/condition/ConditionMonitorTest.java
rename to packages/SystemUI/tests/src/com/android/systemui/shared/condition/ConditionMonitorTest.java
index 17d81c8..7693fee 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/util/condition/ConditionMonitorTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/shared/condition/ConditionMonitorTest.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2021 The Android Open Source Project
+ * Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.systemui.util.condition;
+package com.android.systemui.shared.condition;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
@@ -68,13 +68,15 @@
mConditionMonitor = new Monitor(mExecutor);
}
- public Monitor.Subscription.Builder getDefaultBuilder(Monitor.Callback callback) {
+ public Monitor.Subscription.Builder getDefaultBuilder(
+ Monitor.Callback callback) {
return new Monitor.Subscription.Builder(callback)
.addConditions(mConditions);
}
private Condition createMockCondition() {
- final Condition condition = Mockito.mock(Condition.class);
+ final Condition condition = Mockito.mock(
+ Condition.class);
when(condition.isConditionSet()).thenReturn(true);
return condition;
}
@@ -83,11 +85,14 @@
public void testOverridingCondition() {
final Condition overridingCondition = createMockCondition();
final Condition regularCondition = createMockCondition();
- final Monitor.Callback callback = Mockito.mock(Monitor.Callback.class);
+ final Monitor.Callback callback = Mockito.mock(
+ Monitor.Callback.class);
- final Monitor.Callback referenceCallback = Mockito.mock(Monitor.Callback.class);
+ final Monitor.Callback referenceCallback = Mockito.mock(
+ Monitor.Callback.class);
- final Monitor monitor = new Monitor(mExecutor);
+ final Monitor
+ monitor = new Monitor(mExecutor);
monitor.addSubscription(getDefaultBuilder(callback)
.addCondition(overridingCondition)
@@ -136,9 +141,11 @@
final Condition overridingCondition = createMockCondition();
final Condition overridingCondition2 = createMockCondition();
final Condition regularCondition = createMockCondition();
- final Monitor.Callback callback = Mockito.mock(Monitor.Callback.class);
+ final Monitor.Callback callback = Mockito.mock(
+ Monitor.Callback.class);
- final Monitor monitor = new Monitor(mExecutor);
+ final Monitor
+ monitor = new Monitor(mExecutor);
monitor.addSubscription(getDefaultBuilder(callback)
.addCondition(overridingCondition)
@@ -211,9 +218,11 @@
public void addCallback_addSecondCallback_reportWithExistingValue() {
final Monitor.Callback callback1 =
mock(Monitor.Callback.class);
- final Condition condition = mock(Condition.class);
+ final Condition condition = mock(
+ Condition.class);
when(condition.isConditionMet()).thenReturn(true);
- final Monitor monitor = new Monitor(mExecutor);
+ final Monitor
+ monitor = new Monitor(mExecutor);
monitor.addSubscription(new Monitor.Subscription.Builder(callback1)
.addCondition(condition)
.build());
@@ -229,8 +238,10 @@
@Test
public void addCallback_noConditions_reportAllConditionsMet() {
- final Monitor monitor = new Monitor(mExecutor);
- final Monitor.Callback callback = mock(Monitor.Callback.class);
+ final Monitor
+ monitor = new Monitor(mExecutor);
+ final Monitor.Callback callback = mock(
+ Monitor.Callback.class);
monitor.addSubscription(new Monitor.Subscription.Builder(callback).build());
mExecutor.runAllReady();
@@ -239,8 +250,10 @@
@Test
public void removeCallback_noFailureOnDoubleRemove() {
- final Condition condition = mock(Condition.class);
- final Monitor monitor = new Monitor(mExecutor);
+ final Condition condition = mock(
+ Condition.class);
+ final Monitor
+ monitor = new Monitor(mExecutor);
final Monitor.Callback callback =
mock(Monitor.Callback.class);
final Monitor.Subscription.Token token = monitor.addSubscription(
@@ -255,8 +268,10 @@
@Test
public void removeCallback_shouldNoLongerReceiveUpdate() {
- final Condition condition = mock(Condition.class);
- final Monitor monitor = new Monitor(mExecutor);
+ final Condition condition = mock(
+ Condition.class);
+ final Monitor
+ monitor = new Monitor(mExecutor);
final Monitor.Callback callback =
mock(Monitor.Callback.class);
final Monitor.Subscription.Token token = monitor.addSubscription(
diff --git a/packages/SystemUI/tests/src/com/android/systemui/util/condition/ConditionTest.java b/packages/SystemUI/tests/src/com/android/systemui/shared/condition/ConditionTest.java
similarity index 81%
rename from packages/SystemUI/tests/src/com/android/systemui/util/condition/ConditionTest.java
rename to packages/SystemUI/tests/src/com/android/systemui/shared/condition/ConditionTest.java
index 2878864..8443221 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/util/condition/ConditionTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/shared/condition/ConditionTest.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2021 The Android Open Source Project
+ * Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.systemui.util.condition;
+package com.android.systemui.shared.condition;
import static com.google.common.truth.Truth.assertThat;
@@ -47,16 +47,20 @@
@Test
public void addCallback_addFirstCallback_triggerStart() {
- final Condition.Callback callback = mock(Condition.Callback.class);
+ final Condition.Callback callback = mock(
+ Condition.Callback.class);
mCondition.addCallback(callback);
verify(mCondition).start();
}
@Test
public void addCallback_addMultipleCallbacks_triggerStartOnlyOnce() {
- final Condition.Callback callback1 = mock(Condition.Callback.class);
- final Condition.Callback callback2 = mock(Condition.Callback.class);
- final Condition.Callback callback3 = mock(Condition.Callback.class);
+ final Condition.Callback callback1 = mock(
+ Condition.Callback.class);
+ final Condition.Callback callback2 = mock(
+ Condition.Callback.class);
+ final Condition.Callback callback3 = mock(
+ Condition.Callback.class);
mCondition.addCallback(callback1);
mCondition.addCallback(callback2);
@@ -67,12 +71,14 @@
@Test
public void addCallback_alreadyStarted_triggerUpdate() {
- final Condition.Callback callback1 = mock(Condition.Callback.class);
+ final Condition.Callback callback1 = mock(
+ Condition.Callback.class);
mCondition.addCallback(callback1);
mCondition.fakeUpdateCondition(true);
- final Condition.Callback callback2 = mock(Condition.Callback.class);
+ final Condition.Callback callback2 = mock(
+ Condition.Callback.class);
mCondition.addCallback(callback2);
verify(callback2).onConditionChanged(mCondition);
assertThat(mCondition.isConditionMet()).isTrue();
@@ -80,7 +86,8 @@
@Test
public void removeCallback_removeLastCallback_triggerStop() {
- final Condition.Callback callback = mock(Condition.Callback.class);
+ final Condition.Callback callback = mock(
+ Condition.Callback.class);
mCondition.addCallback(callback);
verify(mCondition, never()).stop();
@@ -92,7 +99,8 @@
public void updateCondition_falseToTrue_reportTrue() {
mCondition.fakeUpdateCondition(false);
- final Condition.Callback callback = mock(Condition.Callback.class);
+ final Condition.Callback callback = mock(
+ Condition.Callback.class);
mCondition.addCallback(callback);
mCondition.fakeUpdateCondition(true);
@@ -104,7 +112,8 @@
public void updateCondition_trueToFalse_reportFalse() {
mCondition.fakeUpdateCondition(true);
- final Condition.Callback callback = mock(Condition.Callback.class);
+ final Condition.Callback callback = mock(
+ Condition.Callback.class);
mCondition.addCallback(callback);
mCondition.fakeUpdateCondition(false);
@@ -116,7 +125,8 @@
public void updateCondition_trueToTrue_reportNothing() {
mCondition.fakeUpdateCondition(true);
- final Condition.Callback callback = mock(Condition.Callback.class);
+ final Condition.Callback callback = mock(
+ Condition.Callback.class);
mCondition.addCallback(callback);
mCondition.fakeUpdateCondition(true);
@@ -127,7 +137,8 @@
public void updateCondition_falseToFalse_reportNothing() {
mCondition.fakeUpdateCondition(false);
- final Condition.Callback callback = mock(Condition.Callback.class);
+ final Condition.Callback callback = mock(
+ Condition.Callback.class);
mCondition.addCallback(callback);
mCondition.fakeUpdateCondition(false);
@@ -149,7 +160,8 @@
final Condition combinedCondition = mCondition.or(
new FakeCondition(/* initialValue= */ false));
- final Condition.Callback callback = mock(Condition.Callback.class);
+ final Condition.Callback callback = mock(
+ Condition.Callback.class);
combinedCondition.addCallback(callback);
assertThat(combinedCondition.isConditionSet()).isTrue();
@@ -164,7 +176,8 @@
final Condition combinedCondition = mCondition.or(
new FakeCondition(/* initialValue= */ true));
- final Condition.Callback callback = mock(Condition.Callback.class);
+ final Condition.Callback callback = mock(
+ Condition.Callback.class);
combinedCondition.addCallback(callback);
assertThat(combinedCondition.isConditionSet()).isTrue();
@@ -179,7 +192,8 @@
final Condition combinedCondition = mCondition.or(
new FakeCondition(/* initialValue= */ true));
- final Condition.Callback callback = mock(Condition.Callback.class);
+ final Condition.Callback callback = mock(
+ Condition.Callback.class);
combinedCondition.addCallback(callback);
assertThat(combinedCondition.isConditionSet()).isTrue();
@@ -195,7 +209,8 @@
final Condition combinedCondition = mCondition.or(
new FakeCondition(/* initialValue= */ null));
- final Condition.Callback callback = mock(Condition.Callback.class);
+ final Condition.Callback callback = mock(
+ Condition.Callback.class);
combinedCondition.addCallback(callback);
assertThat(combinedCondition.isConditionSet()).isTrue();
@@ -211,7 +226,8 @@
final Condition combinedCondition = mCondition.or(
new FakeCondition(/* initialValue= */ null));
- final Condition.Callback callback = mock(Condition.Callback.class);
+ final Condition.Callback callback = mock(
+ Condition.Callback.class);
combinedCondition.addCallback(callback);
assertThat(combinedCondition.isConditionSet()).isFalse();
@@ -226,7 +242,8 @@
final Condition combinedCondition = mCondition.and(
new FakeCondition(/* initialValue= */ false));
- final Condition.Callback callback = mock(Condition.Callback.class);
+ final Condition.Callback callback = mock(
+ Condition.Callback.class);
combinedCondition.addCallback(callback);
assertThat(combinedCondition.isConditionSet()).isTrue();
@@ -241,7 +258,8 @@
final Condition combinedCondition = mCondition.and(
new FakeCondition(/* initialValue= */ true));
- final Condition.Callback callback = mock(Condition.Callback.class);
+ final Condition.Callback callback = mock(
+ Condition.Callback.class);
combinedCondition.addCallback(callback);
assertThat(combinedCondition.isConditionSet()).isTrue();
@@ -256,7 +274,8 @@
final Condition combinedCondition = mCondition.and(
new FakeCondition(/* initialValue= */ false));
- final Condition.Callback callback = mock(Condition.Callback.class);
+ final Condition.Callback callback = mock(
+ Condition.Callback.class);
combinedCondition.addCallback(callback);
assertThat(combinedCondition.isConditionSet()).isTrue();
@@ -272,7 +291,8 @@
final Condition combinedCondition = mCondition.and(
new FakeCondition(/* initialValue= */ null));
- final Condition.Callback callback = mock(Condition.Callback.class);
+ final Condition.Callback callback = mock(
+ Condition.Callback.class);
combinedCondition.addCallback(callback);
assertThat(combinedCondition.isConditionSet()).isFalse();
@@ -288,7 +308,8 @@
final Condition combinedCondition = mCondition.and(
new FakeCondition(/* initialValue= */ null));
- final Condition.Callback callback = mock(Condition.Callback.class);
+ final Condition.Callback callback = mock(
+ Condition.Callback.class);
combinedCondition.addCallback(callback);
assertThat(combinedCondition.isConditionSet()).isTrue();
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/util/condition/FakeCondition.java b/packages/SystemUI/tests/src/com/android/systemui/shared/condition/FakeCondition.java
similarity index 91%
rename from packages/SystemUI/tests/utils/src/com/android/systemui/util/condition/FakeCondition.java
rename to packages/SystemUI/tests/src/com/android/systemui/shared/condition/FakeCondition.java
index 07ed110..55a6d39 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/util/condition/FakeCondition.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/shared/condition/FakeCondition.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2021 The Android Open Source Project
+ * Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.systemui.util.condition;
+package com.android.systemui.shared.condition;
/**
* Fake implementation of {@link Condition}, and provides a way for tests to update
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerBaseTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerBaseTest.java
index faf4592..5431eba 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerBaseTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerBaseTest.java
@@ -72,6 +72,7 @@
import com.android.systemui.dump.DumpManager;
import com.android.systemui.plugins.log.LogBuffer;
import com.android.systemui.settings.UserTracker;
+import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags;
import com.android.systemui.statusbar.pipeline.mobile.util.FakeMobileMappingsProxy;
import com.android.systemui.statusbar.policy.DeviceProvisionedController;
import com.android.systemui.statusbar.policy.DeviceProvisionedController.DeviceProvisionedListener;
@@ -245,6 +246,7 @@
mFakeExecutor,
mCallbackHandler,
mock(AccessPointControllerImpl.class),
+ mock(StatusBarPipelineFlags.class),
mock(DataUsageController.class),
mMockSubDefaults,
mMockProvisionController,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerDataTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerDataTest.java
index ca75a40..9441d49 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerDataTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerDataTest.java
@@ -49,6 +49,7 @@
import com.android.settingslib.net.DataUsageController;
import com.android.systemui.dump.DumpManager;
import com.android.systemui.plugins.log.LogBuffer;
+import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags;
import com.android.systemui.statusbar.policy.DeviceProvisionedController;
import com.android.systemui.util.CarrierConfigTracker;
@@ -150,6 +151,7 @@
mFakeExecutor,
mCallbackHandler,
mock(AccessPointControllerImpl.class),
+ mock(StatusBarPipelineFlags.class),
mock(DataUsageController.class),
mMockSubDefaults,
mock(DeviceProvisionedController.class),
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerSignalTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerSignalTest.java
index 84c242c..4c1f0a8 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerSignalTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerSignalTest.java
@@ -44,6 +44,7 @@
import com.android.systemui.R;
import com.android.systemui.dump.DumpManager;
import com.android.systemui.plugins.log.LogBuffer;
+import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags;
import com.android.systemui.statusbar.policy.DeviceProvisionedController;
import com.android.systemui.util.CarrierConfigTracker;
@@ -78,6 +79,7 @@
mFakeExecutor,
mCallbackHandler,
mock(AccessPointControllerImpl.class),
+ mock(StatusBarPipelineFlags.class),
mock(DataUsageController.class),
mMockSubDefaults,
mMockProvisionController,
@@ -115,6 +117,7 @@
mFakeExecutor,
mCallbackHandler,
mock(AccessPointControllerImpl.class),
+ mock(StatusBarPipelineFlags.class),
mock(DataUsageController.class),
mMockSubDefaults,
mMockProvisionController,
@@ -150,6 +153,7 @@
mFakeExecutor,
mCallbackHandler,
mock(AccessPointControllerImpl.class),
+ mock(StatusBarPipelineFlags.class),
mock(DataUsageController.class),
mMockSubDefaults,
mock(DeviceProvisionedController.class),
@@ -188,6 +192,7 @@
mFakeExecutor,
mCallbackHandler,
mock(AccessPointControllerImpl.class),
+ mock(StatusBarPipelineFlags.class),
mock(DataUsageController.class),
mMockSubDefaults,
mock(DeviceProvisionedController.class),
@@ -274,6 +279,7 @@
mFakeExecutor,
mCallbackHandler,
mock(AccessPointControllerImpl.class),
+ mock(StatusBarPipelineFlags.class),
mock(DataUsageController.class),
mMockSubDefaults,
mock(DeviceProvisionedController.class),
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ManagedProfileControllerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ManagedProfileControllerImplTest.kt
new file mode 100644
index 0000000..7eba3b46
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ManagedProfileControllerImplTest.kt
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.phone
+
+import android.content.pm.UserInfo
+import android.os.UserManager
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.settings.UserTracker
+import com.android.systemui.util.concurrency.FakeExecutor
+import com.android.systemui.util.time.FakeSystemClock
+import junit.framework.Assert
+import org.junit.Before
+import org.junit.Test
+import org.mockito.Mock
+import org.mockito.Mockito.`when`
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+class ManagedProfileControllerImplTest : SysuiTestCase() {
+
+ private val mainExecutor: FakeExecutor = FakeExecutor(FakeSystemClock())
+
+ private lateinit var controller: ManagedProfileControllerImpl
+
+ @Mock private lateinit var userTracker: UserTracker
+ @Mock private lateinit var userManager: UserManager
+
+ @Before
+ fun setup() {
+ MockitoAnnotations.initMocks(this)
+
+ controller = ManagedProfileControllerImpl(context, mainExecutor, userTracker, userManager)
+ }
+
+ @Test
+ fun hasWorkingProfile_isWorkModeEnabled_returnsTrue() {
+ `when`(userTracker.userId).thenReturn(1)
+ setupWorkingProfile(1)
+
+ Assert.assertEquals(true, controller.hasActiveProfile())
+ }
+
+ @Test
+ fun noWorkingProfile_isWorkModeEnabled_returnsFalse() {
+ `when`(userTracker.userId).thenReturn(1)
+
+ Assert.assertEquals(false, controller.hasActiveProfile())
+ }
+
+ @Test
+ fun listeningUserChanges_isWorkModeEnabled_returnsTrue() {
+ `when`(userTracker.userId).thenReturn(1)
+ controller.addCallback(TestCallback)
+ `when`(userTracker.userId).thenReturn(2)
+ setupWorkingProfile(2)
+
+ Assert.assertEquals(true, controller.hasActiveProfile())
+ }
+
+ private fun setupWorkingProfile(userId: Int) {
+ `when`(userManager.getEnabledProfiles(userId))
+ .thenReturn(
+ listOf(UserInfo(userId, "test_user", "", 0, UserManager.USER_TYPE_PROFILE_MANAGED))
+ )
+ }
+
+ private object TestCallback : ManagedProfileController.Callback {
+
+ override fun onManagedProfileChanged() = Unit
+
+ override fun onManagedProfileRemoved() = Unit
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java
index 471f8d3..14a319b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2018 The Android Open Source Project
+ * Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -11,7 +11,7 @@
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
- * limitations under the License
+ * limitations under the License.
*/
package com.android.systemui.statusbar.phone;
@@ -105,7 +105,6 @@
@Mock private KeyguardBouncer.Factory mKeyguardBouncerFactory;
@Mock private KeyguardMessageAreaController.Factory mKeyguardMessageAreaFactory;
@Mock private KeyguardMessageAreaController mKeyguardMessageAreaController;
- @Mock private KeyguardBouncer mPrimaryBouncer;
@Mock private StatusBarKeyguardViewManager.AlternateBouncer mAlternateBouncer;
@Mock private KeyguardMessageArea mKeyguardMessageArea;
@Mock private ShadeController mShadeController;
@@ -133,16 +132,14 @@
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
- when(mKeyguardBouncerFactory.create(
- any(ViewGroup.class),
- any(KeyguardBouncer.PrimaryBouncerExpansionCallback.class)))
- .thenReturn(mPrimaryBouncer);
when(mCentralSurfaces.getBouncerContainer()).thenReturn(mContainer);
when(mContainer.findViewById(anyInt())).thenReturn(mKeyguardMessageArea);
when(mKeyguardMessageAreaFactory.create(any(KeyguardMessageArea.class)))
.thenReturn(mKeyguardMessageAreaController);
when(mBouncerView.getDelegate()).thenReturn(mBouncerViewDelegate);
+ when(mFeatureFlags.isEnabled(MODERN_BOUNCER)).thenReturn(true);
+
mStatusBarKeyguardViewManager =
new StatusBarKeyguardViewManager(
getContext(),
@@ -184,7 +181,7 @@
mStatusBarKeyguardViewManager.show(null);
ArgumentCaptor<KeyguardBouncer.PrimaryBouncerExpansionCallback> callbackArgumentCaptor =
ArgumentCaptor.forClass(KeyguardBouncer.PrimaryBouncerExpansionCallback.class);
- verify(mKeyguardBouncerFactory).create(any(ViewGroup.class),
+ verify(mPrimaryBouncerCallbackInteractor).addBouncerExpansionCallback(
callbackArgumentCaptor.capture());
mBouncerExpansionCallback = callbackArgumentCaptor.getValue();
}
@@ -195,87 +192,87 @@
Runnable cancelAction = () -> {};
mStatusBarKeyguardViewManager.dismissWithAction(
action, cancelAction, false /* afterKeyguardGone */);
- verify(mPrimaryBouncer).showWithDismissAction(eq(action), eq(cancelAction));
+ verify(mPrimaryBouncerInteractor).setDismissAction(eq(action), eq(cancelAction));
+ verify(mPrimaryBouncerInteractor).show(eq(true));
}
@Test
public void showBouncer_onlyWhenShowing() {
mStatusBarKeyguardViewManager.hide(0 /* startTime */, 0 /* fadeoutDuration */);
mStatusBarKeyguardViewManager.showPrimaryBouncer(true /* scrimmed */);
- verify(mPrimaryBouncer, never()).show(anyBoolean(), anyBoolean());
- verify(mPrimaryBouncer, never()).show(anyBoolean());
+ verify(mPrimaryBouncerInteractor, never()).show(anyBoolean());
}
@Test
public void showBouncer_notWhenBouncerAlreadyShowing() {
mStatusBarKeyguardViewManager.hide(0 /* startTime */, 0 /* fadeoutDuration */);
- when(mPrimaryBouncer.isSecure()).thenReturn(true);
+ when(mKeyguardSecurityModel.getSecurityMode(anyInt())).thenReturn(
+ KeyguardSecurityModel.SecurityMode.Password);
mStatusBarKeyguardViewManager.showPrimaryBouncer(true /* scrimmed */);
- verify(mPrimaryBouncer, never()).show(anyBoolean(), anyBoolean());
- verify(mPrimaryBouncer, never()).show(anyBoolean());
+ verify(mPrimaryBouncerInteractor, never()).show(anyBoolean());
}
@Test
public void showBouncer_showsTheBouncer() {
mStatusBarKeyguardViewManager.showPrimaryBouncer(true /* scrimmed */);
- verify(mPrimaryBouncer).show(anyBoolean(), eq(true));
+ verify(mPrimaryBouncerInteractor).show(eq(true));
}
@Test
public void onPanelExpansionChanged_neverShowsDuringHintAnimation() {
when(mNotificationPanelView.isUnlockHintRunning()).thenReturn(true);
mStatusBarKeyguardViewManager.onPanelExpansionChanged(EXPANSION_EVENT);
- verify(mPrimaryBouncer, never()).setExpansion(anyFloat());
+ verify(mPrimaryBouncerInteractor, never()).setPanelExpansion(anyFloat());
}
@Test
public void onPanelExpansionChanged_propagatesToBouncerOnlyIfShowing() {
mStatusBarKeyguardViewManager.onPanelExpansionChanged(EXPANSION_EVENT);
- verify(mPrimaryBouncer, never()).setExpansion(eq(0.5f));
+ verify(mPrimaryBouncerInteractor, never()).setPanelExpansion(eq(0.5f));
- when(mPrimaryBouncer.isShowing()).thenReturn(true);
+ when(mPrimaryBouncerInteractor.isFullyShowing()).thenReturn(true);
mStatusBarKeyguardViewManager.onPanelExpansionChanged(
expansionEvent(/* fraction= */ 0.6f, /* expanded= */ false, /* tracking= */ true));
- verify(mPrimaryBouncer).setExpansion(eq(0.6f));
+ verify(mPrimaryBouncerInteractor).setPanelExpansion(eq(0.6f));
}
@Test
public void onPanelExpansionChanged_duplicateEventsAreIgnored() {
- when(mPrimaryBouncer.isShowing()).thenReturn(true);
+ when(mPrimaryBouncerInteractor.isFullyShowing()).thenReturn(true);
mStatusBarKeyguardViewManager.onPanelExpansionChanged(EXPANSION_EVENT);
- verify(mPrimaryBouncer).setExpansion(eq(0.5f));
+ verify(mPrimaryBouncerInteractor).setPanelExpansion(eq(0.5f));
- reset(mPrimaryBouncer);
+ reset(mPrimaryBouncerInteractor);
mStatusBarKeyguardViewManager.onPanelExpansionChanged(EXPANSION_EVENT);
- verify(mPrimaryBouncer, never()).setExpansion(eq(0.5f));
+ verify(mPrimaryBouncerInteractor, never()).setPanelExpansion(eq(0.5f));
}
@Test
public void onPanelExpansionChanged_hideBouncer_afterKeyguardHidden() {
mStatusBarKeyguardViewManager.hide(0, 0);
- when(mPrimaryBouncer.inTransit()).thenReturn(true);
+ when(mPrimaryBouncerInteractor.isInTransit()).thenReturn(true);
mStatusBarKeyguardViewManager.onPanelExpansionChanged(EXPANSION_EVENT);
- verify(mPrimaryBouncer).setExpansion(eq(KeyguardBouncer.EXPANSION_HIDDEN));
+ verify(mPrimaryBouncerInteractor).setPanelExpansion(eq(KeyguardBouncer.EXPANSION_HIDDEN));
}
@Test
public void onPanelExpansionChanged_showsBouncerWhenSwiping() {
mKeyguardStateController.setCanDismissLockScreen(false);
mStatusBarKeyguardViewManager.onPanelExpansionChanged(EXPANSION_EVENT);
- verify(mPrimaryBouncer).show(eq(false), eq(false));
+ verify(mPrimaryBouncerInteractor).show(eq(false));
// But not when it's already visible
- reset(mPrimaryBouncer);
- when(mPrimaryBouncer.isShowing()).thenReturn(true);
+ reset(mPrimaryBouncerInteractor);
+ when(mPrimaryBouncerInteractor.isFullyShowing()).thenReturn(true);
mStatusBarKeyguardViewManager.onPanelExpansionChanged(EXPANSION_EVENT);
- verify(mPrimaryBouncer, never()).show(eq(false), eq(false));
+ verify(mPrimaryBouncerInteractor, never()).show(eq(false));
// Or animating away
- reset(mPrimaryBouncer);
- when(mPrimaryBouncer.isAnimatingAway()).thenReturn(true);
+ reset(mPrimaryBouncerInteractor);
+ when(mPrimaryBouncerInteractor.isAnimatingAway()).thenReturn(true);
mStatusBarKeyguardViewManager.onPanelExpansionChanged(EXPANSION_EVENT);
- verify(mPrimaryBouncer, never()).show(eq(false), eq(false));
+ verify(mPrimaryBouncerInteractor, never()).show(eq(false));
}
@Test
@@ -287,7 +284,7 @@
/* fraction= */ KeyguardBouncer.EXPANSION_VISIBLE,
/* expanded= */ true,
/* tracking= */ false));
- verify(mPrimaryBouncer, never()).setExpansion(anyFloat());
+ verify(mPrimaryBouncerInteractor, never()).setPanelExpansion(anyFloat());
}
@Test
@@ -304,7 +301,7 @@
/* fraction= */ KeyguardBouncer.EXPANSION_VISIBLE,
/* expanded= */ true,
/* tracking= */ false));
- verify(mPrimaryBouncer, never()).setExpansion(anyFloat());
+ verify(mPrimaryBouncerInteractor, never()).setPanelExpansion(anyFloat());
}
@Test
@@ -315,7 +312,7 @@
/* fraction= */ KeyguardBouncer.EXPANSION_VISIBLE,
/* expanded= */ true,
/* tracking= */ false));
- verify(mPrimaryBouncer, never()).setExpansion(anyFloat());
+ verify(mPrimaryBouncerInteractor, never()).setPanelExpansion(anyFloat());
}
@Test
@@ -332,7 +329,7 @@
/* fraction= */ KeyguardBouncer.EXPANSION_VISIBLE,
/* expanded= */ true,
/* tracking= */ false));
- verify(mPrimaryBouncer, never()).setExpansion(anyFloat());
+ verify(mPrimaryBouncerInteractor, never()).setPanelExpansion(anyFloat());
}
@Test
@@ -343,7 +340,7 @@
/* fraction= */ KeyguardBouncer.EXPANSION_VISIBLE,
/* expanded= */ true,
/* tracking= */ false));
- verify(mPrimaryBouncer, never()).setExpansion(anyFloat());
+ verify(mPrimaryBouncerInteractor, never()).setPanelExpansion(anyFloat());
}
@Test
@@ -351,7 +348,7 @@
mStatusBarKeyguardViewManager.setOccluded(false /* occluded */, true /* animated */);
verify(mCentralSurfaces).animateKeyguardUnoccluding();
- when(mPrimaryBouncer.isShowing()).thenReturn(true);
+ when(mPrimaryBouncerInteractor.isFullyShowing()).thenReturn(true);
clearInvocations(mCentralSurfaces);
mStatusBarKeyguardViewManager.setOccluded(false /* occluded */, true /* animated */);
verify(mCentralSurfaces, never()).animateKeyguardUnoccluding();
@@ -402,7 +399,7 @@
mStatusBarKeyguardViewManager.dismissWithAction(
action, cancelAction, true /* afterKeyguardGone */);
- when(mPrimaryBouncer.isShowing()).thenReturn(false);
+ when(mPrimaryBouncerInteractor.isFullyShowing()).thenReturn(false);
mStatusBarKeyguardViewManager.hideBouncer(true);
mStatusBarKeyguardViewManager.hide(0, 30);
verify(action, never()).onDismiss();
@@ -416,7 +413,7 @@
mStatusBarKeyguardViewManager.dismissWithAction(
action, cancelAction, true /* afterKeyguardGone */);
- when(mPrimaryBouncer.isShowing()).thenReturn(false);
+ when(mPrimaryBouncerInteractor.isFullyShowing()).thenReturn(false);
mStatusBarKeyguardViewManager.hideBouncer(true);
verify(action, never()).onDismiss();
@@ -438,7 +435,7 @@
@Test
public void testShowing_whenAlternateAuthShowing() {
mStatusBarKeyguardViewManager.setAlternateBouncer(mAlternateBouncer);
- when(mPrimaryBouncer.isShowing()).thenReturn(false);
+ when(mPrimaryBouncerInteractor.isFullyShowing()).thenReturn(false);
when(mAlternateBouncer.isShowingAlternateBouncer()).thenReturn(true);
assertTrue(
"Is showing not accurate when alternative auth showing",
@@ -448,7 +445,7 @@
@Test
public void testWillBeShowing_whenAlternateAuthShowing() {
mStatusBarKeyguardViewManager.setAlternateBouncer(mAlternateBouncer);
- when(mPrimaryBouncer.isShowing()).thenReturn(false);
+ when(mPrimaryBouncerInteractor.isFullyShowing()).thenReturn(false);
when(mAlternateBouncer.isShowingAlternateBouncer()).thenReturn(true);
assertTrue(
"Is or will be showing not accurate when alternative auth showing",
@@ -459,7 +456,7 @@
public void testHideAlternateBouncer_onShowBouncer() {
// GIVEN alt auth is showing
mStatusBarKeyguardViewManager.setAlternateBouncer(mAlternateBouncer);
- when(mPrimaryBouncer.isShowing()).thenReturn(false);
+ when(mPrimaryBouncerInteractor.isFullyShowing()).thenReturn(false);
when(mAlternateBouncer.isShowingAlternateBouncer()).thenReturn(true);
reset(mAlternateBouncer);
@@ -472,8 +469,8 @@
@Test
public void testBouncerIsOrWillBeShowing_whenBouncerIsInTransit() {
- when(mPrimaryBouncer.isShowing()).thenReturn(false);
- when(mPrimaryBouncer.inTransit()).thenReturn(true);
+ when(mPrimaryBouncerInteractor.isFullyShowing()).thenReturn(false);
+ when(mPrimaryBouncerInteractor.isInTransit()).thenReturn(true);
assertTrue(
"Is or will be showing should be true when bouncer is in transit",
@@ -484,7 +481,7 @@
public void testShowAltAuth_unlockingWithBiometricNotAllowed() {
// GIVEN alt auth exists, unlocking with biometric isn't allowed
mStatusBarKeyguardViewManager.setAlternateBouncer(mAlternateBouncer);
- when(mPrimaryBouncer.isShowing()).thenReturn(false);
+ when(mPrimaryBouncerInteractor.isFullyShowing()).thenReturn(false);
when(mKeyguardUpdateMonitor.isUnlockingWithBiometricAllowed(anyBoolean()))
.thenReturn(false);
@@ -493,7 +490,7 @@
mStatusBarKeyguardViewManager.showBouncer(scrimmed);
// THEN regular bouncer is shown
- verify(mPrimaryBouncer).show(anyBoolean(), eq(scrimmed));
+ verify(mPrimaryBouncerInteractor).show(eq(scrimmed));
verify(mAlternateBouncer, never()).showAlternateBouncer();
}
@@ -501,7 +498,7 @@
public void testShowAlternateBouncer_unlockingWithBiometricAllowed() {
// GIVEN alt auth exists, unlocking with biometric is allowed
mStatusBarKeyguardViewManager.setAlternateBouncer(mAlternateBouncer);
- when(mPrimaryBouncer.isShowing()).thenReturn(false);
+ when(mPrimaryBouncerInteractor.isFullyShowing()).thenReturn(false);
when(mKeyguardUpdateMonitor.isUnlockingWithBiometricAllowed(anyBoolean())).thenReturn(true);
// WHEN showGenericBouncer is called
@@ -509,30 +506,28 @@
// THEN alt auth bouncer is shown
verify(mAlternateBouncer).showAlternateBouncer();
- verify(mPrimaryBouncer, never()).show(anyBoolean(), anyBoolean());
+ verify(mPrimaryBouncerInteractor, never()).show(anyBoolean());
}
@Test
public void testUpdateResources_delegatesToBouncer() {
mStatusBarKeyguardViewManager.updateResources();
- verify(mPrimaryBouncer).updateResources();
+ verify(mPrimaryBouncerInteractor).updateResources();
}
@Test
public void updateKeyguardPosition_delegatesToBouncer() {
mStatusBarKeyguardViewManager.updateKeyguardPosition(1.0f);
- verify(mPrimaryBouncer).updateKeyguardPosition(1.0f);
+ verify(mPrimaryBouncerInteractor).setKeyguardPosition(1.0f);
}
@Test
public void testIsBouncerInTransit() {
- when(mPrimaryBouncer.inTransit()).thenReturn(true);
+ when(mPrimaryBouncerInteractor.isInTransit()).thenReturn(true);
Truth.assertThat(mStatusBarKeyguardViewManager.isPrimaryBouncerInTransit()).isTrue();
- when(mPrimaryBouncer.inTransit()).thenReturn(false);
- Truth.assertThat(mStatusBarKeyguardViewManager.isPrimaryBouncerInTransit()).isFalse();
- mPrimaryBouncer = null;
+ when(mPrimaryBouncerInteractor.isInTransit()).thenReturn(false);
Truth.assertThat(mStatusBarKeyguardViewManager.isPrimaryBouncerInTransit()).isFalse();
}
@@ -564,7 +559,7 @@
eq(OnBackInvokedDispatcher.PRIORITY_OVERLAY),
mOnBackInvokedCallback.capture());
- when(mPrimaryBouncer.isShowing()).thenReturn(true);
+ when(mPrimaryBouncerInteractor.isFullyShowing()).thenReturn(true);
when(mCentralSurfaces.shouldKeyguardHideImmediately()).thenReturn(true);
/* invoke the back callback directly */
mOnBackInvokedCallback.getValue().onBackInvoked();
@@ -594,13 +589,6 @@
}
@Test
- public void flag_off_DoesNotCallBouncerInteractor() {
- when(mFeatureFlags.isEnabled(MODERN_BOUNCER)).thenReturn(false);
- mStatusBarKeyguardViewManager.hideBouncer(false);
- verify(mPrimaryBouncerInteractor, never()).hide();
- }
-
- @Test
public void hideAlternateBouncer_beforeCentralSurfacesRegistered() {
mStatusBarKeyguardViewManager =
new StatusBarKeyguardViewManager(
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest_Old.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest_Old.java
new file mode 100644
index 0000000..96fba39
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest_Old.java
@@ -0,0 +1,641 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.phone;
+
+import static com.android.systemui.flags.Flags.MODERN_BOUNCER;
+
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyFloat;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.clearInvocations;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.testing.AndroidTestingRunner;
+import android.testing.TestableLooper;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewRootImpl;
+import android.window.OnBackInvokedCallback;
+import android.window.OnBackInvokedDispatcher;
+import android.window.WindowOnBackInvokedDispatcher;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.internal.util.LatencyTracker;
+import com.android.internal.widget.LockPatternUtils;
+import com.android.keyguard.KeyguardMessageArea;
+import com.android.keyguard.KeyguardMessageAreaController;
+import com.android.keyguard.KeyguardSecurityModel;
+import com.android.keyguard.KeyguardUpdateMonitor;
+import com.android.keyguard.ViewMediatorCallback;
+import com.android.systemui.SysuiTestCase;
+import com.android.systemui.dock.DockManager;
+import com.android.systemui.dreams.DreamOverlayStateController;
+import com.android.systemui.flags.FeatureFlags;
+import com.android.systemui.keyguard.data.BouncerView;
+import com.android.systemui.keyguard.data.BouncerViewDelegate;
+import com.android.systemui.keyguard.domain.interactor.PrimaryBouncerCallbackInteractor;
+import com.android.systemui.keyguard.domain.interactor.PrimaryBouncerInteractor;
+import com.android.systemui.navigationbar.NavigationModeController;
+import com.android.systemui.plugins.ActivityStarter.OnDismissAction;
+import com.android.systemui.shade.NotificationPanelViewController;
+import com.android.systemui.shade.ShadeController;
+import com.android.systemui.shade.ShadeExpansionChangeEvent;
+import com.android.systemui.shade.ShadeExpansionStateManager;
+import com.android.systemui.statusbar.NotificationMediaManager;
+import com.android.systemui.statusbar.NotificationShadeWindowController;
+import com.android.systemui.statusbar.StatusBarState;
+import com.android.systemui.statusbar.SysuiStatusBarStateController;
+import com.android.systemui.statusbar.policy.ConfigurationController;
+import com.android.systemui.unfold.SysUIUnfoldComponent;
+
+import com.google.common.truth.Truth;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.MockitoAnnotations;
+
+import java.util.Optional;
+
+/**
+ * StatusBarKeyguardViewManager Test with deprecated KeyguardBouncer.java.
+ * TODO: Delete when deleting {@link KeyguardBouncer}
+ */
+@SmallTest
+@RunWith(AndroidTestingRunner.class)
+@TestableLooper.RunWithLooper
+public class StatusBarKeyguardViewManagerTest_Old extends SysuiTestCase {
+ private static final ShadeExpansionChangeEvent EXPANSION_EVENT =
+ expansionEvent(/* fraction= */ 0.5f, /* expanded= */ false, /* tracking= */ true);
+
+ @Mock private ViewMediatorCallback mViewMediatorCallback;
+ @Mock private LockPatternUtils mLockPatternUtils;
+ @Mock private CentralSurfaces mCentralSurfaces;
+ @Mock private ViewGroup mContainer;
+ @Mock private NotificationPanelViewController mNotificationPanelView;
+ @Mock private BiometricUnlockController mBiometricUnlockController;
+ @Mock private SysuiStatusBarStateController mStatusBarStateController;
+ @Mock private KeyguardUpdateMonitor mKeyguardUpdateMonitor;
+ @Mock private View mNotificationContainer;
+ @Mock private KeyguardBypassController mBypassController;
+ @Mock private KeyguardBouncer.Factory mKeyguardBouncerFactory;
+ @Mock private KeyguardMessageAreaController.Factory mKeyguardMessageAreaFactory;
+ @Mock private KeyguardMessageAreaController mKeyguardMessageAreaController;
+ @Mock private KeyguardBouncer mPrimaryBouncer;
+ @Mock private StatusBarKeyguardViewManager.AlternateBouncer mAlternateBouncer;
+ @Mock private KeyguardMessageArea mKeyguardMessageArea;
+ @Mock private ShadeController mShadeController;
+ @Mock private SysUIUnfoldComponent mSysUiUnfoldComponent;
+ @Mock private DreamOverlayStateController mDreamOverlayStateController;
+ @Mock private LatencyTracker mLatencyTracker;
+ @Mock private FeatureFlags mFeatureFlags;
+ @Mock private KeyguardSecurityModel mKeyguardSecurityModel;
+ @Mock private PrimaryBouncerCallbackInteractor mPrimaryBouncerCallbackInteractor;
+ @Mock private PrimaryBouncerInteractor mPrimaryBouncerInteractor;
+ @Mock private BouncerView mBouncerView;
+ @Mock private BouncerViewDelegate mBouncerViewDelegate;
+
+ private StatusBarKeyguardViewManager mStatusBarKeyguardViewManager;
+ private KeyguardBouncer.PrimaryBouncerExpansionCallback mBouncerExpansionCallback;
+ private FakeKeyguardStateController mKeyguardStateController =
+ spy(new FakeKeyguardStateController());
+
+ @Mock private ViewRootImpl mViewRootImpl;
+ @Mock private WindowOnBackInvokedDispatcher mOnBackInvokedDispatcher;
+ @Captor
+ private ArgumentCaptor<OnBackInvokedCallback> mOnBackInvokedCallback;
+
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ when(mKeyguardBouncerFactory.create(
+ any(ViewGroup.class),
+ any(KeyguardBouncer.PrimaryBouncerExpansionCallback.class)))
+ .thenReturn(mPrimaryBouncer);
+ when(mCentralSurfaces.getBouncerContainer()).thenReturn(mContainer);
+ when(mContainer.findViewById(anyInt())).thenReturn(mKeyguardMessageArea);
+ when(mKeyguardMessageAreaFactory.create(any(KeyguardMessageArea.class)))
+ .thenReturn(mKeyguardMessageAreaController);
+ when(mBouncerView.getDelegate()).thenReturn(mBouncerViewDelegate);
+
+ mStatusBarKeyguardViewManager =
+ new StatusBarKeyguardViewManager(
+ getContext(),
+ mViewMediatorCallback,
+ mLockPatternUtils,
+ mStatusBarStateController,
+ mock(ConfigurationController.class),
+ mKeyguardUpdateMonitor,
+ mDreamOverlayStateController,
+ mock(NavigationModeController.class),
+ mock(DockManager.class),
+ mock(NotificationShadeWindowController.class),
+ mKeyguardStateController,
+ mock(NotificationMediaManager.class),
+ mKeyguardBouncerFactory,
+ mKeyguardMessageAreaFactory,
+ Optional.of(mSysUiUnfoldComponent),
+ () -> mShadeController,
+ mLatencyTracker,
+ mKeyguardSecurityModel,
+ mFeatureFlags,
+ mPrimaryBouncerCallbackInteractor,
+ mPrimaryBouncerInteractor,
+ mBouncerView) {
+ @Override
+ public ViewRootImpl getViewRootImpl() {
+ return mViewRootImpl;
+ }
+ };
+ when(mViewRootImpl.getOnBackInvokedDispatcher())
+ .thenReturn(mOnBackInvokedDispatcher);
+ mStatusBarKeyguardViewManager.registerCentralSurfaces(
+ mCentralSurfaces,
+ mNotificationPanelView,
+ new ShadeExpansionStateManager(),
+ mBiometricUnlockController,
+ mNotificationContainer,
+ mBypassController);
+ mStatusBarKeyguardViewManager.show(null);
+ ArgumentCaptor<KeyguardBouncer.PrimaryBouncerExpansionCallback> callbackArgumentCaptor =
+ ArgumentCaptor.forClass(KeyguardBouncer.PrimaryBouncerExpansionCallback.class);
+ verify(mKeyguardBouncerFactory).create(any(ViewGroup.class),
+ callbackArgumentCaptor.capture());
+ mBouncerExpansionCallback = callbackArgumentCaptor.getValue();
+ }
+
+ @Test
+ public void dismissWithAction_AfterKeyguardGoneSetToFalse() {
+ OnDismissAction action = () -> false;
+ Runnable cancelAction = () -> {};
+ mStatusBarKeyguardViewManager.dismissWithAction(
+ action, cancelAction, false /* afterKeyguardGone */);
+ verify(mPrimaryBouncer).showWithDismissAction(eq(action), eq(cancelAction));
+ }
+
+ @Test
+ public void showBouncer_onlyWhenShowing() {
+ mStatusBarKeyguardViewManager.hide(0 /* startTime */, 0 /* fadeoutDuration */);
+ mStatusBarKeyguardViewManager.showPrimaryBouncer(true /* scrimmed */);
+ verify(mPrimaryBouncer, never()).show(anyBoolean(), anyBoolean());
+ verify(mPrimaryBouncer, never()).show(anyBoolean());
+ }
+
+ @Test
+ public void showBouncer_notWhenBouncerAlreadyShowing() {
+ mStatusBarKeyguardViewManager.hide(0 /* startTime */, 0 /* fadeoutDuration */);
+ when(mPrimaryBouncer.isSecure()).thenReturn(true);
+ mStatusBarKeyguardViewManager.showPrimaryBouncer(true /* scrimmed */);
+ verify(mPrimaryBouncer, never()).show(anyBoolean(), anyBoolean());
+ verify(mPrimaryBouncer, never()).show(anyBoolean());
+ }
+
+ @Test
+ public void showBouncer_showsTheBouncer() {
+ mStatusBarKeyguardViewManager.showPrimaryBouncer(true /* scrimmed */);
+ verify(mPrimaryBouncer).show(anyBoolean(), eq(true));
+ }
+
+ @Test
+ public void onPanelExpansionChanged_neverShowsDuringHintAnimation() {
+ when(mNotificationPanelView.isUnlockHintRunning()).thenReturn(true);
+ mStatusBarKeyguardViewManager.onPanelExpansionChanged(EXPANSION_EVENT);
+ verify(mPrimaryBouncer, never()).setExpansion(anyFloat());
+ }
+
+ @Test
+ public void onPanelExpansionChanged_propagatesToBouncerOnlyIfShowing() {
+ mStatusBarKeyguardViewManager.onPanelExpansionChanged(EXPANSION_EVENT);
+ verify(mPrimaryBouncer, never()).setExpansion(eq(0.5f));
+
+ when(mPrimaryBouncer.isShowing()).thenReturn(true);
+ mStatusBarKeyguardViewManager.onPanelExpansionChanged(
+ expansionEvent(/* fraction= */ 0.6f, /* expanded= */ false, /* tracking= */ true));
+ verify(mPrimaryBouncer).setExpansion(eq(0.6f));
+ }
+
+ @Test
+ public void onPanelExpansionChanged_duplicateEventsAreIgnored() {
+ when(mPrimaryBouncer.isShowing()).thenReturn(true);
+ mStatusBarKeyguardViewManager.onPanelExpansionChanged(EXPANSION_EVENT);
+ verify(mPrimaryBouncer).setExpansion(eq(0.5f));
+
+ reset(mPrimaryBouncer);
+ mStatusBarKeyguardViewManager.onPanelExpansionChanged(EXPANSION_EVENT);
+ verify(mPrimaryBouncer, never()).setExpansion(eq(0.5f));
+ }
+
+ @Test
+ public void onPanelExpansionChanged_hideBouncer_afterKeyguardHidden() {
+ mStatusBarKeyguardViewManager.hide(0, 0);
+ when(mPrimaryBouncer.inTransit()).thenReturn(true);
+
+ mStatusBarKeyguardViewManager.onPanelExpansionChanged(EXPANSION_EVENT);
+ verify(mPrimaryBouncer).setExpansion(eq(KeyguardBouncer.EXPANSION_HIDDEN));
+ }
+
+ @Test
+ public void onPanelExpansionChanged_showsBouncerWhenSwiping() {
+ mKeyguardStateController.setCanDismissLockScreen(false);
+ mStatusBarKeyguardViewManager.onPanelExpansionChanged(EXPANSION_EVENT);
+ verify(mPrimaryBouncer).show(eq(false), eq(false));
+
+ // But not when it's already visible
+ reset(mPrimaryBouncer);
+ when(mPrimaryBouncer.isShowing()).thenReturn(true);
+ mStatusBarKeyguardViewManager.onPanelExpansionChanged(EXPANSION_EVENT);
+ verify(mPrimaryBouncer, never()).show(eq(false), eq(false));
+
+ // Or animating away
+ reset(mPrimaryBouncer);
+ when(mPrimaryBouncer.isAnimatingAway()).thenReturn(true);
+ mStatusBarKeyguardViewManager.onPanelExpansionChanged(EXPANSION_EVENT);
+ verify(mPrimaryBouncer, never()).show(eq(false), eq(false));
+ }
+
+ @Test
+ public void onPanelExpansionChanged_neverTranslatesBouncerWhenWakeAndUnlock() {
+ when(mBiometricUnlockController.getMode())
+ .thenReturn(BiometricUnlockController.MODE_WAKE_AND_UNLOCK);
+ mStatusBarKeyguardViewManager.onPanelExpansionChanged(
+ expansionEvent(
+ /* fraction= */ KeyguardBouncer.EXPANSION_VISIBLE,
+ /* expanded= */ true,
+ /* tracking= */ false));
+ verify(mPrimaryBouncer, never()).setExpansion(anyFloat());
+ }
+
+ @Test
+ public void onPanelExpansionChanged_neverTranslatesBouncerWhenDismissBouncer() {
+ // Since KeyguardBouncer.EXPANSION_VISIBLE = 0 panel expansion, if the unlock is dismissing
+ // the bouncer, there may be an onPanelExpansionChanged(0) call to collapse the panel
+ // which would mistakenly cause the bouncer to show briefly before its visibility
+ // is set to hide. Therefore, we don't want to propagate panelExpansionChanged to the
+ // bouncer if the bouncer is dismissing as a result of a biometric unlock.
+ when(mBiometricUnlockController.getMode())
+ .thenReturn(BiometricUnlockController.MODE_DISMISS_BOUNCER);
+ mStatusBarKeyguardViewManager.onPanelExpansionChanged(
+ expansionEvent(
+ /* fraction= */ KeyguardBouncer.EXPANSION_VISIBLE,
+ /* expanded= */ true,
+ /* tracking= */ false));
+ verify(mPrimaryBouncer, never()).setExpansion(anyFloat());
+ }
+
+ @Test
+ public void onPanelExpansionChanged_neverTranslatesBouncerWhenOccluded() {
+ when(mKeyguardStateController.isOccluded()).thenReturn(true);
+ mStatusBarKeyguardViewManager.onPanelExpansionChanged(
+ expansionEvent(
+ /* fraction= */ KeyguardBouncer.EXPANSION_VISIBLE,
+ /* expanded= */ true,
+ /* tracking= */ false));
+ verify(mPrimaryBouncer, never()).setExpansion(anyFloat());
+ }
+
+ @Test
+ public void onPanelExpansionChanged_neverTranslatesBouncerWhenShowBouncer() {
+ // Since KeyguardBouncer.EXPANSION_VISIBLE = 0 panel expansion, if the unlock is dismissing
+ // the bouncer, there may be an onPanelExpansionChanged(0) call to collapse the panel
+ // which would mistakenly cause the bouncer to show briefly before its visibility
+ // is set to hide. Therefore, we don't want to propagate panelExpansionChanged to the
+ // bouncer if the bouncer is dismissing as a result of a biometric unlock.
+ when(mBiometricUnlockController.getMode())
+ .thenReturn(BiometricUnlockController.MODE_SHOW_BOUNCER);
+ mStatusBarKeyguardViewManager.onPanelExpansionChanged(
+ expansionEvent(
+ /* fraction= */ KeyguardBouncer.EXPANSION_VISIBLE,
+ /* expanded= */ true,
+ /* tracking= */ false));
+ verify(mPrimaryBouncer, never()).setExpansion(anyFloat());
+ }
+
+ @Test
+ public void onPanelExpansionChanged_neverTranslatesBouncerWhenShadeLocked() {
+ when(mStatusBarStateController.getState()).thenReturn(StatusBarState.SHADE_LOCKED);
+ mStatusBarKeyguardViewManager.onPanelExpansionChanged(
+ expansionEvent(
+ /* fraction= */ KeyguardBouncer.EXPANSION_VISIBLE,
+ /* expanded= */ true,
+ /* tracking= */ false));
+ verify(mPrimaryBouncer, never()).setExpansion(anyFloat());
+ }
+
+ @Test
+ public void setOccluded_animatesPanelExpansion_onlyIfBouncerHidden() {
+ mStatusBarKeyguardViewManager.setOccluded(false /* occluded */, true /* animated */);
+ verify(mCentralSurfaces).animateKeyguardUnoccluding();
+
+ when(mPrimaryBouncer.isShowing()).thenReturn(true);
+ clearInvocations(mCentralSurfaces);
+ mStatusBarKeyguardViewManager.setOccluded(false /* occluded */, true /* animated */);
+ verify(mCentralSurfaces, never()).animateKeyguardUnoccluding();
+ }
+
+ @Test
+ public void setOccluded_onKeyguardOccludedChangedCalled() {
+ clearInvocations(mKeyguardStateController);
+ clearInvocations(mKeyguardUpdateMonitor);
+
+ mStatusBarKeyguardViewManager.setOccluded(false /* occluded */, false /* animated */);
+ verify(mKeyguardStateController).notifyKeyguardState(true, false);
+
+ clearInvocations(mKeyguardUpdateMonitor);
+ clearInvocations(mKeyguardStateController);
+
+ mStatusBarKeyguardViewManager.setOccluded(true /* occluded */, false /* animated */);
+ verify(mKeyguardStateController).notifyKeyguardState(true, true);
+
+ clearInvocations(mKeyguardUpdateMonitor);
+ clearInvocations(mKeyguardStateController);
+
+ mStatusBarKeyguardViewManager.setOccluded(false /* occluded */, false /* animated */);
+ verify(mKeyguardStateController).notifyKeyguardState(true, false);
+ }
+
+ @Test
+ public void setOccluded_isInLaunchTransition_onKeyguardOccludedChangedCalled() {
+ mStatusBarKeyguardViewManager.show(null);
+
+ mStatusBarKeyguardViewManager.setOccluded(true /* occluded */, false /* animated */);
+ verify(mKeyguardStateController).notifyKeyguardState(true, true);
+ }
+
+ @Test
+ public void setOccluded_isLaunchingActivityOverLockscreen_onKeyguardOccludedChangedCalled() {
+ when(mCentralSurfaces.isLaunchingActivityOverLockscreen()).thenReturn(true);
+ mStatusBarKeyguardViewManager.show(null);
+
+ mStatusBarKeyguardViewManager.setOccluded(true /* occluded */, false /* animated */);
+ verify(mKeyguardStateController).notifyKeyguardState(true, true);
+ }
+
+ @Test
+ public void testHiding_cancelsGoneRunnable() {
+ OnDismissAction action = mock(OnDismissAction.class);
+ Runnable cancelAction = mock(Runnable.class);
+ mStatusBarKeyguardViewManager.dismissWithAction(
+ action, cancelAction, true /* afterKeyguardGone */);
+
+ when(mPrimaryBouncer.isShowing()).thenReturn(false);
+ mStatusBarKeyguardViewManager.hideBouncer(true);
+ mStatusBarKeyguardViewManager.hide(0, 30);
+ verify(action, never()).onDismiss();
+ verify(cancelAction).run();
+ }
+
+ @Test
+ public void testHidingBouncer_cancelsGoneRunnable() {
+ OnDismissAction action = mock(OnDismissAction.class);
+ Runnable cancelAction = mock(Runnable.class);
+ mStatusBarKeyguardViewManager.dismissWithAction(
+ action, cancelAction, true /* afterKeyguardGone */);
+
+ when(mPrimaryBouncer.isShowing()).thenReturn(false);
+ mStatusBarKeyguardViewManager.hideBouncer(true);
+
+ verify(action, never()).onDismiss();
+ verify(cancelAction).run();
+ }
+
+ @Test
+ public void testHiding_doesntCancelWhenShowing() {
+ OnDismissAction action = mock(OnDismissAction.class);
+ Runnable cancelAction = mock(Runnable.class);
+ mStatusBarKeyguardViewManager.dismissWithAction(
+ action, cancelAction, true /* afterKeyguardGone */);
+
+ mStatusBarKeyguardViewManager.hide(0, 30);
+ verify(action).onDismiss();
+ verify(cancelAction, never()).run();
+ }
+
+ @Test
+ public void testShowing_whenAlternateAuthShowing() {
+ mStatusBarKeyguardViewManager.setAlternateBouncer(mAlternateBouncer);
+ when(mPrimaryBouncer.isShowing()).thenReturn(false);
+ when(mAlternateBouncer.isShowingAlternateBouncer()).thenReturn(true);
+ assertTrue(
+ "Is showing not accurate when alternative auth showing",
+ mStatusBarKeyguardViewManager.isBouncerShowing());
+ }
+
+ @Test
+ public void testWillBeShowing_whenAlternateAuthShowing() {
+ mStatusBarKeyguardViewManager.setAlternateBouncer(mAlternateBouncer);
+ when(mPrimaryBouncer.isShowing()).thenReturn(false);
+ when(mAlternateBouncer.isShowingAlternateBouncer()).thenReturn(true);
+ assertTrue(
+ "Is or will be showing not accurate when alternative auth showing",
+ mStatusBarKeyguardViewManager.primaryBouncerIsOrWillBeShowing());
+ }
+
+ @Test
+ public void testHideAlternateBouncer_onShowBouncer() {
+ // GIVEN alt auth is showing
+ mStatusBarKeyguardViewManager.setAlternateBouncer(mAlternateBouncer);
+ when(mPrimaryBouncer.isShowing()).thenReturn(false);
+ when(mAlternateBouncer.isShowingAlternateBouncer()).thenReturn(true);
+ reset(mAlternateBouncer);
+
+ // WHEN showBouncer is called
+ mStatusBarKeyguardViewManager.showPrimaryBouncer(true);
+
+ // THEN alt bouncer should be hidden
+ verify(mAlternateBouncer).hideAlternateBouncer();
+ }
+
+ @Test
+ public void testBouncerIsOrWillBeShowing_whenBouncerIsInTransit() {
+ when(mPrimaryBouncer.isShowing()).thenReturn(false);
+ when(mPrimaryBouncer.inTransit()).thenReturn(true);
+
+ assertTrue(
+ "Is or will be showing should be true when bouncer is in transit",
+ mStatusBarKeyguardViewManager.primaryBouncerIsOrWillBeShowing());
+ }
+
+ @Test
+ public void testShowAltAuth_unlockingWithBiometricNotAllowed() {
+ // GIVEN alt auth exists, unlocking with biometric isn't allowed
+ mStatusBarKeyguardViewManager.setAlternateBouncer(mAlternateBouncer);
+ when(mPrimaryBouncer.isShowing()).thenReturn(false);
+ when(mKeyguardUpdateMonitor.isUnlockingWithBiometricAllowed(anyBoolean()))
+ .thenReturn(false);
+
+ // WHEN showGenericBouncer is called
+ final boolean scrimmed = true;
+ mStatusBarKeyguardViewManager.showBouncer(scrimmed);
+
+ // THEN regular bouncer is shown
+ verify(mPrimaryBouncer).show(anyBoolean(), eq(scrimmed));
+ verify(mAlternateBouncer, never()).showAlternateBouncer();
+ }
+
+ @Test
+ public void testShowAlternateBouncer_unlockingWithBiometricAllowed() {
+ // GIVEN alt auth exists, unlocking with biometric is allowed
+ mStatusBarKeyguardViewManager.setAlternateBouncer(mAlternateBouncer);
+ when(mPrimaryBouncer.isShowing()).thenReturn(false);
+ when(mKeyguardUpdateMonitor.isUnlockingWithBiometricAllowed(anyBoolean())).thenReturn(true);
+
+ // WHEN showGenericBouncer is called
+ mStatusBarKeyguardViewManager.showBouncer(true);
+
+ // THEN alt auth bouncer is shown
+ verify(mAlternateBouncer).showAlternateBouncer();
+ verify(mPrimaryBouncer, never()).show(anyBoolean(), anyBoolean());
+ }
+
+ @Test
+ public void testUpdateResources_delegatesToBouncer() {
+ mStatusBarKeyguardViewManager.updateResources();
+
+ verify(mPrimaryBouncer).updateResources();
+ }
+
+ @Test
+ public void updateKeyguardPosition_delegatesToBouncer() {
+ mStatusBarKeyguardViewManager.updateKeyguardPosition(1.0f);
+
+ verify(mPrimaryBouncer).updateKeyguardPosition(1.0f);
+ }
+
+ @Test
+ public void testIsBouncerInTransit() {
+ when(mPrimaryBouncer.inTransit()).thenReturn(true);
+ Truth.assertThat(mStatusBarKeyguardViewManager.isPrimaryBouncerInTransit()).isTrue();
+ when(mPrimaryBouncer.inTransit()).thenReturn(false);
+ Truth.assertThat(mStatusBarKeyguardViewManager.isPrimaryBouncerInTransit()).isFalse();
+ mPrimaryBouncer = null;
+ Truth.assertThat(mStatusBarKeyguardViewManager.isPrimaryBouncerInTransit()).isFalse();
+ }
+
+ private static ShadeExpansionChangeEvent expansionEvent(
+ float fraction, boolean expanded, boolean tracking) {
+ return new ShadeExpansionChangeEvent(
+ fraction, expanded, tracking, /* dragDownPxAmount= */ 0f);
+ }
+
+ @Test
+ public void testPredictiveBackCallback_registration() {
+ /* verify that a predictive back callback is registered when the bouncer becomes visible */
+ mBouncerExpansionCallback.onVisibilityChanged(true);
+ verify(mOnBackInvokedDispatcher).registerOnBackInvokedCallback(
+ eq(OnBackInvokedDispatcher.PRIORITY_OVERLAY),
+ mOnBackInvokedCallback.capture());
+
+ /* verify that the same callback is unregistered when the bouncer becomes invisible */
+ mBouncerExpansionCallback.onVisibilityChanged(false);
+ verify(mOnBackInvokedDispatcher).unregisterOnBackInvokedCallback(
+ eq(mOnBackInvokedCallback.getValue()));
+ }
+
+ @Test
+ public void testPredictiveBackCallback_invocationHidesBouncer() {
+ mBouncerExpansionCallback.onVisibilityChanged(true);
+ /* capture the predictive back callback during registration */
+ verify(mOnBackInvokedDispatcher).registerOnBackInvokedCallback(
+ eq(OnBackInvokedDispatcher.PRIORITY_OVERLAY),
+ mOnBackInvokedCallback.capture());
+
+ when(mPrimaryBouncer.isShowing()).thenReturn(true);
+ when(mCentralSurfaces.shouldKeyguardHideImmediately()).thenReturn(true);
+ /* invoke the back callback directly */
+ mOnBackInvokedCallback.getValue().onBackInvoked();
+
+ /* verify that the bouncer will be hidden as a result of the invocation */
+ verify(mCentralSurfaces).setBouncerShowing(eq(false));
+ }
+
+ @Test
+ public void testReportBouncerOnDreamWhenVisible() {
+ mBouncerExpansionCallback.onVisibilityChanged(true);
+ verify(mCentralSurfaces).setBouncerShowingOverDream(false);
+ Mockito.clearInvocations(mCentralSurfaces);
+ when(mDreamOverlayStateController.isOverlayActive()).thenReturn(true);
+ mBouncerExpansionCallback.onVisibilityChanged(true);
+ verify(mCentralSurfaces).setBouncerShowingOverDream(true);
+ }
+
+ @Test
+ public void testReportBouncerOnDreamWhenNotVisible() {
+ mBouncerExpansionCallback.onVisibilityChanged(false);
+ verify(mCentralSurfaces).setBouncerShowingOverDream(false);
+ Mockito.clearInvocations(mCentralSurfaces);
+ when(mDreamOverlayStateController.isOverlayActive()).thenReturn(true);
+ mBouncerExpansionCallback.onVisibilityChanged(false);
+ verify(mCentralSurfaces).setBouncerShowingOverDream(false);
+ }
+
+ @Test
+ public void flag_off_DoesNotCallBouncerInteractor() {
+ when(mFeatureFlags.isEnabled(MODERN_BOUNCER)).thenReturn(false);
+ mStatusBarKeyguardViewManager.hideBouncer(false);
+ verify(mPrimaryBouncerInteractor, never()).hide();
+ }
+
+ @Test
+ public void hideAlternateBouncer_beforeCentralSurfacesRegistered() {
+ mStatusBarKeyguardViewManager =
+ new StatusBarKeyguardViewManager(
+ getContext(),
+ mViewMediatorCallback,
+ mLockPatternUtils,
+ mStatusBarStateController,
+ mock(ConfigurationController.class),
+ mKeyguardUpdateMonitor,
+ mDreamOverlayStateController,
+ mock(NavigationModeController.class),
+ mock(DockManager.class),
+ mock(NotificationShadeWindowController.class),
+ mKeyguardStateController,
+ mock(NotificationMediaManager.class),
+ mKeyguardBouncerFactory,
+ mKeyguardMessageAreaFactory,
+ Optional.of(mSysUiUnfoldComponent),
+ () -> mShadeController,
+ mLatencyTracker,
+ mKeyguardSecurityModel,
+ mFeatureFlags,
+ mPrimaryBouncerCallbackInteractor,
+ mPrimaryBouncerInteractor,
+ mBouncerView) {
+ @Override
+ public ViewRootImpl getViewRootImpl() {
+ return mViewRootImpl;
+ }
+ };
+
+ // the following call before registering centralSurfaces should NOT throw a NPE:
+ mStatusBarKeyguardViewManager.hideAlternateBouncer(true);
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionRepository.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionRepository.kt
index 288f54c..5265ec6 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionRepository.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionRepository.kt
@@ -16,12 +16,13 @@
package com.android.systemui.statusbar.pipeline.mobile.data.repository
-import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileSubscriptionModel
+import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel
import kotlinx.coroutines.flow.MutableStateFlow
-class FakeMobileConnectionRepository : MobileConnectionRepository {
- private val _subscriptionsModelFlow = MutableStateFlow(MobileSubscriptionModel())
- override val subscriptionModelFlow = _subscriptionsModelFlow
+// TODO(b/261632894): remove this in favor of the real impl or DemoMobileConnectionRepository
+class FakeMobileConnectionRepository(override val subId: Int) : MobileConnectionRepository {
+ private val _connectionInfo = MutableStateFlow(MobileConnectionModel())
+ override val connectionInfo = _connectionInfo
private val _dataEnabled = MutableStateFlow(true)
override val dataEnabled = _dataEnabled
@@ -29,8 +30,8 @@
private val _isDefaultDataSubscription = MutableStateFlow(true)
override val isDefaultDataSubscription = _isDefaultDataSubscription
- fun setMobileSubscriptionModel(model: MobileSubscriptionModel) {
- _subscriptionsModelFlow.value = model
+ fun setConnectionInfo(model: MobileConnectionModel) {
+ _connectionInfo.value = model
}
fun setDataEnabled(enabled: Boolean) {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionsRepository.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionsRepository.kt
index 533d5d9..d6af0e6 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionsRepository.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionsRepository.kt
@@ -16,23 +16,43 @@
package com.android.systemui.statusbar.pipeline.mobile.data.repository
-import android.telephony.SubscriptionInfo
import android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID
-import com.android.settingslib.mobile.MobileMappings.Config
+import android.telephony.TelephonyDisplayInfo
+import android.telephony.TelephonyManager
+import com.android.settingslib.SignalIcon
+import com.android.settingslib.mobile.TelephonyIcons
import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectivityModel
-import kotlinx.coroutines.flow.Flow
+import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel
+import com.android.systemui.statusbar.pipeline.mobile.util.MobileMappingsProxy
import kotlinx.coroutines.flow.MutableStateFlow
-class FakeMobileConnectionsRepository : MobileConnectionsRepository {
- private val _subscriptionsFlow = MutableStateFlow<List<SubscriptionInfo>>(listOf())
- override val subscriptionsFlow: Flow<List<SubscriptionInfo>> = _subscriptionsFlow
+// TODO(b/261632894): remove this in favor of the real impl or DemoMobileConnectionsRepository
+class FakeMobileConnectionsRepository(mobileMappings: MobileMappingsProxy) :
+ MobileConnectionsRepository {
+ val GSM_KEY = mobileMappings.toIconKey(GSM)
+ val LTE_KEY = mobileMappings.toIconKey(LTE)
+ val UMTS_KEY = mobileMappings.toIconKey(UMTS)
+ val LTE_ADVANCED_KEY = mobileMappings.toIconKeyOverride(LTE_ADVANCED_PRO)
+
+ /**
+ * To avoid a reliance on [MobileMappings], we'll build a simpler map from network type to
+ * mobile icon. See TelephonyManager.NETWORK_TYPES for a list of types and [TelephonyIcons] for
+ * the exhaustive set of icons
+ */
+ val TEST_MAPPING: Map<String, SignalIcon.MobileIconGroup> =
+ mapOf(
+ GSM_KEY to TelephonyIcons.THREE_G,
+ LTE_KEY to TelephonyIcons.LTE,
+ UMTS_KEY to TelephonyIcons.FOUR_G,
+ LTE_ADVANCED_KEY to TelephonyIcons.NR_5G,
+ )
+
+ private val _subscriptions = MutableStateFlow<List<SubscriptionModel>>(listOf())
+ override val subscriptions = _subscriptions
private val _activeMobileDataSubscriptionId = MutableStateFlow(INVALID_SUBSCRIPTION_ID)
override val activeMobileDataSubscriptionId = _activeMobileDataSubscriptionId
- private val _defaultDataSubRatConfig = MutableStateFlow(Config())
- override val defaultDataSubRatConfig = _defaultDataSubRatConfig
-
private val _defaultDataSubId = MutableStateFlow(INVALID_SUBSCRIPTION_ID)
override val defaultDataSubId = _defaultDataSubId
@@ -41,18 +61,21 @@
private val subIdRepos = mutableMapOf<Int, MobileConnectionRepository>()
override fun getRepoForSubId(subId: Int): MobileConnectionRepository {
- return subIdRepos[subId] ?: FakeMobileConnectionRepository().also { subIdRepos[subId] = it }
+ return subIdRepos[subId]
+ ?: FakeMobileConnectionRepository(subId).also { subIdRepos[subId] = it }
}
private val _globalMobileDataSettingChangedEvent = MutableStateFlow(Unit)
override val globalMobileDataSettingChangedEvent = _globalMobileDataSettingChangedEvent
- fun setSubscriptions(subs: List<SubscriptionInfo>) {
- _subscriptionsFlow.value = subs
- }
+ private val _defaultMobileIconMapping = MutableStateFlow(TEST_MAPPING)
+ override val defaultMobileIconMapping = _defaultMobileIconMapping
- fun setDefaultDataSubRatConfig(config: Config) {
- _defaultDataSubRatConfig.value = config
+ private val _defaultMobileIconGroup = MutableStateFlow(DEFAULT_ICON)
+ override val defaultMobileIconGroup = _defaultMobileIconGroup
+
+ fun setSubscriptions(subs: List<SubscriptionModel>) {
+ _subscriptions.value = subs
}
fun setDefaultDataSubId(id: Int) {
@@ -74,4 +97,14 @@
fun setMobileConnectionRepositoryMap(connections: Map<Int, MobileConnectionRepository>) {
connections.forEach { entry -> subIdRepos[entry.key] = entry.value }
}
+
+ companion object {
+ val DEFAULT_ICON = TelephonyIcons.G
+
+ // Use [MobileMappings] to define some simple definitions
+ const val GSM = TelephonyManager.NETWORK_TYPE_GSM
+ const val LTE = TelephonyManager.NETWORK_TYPE_LTE
+ const val UMTS = TelephonyManager.NETWORK_TYPE_UMTS
+ const val LTE_ADVANCED_PRO = TelephonyDisplayInfo.OVERRIDE_NETWORK_TYPE_LTE_ADVANCED_PRO
+ }
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileRepositorySwitcherTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileRepositorySwitcherTest.kt
new file mode 100644
index 0000000..18ae90d
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileRepositorySwitcherTest.kt
@@ -0,0 +1,225 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.mobile.data.repository
+
+import android.net.ConnectivityManager
+import android.telephony.SubscriptionInfo
+import android.telephony.SubscriptionManager
+import android.telephony.TelephonyManager
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.demomode.DemoMode
+import com.android.systemui.demomode.DemoModeController
+import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.DemoMobileConnectionsRepository
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.DemoModeMobileConnectionDataSource
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.model.FakeNetworkEventModel
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.validMobileEvent
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.prod.MobileConnectionsRepositoryImpl
+import com.android.systemui.statusbar.pipeline.mobile.util.FakeMobileMappingsProxy
+import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.kotlinArgumentCaptor
+import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.whenever
+import com.android.systemui.util.settings.FakeSettings
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.runBlocking
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.mockito.Mock
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+/**
+ * The switcher acts as a dispatcher to either the `prod` or `demo` versions of the repository
+ * interface it's switching on. These tests just need to verify that the entire interface properly
+ * switches over when the value of `demoMode` changes
+ */
+@Suppress("EXPERIMENTAL_IS_NOT_ENABLED")
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(JUnit4::class)
+class MobileRepositorySwitcherTest : SysuiTestCase() {
+ private lateinit var underTest: MobileRepositorySwitcher
+ private lateinit var realRepo: MobileConnectionsRepositoryImpl
+ private lateinit var demoRepo: DemoMobileConnectionsRepository
+ private lateinit var mockDataSource: DemoModeMobileConnectionDataSource
+
+ @Mock private lateinit var connectivityManager: ConnectivityManager
+ @Mock private lateinit var subscriptionManager: SubscriptionManager
+ @Mock private lateinit var telephonyManager: TelephonyManager
+ @Mock private lateinit var logger: ConnectivityPipelineLogger
+ @Mock private lateinit var demoModeController: DemoModeController
+
+ private val globalSettings = FakeSettings()
+ private val fakeNetworkEventsFlow = MutableStateFlow<FakeNetworkEventModel?>(null)
+ private val mobileMappings = FakeMobileMappingsProxy()
+
+ private val scope = CoroutineScope(IMMEDIATE)
+
+ @Before
+ fun setUp() {
+ MockitoAnnotations.initMocks(this)
+
+ // Never start in demo mode
+ whenever(demoModeController.isInDemoMode).thenReturn(false)
+
+ mockDataSource =
+ mock<DemoModeMobileConnectionDataSource>().also {
+ whenever(it.mobileEvents).thenReturn(fakeNetworkEventsFlow)
+ }
+
+ realRepo =
+ MobileConnectionsRepositoryImpl(
+ connectivityManager,
+ subscriptionManager,
+ telephonyManager,
+ logger,
+ mobileMappings,
+ fakeBroadcastDispatcher,
+ globalSettings,
+ context,
+ IMMEDIATE,
+ scope,
+ mock(),
+ )
+
+ demoRepo =
+ DemoMobileConnectionsRepository(
+ dataSource = mockDataSource,
+ scope = scope,
+ context = context,
+ )
+
+ underTest =
+ MobileRepositorySwitcher(
+ scope = scope,
+ realRepository = realRepo,
+ demoMobileConnectionsRepository = demoRepo,
+ demoModeController = demoModeController,
+ )
+ }
+
+ @After
+ fun tearDown() {
+ scope.cancel()
+ }
+
+ @Test
+ fun `active repo matches demo mode setting`() =
+ runBlocking(IMMEDIATE) {
+ whenever(demoModeController.isInDemoMode).thenReturn(false)
+
+ var latest: MobileConnectionsRepository? = null
+ val job = underTest.activeRepo.onEach { latest = it }.launchIn(this)
+
+ assertThat(latest).isEqualTo(realRepo)
+
+ startDemoMode()
+
+ assertThat(latest).isEqualTo(demoRepo)
+
+ finishDemoMode()
+
+ assertThat(latest).isEqualTo(realRepo)
+
+ job.cancel()
+ }
+
+ @Test
+ fun `subscription list updates when demo mode changes`() =
+ runBlocking(IMMEDIATE) {
+ whenever(demoModeController.isInDemoMode).thenReturn(false)
+
+ whenever(subscriptionManager.completeActiveSubscriptionInfoList)
+ .thenReturn(listOf(SUB_1, SUB_2))
+
+ var latest: List<SubscriptionModel>? = null
+ val job = underTest.subscriptions.onEach { latest = it }.launchIn(this)
+
+ // The real subscriptions has 2 subs
+ whenever(subscriptionManager.completeActiveSubscriptionInfoList)
+ .thenReturn(listOf(SUB_1, SUB_2))
+ getSubscriptionCallback().onSubscriptionsChanged()
+
+ assertThat(latest).isEqualTo(listOf(MODEL_1, MODEL_2))
+
+ // Demo mode turns on, and we should see only the demo subscriptions
+ startDemoMode()
+ fakeNetworkEventsFlow.value = validMobileEvent(subId = 3)
+
+ // Demo mobile connections repository makes arbitrarily-formed subscription info
+ // objects, so just validate the data we care about
+ assertThat(latest).hasSize(1)
+ assertThat(latest!![0].subscriptionId).isEqualTo(3)
+
+ finishDemoMode()
+
+ assertThat(latest).isEqualTo(listOf(MODEL_1, MODEL_2))
+
+ job.cancel()
+ }
+
+ private fun startDemoMode() {
+ whenever(demoModeController.isInDemoMode).thenReturn(true)
+ getDemoModeCallback().onDemoModeStarted()
+ }
+
+ private fun finishDemoMode() {
+ whenever(demoModeController.isInDemoMode).thenReturn(false)
+ getDemoModeCallback().onDemoModeFinished()
+ }
+
+ private fun getSubscriptionCallback(): SubscriptionManager.OnSubscriptionsChangedListener {
+ val callbackCaptor =
+ kotlinArgumentCaptor<SubscriptionManager.OnSubscriptionsChangedListener>()
+ verify(subscriptionManager)
+ .addOnSubscriptionsChangedListener(any(), callbackCaptor.capture())
+ return callbackCaptor.value
+ }
+
+ private fun getDemoModeCallback(): DemoMode {
+ val captor = kotlinArgumentCaptor<DemoMode>()
+ verify(demoModeController).addCallback(captor.capture())
+ return captor.value
+ }
+
+ companion object {
+ private val IMMEDIATE = Dispatchers.Main.immediate
+
+ private const val SUB_1_ID = 1
+ private val SUB_1 =
+ mock<SubscriptionInfo>().also { whenever(it.subscriptionId).thenReturn(SUB_1_ID) }
+ private val MODEL_1 = SubscriptionModel(subscriptionId = SUB_1_ID)
+
+ private const val SUB_2_ID = 2
+ private val SUB_2 =
+ mock<SubscriptionInfo>().also { whenever(it.subscriptionId).thenReturn(SUB_2_ID) }
+ private val MODEL_2 = SubscriptionModel(subscriptionId = SUB_2_ID)
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionParameterizedTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionParameterizedTest.kt
new file mode 100644
index 0000000..e943de2
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionParameterizedTest.kt
@@ -0,0 +1,249 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.mobile.data.repository.demo
+
+import android.telephony.Annotation
+import android.telephony.TelephonyManager
+import androidx.test.filters.SmallTest
+import com.android.settingslib.SignalIcon
+import com.android.settingslib.mobile.TelephonyIcons
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.statusbar.pipeline.mobile.data.model.DataConnectionState
+import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.model.FakeNetworkEventModel
+import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.whenever
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.runTest
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+import org.junit.runners.Parameterized.Parameters
+
+/**
+ * Parameterized test for all of the common values of [FakeNetworkEventModel]. This test simply
+ * verifies that passing the given model to [DemoMobileConnectionsRepository] results in the correct
+ * flows emitting from the given connection.
+ */
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(Parameterized::class)
+internal class DemoMobileConnectionParameterizedTest(private val testCase: TestCase) :
+ SysuiTestCase() {
+ private val testDispatcher = UnconfinedTestDispatcher()
+ private val testScope = TestScope(testDispatcher)
+
+ private val fakeNetworkEventFlow = MutableStateFlow<FakeNetworkEventModel?>(null)
+
+ private lateinit var connectionsRepo: DemoMobileConnectionsRepository
+ private lateinit var underTest: DemoMobileConnectionRepository
+ private lateinit var mockDataSource: DemoModeMobileConnectionDataSource
+
+ @Before
+ fun setUp() {
+ // The data source only provides one API, so we can mock it with a flow here for convenience
+ mockDataSource =
+ mock<DemoModeMobileConnectionDataSource>().also {
+ whenever(it.mobileEvents).thenReturn(fakeNetworkEventFlow)
+ }
+
+ connectionsRepo =
+ DemoMobileConnectionsRepository(
+ dataSource = mockDataSource,
+ scope = testScope.backgroundScope,
+ context = context,
+ )
+
+ connectionsRepo.startProcessingCommands()
+ }
+
+ @After
+ fun tearDown() {
+ testScope.cancel()
+ }
+
+ @Test
+ fun demoNetworkData() =
+ testScope.runTest {
+ val networkModel =
+ FakeNetworkEventModel.Mobile(
+ level = testCase.level,
+ dataType = testCase.dataType,
+ subId = testCase.subId,
+ carrierId = testCase.carrierId,
+ inflateStrength = testCase.inflateStrength,
+ activity = testCase.activity,
+ carrierNetworkChange = testCase.carrierNetworkChange,
+ )
+
+ fakeNetworkEventFlow.value = networkModel
+ underTest = connectionsRepo.getRepoForSubId(subId)
+
+ assertConnection(underTest, networkModel)
+ }
+
+ private fun assertConnection(
+ conn: DemoMobileConnectionRepository,
+ model: FakeNetworkEventModel
+ ) {
+ when (model) {
+ is FakeNetworkEventModel.Mobile -> {
+ val connectionInfo: MobileConnectionModel = conn.connectionInfo.value
+ assertThat(conn.subId).isEqualTo(model.subId)
+ assertThat(connectionInfo.cdmaLevel).isEqualTo(model.level)
+ assertThat(connectionInfo.primaryLevel).isEqualTo(model.level)
+ assertThat(connectionInfo.dataActivityDirection).isEqualTo(model.activity)
+ assertThat(connectionInfo.carrierNetworkChangeActive)
+ .isEqualTo(model.carrierNetworkChange)
+
+ // TODO(b/261029387): check these once we start handling them
+ assertThat(connectionInfo.isEmergencyOnly).isFalse()
+ assertThat(connectionInfo.isGsm).isFalse()
+ assertThat(connectionInfo.dataConnectionState)
+ .isEqualTo(DataConnectionState.Connected)
+ }
+ // MobileDisabled isn't combinatorial in nature, and is tested in
+ // DemoMobileConnectionsRepositoryTest.kt
+ else -> {}
+ }
+ }
+
+ /** Matches [FakeNetworkEventModel] */
+ internal data class TestCase(
+ val level: Int,
+ val dataType: SignalIcon.MobileIconGroup,
+ val subId: Int,
+ val carrierId: Int,
+ val inflateStrength: Boolean,
+ @Annotation.DataActivityType val activity: Int,
+ val carrierNetworkChange: Boolean,
+ ) {
+ override fun toString(): String {
+ return "INPUT(level=$level, " +
+ "dataType=${dataType.name}, " +
+ "subId=$subId, " +
+ "carrierId=$carrierId, " +
+ "inflateStrength=$inflateStrength, " +
+ "activity=$activity, " +
+ "carrierNetworkChange=$carrierNetworkChange)"
+ }
+
+ // Convenience for iterating test data and creating new cases
+ fun modifiedBy(
+ level: Int? = null,
+ dataType: SignalIcon.MobileIconGroup? = null,
+ subId: Int? = null,
+ carrierId: Int? = null,
+ inflateStrength: Boolean? = null,
+ @Annotation.DataActivityType activity: Int? = null,
+ carrierNetworkChange: Boolean? = null,
+ ): TestCase =
+ TestCase(
+ level = level ?: this.level,
+ dataType = dataType ?: this.dataType,
+ subId = subId ?: this.subId,
+ carrierId = carrierId ?: this.carrierId,
+ inflateStrength = inflateStrength ?: this.inflateStrength,
+ activity = activity ?: this.activity,
+ carrierNetworkChange = carrierNetworkChange ?: this.carrierNetworkChange
+ )
+ }
+
+ companion object {
+ private val subId = 1
+
+ private val booleanList = listOf(true, false)
+ private val levels = listOf(0, 1, 2, 3)
+ private val dataTypes =
+ listOf(
+ TelephonyIcons.THREE_G,
+ TelephonyIcons.LTE,
+ TelephonyIcons.FOUR_G,
+ TelephonyIcons.NR_5G,
+ TelephonyIcons.NR_5G_PLUS,
+ )
+ private val carrierIds = listOf(1, 10, 100)
+ private val inflateStrength = booleanList
+ private val activity =
+ listOf(
+ TelephonyManager.DATA_ACTIVITY_NONE,
+ TelephonyManager.DATA_ACTIVITY_IN,
+ TelephonyManager.DATA_ACTIVITY_OUT,
+ TelephonyManager.DATA_ACTIVITY_INOUT
+ )
+ private val carrierNetworkChange = booleanList
+
+ @Parameters(name = "{0}") @JvmStatic fun data() = testData()
+
+ /**
+ * Generate some test data. For the sake of convenience, we'll parameterize only non-null
+ * network event data. So given the lists of test data:
+ * ```
+ * list1 = [1, 2, 3]
+ * list2 = [false, true]
+ * list3 = [a, b, c]
+ * ```
+ * We'll generate test cases for:
+ *
+ * Test (1, false, a) Test (2, false, a) Test (3, false, a) Test (1, true, a) Test (1,
+ * false, b) Test (1, false, c)
+ *
+ * NOTE: this is not a combinatorial product of all of the possible sets of parameters.
+ * Since this test is built to exercise demo mode, the general approach is to define a
+ * fully-formed "base case", and from there to make sure to use every valid parameter once,
+ * by defining the rest of the test cases against the base case. Specific use-cases can be
+ * added to the non-parameterized test, or manually below the generated test cases.
+ */
+ private fun testData(): List<TestCase> {
+ val testSet = mutableSetOf<TestCase>()
+
+ val baseCase =
+ TestCase(
+ levels.first(),
+ dataTypes.first(),
+ subId,
+ carrierIds.first(),
+ inflateStrength.first(),
+ activity.first(),
+ carrierNetworkChange.first()
+ )
+
+ val tail =
+ sequenceOf(
+ levels.map { baseCase.modifiedBy(level = it) },
+ dataTypes.map { baseCase.modifiedBy(dataType = it) },
+ carrierIds.map { baseCase.modifiedBy(carrierId = it) },
+ inflateStrength.map { baseCase.modifiedBy(inflateStrength = it) },
+ activity.map { baseCase.modifiedBy(activity = it) },
+ carrierNetworkChange.map { baseCase.modifiedBy(carrierNetworkChange = it) },
+ )
+ .flatten()
+
+ testSet.add(baseCase)
+ tail.toCollection(testSet)
+
+ return testSet.toList()
+ }
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionsRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionsRepositoryTest.kt
new file mode 100644
index 0000000..32d0410
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionsRepositoryTest.kt
@@ -0,0 +1,325 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.mobile.data.repository.demo
+
+import android.telephony.TelephonyManager.DATA_ACTIVITY_INOUT
+import android.telephony.TelephonyManager.UNKNOWN_CARRIER_ID
+import androidx.test.filters.SmallTest
+import com.android.settingslib.SignalIcon
+import com.android.settingslib.mobile.TelephonyIcons.THREE_G
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.statusbar.pipeline.mobile.data.model.DataConnectionState
+import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel
+import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.model.FakeNetworkEventModel
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.model.FakeNetworkEventModel.MobileDisabled
+import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.whenever
+import com.google.common.truth.Truth.assertThat
+import junit.framework.Assert
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+class DemoMobileConnectionsRepositoryTest : SysuiTestCase() {
+ private val testDispatcher = UnconfinedTestDispatcher()
+ private val testScope = TestScope(testDispatcher)
+
+ private val fakeNetworkEventFlow = MutableStateFlow<FakeNetworkEventModel?>(null)
+
+ private lateinit var underTest: DemoMobileConnectionsRepository
+ private lateinit var mockDataSource: DemoModeMobileConnectionDataSource
+
+ @Before
+ fun setUp() {
+ // The data source only provides one API, so we can mock it with a flow here for convenience
+ mockDataSource =
+ mock<DemoModeMobileConnectionDataSource>().also {
+ whenever(it.mobileEvents).thenReturn(fakeNetworkEventFlow)
+ }
+
+ underTest =
+ DemoMobileConnectionsRepository(
+ dataSource = mockDataSource,
+ scope = testScope.backgroundScope,
+ context = context,
+ )
+
+ underTest.startProcessingCommands()
+ }
+
+ @Test
+ fun `network event - create new subscription`() =
+ testScope.runTest {
+ var latest: List<SubscriptionModel>? = null
+ val job = underTest.subscriptions.onEach { latest = it }.launchIn(this)
+
+ assertThat(latest).isEmpty()
+
+ fakeNetworkEventFlow.value = validMobileEvent(subId = 1)
+
+ assertThat(latest).hasSize(1)
+ assertThat(latest!![0].subscriptionId).isEqualTo(1)
+
+ job.cancel()
+ }
+
+ @Test
+ fun `network event - reuses subscription when same Id`() =
+ testScope.runTest {
+ var latest: List<SubscriptionModel>? = null
+ val job = underTest.subscriptions.onEach { latest = it }.launchIn(this)
+
+ assertThat(latest).isEmpty()
+
+ fakeNetworkEventFlow.value = validMobileEvent(subId = 1, level = 1)
+
+ assertThat(latest).hasSize(1)
+ assertThat(latest!![0].subscriptionId).isEqualTo(1)
+
+ // Second network event comes in with the same subId, does not create a new subscription
+ fakeNetworkEventFlow.value = validMobileEvent(subId = 1, level = 2)
+
+ assertThat(latest).hasSize(1)
+ assertThat(latest!![0].subscriptionId).isEqualTo(1)
+
+ job.cancel()
+ }
+
+ @Test
+ fun `multiple subscriptions`() =
+ testScope.runTest {
+ var latest: List<SubscriptionModel>? = null
+ val job = underTest.subscriptions.onEach { latest = it }.launchIn(this)
+
+ fakeNetworkEventFlow.value = validMobileEvent(subId = 1)
+ fakeNetworkEventFlow.value = validMobileEvent(subId = 2)
+
+ assertThat(latest).hasSize(2)
+
+ job.cancel()
+ }
+
+ @Test
+ fun `mobile disabled event - disables connection - subId specified - single conn`() =
+ testScope.runTest {
+ var latest: List<SubscriptionModel>? = null
+ val job = underTest.subscriptions.onEach { latest = it }.launchIn(this)
+
+ fakeNetworkEventFlow.value = validMobileEvent(subId = 1, level = 1)
+
+ fakeNetworkEventFlow.value = MobileDisabled(subId = 1)
+
+ assertThat(latest).hasSize(0)
+
+ job.cancel()
+ }
+
+ @Test
+ fun `mobile disabled event - disables connection - subId not specified - single conn`() =
+ testScope.runTest {
+ var latest: List<SubscriptionModel>? = null
+ val job = underTest.subscriptions.onEach { latest = it }.launchIn(this)
+
+ fakeNetworkEventFlow.value = validMobileEvent(subId = 1, level = 1)
+
+ fakeNetworkEventFlow.value = MobileDisabled(subId = null)
+
+ assertThat(latest).hasSize(0)
+
+ job.cancel()
+ }
+
+ @Test
+ fun `mobile disabled event - disables connection - subId specified - multiple conn`() =
+ testScope.runTest {
+ var latest: List<SubscriptionModel>? = null
+ val job = underTest.subscriptions.onEach { latest = it }.launchIn(this)
+
+ fakeNetworkEventFlow.value = validMobileEvent(subId = 1, level = 1)
+ fakeNetworkEventFlow.value = validMobileEvent(subId = 2, level = 1)
+
+ fakeNetworkEventFlow.value = MobileDisabled(subId = 2)
+
+ assertThat(latest).hasSize(1)
+
+ job.cancel()
+ }
+
+ @Test
+ fun `mobile disabled event - subId not specified - multiple conn - ignores command`() =
+ testScope.runTest {
+ var latest: List<SubscriptionModel>? = null
+ val job = underTest.subscriptions.onEach { latest = it }.launchIn(this)
+
+ fakeNetworkEventFlow.value = validMobileEvent(subId = 1, level = 1)
+ fakeNetworkEventFlow.value = validMobileEvent(subId = 2, level = 1)
+
+ fakeNetworkEventFlow.value = MobileDisabled(subId = null)
+
+ assertThat(latest).hasSize(2)
+
+ job.cancel()
+ }
+
+ /** Regression test for b/261706421 */
+ @Test
+ fun `multiple connections - remove all - does not throw`() =
+ testScope.runTest {
+ var latest: List<SubscriptionModel>? = null
+ val job = underTest.subscriptions.onEach { latest = it }.launchIn(this)
+
+ // Two subscriptions are added
+ fakeNetworkEventFlow.value = validMobileEvent(subId = 1, level = 1)
+ fakeNetworkEventFlow.value = validMobileEvent(subId = 2, level = 1)
+
+ // Then both are removed by turning off demo mode
+ underTest.stopProcessingCommands()
+
+ assertThat(latest).isEmpty()
+
+ job.cancel()
+ }
+
+ @Test
+ fun `demo connection - single subscription`() =
+ testScope.runTest {
+ var currentEvent: FakeNetworkEventModel = validMobileEvent(subId = 1)
+ var connections: List<DemoMobileConnectionRepository>? = null
+ val job =
+ underTest.subscriptions
+ .onEach { infos ->
+ connections =
+ infos.map { info -> underTest.getRepoForSubId(info.subscriptionId) }
+ }
+ .launchIn(this)
+
+ fakeNetworkEventFlow.value = currentEvent
+
+ assertThat(connections).hasSize(1)
+ val connection1 = connections!![0]
+
+ assertConnection(connection1, currentEvent)
+
+ // Exercise the whole api
+
+ currentEvent = validMobileEvent(subId = 1, level = 2)
+ fakeNetworkEventFlow.value = currentEvent
+ assertConnection(connection1, currentEvent)
+
+ job.cancel()
+ }
+
+ @Test
+ fun `demo connection - two connections - update second - no affect on first`() =
+ testScope.runTest {
+ var currentEvent1 = validMobileEvent(subId = 1)
+ var connection1: DemoMobileConnectionRepository? = null
+ var currentEvent2 = validMobileEvent(subId = 2)
+ var connection2: DemoMobileConnectionRepository? = null
+ var connections: List<DemoMobileConnectionRepository>? = null
+ val job =
+ underTest.subscriptions
+ .onEach { infos ->
+ connections =
+ infos.map { info -> underTest.getRepoForSubId(info.subscriptionId) }
+ }
+ .launchIn(this)
+
+ fakeNetworkEventFlow.value = currentEvent1
+ fakeNetworkEventFlow.value = currentEvent2
+ assertThat(connections).hasSize(2)
+ connections!!.forEach {
+ if (it.subId == 1) {
+ connection1 = it
+ } else if (it.subId == 2) {
+ connection2 = it
+ } else {
+ Assert.fail("Unexpected subscription")
+ }
+ }
+
+ assertConnection(connection1!!, currentEvent1)
+ assertConnection(connection2!!, currentEvent2)
+
+ // WHEN the event changes for connection 2, it updates, and connection 1 stays the same
+ currentEvent2 = validMobileEvent(subId = 2, activity = DATA_ACTIVITY_INOUT)
+ fakeNetworkEventFlow.value = currentEvent2
+ assertConnection(connection1!!, currentEvent1)
+ assertConnection(connection2!!, currentEvent2)
+
+ // and vice versa
+ currentEvent1 = validMobileEvent(subId = 1, inflateStrength = true)
+ fakeNetworkEventFlow.value = currentEvent1
+ assertConnection(connection1!!, currentEvent1)
+ assertConnection(connection2!!, currentEvent2)
+
+ job.cancel()
+ }
+
+ private fun assertConnection(
+ conn: DemoMobileConnectionRepository,
+ model: FakeNetworkEventModel
+ ) {
+ when (model) {
+ is FakeNetworkEventModel.Mobile -> {
+ val connectionInfo: MobileConnectionModel = conn.connectionInfo.value
+ assertThat(conn.subId).isEqualTo(model.subId)
+ assertThat(connectionInfo.cdmaLevel).isEqualTo(model.level)
+ assertThat(connectionInfo.primaryLevel).isEqualTo(model.level)
+ assertThat(connectionInfo.dataActivityDirection).isEqualTo(model.activity)
+ assertThat(connectionInfo.carrierNetworkChangeActive)
+ .isEqualTo(model.carrierNetworkChange)
+
+ // TODO(b/261029387) check these once we start handling them
+ assertThat(connectionInfo.isEmergencyOnly).isFalse()
+ assertThat(connectionInfo.isGsm).isFalse()
+ assertThat(connectionInfo.dataConnectionState)
+ .isEqualTo(DataConnectionState.Connected)
+ }
+ else -> {}
+ }
+ }
+}
+
+/** Convenience to create a valid fake network event with minimal params */
+fun validMobileEvent(
+ level: Int? = 1,
+ dataType: SignalIcon.MobileIconGroup? = THREE_G,
+ subId: Int? = 1,
+ carrierId: Int? = UNKNOWN_CARRIER_ID,
+ inflateStrength: Boolean? = false,
+ activity: Int? = null,
+ carrierNetworkChange: Boolean = false,
+): FakeNetworkEventModel =
+ FakeNetworkEventModel.Mobile(
+ level = level,
+ dataType = dataType,
+ subId = subId,
+ carrierId = carrierId,
+ inflateStrength = inflateStrength,
+ activity = activity,
+ carrierNetworkChange = carrierNetworkChange,
+ )
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryTest.kt
similarity index 79%
rename from packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepositoryTest.kt
rename to packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryTest.kt
index 5ce51bb..1fc9c60 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepositoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryTest.kt
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.systemui.statusbar.pipeline.mobile.data.repository
+package com.android.systemui.statusbar.pipeline.mobile.data.repository.prod
import android.os.UserHandle
import android.provider.Settings
@@ -31,14 +31,18 @@
import android.telephony.TelephonyManager.DATA_CONNECTING
import android.telephony.TelephonyManager.DATA_DISCONNECTED
import android.telephony.TelephonyManager.DATA_DISCONNECTING
+import android.telephony.TelephonyManager.DATA_UNKNOWN
import android.telephony.TelephonyManager.NETWORK_TYPE_LTE
import android.telephony.TelephonyManager.NETWORK_TYPE_UNKNOWN
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.statusbar.pipeline.mobile.data.model.DataConnectionState
-import com.android.systemui.statusbar.pipeline.mobile.data.model.DefaultNetworkType
-import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileSubscriptionModel
-import com.android.systemui.statusbar.pipeline.mobile.data.model.OverrideNetworkType
+import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel
+import com.android.systemui.statusbar.pipeline.mobile.data.model.ResolvedNetworkType.DefaultNetworkType
+import com.android.systemui.statusbar.pipeline.mobile.data.model.ResolvedNetworkType.OverrideNetworkType
+import com.android.systemui.statusbar.pipeline.mobile.data.model.ResolvedNetworkType.UnknownNetworkType
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeMobileConnectionsRepository
+import com.android.systemui.statusbar.pipeline.mobile.util.FakeMobileMappingsProxy
import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.argumentCaptor
@@ -70,8 +74,9 @@
@Mock private lateinit var logger: ConnectivityPipelineLogger
private val scope = CoroutineScope(IMMEDIATE)
+ private val mobileMappings = FakeMobileMappingsProxy()
private val globalSettings = FakeSettings()
- private val connectionsRepo = FakeMobileConnectionsRepository()
+ private val connectionsRepo = FakeMobileConnectionsRepository(mobileMappings)
@Before
fun setUp() {
@@ -87,6 +92,7 @@
globalSettings,
connectionsRepo.defaultDataSubId,
connectionsRepo.globalMobileDataSettingChangedEvent,
+ mobileMappings,
IMMEDIATE,
logger,
scope,
@@ -101,10 +107,10 @@
@Test
fun testFlowForSubId_default() =
runBlocking(IMMEDIATE) {
- var latest: MobileSubscriptionModel? = null
- val job = underTest.subscriptionModelFlow.onEach { latest = it }.launchIn(this)
+ var latest: MobileConnectionModel? = null
+ val job = underTest.connectionInfo.onEach { latest = it }.launchIn(this)
- assertThat(latest).isEqualTo(MobileSubscriptionModel())
+ assertThat(latest).isEqualTo(MobileConnectionModel())
job.cancel()
}
@@ -112,8 +118,8 @@
@Test
fun testFlowForSubId_emergencyOnly() =
runBlocking(IMMEDIATE) {
- var latest: MobileSubscriptionModel? = null
- val job = underTest.subscriptionModelFlow.onEach { latest = it }.launchIn(this)
+ var latest: MobileConnectionModel? = null
+ val job = underTest.connectionInfo.onEach { latest = it }.launchIn(this)
val serviceState = ServiceState()
serviceState.isEmergencyOnly = true
@@ -128,8 +134,8 @@
@Test
fun testFlowForSubId_emergencyOnly_toggles() =
runBlocking(IMMEDIATE) {
- var latest: MobileSubscriptionModel? = null
- val job = underTest.subscriptionModelFlow.onEach { latest = it }.launchIn(this)
+ var latest: MobileConnectionModel? = null
+ val job = underTest.connectionInfo.onEach { latest = it }.launchIn(this)
val callback = getTelephonyCallbackForType<ServiceStateListener>()
val serviceState = ServiceState()
@@ -146,8 +152,8 @@
@Test
fun testFlowForSubId_signalStrengths_levelsUpdate() =
runBlocking(IMMEDIATE) {
- var latest: MobileSubscriptionModel? = null
- val job = underTest.subscriptionModelFlow.onEach { latest = it }.launchIn(this)
+ var latest: MobileConnectionModel? = null
+ val job = underTest.connectionInfo.onEach { latest = it }.launchIn(this)
val callback = getTelephonyCallbackForType<TelephonyCallback.SignalStrengthsListener>()
val strength = signalStrength(gsmLevel = 1, cdmaLevel = 2, isGsm = true)
@@ -163,8 +169,8 @@
@Test
fun testFlowForSubId_dataConnectionState_connected() =
runBlocking(IMMEDIATE) {
- var latest: MobileSubscriptionModel? = null
- val job = underTest.subscriptionModelFlow.onEach { latest = it }.launchIn(this)
+ var latest: MobileConnectionModel? = null
+ val job = underTest.connectionInfo.onEach { latest = it }.launchIn(this)
val callback =
getTelephonyCallbackForType<TelephonyCallback.DataConnectionStateListener>()
@@ -178,8 +184,8 @@
@Test
fun testFlowForSubId_dataConnectionState_connecting() =
runBlocking(IMMEDIATE) {
- var latest: MobileSubscriptionModel? = null
- val job = underTest.subscriptionModelFlow.onEach { latest = it }.launchIn(this)
+ var latest: MobileConnectionModel? = null
+ val job = underTest.connectionInfo.onEach { latest = it }.launchIn(this)
val callback =
getTelephonyCallbackForType<TelephonyCallback.DataConnectionStateListener>()
@@ -193,8 +199,8 @@
@Test
fun testFlowForSubId_dataConnectionState_disconnected() =
runBlocking(IMMEDIATE) {
- var latest: MobileSubscriptionModel? = null
- val job = underTest.subscriptionModelFlow.onEach { latest = it }.launchIn(this)
+ var latest: MobileConnectionModel? = null
+ val job = underTest.connectionInfo.onEach { latest = it }.launchIn(this)
val callback =
getTelephonyCallbackForType<TelephonyCallback.DataConnectionStateListener>()
@@ -208,8 +214,8 @@
@Test
fun testFlowForSubId_dataConnectionState_disconnecting() =
runBlocking(IMMEDIATE) {
- var latest: MobileSubscriptionModel? = null
- val job = underTest.subscriptionModelFlow.onEach { latest = it }.launchIn(this)
+ var latest: MobileConnectionModel? = null
+ val job = underTest.connectionInfo.onEach { latest = it }.launchIn(this)
val callback =
getTelephonyCallbackForType<TelephonyCallback.DataConnectionStateListener>()
@@ -221,10 +227,25 @@
}
@Test
+ fun testFlowForSubId_dataConnectionState_unknown() =
+ runBlocking(IMMEDIATE) {
+ var latest: MobileConnectionModel? = null
+ val job = underTest.connectionInfo.onEach { latest = it }.launchIn(this)
+
+ val callback =
+ getTelephonyCallbackForType<TelephonyCallback.DataConnectionStateListener>()
+ callback.onDataConnectionStateChanged(DATA_UNKNOWN, 200 /* unused */)
+
+ assertThat(latest?.dataConnectionState).isEqualTo(DataConnectionState.Unknown)
+
+ job.cancel()
+ }
+
+ @Test
fun testFlowForSubId_dataActivity() =
runBlocking(IMMEDIATE) {
- var latest: MobileSubscriptionModel? = null
- val job = underTest.subscriptionModelFlow.onEach { latest = it }.launchIn(this)
+ var latest: MobileConnectionModel? = null
+ val job = underTest.connectionInfo.onEach { latest = it }.launchIn(this)
val callback = getTelephonyCallbackForType<TelephonyCallback.DataActivityListener>()
callback.onDataActivity(3)
@@ -237,8 +258,8 @@
@Test
fun testFlowForSubId_carrierNetworkChange() =
runBlocking(IMMEDIATE) {
- var latest: MobileSubscriptionModel? = null
- val job = underTest.subscriptionModelFlow.onEach { latest = it }.launchIn(this)
+ var latest: MobileConnectionModel? = null
+ val job = underTest.connectionInfo.onEach { latest = it }.launchIn(this)
val callback = getTelephonyCallbackForType<TelephonyCallback.CarrierNetworkListener>()
callback.onCarrierNetworkChange(true)
@@ -251,11 +272,11 @@
@Test
fun subscriptionFlow_networkType_default() =
runBlocking(IMMEDIATE) {
- var latest: MobileSubscriptionModel? = null
- val job = underTest.subscriptionModelFlow.onEach { latest = it }.launchIn(this)
+ var latest: MobileConnectionModel? = null
+ val job = underTest.connectionInfo.onEach { latest = it }.launchIn(this)
val type = NETWORK_TYPE_UNKNOWN
- val expected = DefaultNetworkType(type)
+ val expected = UnknownNetworkType
assertThat(latest?.resolvedNetworkType).isEqualTo(expected)
@@ -265,12 +286,12 @@
@Test
fun subscriptionFlow_networkType_updatesUsingDefault() =
runBlocking(IMMEDIATE) {
- var latest: MobileSubscriptionModel? = null
- val job = underTest.subscriptionModelFlow.onEach { latest = it }.launchIn(this)
+ var latest: MobileConnectionModel? = null
+ val job = underTest.connectionInfo.onEach { latest = it }.launchIn(this)
val callback = getTelephonyCallbackForType<TelephonyCallback.DisplayInfoListener>()
val type = NETWORK_TYPE_LTE
- val expected = DefaultNetworkType(type)
+ val expected = DefaultNetworkType(type, mobileMappings.toIconKey(type))
val ti = mock<TelephonyDisplayInfo>().also { whenever(it.networkType).thenReturn(type) }
callback.onDisplayInfoChanged(ti)
@@ -282,14 +303,15 @@
@Test
fun subscriptionFlow_networkType_updatesUsingOverride() =
runBlocking(IMMEDIATE) {
- var latest: MobileSubscriptionModel? = null
- val job = underTest.subscriptionModelFlow.onEach { latest = it }.launchIn(this)
+ var latest: MobileConnectionModel? = null
+ val job = underTest.connectionInfo.onEach { latest = it }.launchIn(this)
val callback = getTelephonyCallbackForType<TelephonyCallback.DisplayInfoListener>()
val type = OVERRIDE_NETWORK_TYPE_LTE_CA
- val expected = OverrideNetworkType(type)
+ val expected = OverrideNetworkType(type, mobileMappings.toIconKeyOverride(type))
val ti =
mock<TelephonyDisplayInfo>().also {
+ whenever(it.networkType).thenReturn(type)
whenever(it.overrideNetworkType).thenReturn(type)
}
callback.onDisplayInfoChanged(ti)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryTest.kt
similarity index 81%
rename from packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepositoryTest.kt
rename to packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryTest.kt
index a953a3d..4b82b39 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepositoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryTest.kt
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.systemui.statusbar.pipeline.mobile.data.repository
+package com.android.systemui.statusbar.pipeline.mobile.data.repository.prod
import android.content.Intent
import android.net.ConnectivityManager
@@ -32,6 +32,8 @@
import com.android.internal.telephony.PhoneConstants
import com.android.systemui.SysuiTestCase
import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectivityModel
+import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel
+import com.android.systemui.statusbar.pipeline.mobile.util.FakeMobileMappingsProxy
import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.argumentCaptor
@@ -50,6 +52,7 @@
import org.junit.Assert.assertThrows
import org.junit.Before
import org.junit.Test
+import org.mockito.ArgumentMatchers.anyInt
import org.mockito.Mock
import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations
@@ -65,6 +68,8 @@
@Mock private lateinit var telephonyManager: TelephonyManager
@Mock private lateinit var logger: ConnectivityPipelineLogger
+ private val mobileMappings = FakeMobileMappingsProxy()
+
private val scope = CoroutineScope(IMMEDIATE)
private val globalSettings = FakeSettings()
@@ -72,18 +77,37 @@
fun setUp() {
MockitoAnnotations.initMocks(this)
+ // Set up so the individual connection repositories
+ whenever(telephonyManager.createForSubscriptionId(anyInt())).thenAnswer { invocation ->
+ telephonyManager.also {
+ whenever(telephonyManager.subscriptionId).thenReturn(invocation.getArgument(0))
+ }
+ }
+
+ val connectionFactory: MobileConnectionRepositoryImpl.Factory =
+ MobileConnectionRepositoryImpl.Factory(
+ context = context,
+ telephonyManager = telephonyManager,
+ bgDispatcher = IMMEDIATE,
+ globalSettings = globalSettings,
+ logger = logger,
+ mobileMappingsProxy = mobileMappings,
+ scope = scope,
+ )
+
underTest =
MobileConnectionsRepositoryImpl(
connectivityManager,
subscriptionManager,
telephonyManager,
logger,
+ mobileMappings,
fakeBroadcastDispatcher,
globalSettings,
context,
IMMEDIATE,
scope,
- mock(),
+ connectionFactory,
)
}
@@ -95,21 +119,21 @@
@Test
fun testSubscriptions_initiallyEmpty() =
runBlocking(IMMEDIATE) {
- assertThat(underTest.subscriptionsFlow.value).isEqualTo(listOf<SubscriptionInfo>())
+ assertThat(underTest.subscriptions.value).isEqualTo(listOf<SubscriptionModel>())
}
@Test
fun testSubscriptions_listUpdates() =
runBlocking(IMMEDIATE) {
- var latest: List<SubscriptionInfo>? = null
+ var latest: List<SubscriptionModel>? = null
- val job = underTest.subscriptionsFlow.onEach { latest = it }.launchIn(this)
+ val job = underTest.subscriptions.onEach { latest = it }.launchIn(this)
whenever(subscriptionManager.completeActiveSubscriptionInfoList)
.thenReturn(listOf(SUB_1, SUB_2))
getSubscriptionCallback().onSubscriptionsChanged()
- assertThat(latest).isEqualTo(listOf(SUB_1, SUB_2))
+ assertThat(latest).isEqualTo(listOf(MODEL_1, MODEL_2))
job.cancel()
}
@@ -117,9 +141,9 @@
@Test
fun testSubscriptions_removingSub_updatesList() =
runBlocking(IMMEDIATE) {
- var latest: List<SubscriptionInfo>? = null
+ var latest: List<SubscriptionModel>? = null
- val job = underTest.subscriptionsFlow.onEach { latest = it }.launchIn(this)
+ val job = underTest.subscriptions.onEach { latest = it }.launchIn(this)
// WHEN 2 networks show up
whenever(subscriptionManager.completeActiveSubscriptionInfoList)
@@ -132,7 +156,7 @@
getSubscriptionCallback().onSubscriptionsChanged()
// THEN the subscriptions list represents the newest change
- assertThat(latest).isEqualTo(listOf(SUB_2))
+ assertThat(latest).isEqualTo(listOf(MODEL_2))
job.cancel()
}
@@ -162,7 +186,7 @@
@Test
fun testConnectionRepository_validSubId_isCached() =
runBlocking(IMMEDIATE) {
- val job = underTest.subscriptionsFlow.launchIn(this)
+ val job = underTest.subscriptions.launchIn(this)
whenever(subscriptionManager.completeActiveSubscriptionInfoList)
.thenReturn(listOf(SUB_1))
@@ -179,7 +203,7 @@
@Test
fun testConnectionCache_clearsInvalidSubscriptions() =
runBlocking(IMMEDIATE) {
- val job = underTest.subscriptionsFlow.launchIn(this)
+ val job = underTest.subscriptions.launchIn(this)
whenever(subscriptionManager.completeActiveSubscriptionInfoList)
.thenReturn(listOf(SUB_1, SUB_2))
@@ -202,10 +226,36 @@
job.cancel()
}
+ /** Regression test for b/261706421 */
+ @Test
+ fun testConnectionsCache_clearMultipleSubscriptionsAtOnce_doesNotThrow() =
+ runBlocking(IMMEDIATE) {
+ val job = underTest.subscriptions.launchIn(this)
+
+ whenever(subscriptionManager.completeActiveSubscriptionInfoList)
+ .thenReturn(listOf(SUB_1, SUB_2))
+ getSubscriptionCallback().onSubscriptionsChanged()
+
+ // Get repos to trigger caching
+ val repo1 = underTest.getRepoForSubId(SUB_1_ID)
+ val repo2 = underTest.getRepoForSubId(SUB_2_ID)
+
+ assertThat(underTest.getSubIdRepoCache())
+ .containsExactly(SUB_1_ID, repo1, SUB_2_ID, repo2)
+
+ // All subscriptions disappear
+ whenever(subscriptionManager.completeActiveSubscriptionInfoList).thenReturn(listOf())
+ getSubscriptionCallback().onSubscriptionsChanged()
+
+ assertThat(underTest.getSubIdRepoCache()).isEmpty()
+
+ job.cancel()
+ }
+
@Test
fun testConnectionRepository_invalidSubId_throws() =
runBlocking(IMMEDIATE) {
- val job = underTest.subscriptionsFlow.launchIn(this)
+ val job = underTest.subscriptions.launchIn(this)
assertThrows(IllegalArgumentException::class.java) {
underTest.getRepoForSubId(SUB_1_ID)
@@ -371,10 +421,12 @@
private const val SUB_1_ID = 1
private val SUB_1 =
mock<SubscriptionInfo>().also { whenever(it.subscriptionId).thenReturn(SUB_1_ID) }
+ private val MODEL_1 = SubscriptionModel(subscriptionId = SUB_1_ID)
private const val SUB_2_ID = 2
private val SUB_2 =
mock<SubscriptionInfo>().also { whenever(it.subscriptionId).thenReturn(SUB_2_ID) }
+ private val MODEL_2 = SubscriptionModel(subscriptionId = SUB_2_ID)
private const val NET_ID = 123
private val NETWORK = mock<Network>().apply { whenever(getNetId()).thenReturn(NET_ID) }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconsInteractor.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconsInteractor.kt
index 061c3b54..0d4044d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconsInteractor.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconsInteractor.kt
@@ -16,14 +16,15 @@
package com.android.systemui.statusbar.pipeline.mobile.domain.interactor
-import android.telephony.SubscriptionInfo
import android.telephony.TelephonyDisplayInfo.OVERRIDE_NETWORK_TYPE_LTE_ADVANCED_PRO
import android.telephony.TelephonyManager.NETWORK_TYPE_GSM
import android.telephony.TelephonyManager.NETWORK_TYPE_LTE
import android.telephony.TelephonyManager.NETWORK_TYPE_UMTS
import com.android.settingslib.SignalIcon.MobileIconGroup
import com.android.settingslib.mobile.TelephonyIcons
+import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel
import com.android.systemui.statusbar.pipeline.mobile.util.MobileMappingsProxy
+import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
class FakeMobileIconsInteractor(mobileMappings: MobileMappingsProxy) : MobileIconsInteractor {
@@ -47,8 +48,8 @@
override val isDefaultConnectionFailed = MutableStateFlow(false)
- private val _filteredSubscriptions = MutableStateFlow<List<SubscriptionInfo>>(listOf())
- override val filteredSubscriptions = _filteredSubscriptions
+ private val _filteredSubscriptions = MutableStateFlow<List<SubscriptionModel>>(listOf())
+ override val filteredSubscriptions: Flow<List<SubscriptionModel>> = _filteredSubscriptions
private val _activeDataConnectionHasDataEnabled = MutableStateFlow(false)
override val activeDataConnectionHasDataEnabled = _activeDataConnectionHasDataEnabled
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt
index 7fc1c0f..fd41b5b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt
@@ -24,9 +24,9 @@
import com.android.settingslib.mobile.TelephonyIcons
import com.android.systemui.SysuiTestCase
import com.android.systemui.statusbar.pipeline.mobile.data.model.DataConnectionState
-import com.android.systemui.statusbar.pipeline.mobile.data.model.DefaultNetworkType
-import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileSubscriptionModel
-import com.android.systemui.statusbar.pipeline.mobile.data.model.OverrideNetworkType
+import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel
+import com.android.systemui.statusbar.pipeline.mobile.data.model.ResolvedNetworkType.DefaultNetworkType
+import com.android.systemui.statusbar.pipeline.mobile.data.model.ResolvedNetworkType.OverrideNetworkType
import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeMobileConnectionRepository
import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.FakeMobileIconsInteractor.Companion.FIVE_G_OVERRIDE
import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.FakeMobileIconsInteractor.Companion.FOUR_G
@@ -49,7 +49,7 @@
private lateinit var underTest: MobileIconInteractor
private val mobileMappingsProxy = FakeMobileMappingsProxy()
private val mobileIconsInteractor = FakeMobileIconsInteractor(mobileMappingsProxy)
- private val connectionRepository = FakeMobileConnectionRepository()
+ private val connectionRepository = FakeMobileConnectionRepository(SUB_1_ID)
private val scope = CoroutineScope(IMMEDIATE)
@@ -62,7 +62,6 @@
mobileIconsInteractor.defaultMobileIconMapping,
mobileIconsInteractor.defaultMobileIconGroup,
mobileIconsInteractor.isDefaultConnectionFailed,
- mobileMappingsProxy,
connectionRepository,
)
}
@@ -70,8 +69,8 @@
@Test
fun gsm_level_default_unknown() =
runBlocking(IMMEDIATE) {
- connectionRepository.setMobileSubscriptionModel(
- MobileSubscriptionModel(isGsm = true),
+ connectionRepository.setConnectionInfo(
+ MobileConnectionModel(isGsm = true),
)
var latest: Int? = null
@@ -85,8 +84,8 @@
@Test
fun gsm_usesGsmLevel() =
runBlocking(IMMEDIATE) {
- connectionRepository.setMobileSubscriptionModel(
- MobileSubscriptionModel(
+ connectionRepository.setConnectionInfo(
+ MobileConnectionModel(
isGsm = true,
primaryLevel = GSM_LEVEL,
cdmaLevel = CDMA_LEVEL
@@ -104,8 +103,8 @@
@Test
fun cdma_level_default_unknown() =
runBlocking(IMMEDIATE) {
- connectionRepository.setMobileSubscriptionModel(
- MobileSubscriptionModel(isGsm = false),
+ connectionRepository.setConnectionInfo(
+ MobileConnectionModel(isGsm = false),
)
var latest: Int? = null
@@ -118,8 +117,8 @@
@Test
fun cdma_usesCdmaLevel() =
runBlocking(IMMEDIATE) {
- connectionRepository.setMobileSubscriptionModel(
- MobileSubscriptionModel(
+ connectionRepository.setConnectionInfo(
+ MobileConnectionModel(
isGsm = false,
primaryLevel = GSM_LEVEL,
cdmaLevel = CDMA_LEVEL
@@ -137,8 +136,11 @@
@Test
fun iconGroup_three_g() =
runBlocking(IMMEDIATE) {
- connectionRepository.setMobileSubscriptionModel(
- MobileSubscriptionModel(resolvedNetworkType = DefaultNetworkType(THREE_G)),
+ connectionRepository.setConnectionInfo(
+ MobileConnectionModel(
+ resolvedNetworkType =
+ DefaultNetworkType(THREE_G, mobileMappingsProxy.toIconKey(THREE_G))
+ ),
)
var latest: MobileIconGroup? = null
@@ -152,16 +154,23 @@
@Test
fun iconGroup_updates_on_change() =
runBlocking(IMMEDIATE) {
- connectionRepository.setMobileSubscriptionModel(
- MobileSubscriptionModel(resolvedNetworkType = DefaultNetworkType(THREE_G)),
+ connectionRepository.setConnectionInfo(
+ MobileConnectionModel(
+ resolvedNetworkType =
+ DefaultNetworkType(THREE_G, mobileMappingsProxy.toIconKey(THREE_G))
+ ),
)
var latest: MobileIconGroup? = null
val job = underTest.networkTypeIconGroup.onEach { latest = it }.launchIn(this)
- connectionRepository.setMobileSubscriptionModel(
- MobileSubscriptionModel(
- resolvedNetworkType = DefaultNetworkType(FOUR_G),
+ connectionRepository.setConnectionInfo(
+ MobileConnectionModel(
+ resolvedNetworkType =
+ DefaultNetworkType(
+ FOUR_G,
+ mobileMappingsProxy.toIconKey(FOUR_G),
+ ),
),
)
yield()
@@ -174,8 +183,14 @@
@Test
fun iconGroup_5g_override_type() =
runBlocking(IMMEDIATE) {
- connectionRepository.setMobileSubscriptionModel(
- MobileSubscriptionModel(resolvedNetworkType = OverrideNetworkType(FIVE_G_OVERRIDE)),
+ connectionRepository.setConnectionInfo(
+ MobileConnectionModel(
+ resolvedNetworkType =
+ OverrideNetworkType(
+ FIVE_G_OVERRIDE,
+ mobileMappingsProxy.toIconKeyOverride(FIVE_G_OVERRIDE)
+ )
+ ),
)
var latest: MobileIconGroup? = null
@@ -189,9 +204,13 @@
@Test
fun iconGroup_default_if_no_lookup() =
runBlocking(IMMEDIATE) {
- connectionRepository.setMobileSubscriptionModel(
- MobileSubscriptionModel(
- resolvedNetworkType = DefaultNetworkType(NETWORK_TYPE_UNKNOWN),
+ connectionRepository.setConnectionInfo(
+ MobileConnectionModel(
+ resolvedNetworkType =
+ DefaultNetworkType(
+ NETWORK_TYPE_UNKNOWN,
+ mobileMappingsProxy.toIconKey(NETWORK_TYPE_UNKNOWN)
+ ),
),
)
@@ -238,8 +257,8 @@
var latest: Boolean? = null
val job = underTest.isDataConnected.onEach { latest = it }.launchIn(this)
- connectionRepository.setMobileSubscriptionModel(
- MobileSubscriptionModel(dataConnectionState = DataConnectionState.Connected)
+ connectionRepository.setConnectionInfo(
+ MobileConnectionModel(dataConnectionState = DataConnectionState.Connected)
)
yield()
@@ -254,8 +273,8 @@
var latest: Boolean? = null
val job = underTest.isDataConnected.onEach { latest = it }.launchIn(this)
- connectionRepository.setMobileSubscriptionModel(
- MobileSubscriptionModel(dataConnectionState = DataConnectionState.Disconnected)
+ connectionRepository.setConnectionInfo(
+ MobileConnectionModel(dataConnectionState = DataConnectionState.Disconnected)
)
assertThat(latest).isFalse()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorTest.kt
index b56dcd7..58e57e2 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorTest.kt
@@ -16,17 +16,16 @@
package com.android.systemui.statusbar.pipeline.mobile.domain.interactor
-import android.telephony.SubscriptionInfo
import android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectivityModel
+import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel
import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeMobileConnectionRepository
import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeMobileConnectionsRepository
import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeUserSetupRepository
import com.android.systemui.statusbar.pipeline.mobile.util.FakeMobileMappingsProxy
import com.android.systemui.util.CarrierConfigTracker
-import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.whenever
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.CoroutineScope
@@ -45,8 +44,8 @@
class MobileIconsInteractorTest : SysuiTestCase() {
private lateinit var underTest: MobileIconsInteractor
private val userSetupRepository = FakeUserSetupRepository()
- private val connectionsRepository = FakeMobileConnectionsRepository()
private val mobileMappingsProxy = FakeMobileMappingsProxy()
+ private val connectionsRepository = FakeMobileConnectionsRepository(mobileMappingsProxy)
private val scope = CoroutineScope(IMMEDIATE)
@Mock private lateinit var carrierConfigTracker: CarrierConfigTracker
@@ -69,7 +68,6 @@
MobileIconsInteractorImpl(
connectionsRepository,
carrierConfigTracker,
- mobileMappingsProxy,
userSetupRepository,
scope
)
@@ -80,10 +78,10 @@
@Test
fun filteredSubscriptions_default() =
runBlocking(IMMEDIATE) {
- var latest: List<SubscriptionInfo>? = null
+ var latest: List<SubscriptionModel>? = null
val job = underTest.filteredSubscriptions.onEach { latest = it }.launchIn(this)
- assertThat(latest).isEqualTo(listOf<SubscriptionInfo>())
+ assertThat(latest).isEqualTo(listOf<SubscriptionModel>())
job.cancel()
}
@@ -93,7 +91,7 @@
runBlocking(IMMEDIATE) {
connectionsRepository.setSubscriptions(listOf(SUB_1, SUB_2))
- var latest: List<SubscriptionInfo>? = null
+ var latest: List<SubscriptionModel>? = null
val job = underTest.filteredSubscriptions.onEach { latest = it }.launchIn(this)
assertThat(latest).isEqualTo(listOf(SUB_1, SUB_2))
@@ -109,7 +107,7 @@
whenever(carrierConfigTracker.alwaysShowPrimarySignalBarInOpportunisticNetworkDefault)
.thenReturn(false)
- var latest: List<SubscriptionInfo>? = null
+ var latest: List<SubscriptionModel>? = null
val job = underTest.filteredSubscriptions.onEach { latest = it }.launchIn(this)
// Filtered subscriptions should show the active one when the config is false
@@ -126,7 +124,7 @@
whenever(carrierConfigTracker.alwaysShowPrimarySignalBarInOpportunisticNetworkDefault)
.thenReturn(false)
- var latest: List<SubscriptionInfo>? = null
+ var latest: List<SubscriptionModel>? = null
val job = underTest.filteredSubscriptions.onEach { latest = it }.launchIn(this)
// Filtered subscriptions should show the active one when the config is false
@@ -143,7 +141,7 @@
whenever(carrierConfigTracker.alwaysShowPrimarySignalBarInOpportunisticNetworkDefault)
.thenReturn(true)
- var latest: List<SubscriptionInfo>? = null
+ var latest: List<SubscriptionModel>? = null
val job = underTest.filteredSubscriptions.onEach { latest = it }.launchIn(this)
// Filtered subscriptions should show the primary (non-opportunistic) if the config is
@@ -161,7 +159,7 @@
whenever(carrierConfigTracker.alwaysShowPrimarySignalBarInOpportunisticNetworkDefault)
.thenReturn(true)
- var latest: List<SubscriptionInfo>? = null
+ var latest: List<SubscriptionModel>? = null
val job = underTest.filteredSubscriptions.onEach { latest = it }.launchIn(this)
// Filtered subscriptions should show the primary (non-opportunistic) if the config is
@@ -261,29 +259,19 @@
private val IMMEDIATE = Dispatchers.Main.immediate
private const val SUB_1_ID = 1
- private val SUB_1 =
- mock<SubscriptionInfo>().also { whenever(it.subscriptionId).thenReturn(SUB_1_ID) }
- private val CONNECTION_1 = FakeMobileConnectionRepository()
+ private val SUB_1 = SubscriptionModel(subscriptionId = SUB_1_ID)
+ private val CONNECTION_1 = FakeMobileConnectionRepository(SUB_1_ID)
private const val SUB_2_ID = 2
- private val SUB_2 =
- mock<SubscriptionInfo>().also { whenever(it.subscriptionId).thenReturn(SUB_2_ID) }
- private val CONNECTION_2 = FakeMobileConnectionRepository()
+ private val SUB_2 = SubscriptionModel(subscriptionId = SUB_2_ID)
+ private val CONNECTION_2 = FakeMobileConnectionRepository(SUB_2_ID)
private const val SUB_3_ID = 3
- private val SUB_3_OPP =
- mock<SubscriptionInfo>().also {
- whenever(it.subscriptionId).thenReturn(SUB_3_ID)
- whenever(it.isOpportunistic).thenReturn(true)
- }
- private val CONNECTION_3 = FakeMobileConnectionRepository()
+ private val SUB_3_OPP = SubscriptionModel(subscriptionId = SUB_3_ID, isOpportunistic = true)
+ private val CONNECTION_3 = FakeMobileConnectionRepository(SUB_3_ID)
private const val SUB_4_ID = 4
- private val SUB_4_OPP =
- mock<SubscriptionInfo>().also {
- whenever(it.subscriptionId).thenReturn(SUB_4_ID)
- whenever(it.isOpportunistic).thenReturn(true)
- }
- private val CONNECTION_4 = FakeMobileConnectionRepository()
+ private val SUB_4_OPP = SubscriptionModel(subscriptionId = SUB_4_ID, isOpportunistic = true)
+ private val CONNECTION_4 = FakeMobileConnectionRepository(SUB_4_ID)
}
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinatorTest.kt
index 47c84ab..7014f93 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinatorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinatorTest.kt
@@ -35,6 +35,7 @@
import com.android.systemui.common.shared.model.ContentDescription.Companion.loadContentDescription
import com.android.systemui.common.shared.model.Icon
import com.android.systemui.common.shared.model.Text
+import com.android.systemui.common.shared.model.TintedIcon
import com.android.systemui.plugins.FalsingManager
import com.android.systemui.statusbar.VibratorHelper
import com.android.systemui.statusbar.policy.ConfigurationController
@@ -109,6 +110,35 @@
}
@Test
+ fun displayView_contentDescription_iconHasDescription() {
+ underTest.displayView(
+ createChipbarInfo(
+ Icon.Resource(R.drawable.ic_cake, ContentDescription.Loaded("loadedCD")),
+ Text.Loaded("text"),
+ endItem = null,
+ )
+ )
+
+ val contentDescView = getChipbarView().requireViewById<ViewGroup>(R.id.chipbar_inner)
+ assertThat(contentDescView.contentDescription.toString()).contains("loadedCD")
+ assertThat(contentDescView.contentDescription.toString()).contains("text")
+ }
+
+ @Test
+ fun displayView_contentDescription_iconHasNoDescription() {
+ underTest.displayView(
+ createChipbarInfo(
+ Icon.Resource(R.drawable.ic_cake, contentDescription = null),
+ Text.Loaded("text"),
+ endItem = null,
+ )
+ )
+
+ val contentDescView = getChipbarView().requireViewById<ViewGroup>(R.id.chipbar_inner)
+ assertThat(contentDescView.contentDescription.toString()).isEqualTo("text")
+ }
+
+ @Test
fun displayView_loadedIcon_correctlyRendered() {
val drawable = context.getDrawable(R.drawable.ic_celebration)!!
@@ -370,7 +400,7 @@
vibrationEffect: VibrationEffect? = null,
): ChipbarInfo {
return ChipbarInfo(
- startIcon,
+ TintedIcon(startIcon, tintAttr = null),
text,
endItem,
vibrationEffect,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/util/TestableAlertDialogTest.kt b/packages/SystemUI/tests/src/com/android/systemui/util/TestableAlertDialogTest.kt
new file mode 100644
index 0000000..01dd60a
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/util/TestableAlertDialogTest.kt
@@ -0,0 +1,333 @@
+/*
+ * 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.util
+
+import android.content.DialogInterface
+import android.content.DialogInterface.BUTTON_NEGATIVE
+import android.content.DialogInterface.BUTTON_NEUTRAL
+import android.content.DialogInterface.BUTTON_POSITIVE
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.mock
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.anyInt
+import org.mockito.Mockito.inOrder
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+@TestableLooper.RunWithLooper
+class TestableAlertDialogTest : SysuiTestCase() {
+
+ @Test
+ fun dialogNotShowingWhenCreated() {
+ val dialog = TestableAlertDialog(context)
+
+ assertThat(dialog.isShowing).isFalse()
+ }
+
+ @Test
+ fun dialogShownDoesntCrash() {
+ val dialog = TestableAlertDialog(context)
+
+ dialog.show()
+ }
+
+ @Test
+ fun dialogShowing() {
+ val dialog = TestableAlertDialog(context)
+
+ dialog.show()
+
+ assertThat(dialog.isShowing).isTrue()
+ }
+
+ @Test
+ fun showListenerCalled() {
+ val dialog = TestableAlertDialog(context)
+ val listener: DialogInterface.OnShowListener = mock()
+ dialog.setOnShowListener(listener)
+
+ dialog.show()
+
+ verify(listener).onShow(dialog)
+ }
+
+ @Test
+ fun showListenerRemoved() {
+ val dialog = TestableAlertDialog(context)
+ val listener: DialogInterface.OnShowListener = mock()
+ dialog.setOnShowListener(listener)
+ dialog.setOnShowListener(null)
+
+ dialog.show()
+
+ verify(listener, never()).onShow(any())
+ }
+
+ @Test
+ fun dialogHiddenNotShowing() {
+ val dialog = TestableAlertDialog(context)
+
+ dialog.show()
+ dialog.hide()
+
+ assertThat(dialog.isShowing).isFalse()
+ }
+
+ @Test
+ fun dialogDismissNotShowing() {
+ val dialog = TestableAlertDialog(context)
+
+ dialog.show()
+ dialog.dismiss()
+
+ assertThat(dialog.isShowing).isFalse()
+ }
+
+ @Test
+ fun dismissListenerCalled_ifShowing() {
+ val dialog = TestableAlertDialog(context)
+ val listener: DialogInterface.OnDismissListener = mock()
+ dialog.setOnDismissListener(listener)
+
+ dialog.show()
+ dialog.dismiss()
+
+ verify(listener).onDismiss(dialog)
+ }
+
+ @Test
+ fun dismissListenerNotCalled_ifNotShowing() {
+ val dialog = TestableAlertDialog(context)
+ val listener: DialogInterface.OnDismissListener = mock()
+ dialog.setOnDismissListener(listener)
+
+ dialog.dismiss()
+
+ verify(listener, never()).onDismiss(any())
+ }
+
+ @Test
+ fun dismissListenerRemoved() {
+ val dialog = TestableAlertDialog(context)
+ val listener: DialogInterface.OnDismissListener = mock()
+ dialog.setOnDismissListener(listener)
+ dialog.setOnDismissListener(null)
+
+ dialog.show()
+ dialog.dismiss()
+
+ verify(listener, never()).onDismiss(any())
+ }
+
+ @Test
+ fun cancelListenerCalled_showing() {
+ val dialog = TestableAlertDialog(context)
+ val listener: DialogInterface.OnCancelListener = mock()
+ dialog.setOnCancelListener(listener)
+
+ dialog.show()
+ dialog.cancel()
+
+ verify(listener).onCancel(dialog)
+ }
+
+ @Test
+ fun cancelListenerCalled_notShowing() {
+ val dialog = TestableAlertDialog(context)
+ val listener: DialogInterface.OnCancelListener = mock()
+ dialog.setOnCancelListener(listener)
+
+ dialog.cancel()
+
+ verify(listener).onCancel(dialog)
+ }
+
+ @Test
+ fun dismissCalledOnCancel_showing() {
+ val dialog = TestableAlertDialog(context)
+ val listener: DialogInterface.OnDismissListener = mock()
+ dialog.setOnDismissListener(listener)
+
+ dialog.show()
+ dialog.cancel()
+
+ verify(listener).onDismiss(dialog)
+ }
+
+ @Test
+ fun dialogCancelNotShowing() {
+ val dialog = TestableAlertDialog(context)
+
+ dialog.show()
+ dialog.cancel()
+
+ assertThat(dialog.isShowing).isFalse()
+ }
+
+ @Test
+ fun cancelListenerRemoved() {
+ val dialog = TestableAlertDialog(context)
+ val listener: DialogInterface.OnCancelListener = mock()
+ dialog.setOnCancelListener(listener)
+ dialog.setOnCancelListener(null)
+
+ dialog.show()
+ dialog.cancel()
+
+ verify(listener, never()).onCancel(any())
+ }
+
+ @Test
+ fun positiveButtonClick() {
+ val dialog = TestableAlertDialog(context)
+ val listener: DialogInterface.OnClickListener = mock()
+ dialog.setButton(BUTTON_POSITIVE, "", listener)
+
+ dialog.show()
+ dialog.clickButton(BUTTON_POSITIVE)
+
+ verify(listener).onClick(dialog, BUTTON_POSITIVE)
+ }
+
+ @Test
+ fun positiveButtonListener_noCalledWhenClickOtherButtons() {
+ val dialog = TestableAlertDialog(context)
+ val listener: DialogInterface.OnClickListener = mock()
+ dialog.setButton(BUTTON_POSITIVE, "", listener)
+
+ dialog.show()
+ dialog.clickButton(BUTTON_NEUTRAL)
+ dialog.clickButton(BUTTON_NEGATIVE)
+
+ verify(listener, never()).onClick(any(), anyInt())
+ }
+
+ @Test
+ fun negativeButtonClick() {
+ val dialog = TestableAlertDialog(context)
+ val listener: DialogInterface.OnClickListener = mock()
+ dialog.setButton(BUTTON_NEGATIVE, "", listener)
+
+ dialog.show()
+ dialog.clickButton(BUTTON_NEGATIVE)
+
+ verify(listener).onClick(dialog, DialogInterface.BUTTON_NEGATIVE)
+ }
+
+ @Test
+ fun negativeButtonListener_noCalledWhenClickOtherButtons() {
+ val dialog = TestableAlertDialog(context)
+ val listener: DialogInterface.OnClickListener = mock()
+ dialog.setButton(BUTTON_NEGATIVE, "", listener)
+
+ dialog.show()
+ dialog.clickButton(BUTTON_NEUTRAL)
+ dialog.clickButton(BUTTON_POSITIVE)
+
+ verify(listener, never()).onClick(any(), anyInt())
+ }
+
+ @Test
+ fun neutralButtonClick() {
+ val dialog = TestableAlertDialog(context)
+ val listener: DialogInterface.OnClickListener = mock()
+ dialog.setButton(BUTTON_NEUTRAL, "", listener)
+
+ dialog.show()
+ dialog.clickButton(BUTTON_NEUTRAL)
+
+ verify(listener).onClick(dialog, BUTTON_NEUTRAL)
+ }
+
+ @Test
+ fun neutralButtonListener_noCalledWhenClickOtherButtons() {
+ val dialog = TestableAlertDialog(context)
+ val listener: DialogInterface.OnClickListener = mock()
+ dialog.setButton(BUTTON_NEUTRAL, "", listener)
+
+ dialog.show()
+ dialog.clickButton(BUTTON_POSITIVE)
+ dialog.clickButton(BUTTON_NEGATIVE)
+
+ verify(listener, never()).onClick(any(), anyInt())
+ }
+
+ @Test
+ fun sameClickListenerCalledCorrectly() {
+ val dialog = TestableAlertDialog(context)
+ val listener: DialogInterface.OnClickListener = mock()
+ dialog.setButton(BUTTON_POSITIVE, "", listener)
+ dialog.setButton(BUTTON_NEUTRAL, "", listener)
+ dialog.setButton(BUTTON_NEGATIVE, "", listener)
+
+ dialog.show()
+ dialog.clickButton(BUTTON_POSITIVE)
+ dialog.clickButton(BUTTON_NEGATIVE)
+ dialog.clickButton(BUTTON_NEUTRAL)
+
+ val inOrder = inOrder(listener)
+ inOrder.verify(listener).onClick(dialog, BUTTON_POSITIVE)
+ inOrder.verify(listener).onClick(dialog, BUTTON_NEGATIVE)
+ inOrder.verify(listener).onClick(dialog, BUTTON_NEUTRAL)
+ }
+
+ @Test(expected = IllegalArgumentException::class)
+ fun clickBadButton() {
+ val dialog = TestableAlertDialog(context)
+
+ dialog.clickButton(10000)
+ }
+
+ @Test
+ fun clickButtonDismisses_positive() {
+ val dialog = TestableAlertDialog(context)
+
+ dialog.show()
+ dialog.clickButton(BUTTON_POSITIVE)
+
+ assertThat(dialog.isShowing).isFalse()
+ }
+
+ @Test
+ fun clickButtonDismisses_negative() {
+ val dialog = TestableAlertDialog(context)
+
+ dialog.show()
+ dialog.clickButton(BUTTON_NEGATIVE)
+
+ assertThat(dialog.isShowing).isFalse()
+ }
+
+ @Test
+ fun clickButtonDismisses_neutral() {
+ val dialog = TestableAlertDialog(context)
+
+ dialog.show()
+ dialog.clickButton(BUTTON_NEUTRAL)
+
+ assertThat(dialog.isShowing).isFalse()
+ }
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/FakeFgsManagerController.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/FakeFgsManagerController.kt
index b31f119..ced7955 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/FakeFgsManagerController.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/FakeFgsManagerController.kt
@@ -77,7 +77,5 @@
dialogDismissedListeners.remove(listener)
}
- override fun shouldUpdateFooterVisibility(): Boolean = false
-
override fun visibleButtonsCount(): Int = 0
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/util/DeviceConfigProxyFake.java b/packages/SystemUI/tests/utils/src/com/android/systemui/util/DeviceConfigProxyFake.java
index 21e16a1..8a10bf06 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/util/DeviceConfigProxyFake.java
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/util/DeviceConfigProxyFake.java
@@ -81,11 +81,6 @@
properties.get(namespace).put(name, value);
}
- @Override
- public void enforceReadPermission(String namespace) {
- // no-op
- }
-
private Properties propsForNamespaceAndName(String namespace, String name) {
if (mProperties.containsKey(namespace) && mProperties.get(namespace).containsKey(name)) {
return new Properties.Builder(namespace)
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/util/TestableAlertDialog.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/util/TestableAlertDialog.kt
new file mode 100644
index 0000000..4d79554
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/util/TestableAlertDialog.kt
@@ -0,0 +1,141 @@
+/*
+ * 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.util
+
+import android.app.AlertDialog
+import android.content.Context
+import android.content.DialogInterface
+import java.lang.IllegalArgumentException
+
+/**
+ * [AlertDialog] that is easier to test. Due to [AlertDialog] being a class and not an interface,
+ * there are some things that cannot be avoided, like the creation of a [Handler] on the main thread
+ * (and therefore needing a prepared [Looper] in the test).
+ *
+ * It bypasses calls to show, clicks on buttons, cancel and dismiss so it all can happen bounded in
+ * the test. It tries to be as close in behavior as a real [AlertDialog].
+ *
+ * It will only call [onCreate] as part of its lifecycle, but not any of the other lifecycle methods
+ * in [Dialog].
+ *
+ * In order to test clicking on buttons, use [clickButton] instead of calling [View.callOnClick] on
+ * the view returned by [getButton] to bypass the internal [Handler].
+ */
+class TestableAlertDialog(context: Context) : AlertDialog(context) {
+
+ private var _onDismissListener: DialogInterface.OnDismissListener? = null
+ private var _onCancelListener: DialogInterface.OnCancelListener? = null
+ private var _positiveButtonClickListener: DialogInterface.OnClickListener? = null
+ private var _negativeButtonClickListener: DialogInterface.OnClickListener? = null
+ private var _neutralButtonClickListener: DialogInterface.OnClickListener? = null
+ private var _onShowListener: DialogInterface.OnShowListener? = null
+ private var _dismissOverride: Runnable? = null
+
+ private var showing = false
+ private var visible = false
+ private var created = false
+
+ override fun show() {
+ if (!created) {
+ created = true
+ onCreate(null)
+ }
+ if (isShowing) return
+ showing = true
+ visible = true
+ _onShowListener?.onShow(this)
+ }
+
+ override fun hide() {
+ visible = false
+ }
+
+ override fun isShowing(): Boolean {
+ return visible && showing
+ }
+
+ override fun dismiss() {
+ if (!showing) {
+ return
+ }
+ if (_dismissOverride != null) {
+ _dismissOverride?.run()
+ return
+ }
+ _onDismissListener?.onDismiss(this)
+ showing = false
+ }
+
+ override fun cancel() {
+ _onCancelListener?.onCancel(this)
+ dismiss()
+ }
+
+ override fun setOnDismissListener(listener: DialogInterface.OnDismissListener?) {
+ _onDismissListener = listener
+ }
+
+ override fun setOnCancelListener(listener: DialogInterface.OnCancelListener?) {
+ _onCancelListener = listener
+ }
+
+ override fun setOnShowListener(listener: DialogInterface.OnShowListener?) {
+ _onShowListener = listener
+ }
+
+ override fun takeCancelAndDismissListeners(
+ msg: String?,
+ cancel: DialogInterface.OnCancelListener?,
+ dismiss: DialogInterface.OnDismissListener?
+ ): Boolean {
+ _onCancelListener = cancel
+ _onDismissListener = dismiss
+ return true
+ }
+
+ override fun setButton(
+ whichButton: Int,
+ text: CharSequence?,
+ listener: DialogInterface.OnClickListener?
+ ) {
+ super.setButton(whichButton, text, listener)
+ when (whichButton) {
+ DialogInterface.BUTTON_POSITIVE -> _positiveButtonClickListener = listener
+ DialogInterface.BUTTON_NEGATIVE -> _negativeButtonClickListener = listener
+ DialogInterface.BUTTON_NEUTRAL -> _neutralButtonClickListener = listener
+ else -> Unit
+ }
+ }
+
+ /**
+ * Click one of the buttons in the [AlertDialog] and call the corresponding listener.
+ *
+ * Button ids are from [DialogInterface].
+ */
+ fun clickButton(whichButton: Int) {
+ val listener =
+ when (whichButton) {
+ DialogInterface.BUTTON_POSITIVE -> _positiveButtonClickListener
+ DialogInterface.BUTTON_NEGATIVE -> _negativeButtonClickListener
+ DialogInterface.BUTTON_NEUTRAL -> _neutralButtonClickListener
+ else -> throw IllegalArgumentException("Wrong button $whichButton")
+ }
+ listener?.onClick(this, whichButton)
+ dismiss()
+ }
+}
diff --git a/packages/SystemUI/unfold/src/com/android/systemui/unfold/progress/PhysicsBasedUnfoldTransitionProgressProvider.kt b/packages/SystemUI/unfold/src/com/android/systemui/unfold/progress/PhysicsBasedUnfoldTransitionProgressProvider.kt
index b568186..52fb0a7 100644
--- a/packages/SystemUI/unfold/src/com/android/systemui/unfold/progress/PhysicsBasedUnfoldTransitionProgressProvider.kt
+++ b/packages/SystemUI/unfold/src/com/android/systemui/unfold/progress/PhysicsBasedUnfoldTransitionProgressProvider.kt
@@ -31,6 +31,7 @@
import com.android.systemui.unfold.updates.FoldStateProvider
import com.android.systemui.unfold.updates.FoldStateProvider.FoldUpdate
import com.android.systemui.unfold.updates.FoldStateProvider.FoldUpdatesListener
+import com.android.systemui.unfold.updates.name
/** Maps fold updates to unfold transition progress using DynamicAnimation. */
class PhysicsBasedUnfoldTransitionProgressProvider(
@@ -117,7 +118,7 @@
}
if (DEBUG) {
- Log.d(TAG, "onFoldUpdate = $update")
+ Log.d(TAG, "onFoldUpdate = ${update.name()}")
Trace.traceCounter(Trace.TRACE_TAG_APP, "fold_update", update)
}
}
diff --git a/services/Android.bp b/services/Android.bp
index f6570e9..3f3ba06 100644
--- a/services/Android.bp
+++ b/services/Android.bp
@@ -187,6 +187,7 @@
"framework-tethering.stubs.module_lib",
"service-art.stubs.system_server",
"service-permission.stubs.system_server",
+ "service-rkp.stubs.system_server",
"service-sdksandbox.stubs.system_server",
],
diff --git a/services/api/current.txt b/services/api/current.txt
index d05431d..da5b1fc 100644
--- a/services/api/current.txt
+++ b/services/api/current.txt
@@ -57,7 +57,6 @@
public static interface PackageManagerLocal.FilteredSnapshot extends java.lang.AutoCloseable {
method public void close();
- method public void forAllPackageStates(@NonNull java.util.function.Consumer<com.android.server.pm.pkg.PackageState>);
method @Nullable public com.android.server.pm.pkg.PackageState getPackageState(@NonNull String);
method @NonNull public java.util.Map<java.lang.String,com.android.server.pm.pkg.PackageState> getPackageStates();
}
@@ -99,6 +98,7 @@
public interface PackageState {
method @Nullable public com.android.server.pm.pkg.AndroidPackage getAndroidPackage();
method public int getAppId();
+ method public int getHiddenApiEnforcementPolicy();
method @NonNull public String getPackageName();
method @Nullable public String getPrimaryCpuAbi();
method @Nullable public String getSeInfo();
@@ -129,13 +129,6 @@
}
-package com.android.server.pm.snapshot {
-
- public interface PackageDataSnapshot {
- }
-
-}
-
package com.android.server.role {
public interface RoleServicePlatformHelper {
diff --git a/services/companion/java/com/android/server/companion/virtual/GenericWindowPolicyController.java b/services/companion/java/com/android/server/companion/virtual/GenericWindowPolicyController.java
index f1ba5ff..5e68d52 100644
--- a/services/companion/java/com/android/server/companion/virtual/GenericWindowPolicyController.java
+++ b/services/companion/java/com/android/server/companion/virtual/GenericWindowPolicyController.java
@@ -16,8 +16,7 @@
package com.android.server.companion.virtual;
-import static android.companion.AssociationRequest.DEVICE_PROFILE_APP_STREAMING;
-import static android.companion.AssociationRequest.DEVICE_PROFILE_AUTOMOTIVE_PROJECTION;
+import static android.companion.virtual.VirtualDeviceParams.RECENTS_POLICY_ALLOW_IN_HOST_DEVICE_RECENTS;
import static android.content.pm.ActivityInfo.FLAG_CAN_DISPLAY_ON_REMOTE_DEVICES;
import static android.view.WindowManager.LayoutParams.FLAG_SECURE;
import static android.view.WindowManager.LayoutParams.SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS;
@@ -26,10 +25,10 @@
import android.annotation.Nullable;
import android.app.WindowConfiguration;
import android.app.compat.CompatChanges;
-import android.companion.AssociationRequest;
import android.companion.virtual.VirtualDeviceManager.ActivityListener;
import android.companion.virtual.VirtualDeviceParams;
import android.companion.virtual.VirtualDeviceParams.ActivityPolicy;
+import android.companion.virtual.VirtualDeviceParams.RecentsPolicy;
import android.compat.annotation.ChangeId;
import android.compat.annotation.EnabledSince;
import android.content.ComponentName;
@@ -127,10 +126,10 @@
@GuardedBy("mGenericWindowPolicyControllerLock")
private final ArraySet<RunningAppsChangedListener> mRunningAppsChangedListeners =
new ArraySet<>();
- @Nullable
- private final @AssociationRequest.DeviceProfile String mDeviceProfile;
@Nullable private final SecureWindowCallback mSecureWindowCallback;
@Nullable private final List<String> mDisplayCategories;
+ @RecentsPolicy
+ private final int mDefaultRecentsPolicy;
/**
* Creates a window policy controller that is generic to the different use cases of virtual
@@ -156,7 +155,7 @@
* launching.
* @param secureWindowCallback Callback that is called when a secure window shows on the
* virtual display.
- * @param deviceProfile The {@link AssociationRequest.DeviceProfile} of this virtual device.
+ * @param defaultRecentsPolicy a policy to indicate how to handle activities in recents.
*/
public GenericWindowPolicyController(int windowFlags, int systemWindowFlags,
@NonNull ArraySet<UserHandle> allowedUsers,
@@ -169,8 +168,8 @@
@NonNull PipBlockedCallback pipBlockedCallback,
@NonNull ActivityBlockedCallback activityBlockedCallback,
@NonNull SecureWindowCallback secureWindowCallback,
- @AssociationRequest.DeviceProfile String deviceProfile,
- @NonNull List<String> displayCategories) {
+ @NonNull List<String> displayCategories,
+ @RecentsPolicy int defaultRecentsPolicy) {
super();
mAllowedUsers = allowedUsers;
mAllowedCrossTaskNavigations = new ArraySet<>(allowedCrossTaskNavigations);
@@ -181,10 +180,10 @@
mActivityBlockedCallback = activityBlockedCallback;
setInterestedWindowFlags(windowFlags, systemWindowFlags);
mActivityListener = activityListener;
- mDeviceProfile = deviceProfile;
mPipBlockedCallback = pipBlockedCallback;
mSecureWindowCallback = secureWindowCallback;
mDisplayCategories = displayCategories;
+ mDefaultRecentsPolicy = defaultRecentsPolicy;
}
/**
@@ -318,18 +317,8 @@
}
@Override
- public boolean canShowTasksInRecents() {
- if (mDeviceProfile == null) {
- return true;
- }
- // TODO(b/234075973) : Remove this once proper API is ready.
- switch (mDeviceProfile) {
- case DEVICE_PROFILE_AUTOMOTIVE_PROJECTION:
- return false;
- case DEVICE_PROFILE_APP_STREAMING:
- default:
- return true;
- }
+ public boolean canShowTasksInHostDeviceRecents() {
+ return (mDefaultRecentsPolicy & RECENTS_POLICY_ALLOW_IN_HOST_DEVICE_RECENTS) != 0;
}
@Override
diff --git a/services/companion/java/com/android/server/companion/virtual/InputController.java b/services/companion/java/com/android/server/companion/virtual/InputController.java
index 96c71e5..0cea3d0 100644
--- a/services/companion/java/com/android/server/companion/virtual/InputController.java
+++ b/services/companion/java/com/android/server/companion/virtual/InputController.java
@@ -78,9 +78,8 @@
final Object mLock;
/* Token -> file descriptor associations. */
- @VisibleForTesting
@GuardedBy("mLock")
- final Map<IBinder, InputDeviceDescriptor> mInputDeviceDescriptors = new ArrayMap<>();
+ private final Map<IBinder, InputDeviceDescriptor> mInputDeviceDescriptors = new ArrayMap<>();
private final Handler mHandler;
private final NativeWrapper mNativeWrapper;
diff --git a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
index ef93810..5819861 100644
--- a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
+++ b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
@@ -679,8 +679,8 @@
this::onEnteringPipBlocked,
this::onActivityBlocked,
this::onSecureWindowShown,
- mAssociationInfo.getDeviceProfile(),
- displayCategories);
+ displayCategories,
+ mParams.getDefaultRecentsPolicy());
gwpc.registerRunningAppsChangedListener(/* listener= */ this);
return gwpc;
}
diff --git a/services/core/Android.bp b/services/core/Android.bp
index a61a61b..1e1d610 100644
--- a/services/core/Android.bp
+++ b/services/core/Android.bp
@@ -132,6 +132,7 @@
"framework-tethering.stubs.module_lib",
"service-art.stubs.system_server",
"service-permission.stubs.system_server",
+ "service-rkp.stubs.system_server",
"service-sdksandbox.stubs.system_server",
],
plugins: ["ImmutabilityAnnotationProcessor"],
@@ -144,6 +145,7 @@
static_libs: [
"android.hardware.authsecret-V1.0-java",
+ "android.hardware.authsecret-V1-java",
"android.hardware.boot-V1.0-java",
"android.hardware.boot-V1.1-java",
"android.hardware.boot-V1.2-java",
diff --git a/services/core/java/com/android/server/BinaryTransparencyService.java b/services/core/java/com/android/server/BinaryTransparencyService.java
index 6cd7ce8..6f0971c 100644
--- a/services/core/java/com/android/server/BinaryTransparencyService.java
+++ b/services/core/java/com/android/server/BinaryTransparencyService.java
@@ -476,6 +476,7 @@
}
private void printPackageMeasurements(PackageInfo packageInfo,
+ boolean useSha256,
final PrintWriter pw) {
Map<Integer, byte[]> contentDigests = computeApkContentDigest(
packageInfo.applicationInfo.sourceDir);
@@ -485,6 +486,14 @@
return;
}
+ if (useSha256) {
+ byte[] fileBuff = PackageUtils.createLargeFileBuffer();
+ String hexEncodedSha256Digest =
+ PackageUtils.computeSha256DigestForLargeFile(
+ packageInfo.applicationInfo.sourceDir, fileBuff);
+ pw.print(hexEncodedSha256Digest + ",");
+ }
+
for (Map.Entry<Integer, byte[]> entry : contentDigests.entrySet()) {
Integer algorithmId = entry.getKey();
byte[] contentDigest = entry.getValue();
@@ -497,6 +506,7 @@
}
private void printPackageInstallationInfo(PackageInfo packageInfo,
+ boolean useSha256,
final PrintWriter pw) {
pw.println("--- Package Installation Info ---");
pw.println("Current install location: "
@@ -507,11 +517,13 @@
pw.println("|--> Pre-installed package install location: "
+ origPackageFilepath);
- // TODO(b/259347186): revive this with the proper cmd options.
- /*
- String digest = PackageUtils.computeSha256DigestForLargeFile(
- origPackageFilepath, PackageUtils.createLargeFileBuffer());
- */
+ if (useSha256) {
+ String sha256Digest = PackageUtils.computeSha256DigestForLargeFile(
+ origPackageFilepath, PackageUtils.createLargeFileBuffer());
+ pw.println("|--> Pre-installed package SHA-256 digest: "
+ + sha256Digest);
+ }
+
Map<Integer, byte[]> contentDigests = computeApkContentDigest(
origPackageFilepath);
@@ -531,6 +543,8 @@
}
pw.println("First install time (ms): " + packageInfo.firstInstallTime);
pw.println("Last update time (ms): " + packageInfo.lastUpdateTime);
+ // TODO(b/261493591): Determination of whether a package is preinstalled can be
+ // made more robust
boolean isPreloaded = (packageInfo.firstInstallTime
== packageInfo.lastUpdateTime);
pw.println("Is preloaded: " + isPreloaded);
@@ -560,6 +574,7 @@
pw.println("ERROR: Package's signingInfo is null.");
return;
}
+ // TODO(b/261501773): Handle printing of lineage of rotated keys.
pw.println("--- Package Signer Info ---");
pw.println("Has multiple signers: " + signerInfo.hasMultipleSigners());
Signature[] packageSigners = signerInfo.getApkContentsSigners();
@@ -669,15 +684,35 @@
}
+ private void printHeadersHelper(@NonNull String packageType,
+ boolean useSha256,
+ @NonNull final PrintWriter pw) {
+ pw.print(packageType + " Info [Format: package_name,package_version,");
+ if (useSha256) {
+ pw.print("package_sha256_digest,");
+ }
+ pw.print("content_digest_algorithm:content_digest]:\n");
+ }
+
private int printAllApexs() {
final PrintWriter pw = getOutPrintWriter();
boolean verbose = false;
+ boolean useSha256 = false;
+ boolean printHeaders = true;
String opt;
while ((opt = getNextOption()) != null) {
switch (opt) {
case "-v":
+ case "--verbose":
verbose = true;
break;
+ case "-o":
+ case "--old":
+ useSha256 = true;
+ break;
+ case "--no-headers":
+ printHeaders = false;
+ break;
default:
pw.println("ERROR: Unknown option: " + opt);
return 1;
@@ -690,23 +725,17 @@
return -1;
}
- if (!verbose) {
- pw.println("APEX Info [Format: package_name,package_version,"
- // TODO(b/259347186): revive via special cmd line option
- //+ "package_sha256_digest,"
- + "content_digest_algorithm:content_digest]:");
+ if (!verbose && printHeaders) {
+ printHeadersHelper("APEX", useSha256, pw);
}
for (PackageInfo packageInfo : getCurrentInstalledApexs()) {
- if (verbose) {
- pw.println("APEX Info [Format: package_name,package_version,"
- // TODO(b/259347186): revive via special cmd line option
- //+ "package_sha256_digest,"
- + "content_digest_algorithm:content_digest]:");
+ if (verbose && printHeaders) {
+ printHeadersHelper("APEX", useSha256, pw);
}
String packageName = packageInfo.packageName;
pw.print(packageName + ","
+ packageInfo.getLongVersionCode() + ",");
- printPackageMeasurements(packageInfo, pw);
+ printPackageMeasurements(packageInfo, useSha256, pw);
if (verbose) {
ModuleInfo moduleInfo;
@@ -718,7 +747,7 @@
pw.println("Is a module: false");
}
- printPackageInstallationInfo(packageInfo, pw);
+ printPackageInstallationInfo(packageInfo, useSha256, pw);
printPackageSignerDetails(packageInfo.signingInfo, pw);
pw.println("");
}
@@ -729,12 +758,22 @@
private int printAllModules() {
final PrintWriter pw = getOutPrintWriter();
boolean verbose = false;
+ boolean useSha256 = false;
+ boolean printHeaders = true;
String opt;
while ((opt = getNextOption()) != null) {
switch (opt) {
case "-v":
+ case "--verbose":
verbose = true;
break;
+ case "-o":
+ case "--old":
+ useSha256 = true;
+ break;
+ case "--no-headers":
+ printHeaders = false;
+ break;
default:
pw.println("ERROR: Unknown option: " + opt);
return 1;
@@ -747,32 +786,25 @@
return -1;
}
- if (!verbose) {
- pw.println("Module Info [Format: package_name,package_version,"
- // TODO(b/259347186): revive via special cmd line option
- //+ "package_sha256_digest,"
- + "content_digest_algorithm:content_digest]:");
+ if (!verbose && printHeaders) {
+ printHeadersHelper("Module", useSha256, pw);
}
for (ModuleInfo module : pm.getInstalledModules(PackageManager.MATCH_ALL)) {
String packageName = module.getPackageName();
- if (verbose) {
- pw.println("Module Info [Format: package_name,package_version,"
- // TODO(b/259347186): revive via special cmd line option
- //+ "package_sha256_digest,"
- + "content_digest_algorithm:content_digest]:");
+ if (verbose && printHeaders) {
+ printHeadersHelper("Module", useSha256, pw);
}
try {
PackageInfo packageInfo = pm.getPackageInfo(packageName,
PackageManager.MATCH_APEX
| PackageManager.GET_SIGNING_CERTIFICATES);
- //pw.print("package:");
pw.print(packageInfo.packageName + ",");
pw.print(packageInfo.getLongVersionCode() + ",");
- printPackageMeasurements(packageInfo, pw);
+ printPackageMeasurements(packageInfo, useSha256, pw);
if (verbose) {
printModuleDetails(module, pw);
- printPackageInstallationInfo(packageInfo, pw);
+ printPackageInstallationInfo(packageInfo, useSha256, pw);
printPackageSignerDetails(packageInfo.signingInfo, pw);
pw.println("");
}
@@ -793,41 +825,45 @@
final PrintWriter pw = getOutPrintWriter();
boolean verbose = false;
boolean printLibraries = false;
+ boolean useSha256 = false;
+ boolean printHeaders = true;
String opt;
while ((opt = getNextOption()) != null) {
switch (opt) {
case "-v":
+ case "--verbose":
verbose = true;
break;
case "-l":
printLibraries = true;
break;
+ case "-o":
+ case "--old":
+ useSha256 = true;
+ break;
+ case "--no-headers":
+ printHeaders = false;
+ break;
default:
pw.println("ERROR: Unknown option: " + opt);
return 1;
}
}
- if (!verbose) {
- pw.println("MBA Info [Format: package_name,package_version,"
- // TODO(b/259347186): revive via special cmd line option
- //+ "package_sha256_digest,"
- + "content_digest_algorithm:content_digest]:");
+ if (!verbose && printHeaders) {
+ printHeadersHelper("MBA", useSha256, pw);
}
for (PackageInfo packageInfo : getNewlyInstalledMbas()) {
- if (verbose) {
- pw.println("MBA Info [Format: package_name,package_version,"
- // TODO(b/259347186): revive via special cmd line option
- //+ "package_sha256_digest,"
- + "content_digest_algorithm:content_digest]:");
+ if (verbose && printHeaders) {
+ printHeadersHelper("MBA", useSha256, pw);
}
pw.print(packageInfo.packageName + ",");
pw.print(packageInfo.getLongVersionCode() + ",");
- printPackageMeasurements(packageInfo, pw);
+ printPackageMeasurements(packageInfo, useSha256, pw);
if (verbose) {
printAppDetails(packageInfo, printLibraries, pw);
- printPackageInstallationInfo(packageInfo, pw);
+ printPackageInstallationInfo(packageInfo, useSha256, pw);
printPackageSignerDetails(packageInfo.signingInfo, pw);
pw.println("");
}
@@ -894,27 +930,39 @@
private void printHelpMenu() {
final PrintWriter pw = getOutPrintWriter();
pw.println("Transparency manager (transparency) commands:");
- pw.println(" help");
- pw.println(" Print this help text.");
+ pw.println(" help");
+ pw.println(" Print this help text.");
pw.println("");
- pw.println(" get image_info [-a]");
- pw.println(" Print information about loaded image (firmware). Options:");
- pw.println(" -a: lists all other identifiable partitions.");
+ pw.println(" get image_info [-a]");
+ pw.println(" Print information about loaded image (firmware). Options:");
+ pw.println(" -a: lists all other identifiable partitions.");
pw.println("");
- pw.println(" get apex_info [-v]");
- pw.println(" Print information about installed APEXs on device.");
- pw.println(" -v: lists more verbose information about each APEX.");
+ pw.println(" get apex_info [-o] [-v] [--no-headers]");
+ pw.println(" Print information about installed APEXs on device.");
+ pw.println(" -o: also uses the old digest scheme (SHA256) to compute "
+ + "APEX hashes. WARNING: This can be a very slow and CPU-intensive "
+ + "computation.");
+ pw.println(" -v: lists more verbose information about each APEX.");
+ pw.println(" --no-headers: does not print the header if specified");
pw.println("");
- pw.println(" get module_info [-v]");
- pw.println(" Print information about installed modules on device.");
- pw.println(" -v: lists more verbose information about each module.");
+ pw.println(" get module_info [-o] [-v] [--no-headers]");
+ pw.println(" Print information about installed modules on device.");
+ pw.println(" -o: also uses the old digest scheme (SHA256) to compute "
+ + "module hashes. WARNING: This can be a very slow and "
+ + "CPU-intensive computation.");
+ pw.println(" -v: lists more verbose information about each module.");
+ pw.println(" --no-headers: does not print the header if specified");
pw.println("");
- pw.println(" get mba_info [-v] [-l]");
- pw.println(" Print information about installed mobile bundle apps "
+ pw.println(" get mba_info [-o] [-v] [-l] [--no-headers]");
+ pw.println(" Print information about installed mobile bundle apps "
+ "(MBAs on device).");
- pw.println(" -v: lists more verbose information about each app.");
- pw.println(" -l: lists shared library info. This will only be "
- + "listed with -v");
+ pw.println(" -o: also uses the old digest scheme (SHA256) to compute "
+ + "MBA hashes. WARNING: This can be a very slow and CPU-intensive "
+ + "computation.");
+ pw.println(" -v: lists more verbose information about each app.");
+ pw.println(" -l: lists shared library info. (This option only works "
+ + "when -v option is also specified)");
+ pw.println(" --no-headers: does not print the header if specified");
pw.println("");
}
diff --git a/services/core/java/com/android/server/StorageManagerService.java b/services/core/java/com/android/server/StorageManagerService.java
index 6b6351f..f88f99b 100644
--- a/services/core/java/com/android/server/StorageManagerService.java
+++ b/services/core/java/com/android/server/StorageManagerService.java
@@ -672,6 +672,7 @@
private static final int H_COMPLETE_UNLOCK_USER = 14;
private static final int H_VOLUME_STATE_CHANGED = 15;
private static final int H_CLOUD_MEDIA_PROVIDER_CHANGED = 16;
+ private static final int H_SECURE_KEYGUARD_STATE_CHANGED = 17;
class StorageManagerServiceHandler extends Handler {
public StorageManagerServiceHandler(Looper looper) {
@@ -819,6 +820,14 @@
}
break;
}
+ case H_SECURE_KEYGUARD_STATE_CHANGED: {
+ try {
+ mVold.onSecureKeyguardStateChanged((boolean) msg.obj);
+ } catch (Exception e) {
+ Slog.wtf(TAG, e);
+ }
+ break;
+ }
}
}
}
@@ -1242,12 +1251,12 @@
public void onKeyguardStateChanged(boolean isShowing) {
// Push down current secure keyguard status so that we ignore malicious
// USB devices while locked.
- mSecureKeyguardShowing = isShowing
+ boolean isSecureKeyguardShowing = isShowing
&& mContext.getSystemService(KeyguardManager.class).isDeviceSecure(mCurrentUserId);
- try {
- mVold.onSecureKeyguardStateChanged(mSecureKeyguardShowing);
- } catch (Exception e) {
- Slog.wtf(TAG, e);
+ if (mSecureKeyguardShowing != isSecureKeyguardShowing) {
+ mSecureKeyguardShowing = isSecureKeyguardShowing;
+ mHandler.obtainMessage(H_SECURE_KEYGUARD_STATE_CHANGED, mSecureKeyguardShowing)
+ .sendToTarget();
}
}
diff --git a/services/core/java/com/android/server/TelephonyRegistry.java b/services/core/java/com/android/server/TelephonyRegistry.java
index bd90d85..32afcca 100644
--- a/services/core/java/com/android/server/TelephonyRegistry.java
+++ b/services/core/java/com/android/server/TelephonyRegistry.java
@@ -1041,7 +1041,7 @@
String callingFeatureId, IPhoneStateListener callback,
int[] events, boolean notifyNow) {
Set<Integer> eventList = Arrays.stream(events).boxed().collect(Collectors.toSet());
- listen(renounceFineLocationAccess, renounceFineLocationAccess, callingPackage,
+ listen(renounceFineLocationAccess, renounceCoarseLocationAccess, callingPackage,
callingFeatureId, callback, eventList, notifyNow, subId);
}
@@ -1606,7 +1606,7 @@
if (DBG) {
log("notifyServiceStateForSubscriber: callback.onSSC r=" + r
+ " subId=" + subId + " phoneId=" + phoneId
- + " state=" + state);
+ + " state=" + stateToSend);
}
r.callback.onServiceStateChanged(stateToSend);
} catch (RemoteException ex) {
diff --git a/services/core/java/com/android/server/am/ActiveServices.java b/services/core/java/com/android/server/am/ActiveServices.java
index 0d672bd..35b3db8 100644
--- a/services/core/java/com/android/server/am/ActiveServices.java
+++ b/services/core/java/com/android/server/am/ActiveServices.java
@@ -2981,6 +2981,9 @@
void onShortFgsTimeout(ServiceRecord sr) {
synchronized (mAm) {
if (!sr.shouldTriggerShortFgsTimeout()) {
+ if (DEBUG_SHORT_SERVICE) {
+ Slog.d(TAG_SERVICE, "[STALE] Short FGS timed out: " + sr);
+ }
return;
}
Slog.e(TAG_SERVICE, "Short FGS timed out: " + sr);
@@ -3021,6 +3024,9 @@
tr.mLatencyTracker.waitingOnAMSLockEnded();
if (!sr.shouldTriggerShortFgsAnr()) {
+ if (DEBUG_SHORT_SERVICE) {
+ Slog.d(TAG_SERVICE, "[STALE] Short FGS ANR'ed: " + sr);
+ }
return;
}
@@ -7713,4 +7719,35 @@
Slog.e(TAG, "stopForegroundServiceDelegateLocked delegate does not exist");
}
}
+
+ private static void getClientPackages(ServiceRecord sr, ArraySet<String> output) {
+ var connections = sr.getConnections();
+ for (int conni = connections.size() - 1; conni >= 0; conni--) {
+ var connl = connections.valueAt(conni);
+ for (int i = 0, size = connl.size(); i < size; i++) {
+ var conn = connl.get(i);
+ if (conn.binding.client != null) {
+ output.add(conn.binding.client.info.packageName);
+ }
+ }
+ }
+ }
+
+ /**
+ * Return all client package names of a service.
+ */
+ ArraySet<String> getClientPackagesLocked(@NonNull String servicePackageName) {
+ var results = new ArraySet<String>();
+ int[] users = mAm.mUserController.getUsers();
+ for (int ui = 0; ui < users.length; ui++) {
+ ArrayMap<ComponentName, ServiceRecord> alls = getServicesLocked(users[ui]);
+ for (int i = 0, size = alls.size(); i < size; i++) {
+ ServiceRecord sr = alls.valueAt(i);
+ if (sr.name.getPackageName().equals(servicePackageName)) {
+ getClientPackages(sr, results);
+ }
+ }
+ }
+ return results;
+ }
}
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index 7566bab..fd24300 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -328,6 +328,7 @@
import android.util.ArraySet;
import android.util.EventLog;
import android.util.FeatureFlagUtils;
+import android.util.IndentingPrintWriter;
import android.util.IntArray;
import android.util.Log;
import android.util.Pair;
@@ -759,6 +760,9 @@
final AppErrors mAppErrors;
final PackageWatchdog mPackageWatchdog;
+ @GuardedBy("mDeliveryGroupPolicyIgnoredActions")
+ private final ArraySet<String> mDeliveryGroupPolicyIgnoredActions = new ArraySet();
+
/**
* Uids of apps with current active camera sessions. Access synchronized on
* the IntArray instance itself, and no other locks must be acquired while that
@@ -18191,6 +18195,13 @@
mServices.stopForegroundServiceDelegateLocked(connection);
}
}
+
+ @Override
+ public ArraySet<String> getClientPackages(String servicePackageName) {
+ synchronized (ActivityManagerService.this) {
+ return mServices.getClientPackagesLocked(servicePackageName);
+ }
+ }
}
long inputDispatchingTimedOut(int pid, final boolean aboveSystem, TimeoutRecord timeoutRecord) {
@@ -18322,14 +18333,47 @@
}
}
- public void waitForBroadcastBarrier(@Nullable PrintWriter pw) {
+ public void waitForBroadcastBarrier(@Nullable PrintWriter pw, boolean flushBroadcastLoopers) {
enforceCallingPermission(permission.DUMP, "waitForBroadcastBarrier()");
- BroadcastLoopers.waitForIdle(pw);
+ if (flushBroadcastLoopers) {
+ BroadcastLoopers.waitForBarrier(pw);
+ }
for (BroadcastQueue queue : mBroadcastQueues) {
queue.waitForBarrier(pw);
}
}
+ void setIgnoreDeliveryGroupPolicy(@NonNull String broadcastAction) {
+ Objects.requireNonNull(broadcastAction);
+ enforceCallingPermission(permission.DUMP, "waitForBroadcastBarrier()");
+ synchronized (mDeliveryGroupPolicyIgnoredActions) {
+ mDeliveryGroupPolicyIgnoredActions.add(broadcastAction);
+ }
+ }
+
+ void clearIgnoreDeliveryGroupPolicy(@NonNull String broadcastAction) {
+ Objects.requireNonNull(broadcastAction);
+ enforceCallingPermission(permission.DUMP, "waitForBroadcastBarrier()");
+ synchronized (mDeliveryGroupPolicyIgnoredActions) {
+ mDeliveryGroupPolicyIgnoredActions.remove(broadcastAction);
+ }
+ }
+
+ boolean shouldIgnoreDeliveryGroupPolicy(@Nullable String broadcastAction) {
+ if (broadcastAction == null) {
+ return false;
+ }
+ synchronized (mDeliveryGroupPolicyIgnoredActions) {
+ return mDeliveryGroupPolicyIgnoredActions.contains(broadcastAction);
+ }
+ }
+
+ void dumpDeliveryGroupPolicyIgnoredActions(IndentingPrintWriter ipw) {
+ synchronized (mDeliveryGroupPolicyIgnoredActions) {
+ ipw.println(mDeliveryGroupPolicyIgnoredActions);
+ }
+ }
+
@Override
@ReasonCode
public int getBackgroundRestrictionExemptionReason(int uid) {
diff --git a/services/core/java/com/android/server/am/ActivityManagerShellCommand.java b/services/core/java/com/android/server/am/ActivityManagerShellCommand.java
index 0b94798..94c15ba 100644
--- a/services/core/java/com/android/server/am/ActivityManagerShellCommand.java
+++ b/services/core/java/com/android/server/am/ActivityManagerShellCommand.java
@@ -345,6 +345,10 @@
return runWaitForBroadcastIdle(pw);
case "wait-for-broadcast-barrier":
return runWaitForBroadcastBarrier(pw);
+ case "set-ignore-delivery-group-policy":
+ return runSetIgnoreDeliveryGroupPolicy(pw);
+ case "clear-ignore-delivery-group-policy":
+ return runClearIgnoreDeliveryGroupPolicy(pw);
case "compat":
return runCompat(pw);
case "refresh-settings-cache":
@@ -2006,12 +2010,6 @@
}
int runSwitchUser(PrintWriter pw) throws RemoteException {
- UserManager userManager = mInternal.mContext.getSystemService(UserManager.class);
- final int userSwitchable = userManager.getUserSwitchability();
- if (userSwitchable != UserManager.SWITCHABILITY_STATUS_OK) {
- getErrPrintWriter().println("Error: " + userSwitchable);
- return -1;
- }
boolean wait = false;
String opt;
while ((opt = getNextOption()) != null) {
@@ -2024,6 +2022,14 @@
}
int userId = Integer.parseInt(getNextArgRequired());
+
+ UserManager userManager = mInternal.mContext.getSystemService(UserManager.class);
+ final int userSwitchable = userManager.getUserSwitchability(UserHandle.of(userId));
+ if (userSwitchable != UserManager.SWITCHABILITY_STATUS_OK) {
+ getErrPrintWriter().println("Error: UserSwitchabilityResult=" + userSwitchable);
+ return -1;
+ }
+
boolean switched;
Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "shell_runSwitchUser");
try {
@@ -3131,7 +3137,29 @@
}
int runWaitForBroadcastBarrier(PrintWriter pw) throws RemoteException {
- mInternal.waitForBroadcastBarrier(pw);
+ boolean flushBroadcastLoopers = false;
+ String opt;
+ while ((opt = getNextOption()) != null) {
+ if (opt.equals("--flush-broadcast-loopers")) {
+ flushBroadcastLoopers = true;
+ } else {
+ getErrPrintWriter().println("Error: Unknown option: " + opt);
+ return -1;
+ }
+ }
+ mInternal.waitForBroadcastBarrier(pw, flushBroadcastLoopers);
+ return 0;
+ }
+
+ int runSetIgnoreDeliveryGroupPolicy(PrintWriter pw) throws RemoteException {
+ final String broadcastAction = getNextArgRequired();
+ mInternal.setIgnoreDeliveryGroupPolicy(broadcastAction);
+ return 0;
+ }
+
+ int runClearIgnoreDeliveryGroupPolicy(PrintWriter pw) throws RemoteException {
+ final String broadcastAction = getNextArgRequired();
+ mInternal.clearIgnoreDeliveryGroupPolicy(broadcastAction);
return 0;
}
@@ -4019,7 +4047,10 @@
+ "background.");
pw.println(" set-foreground-service-delegate [--user <USER_ID>] <PACKAGE> start|stop");
pw.println(" Start/stop an app's foreground service delegate.");
- pw.println();
+ pw.println(" set-ignore-delivery-group-policy <ACTION>");
+ pw.println(" Start ignoring delivery group policy set for a broadcast action");
+ pw.println(" clear-ignore-delivery-group-policy <ACTION>");
+ pw.println(" Stop ignoring delivery group policy set for a broadcast action");
Intent.printIntentArgsHelp(pw, "");
}
}
diff --git a/services/core/java/com/android/server/am/BroadcastConstants.java b/services/core/java/com/android/server/am/BroadcastConstants.java
index 1eebd01..f5d1c10 100644
--- a/services/core/java/com/android/server/am/BroadcastConstants.java
+++ b/services/core/java/com/android/server/am/BroadcastConstants.java
@@ -153,12 +153,27 @@
"bcast_extra_running_urgent_process_queues";
private static final int DEFAULT_EXTRA_RUNNING_URGENT_PROCESS_QUEUES = 1;
+ /**
+ * For {@link BroadcastQueueModernImpl}: Maximum number of consecutive urgent
+ * broadcast dispatches allowed before letting broadcasts in lower priority queue
+ * to be scheduled in order to avoid starvation.
+ */
public int MAX_CONSECUTIVE_URGENT_DISPATCHES = DEFAULT_MAX_CONSECUTIVE_URGENT_DISPATCHES;
private static final String KEY_MAX_CONSECUTIVE_URGENT_DISPATCHES =
"bcast_max_consecutive_urgent_dispatches";
private static final int DEFAULT_MAX_CONSECUTIVE_URGENT_DISPATCHES = 3;
/**
+ * For {@link BroadcastQueueModernImpl}: Maximum number of consecutive normal
+ * broadcast dispatches allowed before letting broadcasts in lower priority queue
+ * to be scheduled in order to avoid starvation.
+ */
+ public int MAX_CONSECUTIVE_NORMAL_DISPATCHES = DEFAULT_MAX_CONSECUTIVE_NORMAL_DISPATCHES;
+ private static final String KEY_MAX_CONSECUTIVE_NORMAL_DISPATCHES =
+ "bcast_max_consecutive_normal_dispatches";
+ private static final int DEFAULT_MAX_CONSECUTIVE_NORMAL_DISPATCHES = 10;
+
+ /**
* For {@link BroadcastQueueModernImpl}: Maximum number of active broadcasts
* to dispatch to a "running" process queue before we retire them back to
* being "runnable" to give other processes a chance to run.
@@ -341,6 +356,9 @@
MAX_CONSECUTIVE_URGENT_DISPATCHES = getDeviceConfigInt(
KEY_MAX_CONSECUTIVE_URGENT_DISPATCHES,
DEFAULT_MAX_CONSECUTIVE_URGENT_DISPATCHES);
+ MAX_CONSECUTIVE_NORMAL_DISPATCHES = getDeviceConfigInt(
+ KEY_MAX_CONSECUTIVE_NORMAL_DISPATCHES,
+ DEFAULT_MAX_CONSECUTIVE_NORMAL_DISPATCHES);
MAX_RUNNING_ACTIVE_BROADCASTS = getDeviceConfigInt(KEY_MAX_RUNNING_ACTIVE_BROADCASTS,
DEFAULT_MAX_RUNNING_ACTIVE_BROADCASTS);
MAX_PENDING_BROADCASTS = getDeviceConfigInt(KEY_MAX_PENDING_BROADCASTS,
@@ -396,6 +414,10 @@
TimeUtils.formatDuration(DELAY_URGENT_MILLIS)).println();
pw.print(KEY_MAX_HISTORY_COMPLETE_SIZE, MAX_HISTORY_COMPLETE_SIZE).println();
pw.print(KEY_MAX_HISTORY_SUMMARY_SIZE, MAX_HISTORY_SUMMARY_SIZE).println();
+ pw.print(KEY_MAX_CONSECUTIVE_URGENT_DISPATCHES,
+ MAX_CONSECUTIVE_URGENT_DISPATCHES).println();
+ pw.print(KEY_MAX_CONSECUTIVE_NORMAL_DISPATCHES,
+ MAX_CONSECUTIVE_NORMAL_DISPATCHES).println();
pw.decreaseIndent();
pw.println();
}
diff --git a/services/core/java/com/android/server/am/BroadcastLoopers.java b/services/core/java/com/android/server/am/BroadcastLoopers.java
index b828720..a5535cb 100644
--- a/services/core/java/com/android/server/am/BroadcastLoopers.java
+++ b/services/core/java/com/android/server/am/BroadcastLoopers.java
@@ -18,6 +18,7 @@
import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.os.MessageQueue;
@@ -30,6 +31,7 @@
import java.io.PrintWriter;
import java.util.Objects;
import java.util.concurrent.CountDownLatch;
+import java.util.function.BiConsumer;
/**
* Collection of {@link Looper} that are known to be used for broadcast dispatch
@@ -73,19 +75,44 @@
* still in the future are ignored for the purposes of the idle test.
*/
public static void waitForIdle(@Nullable PrintWriter pw) {
+ waitForCondition(pw, (looper, latch) -> {
+ final MessageQueue queue = looper.getQueue();
+ queue.addIdleHandler(() -> {
+ latch.countDown();
+ return false;
+ });
+ });
+ }
+
+ /**
+ * Wait for all registered {@link Looper} instances to handle currently waiting messages.
+ * Note that {@link Message#when} still in the future are ignored for the purposes
+ * of the idle test.
+ */
+ public static void waitForBarrier(@Nullable PrintWriter pw) {
+ waitForCondition(pw, (looper, latch) -> {
+ (new Handler(looper)).post(() -> {
+ latch.countDown();
+ });
+ });
+ }
+
+ /**
+ * Wait for all registered {@link Looper} instances to meet a certain condition.
+ */
+ private static void waitForCondition(@Nullable PrintWriter pw,
+ @NonNull BiConsumer<Looper, CountDownLatch> condition) {
final CountDownLatch latch;
synchronized (sLoopers) {
final int N = sLoopers.size();
latch = new CountDownLatch(N);
for (int i = 0; i < N; i++) {
- final MessageQueue queue = sLoopers.valueAt(i).getQueue();
+ final Looper looper = sLoopers.valueAt(i);
+ final MessageQueue queue = looper.getQueue();
if (queue.isIdle()) {
latch.countDown();
} else {
- queue.addIdleHandler(() -> {
- latch.countDown();
- return false;
- });
+ condition.accept(looper, latch);
}
}
}
diff --git a/services/core/java/com/android/server/am/BroadcastProcessQueue.java b/services/core/java/com/android/server/am/BroadcastProcessQueue.java
index 66d7fc9..15d2fa3 100644
--- a/services/core/java/com/android/server/am/BroadcastProcessQueue.java
+++ b/services/core/java/com/android/server/am/BroadcastProcessQueue.java
@@ -153,6 +153,12 @@
private int mActiveCountConsecutiveUrgent;
/**
+ * Number of consecutive normal broadcasts that have been dispatched
+ * since the last offload dispatch.
+ */
+ private int mActiveCountConsecutiveNormal;
+
+ /**
* Count of pending broadcasts of these various flavors.
*/
private int mCountForeground;
@@ -164,6 +170,8 @@
private int mCountInstrumented;
private int mCountManifest;
+ private boolean mPrioritizeEarliest;
+
private @UptimeMillisLong long mRunnableAt = Long.MAX_VALUE;
private @Reason int mRunnableAtReason = REASON_EMPTY;
private boolean mRunnableAtInvalidated;
@@ -551,48 +559,75 @@
* Will thrown an exception if there are no pending broadcasts; relies on
* {@link #isEmpty()} being false.
*/
- SomeArgs removeNextBroadcast() {
+ private @Nullable SomeArgs removeNextBroadcast() {
final ArrayDeque<SomeArgs> queue = queueForNextBroadcast();
if (queue == mPendingUrgent) {
mActiveCountConsecutiveUrgent++;
- } else {
+ } else if (queue == mPending) {
mActiveCountConsecutiveUrgent = 0;
+ mActiveCountConsecutiveNormal++;
+ } else if (queue == mPendingOffload) {
+ mActiveCountConsecutiveUrgent = 0;
+ mActiveCountConsecutiveNormal = 0;
}
- return queue.removeFirst();
+ return !isQueueEmpty(queue) ? queue.removeFirst() : null;
}
@Nullable ArrayDeque<SomeArgs> queueForNextBroadcast() {
- ArrayDeque<SomeArgs> nextUrgent = mPendingUrgent.isEmpty() ? null : mPendingUrgent;
- ArrayDeque<SomeArgs> nextNormal = null;
- if (!mPending.isEmpty()) {
- nextNormal = mPending;
- } else if (!mPendingOffload.isEmpty()) {
- nextNormal = mPendingOffload;
+ final ArrayDeque<SomeArgs> nextNormal = queueForNextBroadcast(
+ mPending, mPendingOffload,
+ mActiveCountConsecutiveNormal, constants.MAX_CONSECUTIVE_NORMAL_DISPATCHES);
+ final ArrayDeque<SomeArgs> nextBroadcastQueue = queueForNextBroadcast(
+ mPendingUrgent, nextNormal,
+ mActiveCountConsecutiveUrgent, constants.MAX_CONSECUTIVE_URGENT_DISPATCHES);
+ return nextBroadcastQueue;
+ }
+
+ private @Nullable ArrayDeque<SomeArgs> queueForNextBroadcast(
+ @Nullable ArrayDeque<SomeArgs> highPriorityQueue,
+ @Nullable ArrayDeque<SomeArgs> lowPriorityQueue,
+ int consecutiveHighPriorityCount,
+ int maxHighPriorityDispatchLimit) {
+ // nothing high priority pending, no further decisionmaking
+ if (isQueueEmpty(highPriorityQueue)) {
+ return lowPriorityQueue;
}
- // nothing urgent pending, no further decisionmaking
- if (nextUrgent == null) {
- return nextNormal;
- }
- // nothing but urgent pending, also no further decisionmaking
- if (nextNormal == null) {
- return nextUrgent;
+ // nothing but high priority pending, also no further decisionmaking
+ if (isQueueEmpty(lowPriorityQueue)) {
+ return highPriorityQueue;
}
- // Starvation mitigation: although we prioritize urgent broadcasts by default,
- // we allow non-urgent deliveries to make steady progress even if urgent
- // broadcasts are arriving faster than they can be dispatched.
+ // Starvation mitigation: although we prioritize high priority queues by default,
+ // we allow low priority queues to make steady progress even if broadcasts in
+ // high priority queue are arriving faster than they can be dispatched.
//
- // We do not try to defer to the next non-urgent broadcast if that broadcast
+ // We do not try to defer to the next broadcast in low priority queues if that broadcast
// is ordered and still blocked on delivery to other recipients.
- final SomeArgs nextNormalArgs = nextNormal.peekFirst();
- final BroadcastRecord rNormal = (BroadcastRecord) nextNormalArgs.arg1;
- final int nextNormalIndex = nextNormalArgs.argi1;
- final BroadcastRecord rUrgent = (BroadcastRecord) nextUrgent.peekFirst().arg1;
- final boolean canTakeNormal =
- mActiveCountConsecutiveUrgent >= constants.MAX_CONSECUTIVE_URGENT_DISPATCHES
- && rNormal.enqueueTime <= rUrgent.enqueueTime
- && !blockedOnOrderedDispatch(rNormal, nextNormalIndex);
- return canTakeNormal ? nextNormal : nextUrgent;
+ final SomeArgs nextLPArgs = lowPriorityQueue.peekFirst();
+ final BroadcastRecord nextLPRecord = (BroadcastRecord) nextLPArgs.arg1;
+ final int nextLPRecordIndex = nextLPArgs.argi1;
+ final BroadcastRecord nextHPRecord = (BroadcastRecord) highPriorityQueue.peekFirst().arg1;
+ final boolean shouldConsiderLPQueue = (mPrioritizeEarliest
+ || consecutiveHighPriorityCount >= maxHighPriorityDispatchLimit);
+ final boolean isLPQueueEligible = shouldConsiderLPQueue
+ && nextLPRecord.enqueueTime <= nextHPRecord.enqueueTime
+ && !blockedOnOrderedDispatch(nextLPRecord, nextLPRecordIndex);
+ return isLPQueueEligible ? lowPriorityQueue : highPriorityQueue;
+ }
+
+ private static boolean isQueueEmpty(@Nullable ArrayDeque<SomeArgs> queue) {
+ return (queue == null || queue.isEmpty());
+ }
+
+ /**
+ * When {@code prioritizeEarliest} is set to {@code true}, then earliest enqueued
+ * broadcasts would be prioritized for dispatching, even if there are urgent broadcasts
+ * waiting. This is typically used in case there are callers waiting for "barrier" to be
+ * reached.
+ */
+ @VisibleForTesting
+ void setPrioritizeEarliest(boolean prioritizeEarliest) {
+ mPrioritizeEarliest = prioritizeEarliest;
}
/**
@@ -600,13 +635,13 @@
*/
@Nullable SomeArgs peekNextBroadcast() {
ArrayDeque<SomeArgs> queue = queueForNextBroadcast();
- return (queue != null) ? queue.peekFirst() : null;
+ return !isQueueEmpty(queue) ? queue.peekFirst() : null;
}
@VisibleForTesting
@Nullable BroadcastRecord peekNextBroadcastRecord() {
ArrayDeque<SomeArgs> queue = queueForNextBroadcast();
- return (queue != null) ? (BroadcastRecord) queue.peekFirst().arg1 : null;
+ return !isQueueEmpty(queue) ? (BroadcastRecord) queue.peekFirst().arg1 : null;
}
/**
diff --git a/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java b/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java
index 5d5dbbb..eb5c03b 100644
--- a/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java
+++ b/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java
@@ -91,6 +91,7 @@
import java.util.Set;
import java.util.concurrent.CountDownLatch;
import java.util.function.BooleanSupplier;
+import java.util.function.Consumer;
import java.util.function.Predicate;
/**
@@ -220,10 +221,19 @@
private void enqueueFinishReceiver(@NonNull BroadcastProcessQueue queue,
@DeliveryState int deliveryState, @NonNull String reason) {
+ enqueueFinishReceiver(queue, queue.getActive(), queue.getActiveIndex(),
+ deliveryState, reason);
+ }
+
+ private void enqueueFinishReceiver(@NonNull BroadcastProcessQueue queue,
+ @NonNull BroadcastRecord r, int index,
+ @DeliveryState int deliveryState, @NonNull String reason) {
final SomeArgs args = SomeArgs.obtain();
args.arg1 = queue;
args.argi1 = deliveryState;
args.arg2 = reason;
+ args.arg3 = r;
+ args.argi2 = index;
mLocalHandler.sendMessage(Message.obtain(mLocalHandler, MSG_FINISH_RECEIVER, args));
}
@@ -271,8 +281,10 @@
final BroadcastProcessQueue queue = (BroadcastProcessQueue) args.arg1;
final int deliveryState = args.argi1;
final String reason = (String) args.arg2;
+ final BroadcastRecord r = (BroadcastRecord) args.arg3;
+ final int index = args.argi2;
args.recycle();
- finishReceiverLocked(queue, deliveryState, reason);
+ finishReceiverLocked(queue, deliveryState, reason, r, index);
}
return true;
}
@@ -636,6 +648,9 @@
}
private void applyDeliveryGroupPolicy(@NonNull BroadcastRecord r) {
+ if (mService.shouldIgnoreDeliveryGroupPolicy(r.intent.getAction())) {
+ return;
+ }
final int policy = (r.options != null)
? r.options.getDeliveryGroupPolicy() : BroadcastOptions.DELIVERY_GROUP_POLICY_ALL;
final BroadcastConsumer broadcastConsumer;
@@ -729,10 +744,8 @@
private void scheduleReceiverWarmLocked(@NonNull BroadcastProcessQueue queue) {
checkState(queue.isActive(), "isActive");
- final ProcessRecord app = queue.app;
final BroadcastRecord r = queue.getActive();
final int index = queue.getActiveIndex();
- final Object receiver = r.receivers.get(index);
if (r.terminalCount == 0) {
r.dispatchTime = SystemClock.uptimeMillis();
@@ -740,26 +753,40 @@
r.dispatchClockTime = System.currentTimeMillis();
}
- // If someone already finished this broadcast, finish immediately
+ if (maybeSkipReceiver(queue, r, index)) {
+ return;
+ }
+ dispatchReceivers(queue, r, index);
+ }
+
+ /**
+ * Examine a receiver and possibly skip it. The method returns true if the receiver is
+ * skipped (and therefore no more work is required).
+ */
+ private boolean maybeSkipReceiver(BroadcastProcessQueue queue, BroadcastRecord r, int index) {
final int oldDeliveryState = getDeliveryState(r, index);
+ final ProcessRecord app = queue.app;
+ final Object receiver = r.receivers.get(index);
+
+ // If someone already finished this broadcast, finish immediately
if (isDeliveryStateTerminal(oldDeliveryState)) {
enqueueFinishReceiver(queue, oldDeliveryState, "already terminal state");
- return;
+ return true;
}
// Consider additional cases where we'd want to finish immediately
if (app.isInFullBackup()) {
enqueueFinishReceiver(queue, BroadcastRecord.DELIVERY_SKIPPED, "isInFullBackup");
- return;
+ return true;
}
if (mSkipPolicy.shouldSkip(r, receiver)) {
enqueueFinishReceiver(queue, BroadcastRecord.DELIVERY_SKIPPED, "mSkipPolicy");
- return;
+ return true;
}
final Intent receiverIntent = r.getReceiverIntent(receiver);
if (receiverIntent == null) {
enqueueFinishReceiver(queue, BroadcastRecord.DELIVERY_SKIPPED, "getReceiverIntent");
- return;
+ return true;
}
// Ignore registered receivers from a previous PID
@@ -767,12 +794,29 @@
&& ((BroadcastFilter) receiver).receiverList.pid != app.getPid()) {
enqueueFinishReceiver(queue, BroadcastRecord.DELIVERY_SKIPPED,
"BroadcastFilter for mismatched PID");
- return;
+ return true;
}
+ // The receiver was not handled in this method.
+ return false;
+ }
+
+ /**
+ * Return true if this receiver should be assumed to have been delivered.
+ */
+ private boolean isAssumedDelivered(BroadcastRecord r, int index) {
+ return (r.receivers.get(index) instanceof BroadcastFilter) && !r.ordered;
+ }
+
+ /**
+ * A receiver is about to be dispatched. Start ANR timers, if necessary.
+ */
+ private void dispatchReceivers(BroadcastProcessQueue queue, BroadcastRecord r, int index) {
+ final ProcessRecord app = queue.app;
+ final Object receiver = r.receivers.get(index);
// Skip ANR tracking early during boot, when requested, or when we
// immediately assume delivery success
- final boolean assumeDelivered = (receiver instanceof BroadcastFilter) && !r.ordered;
+ final boolean assumeDelivered = isAssumedDelivered(r, index);
if (mService.mProcessesReady && !r.timeoutExempt && !assumeDelivered) {
queue.lastCpuDelayTime = queue.app.getCpuDelayTime();
@@ -805,6 +849,7 @@
setDeliveryState(queue, app, r, index, receiver, BroadcastRecord.DELIVERY_SCHEDULED,
"scheduleReceiverWarmLocked");
+ final Intent receiverIntent = r.getReceiverIntent(receiver);
final IApplicationThread thread = app.getOnewayThread();
if (thread != null) {
try {
@@ -920,6 +965,19 @@
return finishReceiverLocked(queue, BroadcastRecord.DELIVERY_DELIVERED, "remote app");
}
+ /**
+ * Return true if there are more broadcasts in the queue and the queue is runnable.
+ */
+ private boolean shouldContinueScheduling(@NonNull BroadcastProcessQueue queue) {
+ // If we've made reasonable progress, periodically retire ourselves to
+ // avoid starvation of other processes and stack overflow when a
+ // broadcast is immediately finished without waiting
+ final boolean shouldRetire =
+ (queue.getActiveCountSinceIdle() >= mConstants.MAX_RUNNING_ACTIVE_BROADCASTS);
+
+ return queue.isRunnable() && queue.isProcessWarm() && !shouldRetire;
+ }
+
private boolean finishReceiverLocked(@NonNull BroadcastProcessQueue queue,
@DeliveryState int deliveryState, @NonNull String reason) {
if (!queue.isActive()) {
@@ -927,10 +985,21 @@
return false;
}
- final int cookie = traceBegin("finishReceiver");
- final ProcessRecord app = queue.app;
final BroadcastRecord r = queue.getActive();
final int index = queue.getActiveIndex();
+ return finishReceiverLocked(queue, deliveryState, reason, r, index);
+ }
+
+ private boolean finishReceiverLocked(@NonNull BroadcastProcessQueue queue,
+ @DeliveryState int deliveryState, @NonNull String reason,
+ BroadcastRecord r, int index) {
+ if (!queue.isActive()) {
+ logw("Ignoring finish; no active broadcast for " + queue);
+ return false;
+ }
+
+ final int cookie = traceBegin("finishReceiver");
+ final ProcessRecord app = queue.app;
final Object receiver = r.receivers.get(index);
setDeliveryState(queue, app, r, index, receiver, deliveryState, reason);
@@ -945,18 +1014,11 @@
mLocalHandler.removeMessages(MSG_DELIVERY_TIMEOUT_HARD, queue);
}
- // If we've made reasonable progress, periodically retire ourselves to
- // avoid starvation of other processes and stack overflow when a
- // broadcast is immediately finished without waiting
- final boolean shouldRetire =
- (queue.getActiveCountSinceIdle() >= mConstants.MAX_RUNNING_ACTIVE_BROADCASTS);
-
- final boolean res;
- if (queue.isRunnable() && queue.isProcessWarm() && !shouldRetire) {
+ final boolean res = shouldContinueScheduling(queue);
+ if (res) {
// We're on a roll; move onto the next broadcast for this process
queue.makeActiveNextPending();
scheduleReceiverWarmLocked(queue);
- res = true;
} else {
// We've drained running broadcasts; maybe move back to runnable
queue.makeActiveIdle();
@@ -970,7 +1032,6 @@
// Tell other OS components that app is not actively running, giving
// a chance to update OOM adjustment
notifyStoppedRunning(queue);
- res = false;
}
traceEnd(cookie);
return res;
@@ -1167,6 +1228,17 @@
return didSomething;
}
+ private void forEachQueue(@NonNull Consumer<BroadcastProcessQueue> consumer) {
+ for (int i = 0; i < mProcessQueues.size(); ++i) {
+ BroadcastProcessQueue leaf = mProcessQueues.valueAt(i);
+ while (leaf != null) {
+ consumer.accept(leaf);
+ updateRunnableList(leaf);
+ leaf = leaf.processNameNext;
+ }
+ }
+ }
+
@Override
public void start(@NonNull ContentResolver resolver) {
mFgConstants.startObserving(mHandler, resolver);
@@ -1225,12 +1297,19 @@
final CountDownLatch latch = new CountDownLatch(1);
synchronized (mService) {
mWaitingFor.add(Pair.create(condition, latch));
+ forEachQueue(q -> q.setPrioritizeEarliest(true));
}
enqueueUpdateRunningList();
try {
latch.await();
} catch (InterruptedException e) {
throw new RuntimeException(e);
+ } finally {
+ synchronized (mService) {
+ if (mWaitingFor.isEmpty()) {
+ forEachQueue(q -> q.setPrioritizeEarliest(false));
+ }
+ }
}
}
@@ -1442,10 +1521,10 @@
private void notifyFinishBroadcast(@NonNull BroadcastRecord r) {
mService.notifyBroadcastFinishedLocked(r);
- mHistory.addBroadcastToHistoryLocked(r);
-
r.finishTime = SystemClock.uptimeMillis();
r.nextReceiver = r.receivers.size();
+ mHistory.addBroadcastToHistoryLocked(r);
+
BroadcastQueueImpl.logBootCompletedBroadcastCompletionLatencyIfPossible(r);
if (r.intent.getComponent() == null && r.intent.getPackage() == null
@@ -1609,6 +1688,12 @@
ipw.decreaseIndent();
ipw.println();
+ ipw.println(" Broadcasts with ignored delivery group policies:");
+ ipw.increaseIndent();
+ mService.dumpDeliveryGroupPolicyIgnoredActions(ipw);
+ ipw.decreaseIndent();
+ ipw.println();
+
if (dumpConstants) {
mConstants.dump(ipw);
}
diff --git a/services/core/java/com/android/server/am/SameProcessApplicationThread.java b/services/core/java/com/android/server/am/SameProcessApplicationThread.java
index a3c0111..62fd6e9 100644
--- a/services/core/java/com/android/server/am/SameProcessApplicationThread.java
+++ b/services/core/java/com/android/server/am/SameProcessApplicationThread.java
@@ -18,6 +18,7 @@
import android.annotation.NonNull;
import android.app.IApplicationThread;
+import android.app.ReceiverInfo;
import android.content.IIntentReceiver;
import android.content.Intent;
import android.content.pm.ActivityInfo;
@@ -26,6 +27,7 @@
import android.os.Handler;
import android.os.RemoteException;
+import java.util.List;
import java.util.Objects;
/**
@@ -70,4 +72,20 @@
}
});
}
+
+ @Override
+ public void scheduleReceiverList(List<ReceiverInfo> info) {
+ for (int i = 0; i < info.size(); i++) {
+ ReceiverInfo r = info.get(i);
+ if (r.registered) {
+ scheduleRegisteredReceiver(r.receiver, r.intent,
+ r.resultCode, r.data, r.extras, r.ordered, r.sticky,
+ r.sendingUser, r.processState);
+ } else {
+ scheduleReceiver(r.intent, r.activityInfo, r.compatInfo,
+ r.resultCode, r.data, r.extras, r.sync,
+ r.sendingUser, r.processState);
+ }
+ }
+ }
}
diff --git a/services/core/java/com/android/server/am/ServiceRecord.java b/services/core/java/com/android/server/am/ServiceRecord.java
index ef195aa..8ac10b8 100644
--- a/services/core/java/com/android/server/am/ServiceRecord.java
+++ b/services/core/java/com/android/server/am/ServiceRecord.java
@@ -1416,7 +1416,7 @@
|| !mShortFgsInfo.isCurrent()) {
return false;
}
- return mShortFgsInfo.getTimeoutTime() < SystemClock.uptimeMillis();
+ return mShortFgsInfo.getTimeoutTime() <= SystemClock.uptimeMillis();
}
/**
@@ -1431,7 +1431,7 @@
|| !mShortFgsInfo.isCurrent()) {
return false;
}
- return mShortFgsInfo.getAnrTime() < SystemClock.uptimeMillis();
+ return mShortFgsInfo.getAnrTime() <= SystemClock.uptimeMillis();
}
private boolean isAppAlive() {
diff --git a/services/core/java/com/android/server/app/GameManagerService.java b/services/core/java/com/android/server/app/GameManagerService.java
index cda18b0..46d3ff1 100644
--- a/services/core/java/com/android/server/app/GameManagerService.java
+++ b/services/core/java/com/android/server/app/GameManagerService.java
@@ -524,8 +524,8 @@
private final ArrayMap<Integer, GameModeConfiguration> mModeConfigs = new ArrayMap<>();
// if adding new properties or make any of the below overridable, the method
// copyAndApplyOverride should be updated accordingly
- private boolean mPerfModeOptedIn = false;
- private boolean mBatteryModeOptedIn = false;
+ private boolean mPerfModeOverridden = false;
+ private boolean mBatteryModeOverridden = false;
private boolean mAllowDownscale = true;
private boolean mAllowAngle = true;
private boolean mAllowFpsOverride = true;
@@ -542,8 +542,8 @@
PackageManager.GET_META_DATA, userId);
if (!parseInterventionFromXml(packageManager, ai, packageName)
&& ai.metaData != null) {
- mPerfModeOptedIn = ai.metaData.getBoolean(METADATA_PERFORMANCE_MODE_ENABLE);
- mBatteryModeOptedIn = ai.metaData.getBoolean(METADATA_BATTERY_MODE_ENABLE);
+ mPerfModeOverridden = ai.metaData.getBoolean(METADATA_PERFORMANCE_MODE_ENABLE);
+ mBatteryModeOverridden = ai.metaData.getBoolean(METADATA_BATTERY_MODE_ENABLE);
mAllowDownscale = ai.metaData.getBoolean(METADATA_WM_ALLOW_DOWNSCALE, true);
mAllowAngle = ai.metaData.getBoolean(METADATA_ANGLE_ALLOW_ANGLE, true);
}
@@ -595,9 +595,9 @@
} else {
final TypedArray array = resources.obtainAttributes(attributeSet,
com.android.internal.R.styleable.GameModeConfig);
- mPerfModeOptedIn = array.getBoolean(
+ mPerfModeOverridden = array.getBoolean(
GameModeConfig_supportsPerformanceGameMode, false);
- mBatteryModeOptedIn = array.getBoolean(
+ mBatteryModeOverridden = array.getBoolean(
GameModeConfig_supportsBatteryGameMode,
false);
mAllowDownscale = array.getBoolean(GameModeConfig_allowGameDownscaling,
@@ -610,8 +610,8 @@
}
} catch (NameNotFoundException | XmlPullParserException | IOException ex) {
// set flag back to default values when parsing fails
- mPerfModeOptedIn = false;
- mBatteryModeOptedIn = false;
+ mPerfModeOverridden = false;
+ mBatteryModeOverridden = false;
mAllowDownscale = true;
mAllowAngle = true;
mAllowFpsOverride = true;
@@ -667,8 +667,8 @@
GameModeConfiguration(KeyValueListParser parser) {
mGameMode = parser.getInt(MODE_KEY, GameManager.GAME_MODE_UNSUPPORTED);
- // isGameModeOptedIn() returns if an app will handle all of the changes necessary
- // for a particular game mode. If so, the Android framework (i.e.
+ // willGamePerformOptimizations() returns if an app will handle all of the changes
+ // necessary for a particular game mode. If so, the Android framework (i.e.
// GameManagerService) will not do anything for the app (like window scaling or
// using ANGLE).
mScaling = !mAllowDownscale || willGamePerformOptimizations(mGameMode)
@@ -775,8 +775,8 @@
* "com.android.app.gamemode.battery.enabled" with a value of "true"
*/
public boolean willGamePerformOptimizations(@GameMode int gameMode) {
- return (mBatteryModeOptedIn && gameMode == GameManager.GAME_MODE_BATTERY)
- || (mPerfModeOptedIn && gameMode == GameManager.GAME_MODE_PERFORMANCE);
+ return (mBatteryModeOverridden && gameMode == GameManager.GAME_MODE_BATTERY)
+ || (mPerfModeOverridden && gameMode == GameManager.GAME_MODE_PERFORMANCE);
}
private int getAvailableGameModesBitfield() {
@@ -787,10 +787,10 @@
field |= modeToBitmask(mode);
}
}
- if (mBatteryModeOptedIn) {
+ if (mBatteryModeOverridden) {
field |= modeToBitmask(GameManager.GAME_MODE_BATTERY);
}
- if (mPerfModeOptedIn) {
+ if (mPerfModeOverridden) {
field |= modeToBitmask(GameManager.GAME_MODE_PERFORMANCE);
}
return field;
@@ -814,14 +814,14 @@
}
/**
- * Get an array of a package's opted-in game modes.
+ * Get an array of a package's overridden game modes.
*/
- public @GameMode int[] getOptedInGameModes() {
- if (mBatteryModeOptedIn && mPerfModeOptedIn) {
+ public @GameMode int[] getOverriddenGameModes() {
+ if (mBatteryModeOverridden && mPerfModeOverridden) {
return new int[]{GameManager.GAME_MODE_BATTERY, GameManager.GAME_MODE_PERFORMANCE};
- } else if (mBatteryModeOptedIn) {
+ } else if (mBatteryModeOverridden) {
return new int[]{GameManager.GAME_MODE_BATTERY};
- } else if (mPerfModeOptedIn) {
+ } else if (mPerfModeOverridden) {
return new int[]{GameManager.GAME_MODE_PERFORMANCE};
} else {
return new int[]{};
@@ -864,18 +864,18 @@
public boolean isActive() {
synchronized (mModeConfigLock) {
- return mModeConfigs.size() > 0 || mBatteryModeOptedIn || mPerfModeOptedIn;
+ return mModeConfigs.size() > 0 || mBatteryModeOverridden || mPerfModeOverridden;
}
}
GamePackageConfiguration copyAndApplyOverride(GamePackageConfiguration overrideConfig) {
GamePackageConfiguration copy = new GamePackageConfiguration(mPackageName);
// if a game mode is overridden, we treat it with the highest priority and reset any
- // opt-in game modes so that interventions are always executed.
- copy.mPerfModeOptedIn = mPerfModeOptedIn && !(overrideConfig != null
+ // overridden game modes so that interventions are always executed.
+ copy.mPerfModeOverridden = mPerfModeOverridden && !(overrideConfig != null
&& overrideConfig.getGameModeConfiguration(GameManager.GAME_MODE_PERFORMANCE)
!= null);
- copy.mBatteryModeOptedIn = mBatteryModeOptedIn && !(overrideConfig != null
+ copy.mBatteryModeOverridden = mBatteryModeOverridden && !(overrideConfig != null
&& overrideConfig.getGameModeConfiguration(GameManager.GAME_MODE_BATTERY)
!= null);
@@ -1092,12 +1092,12 @@
final @GameMode int activeGameMode = getGameModeFromSettingsUnchecked(packageName, userId);
final GamePackageConfiguration config = getConfig(packageName, userId);
if (config != null) {
- final @GameMode int[] optedInGameModes = config.getOptedInGameModes();
+ final @GameMode int[] overriddenGameModes = config.getOverriddenGameModes();
final @GameMode int[] availableGameModes = config.getAvailableGameModes();
GameModeInfo.Builder gameModeInfoBuilder = new GameModeInfo.Builder()
.setActiveGameMode(activeGameMode)
.setAvailableGameModes(availableGameModes)
- .setOptedInGameModes(optedInGameModes)
+ .setOverriddenGameModes(overriddenGameModes)
.setDownscalingAllowed(config.mAllowDownscale)
.setFpsOverrideAllowed(config.mAllowFpsOverride);
for (int gameMode : availableGameModes) {
@@ -2059,7 +2059,7 @@
if (atomTag == FrameworkStatsLog.GAME_MODE_INFO) {
data.add(
FrameworkStatsLog.buildStatsEvent(FrameworkStatsLog.GAME_MODE_INFO, uid,
- gameModesToStatsdGameModes(config.getOptedInGameModes()),
+ gameModesToStatsdGameModes(config.getOverriddenGameModes()),
gameModesToStatsdGameModes(config.getAvailableGameModes())));
} else if (atomTag == FrameworkStatsLog.GAME_MODE_CONFIGURATION) {
for (int gameMode : config.getAvailableGameModes()) {
diff --git a/services/core/java/com/android/server/appop/AppOpsCheckingServiceTracingDecorator.java b/services/core/java/com/android/server/appop/AppOpsCheckingServiceTracingDecorator.java
new file mode 100644
index 0000000..4436002
--- /dev/null
+++ b/services/core/java/com/android/server/appop/AppOpsCheckingServiceTracingDecorator.java
@@ -0,0 +1,279 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.appop;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.UserIdInt;
+import android.app.AppOpsManager;
+import android.os.Trace;
+import android.util.ArraySet;
+import android.util.SparseBooleanArray;
+import android.util.SparseIntArray;
+
+import java.io.PrintWriter;
+
+/**
+ * Surrounds all AppOpsCheckingServiceInterface method calls with Trace.traceBegin and
+ * Trace.traceEnd. These traces are used for performance testing.
+ */
+public class AppOpsCheckingServiceTracingDecorator implements AppOpsCheckingServiceInterface {
+ private static final long TRACE_TAG = Trace.TRACE_TAG_SYSTEM_SERVER;
+ private final AppOpsCheckingServiceInterface mService;
+
+ AppOpsCheckingServiceTracingDecorator(
+ @NonNull AppOpsCheckingServiceInterface appOpsCheckingServiceInterface) {
+ mService = appOpsCheckingServiceInterface;
+ }
+
+ @Override
+ public SparseIntArray getNonDefaultUidModes(int uid) {
+ Trace.traceBegin(TRACE_TAG,
+ "TaggedTracingAppOpsCheckingServiceInterfaceImpl#getNonDefaultUidModes");
+ try {
+ return mService.getNonDefaultUidModes(uid);
+ } finally {
+ Trace.traceEnd(TRACE_TAG);
+ }
+ }
+
+ @Override
+ public int getUidMode(int uid, int op) {
+ Trace.traceBegin(TRACE_TAG, "TaggedTracingAppOpsCheckingServiceInterfaceImpl#getUidMode");
+ try {
+ return mService.getUidMode(uid, op);
+ } finally {
+ Trace.traceEnd(TRACE_TAG);
+ }
+ }
+
+ @Override
+ public boolean setUidMode(int uid, int op, @AppOpsManager.Mode int mode) {
+ Trace.traceBegin(TRACE_TAG, "TaggedTracingAppOpsCheckingServiceInterfaceImpl#setUidMode");
+ try {
+ return mService.setUidMode(uid, op, mode);
+ } finally {
+ Trace.traceEnd(TRACE_TAG);
+ }
+ }
+
+ @Override
+ public int getPackageMode(@NonNull String packageName, int op, @UserIdInt int userId) {
+ Trace.traceBegin(TRACE_TAG,
+ "TaggedTracingAppOpsCheckingServiceInterfaceImpl#getPackageMode");
+ try {
+ return mService.getPackageMode(packageName, op, userId);
+ } finally {
+ Trace.traceEnd(TRACE_TAG);
+ }
+ }
+
+ @Override
+ public void setPackageMode(@NonNull String packageName, int op, @AppOpsManager.Mode int mode,
+ @UserIdInt int userId) {
+ Trace.traceBegin(TRACE_TAG,
+ "TaggedTracingAppOpsCheckingServiceInterfaceImpl#setPackageMode");
+ try {
+ mService.setPackageMode(packageName, op, mode, userId);
+ } finally {
+ Trace.traceEnd(TRACE_TAG);
+ }
+ }
+
+ @Override
+ public boolean removePackage(@NonNull String packageName, @UserIdInt int userId) {
+ Trace.traceBegin(TRACE_TAG,
+ "TaggedTracingAppOpsCheckingServiceInterfaceImpl#removePackage");
+ try {
+ return mService.removePackage(packageName, userId);
+ } finally {
+ Trace.traceEnd(TRACE_TAG);
+ }
+ }
+
+ @Override
+ public void removeUid(int uid) {
+ Trace.traceBegin(TRACE_TAG,
+ "TaggedTracingAppOpsCheckingServiceInterfaceImpl#removeUid");
+ try {
+ mService.removeUid(uid);
+ } finally {
+ Trace.traceEnd(TRACE_TAG);
+ }
+ }
+
+ @Override
+ public boolean areUidModesDefault(int uid) {
+ Trace.traceBegin(TRACE_TAG,
+ "TaggedTracingAppOpsCheckingServiceInterfaceImpl#areUidModesDefault");
+ try {
+ return mService.areUidModesDefault(uid);
+ } finally {
+ Trace.traceEnd(TRACE_TAG);
+ }
+ }
+
+ @Override
+ public boolean arePackageModesDefault(String packageName, @UserIdInt int userId) {
+ Trace.traceBegin(TRACE_TAG,
+ "TaggedTracingAppOpsCheckingServiceInterfaceImpl#arePackageModesDefault");
+ try {
+ return mService.arePackageModesDefault(packageName, userId);
+ } finally {
+ Trace.traceEnd(TRACE_TAG);
+ }
+ }
+
+ @Override
+ public void clearAllModes() {
+ Trace.traceBegin(TRACE_TAG,
+ "TaggedTracingAppOpsCheckingServiceInterfaceImpl#clearAllModes");
+ try {
+ mService.clearAllModes();
+ } finally {
+ Trace.traceEnd(TRACE_TAG);
+ }
+ }
+
+ @Override
+ public void startWatchingOpModeChanged(@NonNull OnOpModeChangedListener changedListener,
+ int op) {
+ Trace.traceBegin(TRACE_TAG,
+ "TaggedTracingAppOpsCheckingServiceInterfaceImpl#startWatchingOpModeChanged");
+ try {
+ mService.startWatchingOpModeChanged(changedListener, op);
+ } finally {
+ Trace.traceEnd(TRACE_TAG);
+ }
+ }
+
+ @Override
+ public void startWatchingPackageModeChanged(@NonNull OnOpModeChangedListener changedListener,
+ @NonNull String packageName) {
+ Trace.traceBegin(TRACE_TAG,
+ "TaggedTracingAppOpsCheckingServiceInterfaceImpl#startWatchingPackageModeChanged");
+ try {
+ mService.startWatchingPackageModeChanged(changedListener, packageName);
+ } finally {
+ Trace.traceEnd(TRACE_TAG);
+ }
+ }
+
+ @Override
+ public void removeListener(@NonNull OnOpModeChangedListener changedListener) {
+ Trace.traceBegin(TRACE_TAG,
+ "TaggedTracingAppOpsCheckingServiceInterfaceImpl#removeListener");
+ try {
+ mService.removeListener(changedListener);
+ } finally {
+ Trace.traceEnd(TRACE_TAG);
+ }
+ }
+
+ @Override
+ public ArraySet<OnOpModeChangedListener> getOpModeChangedListeners(int op) {
+ Trace.traceBegin(TRACE_TAG,
+ "TaggedTracingAppOpsCheckingServiceInterfaceImpl#getOpModeChangedListeners");
+ try {
+ return mService.getOpModeChangedListeners(op);
+ } finally {
+ Trace.traceEnd(TRACE_TAG);
+ }
+ }
+
+ @Override
+ public ArraySet<OnOpModeChangedListener> getPackageModeChangedListeners(
+ @NonNull String packageName) {
+ Trace.traceBegin(TRACE_TAG,
+ "TaggedTracingAppOpsCheckingServiceInterfaceImpl#getPackageModeChangedListeners");
+ try {
+ return mService.getPackageModeChangedListeners(packageName);
+ } finally {
+ Trace.traceEnd(TRACE_TAG);
+ }
+ }
+
+ @Override
+ public void notifyWatchersOfChange(int op, int uid) {
+ Trace.traceBegin(TRACE_TAG,
+ "TaggedTracingAppOpsCheckingServiceInterfaceImpl#notifyWatchersOfChange");
+ try {
+ mService.notifyWatchersOfChange(op, uid);
+ } finally {
+ Trace.traceEnd(TRACE_TAG);
+ }
+ }
+
+ @Override
+ public void notifyOpChanged(@NonNull OnOpModeChangedListener changedListener, int op, int uid,
+ @Nullable String packageName) {
+ Trace.traceBegin(TRACE_TAG,
+ "TaggedTracingAppOpsCheckingServiceInterfaceImpl#notifyOpChanged");
+ try {
+ mService.notifyOpChanged(changedListener, op, uid, packageName);
+ } finally {
+ Trace.traceEnd(TRACE_TAG);
+ }
+ }
+
+ @Override
+ public void notifyOpChangedForAllPkgsInUid(int op, int uid, boolean onlyForeground,
+ @Nullable OnOpModeChangedListener callbackToIgnore) {
+ Trace.traceBegin(TRACE_TAG,
+ "TaggedTracingAppOpsCheckingServiceInterfaceImpl#notifyOpChangedForAllPkgsInUid");
+ try {
+ mService.notifyOpChangedForAllPkgsInUid(op, uid, onlyForeground, callbackToIgnore);
+ } finally {
+ Trace.traceEnd(TRACE_TAG);
+ }
+ }
+
+ @Override
+ public SparseBooleanArray evalForegroundUidOps(int uid, SparseBooleanArray foregroundOps) {
+ Trace.traceBegin(TRACE_TAG,
+ "TaggedTracingAppOpsCheckingServiceInterfaceImpl#evalForegroundUidOps");
+ try {
+ return mService.evalForegroundUidOps(uid, foregroundOps);
+ } finally {
+ Trace.traceEnd(TRACE_TAG);
+ }
+ }
+
+ @Override
+ public SparseBooleanArray evalForegroundPackageOps(String packageName,
+ SparseBooleanArray foregroundOps, @UserIdInt int userId) {
+ Trace.traceBegin(TRACE_TAG,
+ "TaggedTracingAppOpsCheckingServiceInterfaceImpl#evalForegroundPackageOps");
+ try {
+ return mService.evalForegroundPackageOps(packageName, foregroundOps, userId);
+ } finally {
+ Trace.traceEnd(TRACE_TAG);
+ }
+ }
+
+ @Override
+ public boolean dumpListeners(int dumpOp, int dumpUid, String dumpPackage,
+ PrintWriter printWriter) {
+ Trace.traceBegin(TRACE_TAG,
+ "TaggedTracingAppOpsCheckingServiceInterfaceImpl#dumpListeners");
+ try {
+ return mService.dumpListeners(dumpOp, dumpUid, dumpPackage, printWriter);
+ } finally {
+ Trace.traceEnd(TRACE_TAG);
+ }
+ }
+}
diff --git a/services/core/java/com/android/server/appop/AppOpsServiceImpl.java b/services/core/java/com/android/server/appop/AppOpsServiceImpl.java
index 70f3bcc..c3d2717 100644
--- a/services/core/java/com/android/server/appop/AppOpsServiceImpl.java
+++ b/services/core/java/com/android/server/appop/AppOpsServiceImpl.java
@@ -44,6 +44,7 @@
import static android.app.AppOpsManager.OP_RECEIVE_AMBIENT_TRIGGER_AUDIO;
import static android.app.AppOpsManager.OP_RECORD_AUDIO;
import static android.app.AppOpsManager.OP_RECORD_AUDIO_HOTWORD;
+import static android.app.AppOpsManager.OP_SCHEDULE_EXACT_ALARM;
import static android.app.AppOpsManager.OP_VIBRATE;
import static android.app.AppOpsManager.OnOpStartedListener.START_TYPE_FAILED;
import static android.app.AppOpsManager.OnOpStartedListener.START_TYPE_STARTED;
@@ -130,6 +131,8 @@
import com.android.server.LocalServices;
import com.android.server.LockGuard;
import com.android.server.SystemServerInitThreadPool;
+import com.android.server.pm.UserManagerInternal;
+import com.android.server.pm.permission.PermissionManagerServiceInternal;
import com.android.server.pm.pkg.AndroidPackage;
import com.android.server.pm.pkg.component.ParsedAttribution;
@@ -163,12 +166,30 @@
static final String TAG = "AppOps";
static final boolean DEBUG = false;
+ /**
+ * Sentinel integer version to denote that there was no appops.xml found on boot.
+ * This will happen when a device boots with no existing userdata.
+ */
+ private static final int NO_FILE_VERSION = -2;
+
+ /**
+ * Sentinel integer version to denote that there was no version in the appops.xml found on boot.
+ * This means the file is coming from a build before versioning was added.
+ */
private static final int NO_VERSION = -1;
+
/**
* Increment by one every time and add the corresponding upgrade logic in
- * {@link #upgradeLocked(int)} below. The first version was 1
+ * {@link #upgradeLocked(int)} below. The first version was 1.
*/
- private static final int CURRENT_VERSION = 1;
+ @VisibleForTesting
+ static final int CURRENT_VERSION = 2;
+
+ /**
+ * This stores the version of appops.xml seen at boot. If this is smaller than
+ * {@link #CURRENT_VERSION}, then we will run {@link #upgradeLocked(int)} on startup.
+ */
+ private int mVersionAtBoot = NO_FILE_VERSION;
// Write at most every 30 minutes.
static final long WRITE_DELAY = DEBUG ? 1000 : 30 * 60 * 1000;
@@ -828,8 +849,8 @@
mSwitchedOps.put(switchCode,
ArrayUtils.appendInt(mSwitchedOps.get(switchCode), switchedCode));
}
- mAppOpsServiceInterface =
- new AppOpsCheckingServiceImpl(this, this, handler, context, mSwitchedOps);
+ mAppOpsServiceInterface = new AppOpsCheckingServiceTracingDecorator(
+ new AppOpsCheckingServiceImpl(this, this, handler, context, mSwitchedOps));
mAppOpsRestrictions = new AppOpsRestrictionsImpl(context, handler,
mAppOpsServiceInterface);
@@ -936,6 +957,10 @@
@Override
public void systemReady() {
+ synchronized (this) {
+ upgradeLocked(mVersionAtBoot);
+ }
+
mConstants.startMonitoring(mContext.getContentResolver());
mHistoricalRegistry.systemReady(mContext.getContentResolver());
@@ -3191,7 +3216,6 @@
@Override
public void readState() {
- int oldVersion = NO_VERSION;
synchronized (mFile) {
synchronized (this) {
FileInputStream stream;
@@ -3216,7 +3240,7 @@
throw new IllegalStateException("no start tag found");
}
- oldVersion = parser.getAttributeInt(null, "v", NO_VERSION);
+ mVersionAtBoot = parser.getAttributeInt(null, "v", NO_VERSION);
int outerDepth = parser.getDepth();
while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
@@ -3261,12 +3285,11 @@
}
}
}
- synchronized (this) {
- upgradeLocked(oldVersion);
- }
}
- private void upgradeRunAnyInBackgroundLocked() {
+ @VisibleForTesting
+ @GuardedBy("this")
+ void upgradeRunAnyInBackgroundLocked() {
for (int i = 0; i < mUidStates.size(); i++) {
final UidState uidState = mUidStates.valueAt(i);
if (uidState == null) {
@@ -3303,8 +3326,45 @@
}
}
+ /**
+ * The interpretation of the default mode - MODE_DEFAULT - for OP_SCHEDULE_EXACT_ALARM is
+ * changing. Simultaneously, we want to change this op's mode from MODE_DEFAULT to MODE_ALLOWED
+ * for already installed apps. For newer apps, it will stay as MODE_DEFAULT.
+ */
+ @VisibleForTesting
+ @GuardedBy("this")
+ void upgradeScheduleExactAlarmLocked() {
+ final PermissionManagerServiceInternal pmsi = LocalServices.getService(
+ PermissionManagerServiceInternal.class);
+ final UserManagerInternal umi = LocalServices.getService(UserManagerInternal.class);
+ final PackageManagerInternal pmi = getPackageManagerInternal();
+
+ final String[] packagesDeclaringPermission = pmsi.getAppOpPermissionPackages(
+ AppOpsManager.opToPermission(OP_SCHEDULE_EXACT_ALARM));
+ final int[] userIds = umi.getUserIds();
+
+ for (final String pkg : packagesDeclaringPermission) {
+ for (int userId : userIds) {
+ final int uid = pmi.getPackageUid(pkg, 0, userId);
+
+ UidState uidState = mUidStates.get(uid);
+ if (uidState == null) {
+ uidState = new UidState(uid);
+ mUidStates.put(uid, uidState);
+ }
+ final int oldMode = uidState.getUidMode(OP_SCHEDULE_EXACT_ALARM);
+ if (oldMode == AppOpsManager.opToDefaultMode(OP_SCHEDULE_EXACT_ALARM)) {
+ uidState.setUidMode(OP_SCHEDULE_EXACT_ALARM, MODE_ALLOWED);
+ }
+ }
+ // This appop is meant to be controlled at a uid level. So we leave package modes as
+ // they are.
+ }
+ }
+
+ @GuardedBy("this")
private void upgradeLocked(int oldVersion) {
- if (oldVersion >= CURRENT_VERSION) {
+ if (oldVersion == NO_FILE_VERSION || oldVersion >= CURRENT_VERSION) {
return;
}
Slog.d(TAG, "Upgrading app-ops xml from version " + oldVersion + " to " + CURRENT_VERSION);
@@ -3313,6 +3373,9 @@
upgradeRunAnyInBackgroundLocked();
// fall through
case 1:
+ upgradeScheduleExactAlarmLocked();
+ // fall through
+ case 2:
// for future upgrades
}
scheduleFastWriteLocked();
diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java
index aa8ee3d..58cf7ef 100644
--- a/services/core/java/com/android/server/audio/AudioService.java
+++ b/services/core/java/com/android/server/audio/AudioService.java
@@ -11427,8 +11427,8 @@
}
public List<AudioRecordingConfiguration> getActiveRecordingConfigurations() {
- final boolean isPrivileged =
- (PackageManager.PERMISSION_GRANTED == mContext.checkCallingPermission(
+ final boolean isPrivileged = Binder.getCallingUid() == Process.SYSTEM_UID
+ || (PackageManager.PERMISSION_GRANTED == mContext.checkCallingPermission(
android.Manifest.permission.MODIFY_AUDIO_ROUTING));
return mRecordMonitor.getActiveRecordingConfigurations(isPrivileged);
}
diff --git a/services/core/java/com/android/server/audio/TEST_MAPPING b/services/core/java/com/android/server/audio/TEST_MAPPING
index f3a73f0..d6c2fb2 100644
--- a/services/core/java/com/android/server/audio/TEST_MAPPING
+++ b/services/core/java/com/android/server/audio/TEST_MAPPING
@@ -13,6 +13,17 @@
"include-filter": "android.media.audio.cts.SpatializerTest"
}
]
+ },
+ {
+ "name": "audiopolicytest",
+ "options": [
+ {
+ "include-filter": "com.android.audiopolicytest.AudioPolicyDeathTest"
+ },
+ {
+ "include-annotation": "android.platform.test.annotations.Presubmit"
+ }
+ ]
}
]
}
diff --git a/services/core/java/com/android/server/connectivity/Vpn.java b/services/core/java/com/android/server/connectivity/Vpn.java
index bc9bc03..4fcde97 100644
--- a/services/core/java/com/android/server/connectivity/Vpn.java
+++ b/services/core/java/com/android/server/connectivity/Vpn.java
@@ -3475,6 +3475,8 @@
return;
} else {
mActiveNetwork = null;
+ mUnderlyingNetworkCapabilities = null;
+ mUnderlyingLinkProperties = null;
}
if (mScheduledHandleNetworkLostFuture != null) {
@@ -3664,9 +3666,6 @@
scheduleRetryNewIkeSession();
}
- mUnderlyingNetworkCapabilities = null;
- mUnderlyingLinkProperties = null;
-
// Close all obsolete state, but keep VPN alive incase a usable network comes up.
// (Mirrors VpnService behavior)
Log.d(TAG, "Resetting state for token: " + mCurrentToken);
diff --git a/services/core/java/com/android/server/display/DisplayDeviceInfo.java b/services/core/java/com/android/server/display/DisplayDeviceInfo.java
index ddb9243..fe1d1a6 100644
--- a/services/core/java/com/android/server/display/DisplayDeviceInfo.java
+++ b/services/core/java/com/android/server/display/DisplayDeviceInfo.java
@@ -245,6 +245,12 @@
public int modeId;
/**
+ * The render frame rate this display is scheduled at.
+ * @see android.view.DisplayInfo#renderFrameRate for more details.
+ */
+ public float renderFrameRate;
+
+ /**
* The default mode of the display.
*/
public int defaultModeId;
@@ -439,6 +445,7 @@
|| width != other.width
|| height != other.height
|| modeId != other.modeId
+ || renderFrameRate != other.renderFrameRate
|| defaultModeId != other.defaultModeId
|| !Arrays.equals(supportedModes, other.supportedModes)
|| !Arrays.equals(supportedColorModes, other.supportedColorModes)
@@ -483,6 +490,7 @@
width = other.width;
height = other.height;
modeId = other.modeId;
+ renderFrameRate = other.renderFrameRate;
defaultModeId = other.defaultModeId;
supportedModes = other.supportedModes;
colorMode = other.colorMode;
@@ -523,6 +531,7 @@
sb.append(name).append("\": uniqueId=\"").append(uniqueId).append("\", ");
sb.append(width).append(" x ").append(height);
sb.append(", modeId ").append(modeId);
+ sb.append(", renderFrameRate ").append(renderFrameRate);
sb.append(", defaultModeId ").append(defaultModeId);
sb.append(", supportedModes ").append(Arrays.toString(supportedModes));
sb.append(", colorMode ").append(colorMode);
diff --git a/services/core/java/com/android/server/display/DisplayManagerService.java b/services/core/java/com/android/server/display/DisplayManagerService.java
index 0d1aca8..ae84e96 100644
--- a/services/core/java/com/android/server/display/DisplayManagerService.java
+++ b/services/core/java/com/android/server/display/DisplayManagerService.java
@@ -36,6 +36,7 @@
import static android.hardware.display.DisplayViewport.VIEWPORT_EXTERNAL;
import static android.hardware.display.DisplayViewport.VIEWPORT_INTERNAL;
import static android.hardware.display.DisplayViewport.VIEWPORT_VIRTUAL;
+import static android.os.Process.FIRST_APPLICATION_UID;
import static android.os.Process.ROOT_UID;
import android.Manifest;
@@ -882,20 +883,27 @@
private DisplayInfo getDisplayInfoForFrameRateOverride(DisplayEventReceiver.FrameRateOverride[]
frameRateOverrides, DisplayInfo info, int callingUid) {
- float frameRateHz = 0;
+ float frameRateHz = info.renderFrameRate;
for (DisplayEventReceiver.FrameRateOverride frameRateOverride : frameRateOverrides) {
if (frameRateOverride.uid == callingUid) {
frameRateHz = frameRateOverride.frameRateHz;
break;
}
}
+
if (frameRateHz == 0) {
return info;
}
+ // For non-apps users we always return the physical refresh rate from display mode
+ boolean displayModeReturnsPhysicalRefreshRate =
+ callingUid < FIRST_APPLICATION_UID
+ || CompatChanges.isChangeEnabled(
+ DISPLAY_MODE_RETURNS_PHYSICAL_REFRESH_RATE, callingUid);
+
// Override the refresh rate only if it is a divisor of the current
// refresh rate. This calculation needs to be in sync with the native code
- // in RefreshRateConfigs::getFrameRateDivisor
+ // in RefreshRateSelector::getFrameRateDivisor
Display.Mode currentMode = info.getMode();
float numPeriods = currentMode.getRefreshRate() / frameRateHz;
float numPeriodsRound = Math.round(numPeriods);
@@ -919,8 +927,7 @@
}
overriddenInfo.refreshRateOverride = mode.getRefreshRate();
- if (!CompatChanges.isChangeEnabled(DISPLAY_MODE_RETURNS_PHYSICAL_REFRESH_RATE,
- callingUid)) {
+ if (!displayModeReturnsPhysicalRefreshRate) {
overriddenInfo.modeId = mode.getModeId();
}
return overriddenInfo;
@@ -928,8 +935,7 @@
}
overriddenInfo.refreshRateOverride = frameRateHz;
- if (!CompatChanges.isChangeEnabled(DISPLAY_MODE_RETURNS_PHYSICAL_REFRESH_RATE,
- callingUid)) {
+ if (!displayModeReturnsPhysicalRefreshRate) {
overriddenInfo.supportedModes = Arrays.copyOf(info.supportedModes,
info.supportedModes.length + 1);
overriddenInfo.supportedModes[overriddenInfo.supportedModes.length - 1] =
diff --git a/services/core/java/com/android/server/display/LocalDisplayAdapter.java b/services/core/java/com/android/server/display/LocalDisplayAdapter.java
index dc5c80f2..be5980b 100644
--- a/services/core/java/com/android/server/display/LocalDisplayAdapter.java
+++ b/services/core/java/com/android/server/display/LocalDisplayAdapter.java
@@ -112,13 +112,13 @@
mSurfaceControlProxy.getPhysicalDisplayToken(physicalDisplayId);
if (displayToken != null) {
SurfaceControl.StaticDisplayInfo staticInfo =
- mSurfaceControlProxy.getStaticDisplayInfo(displayToken);
+ mSurfaceControlProxy.getStaticDisplayInfo(physicalDisplayId);
if (staticInfo == null) {
Slog.w(TAG, "No valid static info found for display device " + physicalDisplayId);
return;
}
SurfaceControl.DynamicDisplayInfo dynamicInfo =
- mSurfaceControlProxy.getDynamicDisplayInfo(displayToken);
+ mSurfaceControlProxy.getDynamicDisplayInfo(physicalDisplayId);
if (dynamicInfo == null) {
Slog.w(TAG, "No valid dynamic info found for display device " + physicalDisplayId);
return;
@@ -226,6 +226,8 @@
private SurfaceControl.DisplayMode[] mSfDisplayModes;
// The active display mode in SurfaceFlinger
private SurfaceControl.DisplayMode mActiveSfDisplayMode;
+ // The active display vsync period in SurfaceFlinger
+ private float mActiveRenderFrameRate;
private DisplayEventReceiver.FrameRateOverride[] mFrameRateOverrides =
new DisplayEventReceiver.FrameRateOverride[0];
@@ -267,7 +269,7 @@
SurfaceControl.DesiredDisplayModeSpecs modeSpecs) {
boolean changed = updateDisplayModesLocked(
dynamicInfo.supportedDisplayModes, dynamicInfo.preferredBootDisplayMode,
- dynamicInfo.activeDisplayModeId, modeSpecs);
+ dynamicInfo.activeDisplayModeId, dynamicInfo.renderFrameRate, modeSpecs);
changed |= updateStaticInfo(staticInfo);
changed |= updateColorModesLocked(dynamicInfo.supportedColorModes,
dynamicInfo.activeColorMode);
@@ -283,7 +285,8 @@
public boolean updateDisplayModesLocked(
SurfaceControl.DisplayMode[] displayModes, int preferredSfDisplayModeId,
- int activeSfDisplayModeId, SurfaceControl.DesiredDisplayModeSpecs modeSpecs) {
+ int activeSfDisplayModeId, float renderFrameRate,
+ SurfaceControl.DesiredDisplayModeSpecs modeSpecs) {
mSfDisplayModes = Arrays.copyOf(displayModes, displayModes.length);
mActiveSfDisplayMode = getModeById(displayModes, activeSfDisplayModeId);
SurfaceControl.DisplayMode preferredSfDisplayMode =
@@ -379,6 +382,16 @@
sendTraversalRequestLocked();
}
+ boolean renderFrameRateChanged = false;
+
+ if (mActiveRenderFrameRate > 0 && mActiveRenderFrameRate != renderFrameRate) {
+ Slog.d(TAG, "The render frame rate was changed from SurfaceFlinger or the display"
+ + " device to " + renderFrameRate);
+ mActiveRenderFrameRate = renderFrameRate;
+ renderFrameRateChanged = true;
+ sendTraversalRequestLocked();
+ }
+
// Check whether surface flinger spontaneously changed display config specs out from
// under us. If so, schedule a traversal to reapply our display config specs.
if (mDisplayModeSpecs.baseModeId != INVALID_MODE_ID) {
@@ -398,7 +411,7 @@
boolean recordsChanged = records.size() != mSupportedModes.size() || modesAdded;
// If the records haven't changed then we're done here.
if (!recordsChanged) {
- return activeModeChanged || preferredModeChanged;
+ return activeModeChanged || preferredModeChanged || renderFrameRateChanged;
}
mSupportedModes.clear();
@@ -410,16 +423,19 @@
if (mDefaultModeId == INVALID_MODE_ID) {
mDefaultModeId = activeRecord.mMode.getModeId();
mDefaultModeGroup = mActiveSfDisplayMode.group;
+ mActiveRenderFrameRate = renderFrameRate;
} else if (modesAdded && activeModeChanged) {
Slog.d(TAG, "New display modes are added and the active mode has changed, "
+ "use active mode as default mode.");
mDefaultModeId = activeRecord.mMode.getModeId();
mDefaultModeGroup = mActiveSfDisplayMode.group;
+ mActiveRenderFrameRate = renderFrameRate;
} else if (findSfDisplayModeIdLocked(mDefaultModeId, mDefaultModeGroup) < 0) {
Slog.w(TAG, "Default display mode no longer available, using currently"
+ " active mode as default.");
mDefaultModeId = activeRecord.mMode.getModeId();
mDefaultModeGroup = mActiveSfDisplayMode.group;
+ mActiveRenderFrameRate = renderFrameRate;
}
// Determine whether the display mode specs' base mode is still there.
@@ -620,6 +636,7 @@
mInfo.width = mActiveSfDisplayMode.width;
mInfo.height = mActiveSfDisplayMode.height;
mInfo.modeId = mActiveModeId;
+ mInfo.renderFrameRate = mActiveRenderFrameRate;
mInfo.defaultModeId = getPreferredModeId();
mInfo.supportedModes = getDisplayModes(mSupportedModes);
mInfo.colorMode = mActiveColorMode;
@@ -995,8 +1012,8 @@
updateDeviceInfoLocked();
}
- public void onActiveDisplayModeChangedLocked(int sfModeId) {
- if (updateActiveModeLocked(sfModeId)) {
+ public void onActiveDisplayModeChangedLocked(int sfModeId, float renderFrameRate) {
+ if (updateActiveModeLocked(sfModeId, renderFrameRate)) {
updateDeviceInfoLocked();
}
}
@@ -1008,8 +1025,9 @@
}
}
- public boolean updateActiveModeLocked(int activeSfModeId) {
- if (mActiveSfDisplayMode.id == activeSfModeId) {
+ public boolean updateActiveModeLocked(int activeSfModeId, float renderFrameRate) {
+ if (mActiveSfDisplayMode.id == activeSfModeId
+ && mActiveRenderFrameRate == renderFrameRate) {
return false;
}
mActiveSfDisplayMode = getModeById(mSfDisplayModes, activeSfModeId);
@@ -1018,6 +1036,7 @@
Slog.w(TAG, "In unknown mode after setting allowed modes"
+ ", activeModeId=" + activeSfModeId);
}
+ mActiveRenderFrameRate = renderFrameRate;
return true;
}
@@ -1114,6 +1133,7 @@
pw.println(" " + sfDisplayMode);
}
pw.println("mActiveSfDisplayMode=" + mActiveSfDisplayMode);
+ pw.println("mActiveRenderFrameRate=" + mActiveRenderFrameRate);
pw.println("mSupportedModes=");
for (int i = 0; i < mSupportedModes.size(); i++) {
pw.println(" " + mSupportedModes.valueAt(i));
@@ -1288,7 +1308,8 @@
public interface DisplayEventListener {
void onHotplug(long timestampNanos, long physicalDisplayId, boolean connected);
- void onModeChanged(long timestampNanos, long physicalDisplayId, int modeId);
+ void onModeChanged(long timestampNanos, long physicalDisplayId, int modeId,
+ long renderPeriod);
void onFrameRateOverridesChanged(long timestampNanos, long physicalDisplayId,
DisplayEventReceiver.FrameRateOverride[] overrides);
@@ -1309,8 +1330,9 @@
}
@Override
- public void onModeChanged(long timestampNanos, long physicalDisplayId, int modeId) {
- mListener.onModeChanged(timestampNanos, physicalDisplayId, modeId);
+ public void onModeChanged(long timestampNanos, long physicalDisplayId, int modeId,
+ long renderPeriod) {
+ mListener.onModeChanged(timestampNanos, physicalDisplayId, modeId, renderPeriod);
}
@Override
@@ -1333,12 +1355,14 @@
}
@Override
- public void onModeChanged(long timestampNanos, long physicalDisplayId, int modeId) {
+ public void onModeChanged(long timestampNanos, long physicalDisplayId, int modeId,
+ long renderPeriod) {
if (DEBUG) {
Slog.d(TAG, "onModeChanged("
+ "timestampNanos=" + timestampNanos
+ ", physicalDisplayId=" + physicalDisplayId
- + ", modeId=" + modeId + ")");
+ + ", modeId=" + modeId
+ + ", renderPeriod=" + renderPeriod + ")");
}
synchronized (getSyncRoot()) {
LocalDisplayDevice device = mDevices.get(physicalDisplayId);
@@ -1349,7 +1373,8 @@
}
return;
}
- device.onActiveDisplayModeChangedLocked(modeId);
+ float renderFrameRate = 1e9f / renderPeriod;
+ device.onActiveDisplayModeChangedLocked(modeId, renderFrameRate);
}
}
@@ -1377,8 +1402,8 @@
@VisibleForTesting
public static class SurfaceControlProxy {
- public SurfaceControl.DynamicDisplayInfo getDynamicDisplayInfo(IBinder token) {
- return SurfaceControl.getDynamicDisplayInfo(token);
+ public SurfaceControl.DynamicDisplayInfo getDynamicDisplayInfo(long displayId) {
+ return SurfaceControl.getDynamicDisplayInfo(displayId);
}
public long[] getPhysicalDisplayIds() {
@@ -1389,8 +1414,8 @@
return DisplayControl.getPhysicalDisplayToken(physicalDisplayId);
}
- public SurfaceControl.StaticDisplayInfo getStaticDisplayInfo(IBinder displayToken) {
- return SurfaceControl.getStaticDisplayInfo(displayToken);
+ public SurfaceControl.StaticDisplayInfo getStaticDisplayInfo(long displayId) {
+ return SurfaceControl.getStaticDisplayInfo(displayId);
}
public SurfaceControl.DesiredDisplayModeSpecs getDesiredDisplayModeSpecs(
diff --git a/services/core/java/com/android/server/display/LogicalDisplay.java b/services/core/java/com/android/server/display/LogicalDisplay.java
index 8dd169bf..c7b27de 100644
--- a/services/core/java/com/android/server/display/LogicalDisplay.java
+++ b/services/core/java/com/android/server/display/LogicalDisplay.java
@@ -377,6 +377,7 @@
mBaseDisplayInfo.logicalHeight = maskedHeight;
mBaseDisplayInfo.rotation = Surface.ROTATION_0;
mBaseDisplayInfo.modeId = deviceInfo.modeId;
+ mBaseDisplayInfo.renderFrameRate = deviceInfo.renderFrameRate;
mBaseDisplayInfo.defaultModeId = deviceInfo.defaultModeId;
mBaseDisplayInfo.supportedModes = Arrays.copyOf(
deviceInfo.supportedModes, deviceInfo.supportedModes.length);
diff --git a/services/core/java/com/android/server/display/OverlayDisplayAdapter.java b/services/core/java/com/android/server/display/OverlayDisplayAdapter.java
index 0e11b53..3e67f0a 100644
--- a/services/core/java/com/android/server/display/OverlayDisplayAdapter.java
+++ b/services/core/java/com/android/server/display/OverlayDisplayAdapter.java
@@ -340,6 +340,7 @@
mInfo.width = mode.getPhysicalWidth();
mInfo.height = mode.getPhysicalHeight();
mInfo.modeId = mode.getModeId();
+ mInfo.renderFrameRate = mode.getRefreshRate();
mInfo.defaultModeId = mModes[0].getModeId();
mInfo.supportedModes = mModes;
mInfo.densityDpi = rawMode.mDensityDpi;
diff --git a/services/core/java/com/android/server/display/VirtualDisplayAdapter.java b/services/core/java/com/android/server/display/VirtualDisplayAdapter.java
index d24630d..a118b2f 100644
--- a/services/core/java/com/android/server/display/VirtualDisplayAdapter.java
+++ b/services/core/java/com/android/server/display/VirtualDisplayAdapter.java
@@ -448,6 +448,7 @@
mInfo.width = mWidth;
mInfo.height = mHeight;
mInfo.modeId = mMode.getModeId();
+ mInfo.renderFrameRate = mMode.getRefreshRate();
mInfo.defaultModeId = mMode.getModeId();
mInfo.supportedModes = new Display.Mode[] { mMode };
mInfo.densityDpi = mDensityDpi;
diff --git a/services/core/java/com/android/server/display/WifiDisplayAdapter.java b/services/core/java/com/android/server/display/WifiDisplayAdapter.java
index c759d98..e832701 100644
--- a/services/core/java/com/android/server/display/WifiDisplayAdapter.java
+++ b/services/core/java/com/android/server/display/WifiDisplayAdapter.java
@@ -646,6 +646,7 @@
mInfo.width = mWidth;
mInfo.height = mHeight;
mInfo.modeId = mMode.getModeId();
+ mInfo.renderFrameRate = mMode.getRefreshRate();
mInfo.defaultModeId = mMode.getModeId();
mInfo.supportedModes = new Display.Mode[] { mMode };
mInfo.presentationDeadlineNanos = 1000000000L / (int) mRefreshRate; // 1 frame
diff --git a/services/core/java/com/android/server/graphics/fonts/FontManagerService.java b/services/core/java/com/android/server/graphics/fonts/FontManagerService.java
index 28dc318..3c5b067 100644
--- a/services/core/java/com/android/server/graphics/fonts/FontManagerService.java
+++ b/services/core/java/com/android/server/graphics/fonts/FontManagerService.java
@@ -187,8 +187,8 @@
}
@Override
- public void setUpFsverity(String filePath, byte[] pkcs7Signature) throws IOException {
- VerityUtils.setUpFsverity(filePath, pkcs7Signature);
+ public void setUpFsverity(String filePath) throws IOException {
+ VerityUtils.setUpFsverity(filePath, /* signature */ (byte[]) null);
}
@Override
diff --git a/services/core/java/com/android/server/graphics/fonts/UpdatableFontDir.java b/services/core/java/com/android/server/graphics/fonts/UpdatableFontDir.java
index 457d5b7..6f93608 100644
--- a/services/core/java/com/android/server/graphics/fonts/UpdatableFontDir.java
+++ b/services/core/java/com/android/server/graphics/fonts/UpdatableFontDir.java
@@ -78,7 +78,7 @@
interface FsverityUtil {
boolean isFromTrustedProvider(String path, byte[] pkcs7Signature);
- void setUpFsverity(String path, byte[] pkcs7Signature) throws IOException;
+ void setUpFsverity(String path) throws IOException;
boolean rename(File src, File dest);
}
@@ -354,8 +354,7 @@
try {
// Do not parse font file before setting up fs-verity.
// setUpFsverity throws IOException if failed.
- mFsverityUtil.setUpFsverity(tempNewFontFile.getAbsolutePath(),
- pkcs7Signature);
+ mFsverityUtil.setUpFsverity(tempNewFontFile.getAbsolutePath());
} catch (IOException e) {
throw new SystemFontException(
FontManager.RESULT_ERROR_VERIFICATION_FAILURE,
diff --git a/services/core/java/com/android/server/input/InputManagerService.java b/services/core/java/com/android/server/input/InputManagerService.java
index 81d782e..0da04a2 100644
--- a/services/core/java/com/android/server/input/InputManagerService.java
+++ b/services/core/java/com/android/server/input/InputManagerService.java
@@ -294,6 +294,9 @@
// Manages Keyboard backlight
private final KeyboardBacklightController mKeyboardBacklightController;
+ // Manages Keyboard modifier keys remapping
+ private final KeyRemapper mKeyRemapper;
+
// Maximum number of milliseconds to wait for input event injection.
private static final int INJECTION_TIMEOUT_MILLIS = 30 * 1000;
@@ -408,6 +411,7 @@
mBatteryController = new BatteryController(mContext, mNative, injector.getLooper());
mKeyboardBacklightController = new KeyboardBacklightController(mContext, mNative,
mDataStore, injector.getLooper());
+ mKeyRemapper = new KeyRemapper(mContext, mNative, mDataStore, injector.getLooper());
mUseDevInputEventForAudioJack =
mContext.getResources().getBoolean(R.bool.config_useDevInputEventForAudioJack);
@@ -536,6 +540,7 @@
mKeyboardLayoutManager.systemRunning();
mBatteryController.systemRunning();
mKeyboardBacklightController.systemRunning();
+ mKeyRemapper.systemRunning();
}
private void reloadDeviceAliases() {
@@ -2738,6 +2743,27 @@
return mKeyboardLayoutManager.getKeyboardLayoutOverlay(identifier);
}
+ @EnforcePermission(Manifest.permission.REMAP_MODIFIER_KEYS)
+ @Override // Binder call
+ public void remapModifierKey(int fromKey, int toKey) {
+ super.remapModifierKey_enforcePermission();
+ mKeyRemapper.remapKey(fromKey, toKey);
+ }
+
+ @EnforcePermission(Manifest.permission.REMAP_MODIFIER_KEYS)
+ @Override // Binder call
+ public void clearAllModifierKeyRemappings() {
+ super.clearAllModifierKeyRemappings_enforcePermission();
+ mKeyRemapper.clearAllKeyRemappings();
+ }
+
+ @EnforcePermission(Manifest.permission.REMAP_MODIFIER_KEYS)
+ @Override // Binder call
+ public Map<Integer, Integer> getModifierKeyRemapping() {
+ super.getModifierKeyRemapping_enforcePermission();
+ return mKeyRemapper.getKeyRemapping();
+ }
+
// Native callback.
@SuppressWarnings("unused")
private String getDeviceAlias(String uniqueId) {
diff --git a/services/core/java/com/android/server/input/KeyRemapper.java b/services/core/java/com/android/server/input/KeyRemapper.java
new file mode 100644
index 0000000..950e094
--- /dev/null
+++ b/services/core/java/com/android/server/input/KeyRemapper.java
@@ -0,0 +1,161 @@
+/*
+ * 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.input;
+
+import android.content.Context;
+import android.hardware.input.InputManager;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.view.InputDevice;
+
+import com.android.internal.annotations.GuardedBy;
+
+import java.util.Map;
+import java.util.Objects;
+
+/**
+ * A component of {@link InputManagerService} responsible for managing key remappings.
+ *
+ * @hide
+ */
+final class KeyRemapper implements InputManager.InputDeviceListener {
+
+ private static final int MSG_UPDATE_EXISTING_DEVICES = 1;
+ private static final int MSG_REMAP_KEY = 2;
+ private static final int MSG_CLEAR_ALL_REMAPPING = 3;
+
+ private final Context mContext;
+ private final NativeInputManagerService mNative;
+ // The PersistentDataStore should be locked before use.
+ @GuardedBy("mDataStore")
+ private final PersistentDataStore mDataStore;
+ private final Handler mHandler;
+
+ KeyRemapper(Context context, NativeInputManagerService nativeService,
+ PersistentDataStore dataStore, Looper looper) {
+ mContext = context;
+ mNative = nativeService;
+ mDataStore = dataStore;
+ mHandler = new Handler(looper, this::handleMessage);
+ }
+
+ public void systemRunning() {
+ InputManager inputManager = Objects.requireNonNull(
+ mContext.getSystemService(InputManager.class));
+ inputManager.registerInputDeviceListener(this, mHandler);
+
+ Message msg = Message.obtain(mHandler, MSG_UPDATE_EXISTING_DEVICES,
+ inputManager.getInputDeviceIds());
+ mHandler.sendMessage(msg);
+ }
+
+ public void remapKey(int fromKey, int toKey) {
+ Message msg = Message.obtain(mHandler, MSG_REMAP_KEY, fromKey, toKey);
+ mHandler.sendMessage(msg);
+ }
+
+ public void clearAllKeyRemappings() {
+ Message msg = Message.obtain(mHandler, MSG_CLEAR_ALL_REMAPPING);
+ mHandler.sendMessage(msg);
+ }
+
+ public Map<Integer, Integer> getKeyRemapping() {
+ synchronized (mDataStore) {
+ return mDataStore.getKeyRemapping();
+ }
+ }
+
+ private void addKeyRemapping(int fromKey, int toKey) {
+ InputManager inputManager = Objects.requireNonNull(
+ mContext.getSystemService(InputManager.class));
+ for (int deviceId : inputManager.getInputDeviceIds()) {
+ InputDevice inputDevice = inputManager.getInputDevice(deviceId);
+ if (inputDevice != null && !inputDevice.isVirtual() && inputDevice.isFullKeyboard()) {
+ mNative.addKeyRemapping(deviceId, fromKey, toKey);
+ }
+ }
+ }
+
+ private void remapKeyInternal(int fromKey, int toKey) {
+ addKeyRemapping(fromKey, toKey);
+ synchronized (mDataStore) {
+ try {
+ if (fromKey == toKey) {
+ mDataStore.clearMappedKey(fromKey);
+ } else {
+ mDataStore.remapKey(fromKey, toKey);
+ }
+ } finally {
+ mDataStore.saveIfNeeded();
+ }
+ }
+ }
+
+ private void clearAllRemappingsInternal() {
+ synchronized (mDataStore) {
+ try {
+ Map<Integer, Integer> keyRemapping = mDataStore.getKeyRemapping();
+ for (int fromKey : keyRemapping.keySet()) {
+ mDataStore.clearMappedKey(fromKey);
+
+ // Remapping to itself will clear the remapping on native side
+ addKeyRemapping(fromKey, fromKey);
+ }
+ } finally {
+ mDataStore.saveIfNeeded();
+ }
+ }
+ }
+
+ @Override
+ public void onInputDeviceAdded(int deviceId) {
+ InputManager inputManager = Objects.requireNonNull(
+ mContext.getSystemService(InputManager.class));
+ InputDevice inputDevice = inputManager.getInputDevice(deviceId);
+ if (inputDevice != null && !inputDevice.isVirtual() && inputDevice.isFullKeyboard()) {
+ Map<Integer, Integer> remapping = getKeyRemapping();
+ remapping.forEach(
+ (fromKey, toKey) -> mNative.addKeyRemapping(deviceId, fromKey, toKey));
+ }
+ }
+
+ @Override
+ public void onInputDeviceRemoved(int deviceId) {
+ }
+
+ @Override
+ public void onInputDeviceChanged(int deviceId) {
+ }
+
+ private boolean handleMessage(Message msg) {
+ switch (msg.what) {
+ case MSG_UPDATE_EXISTING_DEVICES:
+ for (int deviceId : (int[]) msg.obj) {
+ onInputDeviceAdded(deviceId);
+ }
+ return true;
+ case MSG_REMAP_KEY:
+ remapKeyInternal(msg.arg1, msg.arg2);
+ return true;
+ case MSG_CLEAR_ALL_REMAPPING:
+ clearAllRemappingsInternal();
+ return true;
+ }
+ return false;
+ }
+}
diff --git a/services/core/java/com/android/server/input/NativeInputManagerService.java b/services/core/java/com/android/server/input/NativeInputManagerService.java
index cfa7fb1..8781c6e 100644
--- a/services/core/java/com/android/server/input/NativeInputManagerService.java
+++ b/services/core/java/com/android/server/input/NativeInputManagerService.java
@@ -47,6 +47,8 @@
int getSwitchState(int deviceId, int sourceMask, int sw);
+ void addKeyRemapping(int deviceId, int fromKeyCode, int toKeyCode);
+
boolean hasKeys(int deviceId, int sourceMask, int[] keyCodes, boolean[] keyExists);
int getKeyCodeForKeyLocation(int deviceId, int locationKeyCode);
@@ -235,6 +237,9 @@
public native int getSwitchState(int deviceId, int sourceMask, int sw);
@Override
+ public native void addKeyRemapping(int deviceId, int fromKeyCode, int toKeyCode);
+
+ @Override
public native boolean hasKeys(int deviceId, int sourceMask, int[] keyCodes,
boolean[] keyExists);
diff --git a/services/core/java/com/android/server/input/PersistentDataStore.java b/services/core/java/com/android/server/input/PersistentDataStore.java
index 1bb10c7..375377a7 100644
--- a/services/core/java/com/android/server/input/PersistentDataStore.java
+++ b/services/core/java/com/android/server/input/PersistentDataStore.java
@@ -27,14 +27,13 @@
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.ArrayUtils;
import com.android.internal.util.XmlUtils;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
import libcore.io.IoUtils;
import org.xmlpull.v1.XmlPullParserException;
-import com.android.modules.utils.TypedXmlPullParser;
-import com.android.modules.utils.TypedXmlSerializer;
-
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
@@ -64,6 +63,8 @@
final class PersistentDataStore {
static final String TAG = "InputManager";
+ private static final int INVALID_VALUE = -1;
+
// Input device state by descriptor.
private final HashMap<String, InputDeviceState> mInputDevices =
new HashMap<String, InputDeviceState>();
@@ -77,6 +78,9 @@
// True if there are changes to be saved.
private boolean mDirty;
+ // Storing key remapping
+ private Map<Integer, Integer> mKeyRemapping = new HashMap<>();
+
public PersistentDataStore() {
this(new Injector());
}
@@ -187,6 +191,30 @@
return state.getKeyboardBacklightBrightness(lightId);
}
+ public boolean remapKey(int fromKey, int toKey) {
+ loadIfNeeded();
+ if (mKeyRemapping.getOrDefault(fromKey, INVALID_VALUE) == toKey) {
+ return false;
+ }
+ mKeyRemapping.put(fromKey, toKey);
+ setDirty();
+ return true;
+ }
+
+ public boolean clearMappedKey(int key) {
+ loadIfNeeded();
+ if (mKeyRemapping.containsKey(key)) {
+ mKeyRemapping.remove(key);
+ setDirty();
+ }
+ return true;
+ }
+
+ public Map<Integer, Integer> getKeyRemapping() {
+ loadIfNeeded();
+ return new HashMap<>(mKeyRemapping);
+ }
+
public boolean removeUninstalledKeyboardLayouts(Set<String> availableKeyboardLayouts) {
boolean changed = false;
for (InputDeviceState state : mInputDevices.values()) {
@@ -229,6 +257,7 @@
}
private void clearState() {
+ mKeyRemapping.clear();
mInputDevices.clear();
}
@@ -280,7 +309,9 @@
XmlUtils.beginDocument(parser, "input-manager-state");
final int outerDepth = parser.getDepth();
while (XmlUtils.nextElementWithin(parser, outerDepth)) {
- if (parser.getName().equals("input-devices")) {
+ if (parser.getName().equals("key-remapping")) {
+ loadKeyRemappingFromXml(parser);
+ } else if (parser.getName().equals("input-devices")) {
loadInputDevicesFromXml(parser);
}
}
@@ -307,10 +338,31 @@
}
}
+ private void loadKeyRemappingFromXml(TypedXmlPullParser parser)
+ throws IOException, XmlPullParserException {
+ final int outerDepth = parser.getDepth();
+ while (XmlUtils.nextElementWithin(parser, outerDepth)) {
+ if (parser.getName().equals("remap")) {
+ int fromKey = parser.getAttributeInt(null, "from-key");
+ int toKey = parser.getAttributeInt(null, "to-key");
+ mKeyRemapping.put(fromKey, toKey);
+ }
+ }
+ }
+
private void saveToXml(TypedXmlSerializer serializer) throws IOException {
serializer.startDocument(null, true);
serializer.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true);
serializer.startTag(null, "input-manager-state");
+ serializer.startTag(null, "key-remapping");
+ for (int fromKey : mKeyRemapping.keySet()) {
+ int toKey = mKeyRemapping.get(fromKey);
+ serializer.startTag(null, "remap");
+ serializer.attributeInt(null, "from-key", fromKey);
+ serializer.attributeInt(null, "to-key", toKey);
+ serializer.endTag(null, "remap");
+ }
+ serializer.endTag(null, "key-remapping");
serializer.startTag(null, "input-devices");
for (Map.Entry<String, InputDeviceState> entry : mInputDevices.entrySet()) {
final String descriptor = entry.getKey();
@@ -329,7 +381,6 @@
private static final String[] CALIBRATION_NAME = { "x_scale",
"x_ymix", "x_offset", "y_xmix", "y_scale", "y_offset" };
- private static final int INVALID_VALUE = -1;
private final TouchCalibration[] mTouchCalibration = new TouchCalibration[4];
@Nullable
private String mCurrentKeyboardLayout;
diff --git a/services/core/java/com/android/server/locksettings/LockSettingsService.java b/services/core/java/com/android/server/locksettings/LockSettingsService.java
index 0ae3a02..99d77d6 100644
--- a/services/core/java/com/android/server/locksettings/LockSettingsService.java
+++ b/services/core/java/com/android/server/locksettings/LockSettingsService.java
@@ -69,7 +69,7 @@
import android.content.pm.UserInfo;
import android.database.ContentObserver;
import android.database.sqlite.SQLiteDatabase;
-import android.hardware.authsecret.V1_0.IAuthSecret;
+import android.hardware.authsecret.IAuthSecret;
import android.hardware.biometrics.BiometricManager;
import android.hardware.face.Face;
import android.hardware.face.FaceManager;
@@ -265,7 +265,8 @@
protected boolean mHasSecureLockScreen;
protected IGateKeeperService mGateKeeperService;
- protected IAuthSecret mAuthSecretService;
+ protected IAuthSecret mAuthSecretServiceAidl;
+ protected android.hardware.authsecret.V1_0.IAuthSecret mAuthSecretServiceHidl;
private static final String GSI_RUNNING_PROP = "ro.gsid.image_running";
@@ -833,12 +834,19 @@
}
private void getAuthSecretHal() {
- try {
- mAuthSecretService = IAuthSecret.getService(/* retry */ true);
- } catch (NoSuchElementException e) {
- Slog.i(TAG, "Device doesn't implement AuthSecret HAL");
- } catch (RemoteException e) {
- Slog.w(TAG, "Failed to get AuthSecret HAL", e);
+ mAuthSecretServiceAidl = IAuthSecret.Stub.asInterface(ServiceManager.
+ waitForDeclaredService(IAuthSecret.DESCRIPTOR + "/default"));
+ if (mAuthSecretServiceAidl == null) {
+ Slog.i(TAG, "Device doesn't implement AuthSecret HAL(aidl), try to get hidl version");
+
+ try {
+ mAuthSecretServiceHidl =
+ android.hardware.authsecret.V1_0.IAuthSecret.getService(/* retry */ true);
+ } catch (NoSuchElementException e) {
+ Slog.i(TAG, "Device doesn't implement AuthSecret HAL(hidl)");
+ } catch (RemoteException e) {
+ Slog.w(TAG, "Failed to get AuthSecret HAL(hidl)", e);
+ }
}
}
@@ -2601,17 +2609,25 @@
// If the given user is the primary user, pass the auth secret to the HAL. Only the system
// user can be primary. Check for the system user ID before calling getUserInfo(), as other
// users may still be under construction.
- if (mAuthSecretService != null && userId == UserHandle.USER_SYSTEM &&
+ if (userId == UserHandle.USER_SYSTEM &&
mUserManager.getUserInfo(userId).isPrimary()) {
- try {
- final byte[] rawSecret = sp.deriveVendorAuthSecret();
- final ArrayList<Byte> secret = new ArrayList<>(rawSecret.length);
- for (int i = 0; i < rawSecret.length; ++i) {
- secret.add(rawSecret[i]);
+ final byte[] rawSecret = sp.deriveVendorAuthSecret();
+ if (mAuthSecretServiceAidl != null) {
+ try {
+ mAuthSecretServiceAidl.setPrimaryUserCredential(rawSecret);
+ } catch (RemoteException e) {
+ Slog.w(TAG, "Failed to pass primary user secret to AuthSecret HAL(aidl)", e);
}
- mAuthSecretService.primaryUserCredential(secret);
- } catch (RemoteException e) {
- Slog.w(TAG, "Failed to pass primary user secret to AuthSecret HAL", e);
+ } else if (mAuthSecretServiceHidl != null) {
+ try {
+ final ArrayList<Byte> secret = new ArrayList<>(rawSecret.length);
+ for (int i = 0; i < rawSecret.length; ++i) {
+ secret.add(rawSecret[i]);
+ }
+ mAuthSecretServiceHidl.primaryUserCredential(secret);
+ } catch (RemoteException e) {
+ Slog.w(TAG, "Failed to pass primary user secret to AuthSecret HAL(hidl)", e);
+ }
}
}
}
diff --git a/services/core/java/com/android/server/notification/NotificationRecordLogger.java b/services/core/java/com/android/server/notification/NotificationRecordLogger.java
index ef1e11c..4031c83 100644
--- a/services/core/java/com/android/server/notification/NotificationRecordLogger.java
+++ b/services/core/java/com/android/server/notification/NotificationRecordLogger.java
@@ -18,8 +18,8 @@
import static android.service.notification.NotificationListenerService.REASON_ASSISTANT_CANCEL;
import static android.service.notification.NotificationListenerService.REASON_CANCEL;
+import static android.service.notification.NotificationListenerService.REASON_CLEAR_DATA;
import static android.service.notification.NotificationListenerService.REASON_CLICK;
-import static android.service.notification.NotificationListenerService.REASON_TIMEOUT;
import android.annotation.NonNull;
import android.annotation.Nullable;
@@ -172,7 +172,12 @@
NOTIFICATION_CANCEL_SNOOZED(181),
@UiEvent(doc = "Notification was canceled due to timeout")
NOTIFICATION_CANCEL_TIMEOUT(182),
- // Values 183-189 reserved for future system dismissal reasons
+ @UiEvent(doc = "Notification was canceled due to the backing channel being deleted")
+ NOTIFICATION_CANCEL_CHANNEL_REMOVED(1261),
+ @UiEvent(doc = "Notification was canceled due to the app's storage being cleared")
+ NOTIFICATION_CANCEL_CLEAR_DATA(1262),
+ // Values above this line must remain in the same order as the corresponding
+ // NotificationCancelReason enum values.
@UiEvent(doc = "Notification was canceled due to user dismissal of a peeking notification.")
NOTIFICATION_CANCEL_USER_PEEK(190),
@UiEvent(doc = "Notification was canceled due to user dismissal from the always-on display")
@@ -208,7 +213,7 @@
// Most cancel reasons do not have a meaningful surface. Reason codes map directly
// to NotificationCancelledEvent codes.
if (surface == NotificationStats.DISMISSAL_OTHER) {
- if ((REASON_CLICK <= reason) && (reason <= REASON_TIMEOUT)) {
+ if ((REASON_CLICK <= reason) && (reason <= REASON_CLEAR_DATA)) {
return NotificationCancelledEvent.values()[reason];
}
if (reason == REASON_ASSISTANT_CANCEL) {
diff --git a/services/core/java/com/android/server/pm/AppStateHelper.java b/services/core/java/com/android/server/pm/AppStateHelper.java
index 9ea350f..e6e8212a 100644
--- a/services/core/java/com/android/server/pm/AppStateHelper.java
+++ b/services/core/java/com/android/server/pm/AppStateHelper.java
@@ -16,15 +16,22 @@
package com.android.server.pm;
+import static android.media.AudioAttributes.USAGE_VOICE_COMMUNICATION;
+import static android.media.AudioAttributes.USAGE_VOICE_COMMUNICATION_SIGNALLING;
+
import android.app.ActivityManager;
import android.app.ActivityManager.RunningAppProcessInfo;
+import android.app.ActivityManagerInternal;
import android.content.Context;
+import android.media.AudioManager;
import android.media.IAudioService;
import android.os.ServiceManager;
+import android.telecom.TelecomManager;
import android.text.TextUtils;
import android.util.ArraySet;
import com.android.internal.util.ArrayUtils;
+import com.android.server.LocalServices;
import java.util.ArrayList;
import java.util.List;
@@ -72,6 +79,43 @@
}
/**
+ * True if any app is using voice communication.
+ */
+ private boolean hasVoiceCall() {
+ var am = mContext.getSystemService(AudioManager.class);
+ try {
+ for (var apc : am.getActivePlaybackConfigurations()) {
+ if (!apc.isActive()) {
+ continue;
+ }
+ var usage = apc.getAudioAttributes().getUsage();
+ if (usage == USAGE_VOICE_COMMUNICATION
+ || usage == USAGE_VOICE_COMMUNICATION_SIGNALLING) {
+ return true;
+ }
+ }
+ } catch (Exception ignore) {
+ }
+ return false;
+ }
+
+ /**
+ * True if the app is recording audio.
+ */
+ private boolean isRecordingAudio(String packageName) {
+ var am = mContext.getSystemService(AudioManager.class);
+ try {
+ for (var arc : am.getActiveRecordingConfigurations()) {
+ if (TextUtils.equals(arc.getClientPackageName(), packageName)) {
+ return true;
+ }
+ }
+ } catch (Exception ignore) {
+ }
+ return false;
+ }
+
+ /**
* True if the app is in the foreground.
*/
private boolean isAppForeground(String packageName) {
@@ -89,8 +133,7 @@
* True if the app is playing/recording audio.
*/
private boolean hasActiveAudio(String packageName) {
- // TODO(b/235306967): also check recording
- return hasAudioFocus(packageName);
+ return hasAudioFocus(packageName) || isRecordingAudio(packageName);
}
/**
@@ -143,16 +186,16 @@
* True if there is an ongoing phone call.
*/
public boolean isInCall() {
- // To be implemented
- return false;
+ // TelecomManager doesn't handle the case where some apps don't implement ConnectionService.
+ // We check apps using voice communication to detect if the device is in call.
+ var tm = mContext.getSystemService(TelecomManager.class);
+ return tm.isInCall() || hasVoiceCall();
}
/**
* Returns a list of packages which depend on {@code packageNames}. These are the packages
* that will be affected when updating {@code packageNames} and should participate in
* the evaluation of install constraints.
- *
- * TODO(b/235306967): Also include bounded services as dependency.
*/
public List<String> getDependencyPackages(List<String> packageNames) {
var results = new ArraySet<String>();
@@ -167,6 +210,10 @@
}
}
}
+ var amInternal = LocalServices.getService(ActivityManagerInternal.class);
+ for (var packageName : packageNames) {
+ results.addAll(amInternal.getClientPackages(packageName));
+ }
return new ArrayList<>(results);
}
}
diff --git a/services/core/java/com/android/server/pm/PackageInstallerService.java b/services/core/java/com/android/server/pm/PackageInstallerService.java
index 409d352..4803c5e 100644
--- a/services/core/java/com/android/server/pm/PackageInstallerService.java
+++ b/services/core/java/com/android/server/pm/PackageInstallerService.java
@@ -154,6 +154,11 @@
/** Destroy sessions older than this on storage free request */
private static final long MAX_SESSION_AGE_ON_LOW_STORAGE_MILLIS = 8 * DateUtils.HOUR_IN_MILLIS;
+ /** Threshold of historical sessions size */
+ private static final int HISTORICAL_SESSIONS_THRESHOLD = 500;
+ /** Size of historical sessions to be cleared when reaching threshold */
+ private static final int HISTORICAL_CLEAR_SIZE = 400;
+
/**
* Allow verification-skipping if it's a development app installed through ADB with
* disable verification flag specified.
@@ -549,6 +554,10 @@
CharArrayWriter writer = new CharArrayWriter();
IndentingPrintWriter pw = new IndentingPrintWriter(writer, " ");
session.dump(pw);
+ if (mHistoricalSessions.size() > HISTORICAL_SESSIONS_THRESHOLD) {
+ Slog.d(TAG, "Historical sessions size reaches threshold, clear the oldest");
+ mHistoricalSessions.subList(0, HISTORICAL_CLEAR_SIZE).clear();
+ }
mHistoricalSessions.add(writer.toString());
int installerUid = session.getInstallerUid();
diff --git a/services/core/java/com/android/server/pm/PackageManagerLocal.java b/services/core/java/com/android/server/pm/PackageManagerLocal.java
index fad61b8..d163d3d 100644
--- a/services/core/java/com/android/server/pm/PackageManagerLocal.java
+++ b/services/core/java/com/android/server/pm/PackageManagerLocal.java
@@ -30,7 +30,6 @@
import java.lang.annotation.RetentionPolicy;
import java.util.List;
import java.util.Map;
-import java.util.function.Consumer;
/**
* In-process API for server side PackageManager related infrastructure.
@@ -177,8 +176,6 @@
@NonNull
Map<String, PackageState> getPackageStates();
- void forAllPackageStates(@NonNull Consumer<PackageState> consumer);
-
@Override
void close();
}
diff --git a/services/core/java/com/android/server/pm/PackageSetting.java b/services/core/java/com/android/server/pm/PackageSetting.java
index 6dcbb52..b18179e 100644
--- a/services/core/java/com/android/server/pm/PackageSetting.java
+++ b/services/core/java/com/android/server/pm/PackageSetting.java
@@ -1380,7 +1380,11 @@
return mSecondaryCpuAbi;
}
-
+ @ApplicationInfo.HiddenApiEnforcementPolicy
+ @Override
+ public int getHiddenApiEnforcementPolicy() {
+ return AndroidPackageUtils.getHiddenApiEnforcementPolicy(getAndroidPackage(), this);
+ }
// Code below generated by codegen v1.0.23.
//
diff --git a/services/core/java/com/android/server/pm/Settings.java b/services/core/java/com/android/server/pm/Settings.java
index bc9f7b2..89b74f4 100644
--- a/services/core/java/com/android/server/pm/Settings.java
+++ b/services/core/java/com/android/server/pm/Settings.java
@@ -369,6 +369,8 @@
// Current settings file.
private final File mSettingsFilename;
+ // Compressed current settings file.
+ private final File mCompressedSettingsFilename;
// Previous settings file.
// Removed when the current settings file successfully stored.
private final File mPreviousSettingsFilename;
@@ -639,6 +641,7 @@
mRuntimePermissionsPersistence = null;
mPermissionDataProvider = null;
mSettingsFilename = null;
+ mCompressedSettingsFilename = null;
mPreviousSettingsFilename = null;
mPackageListFilename = null;
mStoppedPackagesFilename = null;
@@ -710,6 +713,7 @@
|FileUtils.S_IROTH|FileUtils.S_IXOTH,
-1, -1);
mSettingsFilename = new File(mSystemDir, "packages.xml");
+ mCompressedSettingsFilename = new File(mSystemDir, "packages.compressed");
mPreviousSettingsFilename = new File(mSystemDir, "packages-backup.xml");
mPackageListFilename = new File(mSystemDir, "packages.list");
FileUtils.setPermissions(mPackageListFilename, 0640, SYSTEM_UID, PACKAGE_INFO_GID);
@@ -751,6 +755,7 @@
mLock = null;
mRuntimePermissionsPersistence = r.mRuntimePermissionsPersistence;
mSettingsFilename = null;
+ mCompressedSettingsFilename = null;
mPreviousSettingsFilename = null;
mPackageListFilename = null;
mStoppedPackagesFilename = null;
@@ -2465,7 +2470,7 @@
mReadMessages.append("Reading from backup stopped packages file\n");
PackageManagerService.reportSettingsProblem(Log.INFO,
"Need to read from backup stopped packages file");
- if (mSettingsFilename.exists()) {
+ if (mStoppedPackagesFilename.exists()) {
// If both the backup and normal file exist, we
// ignore the normal one since it might have been
// corrupted.
@@ -2588,6 +2593,8 @@
Slog.w(PackageManagerService.TAG, "Preserving older settings backup");
}
}
+ // Compressed settings are not valid anymore.
+ mCompressedSettingsFilename.delete();
mPastSignatures.clear();
@@ -2677,10 +2684,30 @@
mPreviousSettingsFilename.delete();
FileUtils.setPermissions(mSettingsFilename.toString(),
- FileUtils.S_IRUSR|FileUtils.S_IWUSR
- |FileUtils.S_IRGRP|FileUtils.S_IWGRP,
+ FileUtils.S_IRUSR | FileUtils.S_IWUSR | FileUtils.S_IRGRP | FileUtils.S_IWGRP,
-1, -1);
+ final FileInputStream fis = new FileInputStream(mSettingsFilename);
+ final AtomicFile compressed = new AtomicFile(mCompressedSettingsFilename);
+ final FileOutputStream fos = compressed.startWrite();
+
+ BackgroundThread.getHandler().post(() -> {
+ try {
+ if (!nativeCompressLz4(fis.getFD().getInt$(), fos.getFD().getInt$())) {
+ throw new IOException("Failed to compress");
+ }
+ compressed.finishWrite(fos);
+ FileUtils.setPermissions(mCompressedSettingsFilename.toString(),
+ FileUtils.S_IRUSR | FileUtils.S_IWUSR | FileUtils.S_IRGRP
+ | FileUtils.S_IWGRP, -1, -1);
+ } catch (IOException e) {
+ Slog.e(PackageManagerService.TAG, "Failed to write compressed settings file: "
+ + mCompressedSettingsFilename, e);
+ compressed.delete();
+ }
+ IoUtils.closeQuietly(fis);
+ });
+
writeKernelMappingLPr();
writePackageListLPr();
writeAllUsersPackageRestrictionsLPr(sync);
@@ -2703,6 +2730,8 @@
//Debug.stopMethodTracing();
}
+ private native boolean nativeCompressLz4(int inputFd, int outputFd);
+
private void writeKernelRemoveUserLPr(int userId) {
if (mKernelMappingFilename == null) return;
diff --git a/services/core/java/com/android/server/pm/ShortcutPackage.java b/services/core/java/com/android/server/pm/ShortcutPackage.java
index 0362ddd..4fddc9c 100644
--- a/services/core/java/com/android/server/pm/ShortcutPackage.java
+++ b/services/core/java/com/android/server/pm/ShortcutPackage.java
@@ -1470,9 +1470,15 @@
}
// Then make sure none of the activities have more than the max number of shortcuts.
+ int total = 0;
for (int i = counts.size() - 1; i >= 0; i--) {
- service.enforceMaxActivityShortcuts(counts.valueAt(i));
+ int count = counts.valueAt(i);
+ service.enforceMaxActivityShortcuts(count);
+ total += count;
}
+
+ // Finally make sure that the app doesn't have more than the max number of shortcuts.
+ service.enforceMaxAppShortcuts(total);
}
/**
diff --git a/services/core/java/com/android/server/pm/ShortcutService.java b/services/core/java/com/android/server/pm/ShortcutService.java
index 83720f1..12a33ee 100644
--- a/services/core/java/com/android/server/pm/ShortcutService.java
+++ b/services/core/java/com/android/server/pm/ShortcutService.java
@@ -181,6 +181,9 @@
static final int DEFAULT_MAX_SHORTCUTS_PER_ACTIVITY = 15;
@VisibleForTesting
+ static final int DEFAULT_MAX_SHORTCUTS_PER_APP = 60;
+
+ @VisibleForTesting
static final int DEFAULT_MAX_ICON_DIMENSION_DP = 96;
@VisibleForTesting
@@ -257,6 +260,11 @@
String KEY_MAX_SHORTCUTS = "max_shortcuts";
/**
+ * Key name for the max dynamic shortcuts per app. (int)
+ */
+ String KEY_MAX_SHORTCUTS_PER_APP = "max_shortcuts_per_app";
+
+ /**
* Key name for icon compression quality, 0-100.
*/
String KEY_ICON_QUALITY = "icon_quality";
@@ -329,9 +337,14 @@
new SparseArray<>();
/**
+ * Max number of dynamic + manifest shortcuts that each activity can have at a time.
+ */
+ private int mMaxShortcutsPerActivity;
+
+ /**
* Max number of dynamic + manifest shortcuts that each application can have at a time.
*/
- private int mMaxShortcuts;
+ private int mMaxShortcutsPerApp;
/**
* Max number of updating API calls that each application can make during the interval.
@@ -804,9 +817,12 @@
mMaxUpdatesPerInterval = Math.max(0, (int) parser.getLong(
ConfigConstants.KEY_MAX_UPDATES_PER_INTERVAL, DEFAULT_MAX_UPDATES_PER_INTERVAL));
- mMaxShortcuts = Math.max(0, (int) parser.getLong(
+ mMaxShortcutsPerActivity = Math.max(0, (int) parser.getLong(
ConfigConstants.KEY_MAX_SHORTCUTS, DEFAULT_MAX_SHORTCUTS_PER_ACTIVITY));
+ mMaxShortcutsPerApp = Math.max(0, (int) parser.getLong(
+ ConfigConstants.KEY_MAX_SHORTCUTS_PER_APP, DEFAULT_MAX_SHORTCUTS_PER_APP));
+
final int iconDimensionDp = Math.max(1, injectIsLowRamDevice()
? (int) parser.getLong(
ConfigConstants.KEY_MAX_ICON_DIMENSION_DP_LOWRAM,
@@ -1746,16 +1762,33 @@
* {@link #getMaxActivityShortcuts()}.
*/
void enforceMaxActivityShortcuts(int numShortcuts) {
- if (numShortcuts > mMaxShortcuts) {
+ if (numShortcuts > mMaxShortcutsPerActivity) {
throw new IllegalArgumentException("Max number of dynamic shortcuts exceeded");
}
}
/**
+ * @throws IllegalArgumentException if {@code numShortcuts} is bigger than
+ * {@link #getMaxAppShortcuts()}.
+ */
+ void enforceMaxAppShortcuts(int numShortcuts) {
+ if (numShortcuts > mMaxShortcutsPerApp) {
+ throw new IllegalArgumentException("Max number of dynamic shortcuts per app exceeded");
+ }
+ }
+
+ /**
* Return the max number of dynamic + manifest shortcuts for each launcher icon.
*/
int getMaxActivityShortcuts() {
- return mMaxShortcuts;
+ return mMaxShortcutsPerActivity;
+ }
+
+ /**
+ * Return the max number of dynamic + manifest shortcuts for each launcher icon.
+ */
+ int getMaxAppShortcuts() {
+ return mMaxShortcutsPerApp;
}
/**
@@ -2188,6 +2221,8 @@
ps.ensureNotImmutable(shortcut.getId(), /*ignoreInvisible=*/ true);
fillInDefaultActivity(Arrays.asList(shortcut));
+ enforceMaxAppShortcuts(ps.getShortcutCount());
+
if (!shortcut.hasRank()) {
shortcut.setRank(0);
}
@@ -2575,7 +2610,7 @@
throws RemoteException {
verifyCaller(packageName, userId);
- return mMaxShortcuts;
+ return mMaxShortcutsPerActivity;
}
@Override
@@ -4724,7 +4759,7 @@
pw.print(" maxUpdatesPerInterval: ");
pw.println(mMaxUpdatesPerInterval);
pw.print(" maxShortcutsPerActivity: ");
- pw.println(mMaxShortcuts);
+ pw.println(mMaxShortcutsPerActivity);
pw.println();
mStatLogger.dump(pw, " ");
@@ -5211,7 +5246,7 @@
@VisibleForTesting
int getMaxShortcutsForTest() {
- return mMaxShortcuts;
+ return mMaxShortcutsPerActivity;
}
@VisibleForTesting
diff --git a/services/core/java/com/android/server/pm/UserManagerService.java b/services/core/java/com/android/server/pm/UserManagerService.java
index 3234e87..7191daa 100644
--- a/services/core/java/com/android/server/pm/UserManagerService.java
+++ b/services/core/java/com/android/server/pm/UserManagerService.java
@@ -93,6 +93,7 @@
import android.provider.Settings;
import android.service.voice.VoiceInteractionManagerInternal;
import android.stats.devicepolicy.DevicePolicyEnums;
+import android.telecom.TelecomManager;
import android.text.TextUtils;
import android.util.ArrayMap;
import android.util.ArraySet;
@@ -128,7 +129,6 @@
import com.android.server.am.UserState;
import com.android.server.pm.UserManagerInternal.UserLifecycleListener;
import com.android.server.pm.UserManagerInternal.UserRestrictionsListener;
-import com.android.server.pm.UserManagerInternal.UserVisibilityListener;
import com.android.server.storage.DeviceStorageMonitorInternal;
import com.android.server.utils.Slogf;
import com.android.server.utils.TimingsTraceAndSlog;
@@ -1970,6 +1970,63 @@
}
}
+ /**
+ * Returns whether switching users is currently allowed for the provided user.
+ * <p>
+ * Switching users is not allowed in the following cases:
+ * <li>the user is in a phone call</li>
+ * <li>{@link UserManager#DISALLOW_USER_SWITCH} is set</li>
+ * <li>system user hasn't been unlocked yet</li>
+ *
+ * @return A {@link UserManager.UserSwitchabilityResult} flag indicating if the user is
+ * switchable.
+ */
+ public @UserManager.UserSwitchabilityResult int getUserSwitchability(int userId) {
+ checkManageOrInteractPermissionIfCallerInOtherProfileGroup(userId, "getUserSwitchability");
+
+ final TimingsTraceAndSlog t = new TimingsTraceAndSlog();
+ t.traceBegin("getUserSwitchability-" + userId);
+
+ int flags = UserManager.SWITCHABILITY_STATUS_OK;
+
+ t.traceBegin("TM.isInCall");
+ final long identity = Binder.clearCallingIdentity();
+ try {
+ final TelecomManager telecomManager = mContext.getSystemService(TelecomManager.class);
+ if (telecomManager != null && telecomManager.isInCall()) {
+ flags |= UserManager.SWITCHABILITY_STATUS_USER_IN_CALL;
+ }
+ } finally {
+ Binder.restoreCallingIdentity(identity);
+ }
+ t.traceEnd();
+
+ t.traceBegin("hasUserRestriction-DISALLOW_USER_SWITCH");
+ if (mLocalService.hasUserRestriction(DISALLOW_USER_SWITCH, userId)) {
+ flags |= UserManager.SWITCHABILITY_STATUS_USER_SWITCH_DISALLOWED;
+ }
+ t.traceEnd();
+
+ // System User is always unlocked in Headless System User Mode, so ignore this flag
+ if (!isHeadlessSystemUserMode()) {
+ t.traceBegin("getInt-ALLOW_USER_SWITCHING_WHEN_SYSTEM_USER_LOCKED");
+ final boolean allowUserSwitchingWhenSystemUserLocked = Settings.Global.getInt(
+ mContext.getContentResolver(),
+ Settings.Global.ALLOW_USER_SWITCHING_WHEN_SYSTEM_USER_LOCKED, 0) != 0;
+ t.traceEnd();
+ t.traceBegin("isUserUnlocked-USER_SYSTEM");
+ final boolean systemUserUnlocked = mLocalService.isUserUnlocked(UserHandle.USER_SYSTEM);
+ t.traceEnd();
+
+ if (!allowUserSwitchingWhenSystemUserLocked && !systemUserUnlocked) {
+ flags |= UserManager.SWITCHABILITY_STATUS_SYSTEM_USER_LOCKED;
+ }
+ }
+ t.traceEnd();
+
+ return flags;
+ }
+
@VisibleForTesting
boolean isUserSwitcherEnabled(@UserIdInt int mUserId) {
boolean multiUserSettingOn = Settings.Global.getInt(mContext.getContentResolver(),
diff --git a/services/core/java/com/android/server/pm/local/PackageManagerLocalImpl.java b/services/core/java/com/android/server/pm/local/PackageManagerLocalImpl.java
index 024b63e..f8e1547 100644
--- a/services/core/java/com/android/server/pm/local/PackageManagerLocalImpl.java
+++ b/services/core/java/com/android/server/pm/local/PackageManagerLocalImpl.java
@@ -34,7 +34,6 @@
import java.util.Collections;
import java.util.List;
import java.util.Map;
-import java.util.function.Consumer;
/** @hide */
public class PackageManagerLocalImpl implements PackageManagerLocal {
@@ -198,18 +197,5 @@
return mFilteredPackageStates;
}
-
- @Override
- public void forAllPackageStates(@NonNull Consumer<PackageState> consumer) {
- checkClosed();
-
- var packageStates = mSnapshot.getPackageStates();
- for (int index = 0, size = packageStates.size(); index < size; index++) {
- var packageState = packageStates.valueAt(index);
- if (!mSnapshot.shouldFilterApplication(packageState, mCallingUid, mUserId)) {
- consumer.accept(packageState);
- }
- }
- }
}
}
diff --git a/services/core/java/com/android/server/pm/parsing/pkg/AndroidPackageUtils.java b/services/core/java/com/android/server/pm/parsing/pkg/AndroidPackageUtils.java
index c76b129..82b5fa2 100644
--- a/services/core/java/com/android/server/pm/parsing/pkg/AndroidPackageUtils.java
+++ b/services/core/java/com/android/server/pm/parsing/pkg/AndroidPackageUtils.java
@@ -234,10 +234,12 @@
|| !pkg.getLibraryNames().isEmpty();
}
- public static int getHiddenApiEnforcementPolicy(AndroidPackage pkg,
+ public static int getHiddenApiEnforcementPolicy(@Nullable AndroidPackage pkg,
@NonNull PackageStateInternal pkgSetting) {
boolean isAllowedToUseHiddenApis;
- if (pkg.isSignedWithPlatformKey()) {
+ if (pkg == null) {
+ isAllowedToUseHiddenApis = false;
+ } else if (pkg.isSignedWithPlatformKey()) {
isAllowedToUseHiddenApis = true;
} else if (pkg.isSystem() || pkgSetting.getTransientState().isUpdatedSystemApp()) {
isAllowedToUseHiddenApis = pkg.isUsesNonSdkApi()
diff --git a/services/core/java/com/android/server/pm/permission/Permission.java b/services/core/java/com/android/server/pm/permission/Permission.java
index 69e7bf1..165c52d 100644
--- a/services/core/java/com/android/server/pm/permission/Permission.java
+++ b/services/core/java/com/android/server/pm/permission/Permission.java
@@ -322,6 +322,10 @@
return (mPermissionInfo.protectionLevel & PermissionInfo.PROTECTION_FLAG_COMPANION) != 0;
}
+ public boolean isModule() {
+ return (mPermissionInfo.protectionLevel & PermissionInfo.PROTECTION_FLAG_MODULE) != 0;
+ }
+
public boolean isRetailDemo() {
return (mPermissionInfo.protectionLevel & PermissionInfo.PROTECTION_FLAG_RETAIL_DEMO) != 0;
}
diff --git a/services/core/java/com/android/server/pm/permission/PermissionManagerServiceImpl.java b/services/core/java/com/android/server/pm/permission/PermissionManagerServiceImpl.java
index e56edeb..2a2bcab 100644
--- a/services/core/java/com/android/server/pm/permission/PermissionManagerServiceImpl.java
+++ b/services/core/java/com/android/server/pm/permission/PermissionManagerServiceImpl.java
@@ -238,6 +238,8 @@
NOTIFICATION_PERMISSIONS.add(Manifest.permission.POST_NOTIFICATIONS);
}
+ @NonNull private final ApexManager mApexManager;
+
/** Set of source package names for Privileged Permission Allowlist */
private final ArraySet<String> mPrivilegedPermissionAllowlistSourcePackageNames =
new ArraySet<>();
@@ -421,6 +423,7 @@
mPackageManagerInt = LocalServices.getService(PackageManagerInternal.class);
mUserManagerInt = LocalServices.getService(UserManagerInternal.class);
mIsLeanback = availableFeatures.containsKey(PackageManager.FEATURE_LEANBACK);
+ mApexManager = ApexManager.getInstance();
mPrivilegedPermissionAllowlistSourcePackageNames.add(PLATFORM_PACKAGE_NAME);
// PackageManager.hasSystemFeature() is not used here because PackageManagerService
@@ -3309,9 +3312,8 @@
return true;
}
final String permissionName = permission.getName();
- final ApexManager apexManager = ApexManager.getInstance();
final String containingApexPackageName =
- apexManager.getActiveApexPackageNameContainingPackage(packageName);
+ mApexManager.getActiveApexPackageNameContainingPackage(packageName);
if (isInSystemConfigPrivAppPermissions(pkg, permissionName,
containingApexPackageName)) {
return true;
@@ -3365,8 +3367,7 @@
} else if (pkg.isSystemExt()) {
permissions = systemConfig.getSystemExtPrivAppPermissions(pkg.getPackageName());
} else if (containingApexPackageName != null) {
- final ApexManager apexManager = ApexManager.getInstance();
- final String apexName = apexManager.getApexModuleNameForPackageName(
+ final String apexName = mApexManager.getApexModuleNameForPackageName(
containingApexPackageName);
final Set<String> privAppPermissions = systemConfig.getPrivAppPermissions(
pkg.getPackageName());
@@ -3582,6 +3583,11 @@
// Special permission for the recents app.
allowed = true;
}
+ if (!allowed && bp.isModule() && mApexManager.getActiveApexPackageNameContainingPackage(
+ pkg.getPackageName()) != null) {
+ // Special permission granted for APKs inside APEX modules.
+ allowed = true;
+ }
return allowed;
}
diff --git a/services/core/java/com/android/server/pm/pkg/PackageState.java b/services/core/java/com/android/server/pm/pkg/PackageState.java
index e8d0640..67b7647 100644
--- a/services/core/java/com/android/server/pm/pkg/PackageState.java
+++ b/services/core/java/com/android/server/pm/pkg/PackageState.java
@@ -112,6 +112,19 @@
int getAppId();
/**
+ * Retrieves effective hidden API policy for this app. The state can be dependent on
+ * {@link #getAndroidPackage()} availability and whether the app is a system app.
+ *
+ * Note that during process start, this policy may be mutated by device specific process
+ * configuration, so this value isn't truly final.
+ *
+ * @return The (mostly) final {@link ApplicationInfo.HiddenApiEnforcementPolicy} that should be
+ * applied to this package.
+ */
+ @ApplicationInfo.HiddenApiEnforcementPolicy
+ int getHiddenApiEnforcementPolicy();
+
+ /**
* @see PackageInfo#packageName
* @see AndroidPackage#getPackageName()
*/
@@ -139,6 +152,18 @@
String getSeInfo();
/**
+ * @return State for a user or {@link PackageUserState#DEFAULT} if the state doesn't exist.
+ */
+ @NonNull
+ PackageUserState getStateForUser(@NonNull UserHandle user);
+
+ /**
+ * @see R.styleable#AndroidManifestUsesLibrary
+ */
+ @NonNull
+ List<SharedLibrary> getUsesLibraries();
+
+ /**
* @see AndroidPackage#isPrivileged()
*/
boolean isPrivileged();
@@ -154,18 +179,6 @@
*/
boolean isUpdatedSystemApp();
- /**
- * @return State for a user or {@link PackageUserState#DEFAULT} if the state doesn't exist.
- */
- @NonNull
- PackageUserState getStateForUser(@NonNull UserHandle user);
-
- /**
- * @see R.styleable#AndroidManifestUsesLibrary
- */
- @NonNull
- List<SharedLibrary> getUsesLibraries();
-
// Methods below this comment are not yet exposed as API
/**
diff --git a/services/core/java/com/android/server/pm/pkg/PackageStateImpl.java b/services/core/java/com/android/server/pm/pkg/PackageStateImpl.java
index e552a34..43d019a 100644
--- a/services/core/java/com/android/server/pm/pkg/PackageStateImpl.java
+++ b/services/core/java/com/android/server/pm/pkg/PackageStateImpl.java
@@ -19,6 +19,7 @@
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManagerInternal;
import android.content.pm.SigningInfo;
@@ -118,6 +119,8 @@
private final int mCategoryOverride;
@Nullable
private final String mCpuAbiOverride;
+ @ApplicationInfo.HiddenApiEnforcementPolicy
+ private final int mHiddenApiEnforcementPolicy;
private final long mLastModifiedTime;
private final long mLastUpdateTime;
private final long mLongVersionCode;
@@ -170,6 +173,7 @@
mAppId = pkgState.getAppId();
mCategoryOverride = pkgState.getCategoryOverride();
mCpuAbiOverride = pkgState.getCpuAbiOverride();
+ mHiddenApiEnforcementPolicy = pkgState.getHiddenApiEnforcementPolicy();
mLastModifiedTime = pkgState.getLastModifiedTime();
mLastUpdateTime = pkgState.getLastUpdateTime();
mLongVersionCode = pkgState.getVersionCode();
@@ -545,7 +549,7 @@
}
@DataClass.Generated(
- time = 1665778832625L,
+ time = 1666719622708L,
codegenVersion = "1.0.23",
sourceFile = "frameworks/base/services/core/java/com/android/server/pm/pkg/PackageStateImpl.java",
inputSignatures = "private int mBooleans\nprivate final long mCeDataInode\nprivate final @android.annotation.NonNull android.util.ArraySet<java.lang.String> mDisabledComponents\nprivate final @android.content.pm.PackageManager.DistractionRestriction int mDistractionFlags\nprivate final @android.annotation.NonNull android.util.ArraySet<java.lang.String> mEnabledComponents\nprivate final int mEnabledState\nprivate final @android.annotation.Nullable java.lang.String mHarmfulAppWarning\nprivate final @android.content.pm.PackageManager.InstallReason int mInstallReason\nprivate final @android.annotation.Nullable java.lang.String mLastDisableAppCaller\nprivate final @android.annotation.NonNull android.content.pm.overlay.OverlayPaths mOverlayPaths\nprivate final @android.annotation.NonNull java.util.Map<java.lang.String,android.content.pm.overlay.OverlayPaths> mSharedLibraryOverlayPaths\nprivate final @android.content.pm.PackageManager.UninstallReason int mUninstallReason\nprivate final @android.annotation.Nullable java.lang.String mSplashScreenTheme\nprivate final long mFirstInstallTime\npublic static com.android.server.pm.pkg.PackageUserState copy(com.android.server.pm.pkg.PackageUserState)\nprivate void setBoolean(int,boolean)\nprivate boolean getBoolean(int)\npublic @java.lang.Override boolean isHidden()\npublic @java.lang.Override boolean isInstalled()\npublic @java.lang.Override boolean isInstantApp()\npublic @java.lang.Override boolean isNotLaunched()\npublic @java.lang.Override boolean isStopped()\npublic @java.lang.Override boolean isSuspended()\npublic @java.lang.Override boolean isVirtualPreload()\npublic @java.lang.Override boolean isComponentEnabled(java.lang.String)\npublic @java.lang.Override boolean isComponentDisabled(java.lang.String)\npublic @java.lang.Override android.content.pm.overlay.OverlayPaths getAllOverlayPaths()\nclass UserStateImpl extends java.lang.Object implements [com.android.server.pm.pkg.PackageUserState]\nprivate static final int HIDDEN\nprivate static final int INSTALLED\nprivate static final int INSTANT_APP\nprivate static final int NOT_LAUNCHED\nprivate static final int STOPPED\nprivate static final int SUSPENDED\nprivate static final int VIRTUAL_PRELOAD\nclass Booleans extends java.lang.Object implements []\n@com.android.internal.util.DataClass(genConstructor=false)")
@@ -609,6 +613,11 @@
}
@DataClass.Generated.Member
+ public @ApplicationInfo.HiddenApiEnforcementPolicy int getHiddenApiEnforcementPolicy() {
+ return mHiddenApiEnforcementPolicy;
+ }
+
+ @DataClass.Generated.Member
public long getLastModifiedTime() {
return mLastModifiedTime;
}
@@ -705,10 +714,10 @@
}
@DataClass.Generated(
- time = 1665778832668L,
+ time = 1666719622749L,
codegenVersion = "1.0.23",
sourceFile = "frameworks/base/services/core/java/com/android/server/pm/pkg/PackageStateImpl.java",
- inputSignatures = "private int mBooleans\nprivate final @android.annotation.Nullable com.android.server.pm.pkg.AndroidPackage mAndroidPackage\nprivate final @android.annotation.NonNull java.lang.String mPackageName\nprivate final @android.annotation.Nullable java.lang.String mVolumeUuid\nprivate final int mAppId\nprivate final int mCategoryOverride\nprivate final @android.annotation.Nullable java.lang.String mCpuAbiOverride\nprivate final long mLastModifiedTime\nprivate final long mLastUpdateTime\nprivate final long mLongVersionCode\nprivate final @android.annotation.NonNull java.util.Map<java.lang.String,java.util.Set<java.lang.String>> mMimeGroups\nprivate final @android.annotation.NonNull java.io.File mPath\nprivate final @android.annotation.Nullable java.lang.String mPrimaryCpuAbi\nprivate final @android.annotation.Nullable java.lang.String mSecondaryCpuAbi\nprivate final @android.annotation.Nullable java.lang.String mSeInfo\nprivate final boolean mHasSharedUser\nprivate final int mSharedUserAppId\nprivate final @android.annotation.NonNull java.lang.String[] mUsesSdkLibraries\nprivate final @android.annotation.NonNull long[] mUsesSdkLibrariesVersionsMajor\nprivate final @android.annotation.NonNull java.lang.String[] mUsesStaticLibraries\nprivate final @android.annotation.NonNull long[] mUsesStaticLibrariesVersions\nprivate final @android.annotation.NonNull java.util.List<com.android.server.pm.pkg.SharedLibrary> mUsesLibraries\nprivate final @android.annotation.NonNull java.util.List<java.lang.String> mUsesLibraryFiles\nprivate final @android.annotation.NonNull long[] mLastPackageUsageTime\nprivate final @android.annotation.NonNull android.content.pm.SigningInfo mSigningInfo\nprivate final @android.annotation.NonNull android.util.SparseArray<com.android.server.pm.pkg.PackageUserState> mUserStates\npublic static com.android.server.pm.pkg.PackageState copy(com.android.server.pm.pkg.PackageStateInternal)\nprivate void setBoolean(int,boolean)\nprivate boolean getBoolean(int)\npublic @android.annotation.NonNull @java.lang.Override com.android.server.pm.pkg.PackageUserState getStateForUser(android.os.UserHandle)\npublic @java.lang.Override boolean isExternalStorage()\npublic @java.lang.Override boolean isForceQueryableOverride()\npublic @java.lang.Override boolean isHiddenUntilInstalled()\npublic @java.lang.Override boolean isInstallPermissionsFixed()\npublic @java.lang.Override boolean isOdm()\npublic @java.lang.Override boolean isOem()\npublic @java.lang.Override boolean isPrivileged()\npublic @java.lang.Override boolean isProduct()\npublic @java.lang.Override boolean isRequiredForSystemUser()\npublic @java.lang.Override boolean isSystem()\npublic @java.lang.Override boolean isSystemExt()\npublic @java.lang.Override boolean isUpdateAvailable()\npublic @java.lang.Override boolean isUpdatedSystemApp()\npublic @java.lang.Override boolean isApkInUpdatedApex()\npublic @java.lang.Override boolean isVendor()\npublic @java.lang.Override long getVersionCode()\npublic @java.lang.Override boolean hasSharedUser()\npublic @java.lang.Override int getSharedUserAppId()\nclass PackageStateImpl extends java.lang.Object implements [com.android.server.pm.pkg.PackageState]\nprivate static final int SYSTEM\nprivate static final int EXTERNAL_STORAGE\nprivate static final int PRIVILEGED\nprivate static final int OEM\nprivate static final int VENDOR\nprivate static final int PRODUCT\nprivate static final int SYSTEM_EXT\nprivate static final int REQUIRED_FOR_SYSTEM_USER\nprivate static final int ODM\nprivate static final int FORCE_QUERYABLE_OVERRIDE\nprivate static final int HIDDEN_UNTIL_INSTALLED\nprivate static final int INSTALL_PERMISSIONS_FIXED\nprivate static final int UPDATE_AVAILABLE\nprivate static final int UPDATED_SYSTEM_APP\nprivate static final int APK_IN_UPDATED_APEX\nclass Booleans extends java.lang.Object implements []\n@com.android.internal.util.DataClass(genConstructor=false)")
+ inputSignatures = "private int mBooleans\nprivate final @android.annotation.Nullable com.android.server.pm.pkg.AndroidPackage mAndroidPackage\nprivate final @android.annotation.NonNull java.lang.String mPackageName\nprivate final @android.annotation.Nullable java.lang.String mVolumeUuid\nprivate final int mAppId\nprivate final int mCategoryOverride\nprivate final @android.annotation.Nullable java.lang.String mCpuAbiOverride\nprivate final @android.content.pm.ApplicationInfo.HiddenApiEnforcementPolicy int mHiddenApiEnforcementPolicy\nprivate final long mLastModifiedTime\nprivate final long mLastUpdateTime\nprivate final long mLongVersionCode\nprivate final @android.annotation.NonNull java.util.Map<java.lang.String,java.util.Set<java.lang.String>> mMimeGroups\nprivate final @android.annotation.NonNull java.io.File mPath\nprivate final @android.annotation.Nullable java.lang.String mPrimaryCpuAbi\nprivate final @android.annotation.Nullable java.lang.String mSecondaryCpuAbi\nprivate final @android.annotation.Nullable java.lang.String mSeInfo\nprivate final boolean mHasSharedUser\nprivate final int mSharedUserAppId\nprivate final @android.annotation.NonNull java.lang.String[] mUsesSdkLibraries\nprivate final @android.annotation.NonNull long[] mUsesSdkLibrariesVersionsMajor\nprivate final @android.annotation.NonNull java.lang.String[] mUsesStaticLibraries\nprivate final @android.annotation.NonNull long[] mUsesStaticLibrariesVersions\nprivate final @android.annotation.NonNull java.util.List<com.android.server.pm.pkg.SharedLibrary> mUsesLibraries\nprivate final @android.annotation.NonNull java.util.List<java.lang.String> mUsesLibraryFiles\nprivate final @android.annotation.NonNull long[] mLastPackageUsageTime\nprivate final @android.annotation.NonNull android.content.pm.SigningInfo mSigningInfo\nprivate final @android.annotation.NonNull android.util.SparseArray<com.android.server.pm.pkg.PackageUserState> mUserStates\npublic static com.android.server.pm.pkg.PackageState copy(com.android.server.pm.pkg.PackageStateInternal)\nprivate void setBoolean(int,boolean)\nprivate boolean getBoolean(int)\npublic @android.annotation.NonNull @java.lang.Override com.android.server.pm.pkg.PackageUserState getStateForUser(android.os.UserHandle)\npublic @java.lang.Override boolean isExternalStorage()\npublic @java.lang.Override boolean isForceQueryableOverride()\npublic @java.lang.Override boolean isHiddenUntilInstalled()\npublic @java.lang.Override boolean isInstallPermissionsFixed()\npublic @java.lang.Override boolean isOdm()\npublic @java.lang.Override boolean isOem()\npublic @java.lang.Override boolean isPrivileged()\npublic @java.lang.Override boolean isProduct()\npublic @java.lang.Override boolean isRequiredForSystemUser()\npublic @java.lang.Override boolean isSystem()\npublic @java.lang.Override boolean isSystemExt()\npublic @java.lang.Override boolean isUpdateAvailable()\npublic @java.lang.Override boolean isUpdatedSystemApp()\npublic @java.lang.Override boolean isApkInUpdatedApex()\npublic @java.lang.Override boolean isVendor()\npublic @java.lang.Override long getVersionCode()\npublic @java.lang.Override boolean hasSharedUser()\npublic @java.lang.Override int getSharedUserAppId()\nclass PackageStateImpl extends java.lang.Object implements [com.android.server.pm.pkg.PackageState]\nprivate static final int SYSTEM\nprivate static final int EXTERNAL_STORAGE\nprivate static final int PRIVILEGED\nprivate static final int OEM\nprivate static final int VENDOR\nprivate static final int PRODUCT\nprivate static final int SYSTEM_EXT\nprivate static final int REQUIRED_FOR_SYSTEM_USER\nprivate static final int ODM\nprivate static final int FORCE_QUERYABLE_OVERRIDE\nprivate static final int HIDDEN_UNTIL_INSTALLED\nprivate static final int INSTALL_PERMISSIONS_FIXED\nprivate static final int UPDATE_AVAILABLE\nprivate static final int UPDATED_SYSTEM_APP\nprivate static final int APK_IN_UPDATED_APEX\nclass Booleans extends java.lang.Object implements []\n@com.android.internal.util.DataClass(genConstructor=false)")
@Deprecated
private void __metadata() {}
diff --git a/services/core/java/com/android/server/pm/snapshot/PackageDataSnapshot.java b/services/core/java/com/android/server/pm/snapshot/PackageDataSnapshot.java
index b2080b2..90a0c7c 100644
--- a/services/core/java/com/android/server/pm/snapshot/PackageDataSnapshot.java
+++ b/services/core/java/com/android/server/pm/snapshot/PackageDataSnapshot.java
@@ -16,7 +16,6 @@
package com.android.server.pm.snapshot;
-import android.annotation.SystemApi;
import android.content.pm.PackageManagerInternal;
import com.android.server.pm.Computer;
@@ -32,6 +31,5 @@
*
* @hide
*/
-@SystemApi(client = SystemApi.Client.SYSTEM_SERVER)
public interface PackageDataSnapshot {
}
diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java
index 3076b90..a4a0853 100644
--- a/services/core/java/com/android/server/policy/PhoneWindowManager.java
+++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java
@@ -1714,6 +1714,11 @@
}
}
+ private void showSystemSettings() {
+ startActivityAsUser(new Intent(android.provider.Settings.ACTION_SETTINGS),
+ UserHandle.CURRENT_OR_SELF);
+ }
+
private void showPictureInPictureMenu(KeyEvent event) {
if (DEBUG_INPUT) Log.d(TAG, "showPictureInPictureMenu event=" + event);
mHandler.removeMessages(MSG_SHOW_PICTURE_IN_PICTURE_MENU);
@@ -2900,6 +2905,20 @@
}
}
return key_consumed;
+ case KeyEvent.KEYCODE_A:
+ if (down && event.isMetaPressed()) {
+ launchAssistAction(Intent.EXTRA_ASSIST_INPUT_HINT_KEYBOARD,
+ event.getDeviceId(),
+ event.getEventTime(), AssistUtils.INVOCATION_TYPE_UNKNOWN);
+ return key_consumed;
+ }
+ break;
+ case KeyEvent.KEYCODE_I:
+ if (down && event.isMetaPressed()) {
+ showSystemSettings();
+ return key_consumed;
+ }
+ break;
case KeyEvent.KEYCODE_N:
if (down && event.isMetaPressed()) {
IStatusBarService service = getStatusBarService();
@@ -2929,6 +2948,18 @@
return key_consumed;
}
break;
+ case KeyEvent.KEYCODE_DPAD_LEFT:
+ if (down && event.isMetaPressed() && event.isCtrlPressed() && repeatCount == 0) {
+ enterStageSplitFromRunningApp(true /* leftOrTop */);
+ return key_consumed;
+ }
+ break;
+ case KeyEvent.KEYCODE_DPAD_RIGHT:
+ if (down && event.isMetaPressed() && event.isCtrlPressed() && repeatCount == 0) {
+ enterStageSplitFromRunningApp(false /* leftOrTop */);
+ return key_consumed;
+ }
+ break;
case KeyEvent.KEYCODE_SLASH:
if (down && repeatCount == 0 && event.isMetaPressed() && !keyguardOn) {
toggleKeyboardShortcutsMenu(event.getDeviceId());
@@ -3028,12 +3059,13 @@
}
break;
case KeyEvent.KEYCODE_TAB:
- if (event.isMetaPressed()) {
- // Pass through keyboard navigation keys.
- return key_not_consumed;
- }
- // Display task switcher for ALT-TAB.
- if (down && repeatCount == 0) {
+ if (down && event.isMetaPressed()) {
+ if (!keyguardOn && isUserSetupComplete()) {
+ showRecentApps(false);
+ return key_consumed;
+ }
+ } else if (down && repeatCount == 0) {
+ // Display task switcher for ALT-TAB.
if (mRecentAppsHeldModifiers == 0 && !keyguardOn && isUserSetupComplete()) {
final int shiftlessModifiers =
event.getModifiers() & ~KeyEvent.META_SHIFT_MASK;
@@ -3088,9 +3120,7 @@
mPendingCapsLockToggle = false;
} else if (mPendingMetaAction) {
if (!canceled) {
- launchAssistAction(Intent.EXTRA_ASSIST_INPUT_HINT_KEYBOARD,
- event.getDeviceId(),
- event.getEventTime(), AssistUtils.INVOCATION_TYPE_UNKNOWN);
+ // TODO: launch all apps here.
}
mPendingMetaAction = false;
}
@@ -3583,6 +3613,13 @@
}
}
+ private void enterStageSplitFromRunningApp(boolean leftOrTop) {
+ StatusBarManagerInternal statusbar = getStatusBarManagerInternal();
+ if (statusbar != null) {
+ statusbar.enterStageSplitFromRunningApp(leftOrTop);
+ }
+ }
+
void launchHomeFromHotKey(int displayId) {
launchHomeFromHotKey(displayId, true /* awakenFromDreams */, true /*respectKeyguard*/);
}
diff --git a/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java b/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java
index 0c5e451..af4fa85 100644
--- a/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java
+++ b/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java
@@ -12229,6 +12229,7 @@
@GuardedBy("this")
private void incrementPerRatDataLocked(ModemActivityInfo deltaInfo, long elapsedRealtimeMs) {
final int infoSize = deltaInfo.getSpecificInfoLength();
+
if (infoSize == 1 && deltaInfo.getSpecificInfoRat(0)
== AccessNetworkConstants.AccessNetworkType.UNKNOWN
&& deltaInfo.getSpecificInfoFrequencyRange(0)
diff --git a/services/core/java/com/android/server/power/stats/MobileRadioPowerCalculator.java b/services/core/java/com/android/server/power/stats/MobileRadioPowerCalculator.java
index 806ed64..2c7aea9 100644
--- a/services/core/java/com/android/server/power/stats/MobileRadioPowerCalculator.java
+++ b/services/core/java/com/android/server/power/stats/MobileRadioPowerCalculator.java
@@ -15,45 +15,79 @@
*/
package com.android.server.power.stats;
+import android.annotation.Nullable;
import android.os.BatteryConsumer;
import android.os.BatteryStats;
import android.os.BatteryUsageStats;
import android.os.BatteryUsageStatsQuery;
import android.os.UidBatteryConsumer;
import android.telephony.CellSignalStrength;
+import android.telephony.ServiceState;
import android.util.Log;
+import android.util.LongArrayQueue;
import android.util.SparseArray;
import com.android.internal.os.PowerProfile;
+import com.android.internal.power.ModemPowerProfile;
+
+import java.util.ArrayList;
public class MobileRadioPowerCalculator extends PowerCalculator {
private static final String TAG = "MobRadioPowerCalculator";
private static final boolean DEBUG = PowerCalculator.DEBUG;
+ private static final double MILLIS_IN_HOUR = 1000.0 * 60 * 60;
+
private static final int NUM_SIGNAL_STRENGTH_LEVELS =
CellSignalStrength.getNumSignalStrengthLevels();
private static final BatteryConsumer.Key[] UNINITIALIZED_KEYS = new BatteryConsumer.Key[0];
+ private static final int IGNORE = -1;
- private final UsageBasedPowerEstimator mActivePowerEstimator;
+ private final UsageBasedPowerEstimator mActivePowerEstimator; // deprecated
private final UsageBasedPowerEstimator[] mIdlePowerEstimators =
- new UsageBasedPowerEstimator[NUM_SIGNAL_STRENGTH_LEVELS];
- private final UsageBasedPowerEstimator mScanPowerEstimator;
+ new UsageBasedPowerEstimator[NUM_SIGNAL_STRENGTH_LEVELS]; // deprecated
+ private final UsageBasedPowerEstimator mScanPowerEstimator; // deprecated
+
+ @Nullable
+ private final UsageBasedPowerEstimator mSleepPowerEstimator;
+ @Nullable
+ private final UsageBasedPowerEstimator mIdlePowerEstimator;
+
+ private final PowerProfile mPowerProfile;
private static class PowerAndDuration {
- public long durationMs;
+ public long remainingDurationMs;
public double remainingPowerMah;
public long totalAppDurationMs;
public double totalAppPowerMah;
- public long signalDurationMs;
- public long noCoverageDurationMs;
}
public MobileRadioPowerCalculator(PowerProfile profile) {
- // Power consumption when radio is active
+ mPowerProfile = profile;
+
+ final double sleepDrainRateMa = mPowerProfile.getAverageBatteryDrainOrDefaultMa(
+ PowerProfile.SUBSYSTEM_MODEM | ModemPowerProfile.MODEM_DRAIN_TYPE_SLEEP,
+ Double.NaN);
+ if (Double.isNaN(sleepDrainRateMa)) {
+ mSleepPowerEstimator = null;
+ } else {
+ mSleepPowerEstimator = new UsageBasedPowerEstimator(sleepDrainRateMa);
+ }
+
+ final double idleDrainRateMa = mPowerProfile.getAverageBatteryDrainOrDefaultMa(
+ PowerProfile.SUBSYSTEM_MODEM | ModemPowerProfile.MODEM_DRAIN_TYPE_IDLE,
+ Double.NaN);
+ if (Double.isNaN(idleDrainRateMa)) {
+ mIdlePowerEstimator = null;
+ } else {
+ mIdlePowerEstimator = new UsageBasedPowerEstimator(idleDrainRateMa);
+ }
+
+ // Instantiate legacy power estimators
double powerRadioActiveMa =
- profile.getAveragePowerOrDefault(PowerProfile.POWER_RADIO_ACTIVE, -1);
- if (powerRadioActiveMa == -1) {
+ profile.getAveragePowerOrDefault(PowerProfile.POWER_RADIO_ACTIVE, Double.NaN);
+ if (Double.isNaN(powerRadioActiveMa)) {
double sum = 0;
sum += profile.getAveragePower(PowerProfile.POWER_MODEM_CONTROLLER_RX);
for (int i = 0; i < NUM_SIGNAL_STRENGTH_LEVELS; i++) {
@@ -61,11 +95,10 @@
}
powerRadioActiveMa = sum / (NUM_SIGNAL_STRENGTH_LEVELS + 1);
}
-
mActivePowerEstimator = new UsageBasedPowerEstimator(powerRadioActiveMa);
- // Power consumption when radio is on, but idle
- if (profile.getAveragePowerOrDefault(PowerProfile.POWER_RADIO_ON, -1) != -1) {
+ if (!Double.isNaN(
+ profile.getAveragePowerOrDefault(PowerProfile.POWER_RADIO_ON, Double.NaN))) {
for (int i = 0; i < NUM_SIGNAL_STRENGTH_LEVELS; i++) {
mIdlePowerEstimators[i] = new UsageBasedPowerEstimator(
profile.getAveragePower(PowerProfile.POWER_RADIO_ON, i));
@@ -95,6 +128,23 @@
PowerAndDuration total = new PowerAndDuration();
+ final long totalConsumptionUC = batteryStats.getMobileRadioMeasuredBatteryConsumptionUC();
+ final int powerModel = getPowerModel(totalConsumptionUC, query);
+
+ final double totalActivePowerMah;
+ final ArrayList<UidBatteryConsumer.Builder> apps;
+ final LongArrayQueue appDurationsMs;
+ if (powerModel == BatteryConsumer.POWER_MODEL_MEASURED_ENERGY) {
+ // Measured energy is available, don't bother calculating power.
+ totalActivePowerMah = Double.NaN;
+ apps = null;
+ appDurationsMs = null;
+ } else {
+ totalActivePowerMah = calculateActiveModemPowerMah(batteryStats, rawRealtimeUs);
+ apps = new ArrayList();
+ appDurationsMs = new LongArrayQueue();
+ }
+
final SparseArray<UidBatteryConsumer.Builder> uidBatteryConsumerBuilders =
builder.getUidBatteryConsumerBuilders();
BatteryConsumer.Key[] keys = UNINITIALIZED_KEYS;
@@ -110,132 +160,352 @@
}
}
- calculateApp(app, uid, total, query, keys);
+ // Sum and populate each app's active radio duration.
+ final long radioActiveDurationMs = calculateDuration(uid,
+ BatteryStats.STATS_SINCE_CHARGED);
+ if (!app.isVirtualUid()) {
+ total.totalAppDurationMs += radioActiveDurationMs;
+ }
+ app.setUsageDurationMillis(BatteryConsumer.POWER_COMPONENT_MOBILE_RADIO,
+ radioActiveDurationMs);
+
+ if (powerModel == BatteryConsumer.POWER_MODEL_MEASURED_ENERGY) {
+ // Measured energy is available, populate the consumed power now.
+ final long appConsumptionUC = uid.getMobileRadioMeasuredBatteryConsumptionUC();
+ if (appConsumptionUC != BatteryStats.POWER_DATA_UNAVAILABLE) {
+ final double appConsumptionMah = uCtoMah(appConsumptionUC);
+ if (!app.isVirtualUid()) {
+ total.totalAppPowerMah += appConsumptionMah;
+ }
+ app.setConsumedPower(BatteryConsumer.POWER_COMPONENT_MOBILE_RADIO,
+ appConsumptionMah, powerModel);
+
+ if (query.isProcessStateDataNeeded() && keys != null) {
+ for (BatteryConsumer.Key key : keys) {
+ final int processState = key.processState;
+ if (processState == BatteryConsumer.PROCESS_STATE_UNSPECIFIED) {
+ // Already populated with the total across all process states
+ continue;
+ }
+ final long consumptionInStateUc =
+ uid.getMobileRadioMeasuredBatteryConsumptionUC(processState);
+ final double powerInStateMah = uCtoMah(consumptionInStateUc);
+ app.setConsumedPower(key, powerInStateMah, powerModel);
+ }
+ }
+ }
+ } else {
+ // Cache the app and its active duration for later calculations.
+ apps.add(app);
+ appDurationsMs.addLast(radioActiveDurationMs);
+ }
}
- final long totalConsumptionUC = batteryStats.getMobileRadioMeasuredBatteryConsumptionUC();
- final int powerModel = getPowerModel(totalConsumptionUC, query);
- calculateRemaining(total, powerModel, batteryStats, rawRealtimeUs, totalConsumptionUC);
+ long totalActiveDurationMs = batteryStats.getMobileRadioActiveTime(rawRealtimeUs,
+ BatteryStats.STATS_SINCE_CHARGED) / 1000;
+ if (totalActiveDurationMs < total.totalAppDurationMs) {
+ totalActiveDurationMs = total.totalAppDurationMs;
+ }
+
+ if (powerModel != BatteryConsumer.POWER_MODEL_MEASURED_ENERGY) {
+ // Need to smear the calculated total active power across the apps based on app
+ // active durations.
+ final int appSize = apps.size();
+ for (int i = 0; i < appSize; i++) {
+ final UidBatteryConsumer.Builder app = apps.get(i);
+ final long activeDurationMs = appDurationsMs.get(i);
+
+ // Proportionally attribute radio power consumption based on active duration.
+ final double appConsumptionMah;
+ if (totalActiveDurationMs == 0.0) {
+ appConsumptionMah = 0.0;
+ } else {
+ appConsumptionMah =
+ (totalActivePowerMah * activeDurationMs) / totalActiveDurationMs;
+ }
+
+ if (!app.isVirtualUid()) {
+ total.totalAppPowerMah += appConsumptionMah;
+ }
+ app.setConsumedPower(BatteryConsumer.POWER_COMPONENT_MOBILE_RADIO,
+ appConsumptionMah, powerModel);
+
+ if (query.isProcessStateDataNeeded() && keys != null) {
+ final BatteryStats.Uid uid = app.getBatteryStatsUid();
+ for (BatteryConsumer.Key key : keys) {
+ final int processState = key.processState;
+ if (processState == BatteryConsumer.PROCESS_STATE_UNSPECIFIED) {
+ // Already populated with the total across all process states
+ continue;
+ }
+
+ final long durationInStateMs =
+ uid.getMobileRadioActiveTimeInProcessState(processState) / 1000;
+ // Proportionally attribute per process state radio power consumption
+ // based on time state duration.
+ final double powerInStateMah;
+ if (activeDurationMs == 0.0) {
+ powerInStateMah = 0.0;
+ } else {
+ powerInStateMah =
+ (appConsumptionMah * durationInStateMs) / activeDurationMs;
+ }
+ app.setConsumedPower(key, powerInStateMah, powerModel);
+ }
+ }
+ }
+ }
+
+ total.remainingDurationMs = totalActiveDurationMs - total.totalAppDurationMs;
+
+ // Calculate remaining power consumption.
+ if (powerModel == BatteryConsumer.POWER_MODEL_MEASURED_ENERGY) {
+ total.remainingPowerMah = uCtoMah(totalConsumptionUC) - total.totalAppPowerMah;
+ if (total.remainingPowerMah < 0) total.remainingPowerMah = 0;
+ } else {
+ // Smear unattributed active time and add it to the remaining power consumption.
+ total.remainingPowerMah +=
+ (totalActivePowerMah * total.remainingDurationMs) / totalActiveDurationMs;
+
+ // Calculate the inactive modem power consumption.
+ final BatteryStats.ControllerActivityCounter modemActivity =
+ batteryStats.getModemControllerActivity();
+ if (modemActivity != null && (mSleepPowerEstimator != null
+ || mIdlePowerEstimator != null)) {
+ final long sleepDurationMs = modemActivity.getSleepTimeCounter().getCountLocked(
+ BatteryStats.STATS_SINCE_CHARGED);
+ total.remainingPowerMah += mSleepPowerEstimator.calculatePower(sleepDurationMs);
+ final long idleDurationMs = modemActivity.getIdleTimeCounter().getCountLocked(
+ BatteryStats.STATS_SINCE_CHARGED);
+ total.remainingPowerMah += mIdlePowerEstimator.calculatePower(idleDurationMs);
+ } else {
+ // Modem activity counters unavailable. Use legacy calculations for inactive usage.
+ final long scanningTimeMs = batteryStats.getPhoneSignalScanningTime(rawRealtimeUs,
+ BatteryStats.STATS_SINCE_CHARGED) / 1000;
+ total.remainingPowerMah += calcScanTimePowerMah(scanningTimeMs);
+ for (int i = 0; i < NUM_SIGNAL_STRENGTH_LEVELS; i++) {
+ long strengthTimeMs = batteryStats.getPhoneSignalStrengthTime(i, rawRealtimeUs,
+ BatteryStats.STATS_SINCE_CHARGED) / 1000;
+ total.remainingPowerMah += calcIdlePowerAtSignalStrengthMah(strengthTimeMs, i);
+ }
+ }
+
+ }
if (total.remainingPowerMah != 0 || total.totalAppPowerMah != 0) {
builder.getAggregateBatteryConsumerBuilder(
- BatteryUsageStats.AGGREGATE_BATTERY_CONSUMER_SCOPE_DEVICE)
+ BatteryUsageStats.AGGREGATE_BATTERY_CONSUMER_SCOPE_DEVICE)
.setUsageDurationMillis(BatteryConsumer.POWER_COMPONENT_MOBILE_RADIO,
- total.durationMs)
+ total.remainingDurationMs + total.totalAppDurationMs)
.setConsumedPower(BatteryConsumer.POWER_COMPONENT_MOBILE_RADIO,
total.remainingPowerMah + total.totalAppPowerMah, powerModel);
builder.getAggregateBatteryConsumerBuilder(
- BatteryUsageStats.AGGREGATE_BATTERY_CONSUMER_SCOPE_ALL_APPS)
+ BatteryUsageStats.AGGREGATE_BATTERY_CONSUMER_SCOPE_ALL_APPS)
.setUsageDurationMillis(BatteryConsumer.POWER_COMPONENT_MOBILE_RADIO,
- total.durationMs)
+ total.totalAppDurationMs)
.setConsumedPower(BatteryConsumer.POWER_COMPONENT_MOBILE_RADIO,
total.totalAppPowerMah, powerModel);
}
}
- private void calculateApp(UidBatteryConsumer.Builder app, BatteryStats.Uid u,
- PowerAndDuration total,
- BatteryUsageStatsQuery query, BatteryConsumer.Key[] keys) {
- final long radioActiveDurationMs = calculateDuration(u, BatteryStats.STATS_SINCE_CHARGED);
- final long consumptionUC = u.getMobileRadioMeasuredBatteryConsumptionUC();
- final int powerModel = getPowerModel(consumptionUC, query);
- final double powerMah = calculatePower(u, powerModel, radioActiveDurationMs, consumptionUC);
-
- if (!app.isVirtualUid()) {
- total.totalAppDurationMs += radioActiveDurationMs;
- total.totalAppPowerMah += powerMah;
- }
-
- app.setUsageDurationMillis(BatteryConsumer.POWER_COMPONENT_MOBILE_RADIO,
- radioActiveDurationMs)
- .setConsumedPower(BatteryConsumer.POWER_COMPONENT_MOBILE_RADIO, powerMah,
- powerModel);
-
- if (query.isProcessStateDataNeeded() && keys != null) {
- for (BatteryConsumer.Key key: keys) {
- final int processState = key.processState;
- if (processState == BatteryConsumer.PROCESS_STATE_UNSPECIFIED) {
- // Already populated with the total across all process states
- continue;
- }
-
- final long durationInStateMs =
- u.getMobileRadioActiveTimeInProcessState(processState) / 1000;
- final long consumptionInStateUc =
- u.getMobileRadioMeasuredBatteryConsumptionUC(processState);
- final double powerInStateMah = calculatePower(u, powerModel, durationInStateMs,
- consumptionInStateUc);
- app.setConsumedPower(key, powerInStateMah, powerModel);
- }
- }
- }
-
private long calculateDuration(BatteryStats.Uid u, int statsType) {
return u.getMobileRadioActiveTime(statsType) / 1000;
}
- private double calculatePower(BatteryStats.Uid u, @BatteryConsumer.PowerModel int powerModel,
- long radioActiveDurationMs, long measuredChargeUC) {
- if (powerModel == BatteryConsumer.POWER_MODEL_MEASURED_ENERGY) {
- return uCtoMah(measuredChargeUC);
+ private double calculateActiveModemPowerMah(BatteryStats bs, long elapsedRealtimeUs) {
+ final long elapsedRealtimeMs = elapsedRealtimeUs / 1000;
+ final int txLvlCount = CellSignalStrength.getNumSignalStrengthLevels();
+ double consumptionMah = 0.0;
+
+ if (DEBUG) {
+ Log.d(TAG, "Calculating radio power consumption at elapased real timestamp : "
+ + elapsedRealtimeMs + " ms");
}
- if (radioActiveDurationMs > 0) {
- return calcPowerFromRadioActiveDurationMah(radioActiveDurationMs);
+ boolean hasConstants = false;
+
+ for (int rat = 0; rat < BatteryStats.RADIO_ACCESS_TECHNOLOGY_COUNT; rat++) {
+ final int freqCount = rat == BatteryStats.RADIO_ACCESS_TECHNOLOGY_NR
+ ? ServiceState.FREQUENCY_RANGE_COUNT : 1;
+ for (int freq = 0; freq < freqCount; freq++) {
+ for (int txLvl = 0; txLvl < txLvlCount; txLvl++) {
+ final long txDurationMs = bs.getActiveTxRadioDurationMs(rat, freq, txLvl,
+ elapsedRealtimeMs);
+ if (txDurationMs == BatteryStats.DURATION_UNAVAILABLE) {
+ continue;
+ }
+ final double txConsumptionMah = calcTxStatePowerMah(rat, freq, txLvl,
+ txDurationMs);
+ if (Double.isNaN(txConsumptionMah)) {
+ continue;
+ }
+ hasConstants = true;
+ consumptionMah += txConsumptionMah;
+ }
+
+ final long rxDurationMs = bs.getActiveRxRadioDurationMs(rat, freq,
+ elapsedRealtimeMs);
+ if (rxDurationMs == BatteryStats.DURATION_UNAVAILABLE) {
+ continue;
+ }
+ final double rxConsumptionMah = calcRxStatePowerMah(rat, freq, rxDurationMs);
+ if (Double.isNaN(rxConsumptionMah)) {
+ continue;
+ }
+ hasConstants = true;
+ consumptionMah += rxConsumptionMah;
+ }
}
- return 0;
+
+ if (!hasConstants) {
+ final long radioActiveDurationMs = bs.getMobileRadioActiveTime(elapsedRealtimeUs,
+ BatteryStats.STATS_SINCE_CHARGED) / 1000;
+ if (DEBUG) {
+ Log.d(TAG,
+ "Failed to calculate radio power consumption. Reattempted with legacy "
+ + "method. Radio active duration : "
+ + radioActiveDurationMs + " ms");
+ }
+ if (radioActiveDurationMs > 0) {
+ consumptionMah = calcPowerFromRadioActiveDurationMah(radioActiveDurationMs);
+ } else {
+ consumptionMah = 0.0;
+ }
+ }
+
+ if (DEBUG) {
+ Log.d(TAG, "Total active radio power consumption calculated to be " + consumptionMah
+ + " mAH.");
+ }
+
+ return consumptionMah;
}
- private void calculateRemaining(PowerAndDuration total,
- @BatteryConsumer.PowerModel int powerModel, BatteryStats batteryStats,
- long rawRealtimeUs, long totalConsumptionUC) {
- long signalTimeMs = 0;
- double powerMah = 0;
+ private static long buildModemPowerProfileKey(@ModemPowerProfile.ModemDrainType int drainType,
+ @BatteryStats.RadioAccessTechnology int rat, @ServiceState.FrequencyRange int freqRange,
+ int txLevel) {
+ long key = PowerProfile.SUBSYSTEM_MODEM;
- if (powerModel == BatteryConsumer.POWER_MODEL_MEASURED_ENERGY) {
- powerMah = uCtoMah(totalConsumptionUC) - total.totalAppPowerMah;
- if (powerMah < 0) powerMah = 0;
+ // Attach Modem drain type to the key if specified.
+ if (drainType != IGNORE) {
+ key |= drainType;
}
- for (int i = 0; i < NUM_SIGNAL_STRENGTH_LEVELS; i++) {
- long strengthTimeMs = batteryStats.getPhoneSignalStrengthTime(i, rawRealtimeUs,
- BatteryStats.STATS_SINCE_CHARGED) / 1000;
- if (powerModel == BatteryConsumer.POWER_MODEL_POWER_PROFILE) {
- final double p = calcIdlePowerAtSignalStrengthMah(strengthTimeMs, i);
- if (DEBUG && p != 0) {
- Log.d(TAG, "Cell strength #" + i + ": time=" + strengthTimeMs + " power="
- + BatteryStats.formatCharge(p));
- }
- powerMah += p;
- }
- signalTimeMs += strengthTimeMs;
- if (i == 0) {
- total.noCoverageDurationMs = strengthTimeMs;
- }
+ // Attach RadioAccessTechnology to the key if specified.
+ switch (rat) {
+ case IGNORE:
+ // do nothing
+ break;
+ case BatteryStats.RADIO_ACCESS_TECHNOLOGY_OTHER:
+ key |= ModemPowerProfile.MODEM_RAT_TYPE_DEFAULT;
+ break;
+ case BatteryStats.RADIO_ACCESS_TECHNOLOGY_LTE:
+ key |= ModemPowerProfile.MODEM_RAT_TYPE_LTE;
+ break;
+ case BatteryStats.RADIO_ACCESS_TECHNOLOGY_NR:
+ key |= ModemPowerProfile.MODEM_RAT_TYPE_NR;
+ break;
+ default:
+ Log.w(TAG, "Unexpected RadioAccessTechnology : " + rat);
}
- final long scanningTimeMs = batteryStats.getPhoneSignalScanningTime(rawRealtimeUs,
- BatteryStats.STATS_SINCE_CHARGED) / 1000;
- long radioActiveTimeMs = batteryStats.getMobileRadioActiveTime(rawRealtimeUs,
- BatteryStats.STATS_SINCE_CHARGED) / 1000;
- long remainingActiveTimeMs = radioActiveTimeMs - total.totalAppDurationMs;
-
- if (powerModel == BatteryConsumer.POWER_MODEL_POWER_PROFILE) {
- final double p = calcScanTimePowerMah(scanningTimeMs);
- if (DEBUG && p != 0) {
- Log.d(TAG, "Cell radio scanning: time=" + scanningTimeMs
- + " power=" + BatteryStats.formatCharge(p));
- }
- powerMah += p;
-
- if (remainingActiveTimeMs > 0) {
- powerMah += calcPowerFromRadioActiveDurationMah(remainingActiveTimeMs);
- }
+ // Attach NR Frequency Range to the key if specified.
+ switch (freqRange) {
+ case IGNORE:
+ // do nothing
+ break;
+ case ServiceState.FREQUENCY_RANGE_UNKNOWN:
+ key |= ModemPowerProfile.MODEM_NR_FREQUENCY_RANGE_DEFAULT;
+ break;
+ case ServiceState.FREQUENCY_RANGE_LOW:
+ key |= ModemPowerProfile.MODEM_NR_FREQUENCY_RANGE_LOW;
+ break;
+ case ServiceState.FREQUENCY_RANGE_MID:
+ key |= ModemPowerProfile.MODEM_NR_FREQUENCY_RANGE_MID;
+ break;
+ case ServiceState.FREQUENCY_RANGE_HIGH:
+ key |= ModemPowerProfile.MODEM_NR_FREQUENCY_RANGE_HIGH;
+ break;
+ case ServiceState.FREQUENCY_RANGE_MMWAVE:
+ key |= ModemPowerProfile.MODEM_NR_FREQUENCY_RANGE_MMWAVE;
+ break;
+ default:
+ Log.w(TAG, "Unexpected NR frequency range : " + freqRange);
}
- total.durationMs = radioActiveTimeMs;
- total.remainingPowerMah = powerMah;
- total.signalDurationMs = signalTimeMs;
+
+ // Attach transmission level to the key if specified.
+ switch (txLevel) {
+ case IGNORE:
+ // do nothing
+ break;
+ case 0:
+ key |= ModemPowerProfile.MODEM_TX_LEVEL_0;
+ break;
+ case 1:
+ key |= ModemPowerProfile.MODEM_TX_LEVEL_1;
+ break;
+ case 2:
+ key |= ModemPowerProfile.MODEM_TX_LEVEL_2;
+ break;
+ case 3:
+ key |= ModemPowerProfile.MODEM_TX_LEVEL_3;
+ break;
+ case 4:
+ key |= ModemPowerProfile.MODEM_TX_LEVEL_4;
+ break;
+ default:
+ Log.w(TAG, "Unexpected transmission level : " + txLevel);
+ }
+ return key;
+ }
+
+ /**
+ * Calculates active receive radio power consumption (in milliamp-hours) from the given state's
+ * duration.
+ */
+ public double calcRxStatePowerMah(@BatteryStats.RadioAccessTechnology int rat,
+ @ServiceState.FrequencyRange int freqRange, long rxDurationMs) {
+ final long rxKey = buildModemPowerProfileKey(ModemPowerProfile.MODEM_DRAIN_TYPE_RX, rat,
+ freqRange, IGNORE);
+ final double drainRateMa = mPowerProfile.getAverageBatteryDrainOrDefaultMa(rxKey,
+ Double.NaN);
+ if (Double.isNaN(drainRateMa)) {
+ Log.w(TAG, "Unavailable Power Profile constant for key 0x" + Long.toHexString(rxKey));
+ return Double.NaN;
+ }
+
+ final double consumptionMah = drainRateMa * rxDurationMs / MILLIS_IN_HOUR;
+ if (DEBUG) {
+ Log.d(TAG, "Calculated RX consumption " + consumptionMah + " mAH from a drain rate of "
+ + drainRateMa + " mA and a duration of " + rxDurationMs + " ms for "
+ + ModemPowerProfile.keyToString((int) rxKey));
+ }
+ return consumptionMah;
+ }
+
+ /**
+ * Calculates active transmit radio power consumption (in milliamp-hours) from the given state's
+ * duration.
+ */
+ public double calcTxStatePowerMah(@BatteryStats.RadioAccessTechnology int rat,
+ @ServiceState.FrequencyRange int freqRange, int txLevel, long txDurationMs) {
+ final long txKey = buildModemPowerProfileKey(ModemPowerProfile.MODEM_DRAIN_TYPE_TX, rat,
+ freqRange, txLevel);
+ final double drainRateMa = mPowerProfile.getAverageBatteryDrainOrDefaultMa(txKey,
+ Double.NaN);
+ if (Double.isNaN(drainRateMa)) {
+ Log.w(TAG, "Unavailable Power Profile constant for key 0x" + Long.toHexString(txKey));
+ return Double.NaN;
+ }
+
+ final double consumptionMah = drainRateMa * txDurationMs / MILLIS_IN_HOUR;
+ if (DEBUG) {
+ Log.d(TAG, "Calculated TX consumption " + consumptionMah + " mAH from a drain rate of "
+ + drainRateMa + " mA and a duration of " + txDurationMs + " ms for "
+ + ModemPowerProfile.keyToString((int) txKey));
+ }
+ return consumptionMah;
}
/**
diff --git a/services/core/java/com/android/server/security/rkp/OWNERS b/services/core/java/com/android/server/security/rkp/OWNERS
new file mode 100644
index 0000000..348f940
--- /dev/null
+++ b/services/core/java/com/android/server/security/rkp/OWNERS
@@ -0,0 +1 @@
+file:platform/frameworks/base:master:/core/java/android/security/rkp/OWNERS
diff --git a/services/core/java/com/android/server/security/rkp/RemoteProvisioningService.java b/services/core/java/com/android/server/security/rkp/RemoteProvisioningService.java
new file mode 100644
index 0000000..65a4b38
--- /dev/null
+++ b/services/core/java/com/android/server/security/rkp/RemoteProvisioningService.java
@@ -0,0 +1,126 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.security.rkp;
+
+import android.content.Context;
+import android.os.Binder;
+import android.os.OutcomeReceiver;
+import android.os.RemoteException;
+import android.security.rkp.IGetKeyCallback;
+import android.security.rkp.IGetRegistrationCallback;
+import android.security.rkp.IRegistration;
+import android.security.rkp.IRemoteProvisioning;
+import android.security.rkp.service.RegistrationProxy;
+import android.util.Log;
+
+import com.android.server.SystemService;
+
+import java.time.Duration;
+
+/**
+ * Implements the remote provisioning system service. This service is backed by a mainline
+ * module, allowing the underlying implementation to be updated. The code here is a thin
+ * proxy for the code in android.security.rkp.service.
+ *
+ * @hide
+ */
+public class RemoteProvisioningService extends SystemService {
+ public static final String TAG = "RemoteProvisionSysSvc";
+ private static final Duration CREATE_REGISTRATION_TIMEOUT = Duration.ofSeconds(10);
+ private final RemoteProvisioningImpl mBinderImpl = new RemoteProvisioningImpl();
+
+ /** @hide */
+ public RemoteProvisioningService(Context context) {
+ super(context);
+ }
+
+ @Override
+ public void onStart() {
+ publishBinderService(Context.REMOTE_PROVISIONING_SERVICE, mBinderImpl);
+ }
+
+ private final class RemoteProvisioningImpl extends IRemoteProvisioning.Stub {
+
+ final class RegistrationBinder extends IRegistration.Stub {
+ static final String TAG = RemoteProvisioningService.TAG;
+ private final RegistrationProxy mRegistration;
+
+ RegistrationBinder(RegistrationProxy registration) {
+ mRegistration = registration;
+ }
+
+ @Override
+ public void getKey(int keyId, IGetKeyCallback callback) {
+ Log.e(TAG, "RegistrationBinder.getKey NOT YET IMPLEMENTED");
+ }
+
+ @Override
+ public void cancelGetKey(IGetKeyCallback callback) {
+ Log.e(TAG, "RegistrationBinder.cancelGetKey NOT YET IMPLEMENTED");
+ }
+
+ @Override
+ public void storeUpgradedKey(byte[] oldKeyBlob, byte[] newKeyBlob) {
+ Log.e(TAG, "RegistrationBinder.storeUpgradedKey NOT YET IMPLEMENTED");
+ }
+ }
+
+ @Override
+ public void getRegistration(String irpcName, IGetRegistrationCallback callback)
+ throws RemoteException {
+ final int callerUid = Binder.getCallingUidOrThrow();
+ final long callingIdentity = Binder.clearCallingIdentity();
+ try {
+ Log.i(TAG, "getRegistration(" + irpcName + ")");
+ RegistrationProxy.createAsync(
+ getContext(),
+ callerUid,
+ irpcName,
+ CREATE_REGISTRATION_TIMEOUT,
+ getContext().getMainExecutor(),
+ new OutcomeReceiver<>() {
+ @Override
+ public void onResult(RegistrationProxy registration) {
+ try {
+ callback.onSuccess(new RegistrationBinder(registration));
+ } catch (RemoteException e) {
+ Log.e(TAG, "Error calling success callback", e);
+ }
+ }
+
+ @Override
+ public void onError(Exception error) {
+ try {
+ callback.onError(error.toString());
+ } catch (RemoteException e) {
+ Log.e(TAG, "Error calling error callback", e);
+ }
+ }
+ });
+ } finally {
+ Binder.restoreCallingIdentity(callingIdentity);
+ }
+ }
+
+ @Override
+ public void cancelGetRegistration(IGetRegistrationCallback callback)
+ throws RemoteException {
+ Log.i(TAG, "cancelGetRegistration()");
+ callback.onError("cancelGetRegistration not yet implemented");
+ }
+ }
+}
diff --git a/services/core/java/com/android/server/statusbar/StatusBarManagerInternal.java b/services/core/java/com/android/server/statusbar/StatusBarManagerInternal.java
index e835845..392fda9 100644
--- a/services/core/java/com/android/server/statusbar/StatusBarManagerInternal.java
+++ b/services/core/java/com/android/server/statusbar/StatusBarManagerInternal.java
@@ -210,4 +210,11 @@
* Called when requested to go to fullscreen from the active split app.
*/
void goToFullscreenFromSplit();
+
+ /**
+ * Enters stage split from a current running app.
+ *
+ * @see com.android.internal.statusbar.IStatusBar#enterStageSplitFromRunningApp
+ */
+ void enterStageSplitFromRunningApp(boolean leftOrTop);
}
diff --git a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java
index bdc1362..8d71d9c 100644
--- a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java
+++ b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java
@@ -735,6 +735,15 @@
} catch (RemoteException ex) { }
}
}
+
+ @Override
+ public void enterStageSplitFromRunningApp(boolean leftOrTop) {
+ if (mBar != null) {
+ try {
+ mBar.enterStageSplitFromRunningApp(leftOrTop);
+ } catch (RemoteException ex) { }
+ }
+ }
};
private final GlobalActionsProvider mGlobalActionsProvider = new GlobalActionsProvider() {
diff --git a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java
index 5d08461..4480d52 100644
--- a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java
+++ b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java
@@ -34,6 +34,7 @@
import android.annotation.NonNull;
import android.app.ActivityManager;
+import android.app.ActivityOptions;
import android.app.AppGlobals;
import android.app.AppOpsManager;
import android.app.ILocalWallpaperColorConsumer;
@@ -3166,6 +3167,15 @@
}
}
+ final ActivityOptions clientOptions = ActivityOptions.makeBasic();
+ clientOptions.setIgnorePendingIntentCreatorForegroundState(true);
+ PendingIntent clientIntent = PendingIntent.getActivityAsUser(
+ mContext, 0, Intent.createChooser(
+ new Intent(Intent.ACTION_SET_WALLPAPER),
+ mContext.getText(com.android.internal.R.string.chooser_wallpaper)),
+ PendingIntent.FLAG_IMMUTABLE, clientOptions.toBundle(),
+ UserHandle.of(serviceUserId));
+
// Bind the service!
if (DEBUG) Slog.v(TAG, "Binding to:" + componentName);
final int componentUid = mIPackageManager.getPackageUid(componentName.getPackageName(),
@@ -3174,11 +3184,7 @@
intent.setComponent(componentName);
intent.putExtra(Intent.EXTRA_CLIENT_LABEL,
com.android.internal.R.string.wallpaper_binding_label);
- intent.putExtra(Intent.EXTRA_CLIENT_INTENT, PendingIntent.getActivityAsUser(
- mContext, 0,
- Intent.createChooser(new Intent(Intent.ACTION_SET_WALLPAPER),
- mContext.getText(com.android.internal.R.string.chooser_wallpaper)),
- PendingIntent.FLAG_IMMUTABLE, null, new UserHandle(serviceUserId)));
+ intent.putExtra(Intent.EXTRA_CLIENT_INTENT, clientIntent);
if (!mContext.bindServiceAsUser(intent, newConn,
Context.BIND_AUTO_CREATE | Context.BIND_SHOWING_UI
| Context.BIND_FOREGROUND_SERVICE_WHILE_AWAKE
diff --git a/services/core/java/com/android/server/wearable/WearableSensingManagerService.java b/services/core/java/com/android/server/wearable/WearableSensingManagerService.java
index e155a06..e145898 100644
--- a/services/core/java/com/android/server/wearable/WearableSensingManagerService.java
+++ b/services/core/java/com/android/server/wearable/WearableSensingManagerService.java
@@ -38,6 +38,7 @@
import android.util.Slog;
import com.android.internal.R;
+import com.android.internal.annotations.VisibleForTesting;
import com.android.server.LocalServices;
import com.android.server.SystemService;
import com.android.server.infra.AbstractMasterSystemService;
@@ -161,6 +162,32 @@
return null;
}
+ @VisibleForTesting
+ void provideDataStream(@UserIdInt int userId, ParcelFileDescriptor parcelFileDescriptor,
+ RemoteCallback callback) {
+ synchronized (mLock) {
+ final WearableSensingManagerPerUserService mService = getServiceForUserLocked(userId);
+ if (mService != null) {
+ mService.onProvideDataStream(parcelFileDescriptor, callback);
+ } else {
+ Slog.w(TAG, "Service not available.");
+ }
+ }
+ }
+
+ @VisibleForTesting
+ void provideData(@UserIdInt int userId, PersistableBundle data, SharedMemory sharedMemory,
+ RemoteCallback callback) {
+ synchronized (mLock) {
+ final WearableSensingManagerPerUserService mService = getServiceForUserLocked(userId);
+ if (mService != null) {
+ mService.onProvidedData(data, sharedMemory, callback);
+ } else {
+ Slog.w(TAG, "Service not available.");
+ }
+ }
+ }
+
private final class WearableSensingManagerInternal extends IWearableSensingManager.Stub {
final WearableSensingManagerPerUserService mService = getServiceForUserLocked(
UserHandle.getCallingUserId());
@@ -205,6 +232,8 @@
@Override
public void onShellCommand(FileDescriptor in, FileDescriptor out, FileDescriptor err,
String[] args, ShellCallback callback, ResultReceiver resultReceiver) {
+ new WearableSensingShellCommand(WearableSensingManagerService.this).exec(
+ this, in, out, err, args, callback, resultReceiver);
}
}
}
diff --git a/services/core/java/com/android/server/wearable/WearableSensingShellCommand.java b/services/core/java/com/android/server/wearable/WearableSensingShellCommand.java
new file mode 100644
index 0000000..842bccb
--- /dev/null
+++ b/services/core/java/com/android/server/wearable/WearableSensingShellCommand.java
@@ -0,0 +1,212 @@
+/*
+ * 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.wearable;
+
+import android.annotation.NonNull;
+import android.app.wearable.WearableSensingManager;
+import android.content.ComponentName;
+import android.os.Binder;
+import android.os.ParcelFileDescriptor;
+import android.os.PersistableBundle;
+import android.os.RemoteCallback;
+import android.os.ShellCommand;
+import android.util.Slog;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.PrintWriter;
+
+final class WearableSensingShellCommand extends ShellCommand {
+ private static final String TAG = WearableSensingShellCommand.class.getSimpleName();
+
+ static final TestableCallbackInternal sTestableCallbackInternal =
+ new TestableCallbackInternal();
+
+ @NonNull
+ private final WearableSensingManagerService mService;
+
+ private static ParcelFileDescriptor[] sPipe;
+
+ WearableSensingShellCommand(@NonNull WearableSensingManagerService service) {
+ mService = service;
+ }
+
+ /** Callbacks for WearableSensingService results used internally for testing. */
+ static class TestableCallbackInternal {
+ private int mLastStatus;
+
+ public int getLastStatus() {
+ return mLastStatus;
+ }
+
+ @NonNull
+ private RemoteCallback createRemoteStatusCallback() {
+ return new RemoteCallback(result -> {
+ int status = result.getInt(WearableSensingManager.STATUS_RESPONSE_BUNDLE_KEY);
+ final long token = Binder.clearCallingIdentity();
+ try {
+ mLastStatus = status;
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ });
+ }
+ }
+
+ @Override
+ public int onCommand(String cmd) {
+ if (cmd == null) {
+ return handleDefaultCommands(cmd);
+ }
+
+ switch (cmd) {
+ case "create-data-stream":
+ return createDataStream();
+ case "destroy-data-stream":
+ return destroyDataStream();
+ case "provide-data-stream":
+ return provideDataStream();
+ case "write-to-data-stream":
+ return writeToDataStream();
+ case "provide-data":
+ return provideData();
+ case "get-last-status-code":
+ return getLastStatusCode();
+ case "get-bound-package":
+ return getBoundPackageName();
+ case "set-temporary-service":
+ return setTemporaryService();
+ default:
+ return handleDefaultCommands(cmd);
+ }
+ }
+
+ @Override
+ public void onHelp() {
+ PrintWriter pw = getOutPrintWriter();
+ pw.println("WearableSensingCommands commands: ");
+ pw.println(" help");
+ pw.println(" Print this help text.");
+ pw.println();
+ pw.println(" create-data-stream: Creates a data stream to be provided.");
+ pw.println(" destroy-data-stream: Destroys a data stream if one was previously created.");
+ pw.println(" provide-data-stream USER_ID: "
+ + "Provides data stream to WearableSensingService.");
+ pw.println(" write-to-data-stream STRING: writes string to data stream.");
+ pw.println(" provide-data USER_ID KEY INTEGER: provide integer as data with key.");
+ pw.println(" get-last-status-code: Prints the latest request status code.");
+ pw.println(" get-bound-package USER_ID:"
+ + " Print the bound package that implements the service.");
+ pw.println(" set-temporary-service USER_ID [PACKAGE_NAME] [COMPONENT_NAME DURATION]");
+ pw.println(" Temporarily (for DURATION ms) changes the service implementation.");
+ pw.println(" To reset, call with just the USER_ID argument.");
+ }
+
+ private int createDataStream() {
+ Slog.d(TAG, "createDataStream");
+ try {
+ sPipe = ParcelFileDescriptor.createPipe();
+ } catch (IOException e) {
+ Slog.d(TAG, "Failed to createDataStream.", e);
+ }
+ return 0;
+ }
+
+ private int destroyDataStream() {
+ Slog.d(TAG, "destroyDataStream");
+ try {
+ if (sPipe != null) {
+ sPipe[0].close();
+ sPipe[1].close();
+ }
+ } catch (IOException e) {
+ Slog.d(TAG, "Failed to destroyDataStream.", e);
+ }
+ return 0;
+ }
+
+ private int provideDataStream() {
+ Slog.d(TAG, "provideDataStream");
+ if (sPipe != null) {
+ final int userId = Integer.parseInt(getNextArgRequired());
+ mService.provideDataStream(userId, sPipe[0],
+ sTestableCallbackInternal.createRemoteStatusCallback());
+ }
+ return 0;
+ }
+
+ private int writeToDataStream() {
+ Slog.d(TAG, "writeToDataStream");
+ if (sPipe != null) {
+ final String value = getNextArgRequired();
+ try {
+ ParcelFileDescriptor writePipe = sPipe[1].dup();
+ OutputStream os = new ParcelFileDescriptor.AutoCloseOutputStream(writePipe);
+ os.write(value.getBytes());
+ } catch (IOException e) {
+ Slog.d(TAG, "Failed to writeToDataStream.", e);
+ }
+ }
+ return 0;
+ }
+
+ private int provideData() {
+ Slog.d(TAG, "provideData");
+ final int userId = Integer.parseInt(getNextArgRequired());
+ final String key = getNextArgRequired();
+ final int value = Integer.parseInt(getNextArgRequired());
+ PersistableBundle data = new PersistableBundle();
+ data.putInt(key, value);
+
+ mService.provideData(userId, data, null,
+ sTestableCallbackInternal.createRemoteStatusCallback());
+ return 0;
+ }
+
+ private int getLastStatusCode() {
+ Slog.d(TAG, "getLastStatusCode");
+ final PrintWriter resultPrinter = getOutPrintWriter();
+ int lastStatus = sTestableCallbackInternal.getLastStatus();
+ resultPrinter.println(lastStatus);
+ return 0;
+ }
+
+ private int setTemporaryService() {
+ final PrintWriter out = getOutPrintWriter();
+ final int userId = Integer.parseInt(getNextArgRequired());
+ final String serviceName = getNextArg();
+ if (serviceName == null) {
+ mService.resetTemporaryService(userId);
+ out.println("WearableSensingManagerService temporary reset. ");
+ return 0;
+ }
+
+ final int duration = Integer.parseInt(getNextArgRequired());
+ mService.setTemporaryService(userId, serviceName, duration);
+ out.println("WearableSensingService temporarily set to " + serviceName
+ + " for " + duration + "ms");
+ return 0;
+ }
+
+ private int getBoundPackageName() {
+ final PrintWriter resultPrinter = getOutPrintWriter();
+ final int userId = Integer.parseInt(getNextArgRequired());
+ final ComponentName componentName = mService.getComponentName(userId);
+ resultPrinter.println(componentName == null ? "" : componentName.getPackageName());
+ return 0;
+ }
+}
diff --git a/services/core/java/com/android/server/wm/ActivityMetricsLogger.java b/services/core/java/com/android/server/wm/ActivityMetricsLogger.java
index e1ab291..13111fb 100644
--- a/services/core/java/com/android/server/wm/ActivityMetricsLogger.java
+++ b/services/core/java/com/android/server/wm/ActivityMetricsLogger.java
@@ -740,7 +740,7 @@
// visible such as after the top task is finished.
for (int i = mTransitionInfoList.size() - 2; i >= 0; i--) {
final TransitionInfo prevInfo = mTransitionInfoList.get(i);
- if (prevInfo.mIsDrawn || !prevInfo.mLastLaunchedActivity.mVisibleRequested) {
+ if (prevInfo.mIsDrawn || !prevInfo.mLastLaunchedActivity.isVisibleRequested()) {
scheduleCheckActivityToBeDrawn(prevInfo.mLastLaunchedActivity, 0 /* delay */);
}
}
@@ -867,7 +867,7 @@
return;
}
if (DEBUG_METRICS) {
- Slog.i(TAG, "notifyVisibilityChanged " + r + " visible=" + r.mVisibleRequested
+ Slog.i(TAG, "notifyVisibilityChanged " + r + " visible=" + r.isVisibleRequested()
+ " state=" + r.getState() + " finishing=" + r.finishing);
}
if (r.isState(ActivityRecord.State.RESUMED) && r.mDisplayContent.isSleeping()) {
@@ -876,7 +876,7 @@
// the tracking of launch event.
return;
}
- if (!r.mVisibleRequested || r.finishing) {
+ if (!r.isVisibleRequested() || r.finishing) {
// Check if the tracker can be cancelled because the last launched activity may be
// no longer visible.
scheduleCheckActivityToBeDrawn(r, 0 /* delay */);
@@ -909,7 +909,7 @@
// activities in this task may be finished, invisible or drawn, so the transition event
// should be cancelled.
if (t != null && t.forAllActivities(
- a -> a.mVisibleRequested && !a.isReportedDrawn() && !a.finishing)) {
+ a -> a.isVisibleRequested() && !a.isReportedDrawn() && !a.finishing)) {
return;
}
diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java
index a7003fd..aec06f0 100644
--- a/services/core/java/com/android/server/wm/ActivityRecord.java
+++ b/services/core/java/com/android/server/wm/ActivityRecord.java
@@ -800,7 +800,7 @@
// it will sometimes be true a little earlier: when the activity record has
// been shown, but is still waiting for its app transition to execute
// before making its windows shown.
- boolean mVisibleRequested;
+ private boolean mVisibleRequested;
// Last visibility state we reported to the app token.
boolean reportedVisible;
@@ -3622,7 +3622,7 @@
// implied that the current finishing activity should be added into stopping list rather
// than destroy immediately.
final boolean isNextNotYetVisible = next != null
- && (!next.nowVisible || !next.mVisibleRequested);
+ && (!next.nowVisible || !next.isVisibleRequested());
// Clear last paused activity to ensure top activity can be resumed during sleeping.
if (isNextNotYetVisible && mDisplayContent.isSleeping()
@@ -4440,7 +4440,7 @@
void transferStartingWindowFromHiddenAboveTokenIfNeeded() {
task.forAllActivities(fromActivity -> {
if (fromActivity == this) return true;
- return !fromActivity.mVisibleRequested && transferStartingWindow(fromActivity);
+ return !fromActivity.isVisibleRequested() && transferStartingWindow(fromActivity);
});
}
@@ -5105,7 +5105,8 @@
* This is the only place that writes {@link #mVisibleRequested} (except unit test). The caller
* outside of this class should use {@link #setVisibility}.
*/
- private void setVisibleRequested(boolean visible) {
+ @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
+ void setVisibleRequested(boolean visible) {
if (visible == mVisibleRequested) {
return;
}
@@ -6544,7 +6545,7 @@
if (associatedTask == null) {
removeStartingWindow();
} else if (associatedTask.getActivity(
- r -> r.mVisibleRequested && !r.firstWindowDrawn) == null) {
+ r -> r.isVisibleRequested() && !r.firstWindowDrawn) == null) {
// The last drawn activity may not be the one that owns the starting window.
final ActivityRecord r = associatedTask.topActivityContainsStartingWindow();
if (r != null) {
diff --git a/services/core/java/com/android/server/wm/ActivityServiceConnectionsHolder.java b/services/core/java/com/android/server/wm/ActivityServiceConnectionsHolder.java
index 30c7b23..0859d40 100644
--- a/services/core/java/com/android/server/wm/ActivityServiceConnectionsHolder.java
+++ b/services/core/java/com/android/server/wm/ActivityServiceConnectionsHolder.java
@@ -92,7 +92,7 @@
public boolean isActivityVisible() {
synchronized (mService.mGlobalLock) {
- return mActivity.mVisibleRequested || mActivity.isState(RESUMED, PAUSING);
+ return mActivity.isVisibleRequested() || mActivity.isState(RESUMED, PAUSING);
}
}
diff --git a/services/core/java/com/android/server/wm/ActivityStartController.java b/services/core/java/com/android/server/wm/ActivityStartController.java
index 56aae2d6..05ec3b5 100644
--- a/services/core/java/com/android/server/wm/ActivityStartController.java
+++ b/services/core/java/com/android/server/wm/ActivityStartController.java
@@ -561,7 +561,7 @@
if (rootTask == null) return false;
final RemoteTransition remote = options.getRemoteTransition();
final ActivityRecord r = rootTask.topRunningActivity();
- if (r == null || r.mVisibleRequested || !r.attachedToProcess() || remote == null
+ if (r == null || r.isVisibleRequested() || !r.attachedToProcess() || remote == null
|| !r.mActivityComponent.equals(intent.getComponent())
// Recents keeps invisible while device is locked.
|| r.mDisplayContent.isKeyguardLocked()) {
diff --git a/services/core/java/com/android/server/wm/ActivityStarter.java b/services/core/java/com/android/server/wm/ActivityStarter.java
index 5c83c4f..1e06375 100644
--- a/services/core/java/com/android/server/wm/ActivityStarter.java
+++ b/services/core/java/com/android/server/wm/ActivityStarter.java
@@ -2637,7 +2637,7 @@
// If the activity is visible in multi-windowing mode, it may already be on
// the top (visible to user but not the global top), then the result code
// should be START_DELIVERED_TO_TOP instead of START_TASK_TO_FRONT.
- final boolean wasTopOfVisibleRootTask = intentActivity.mVisibleRequested
+ final boolean wasTopOfVisibleRootTask = intentActivity.isVisibleRequested()
&& intentActivity.inMultiWindowMode()
&& intentActivity == mTargetRootTask.topRunningActivity();
// We only want to move to the front, if we aren't going to launch on a
diff --git a/services/core/java/com/android/server/wm/BackNavigationController.java b/services/core/java/com/android/server/wm/BackNavigationController.java
index 8680f1b..14131e6 100644
--- a/services/core/java/com/android/server/wm/BackNavigationController.java
+++ b/services/core/java/com/android/server/wm/BackNavigationController.java
@@ -235,6 +235,7 @@
// We don't have an application callback, let's find the destination of the back gesture
// The search logic should align with ActivityClientController#finishActivity
prevActivity = currentTask.topRunningActivity(currentActivity.token, INVALID_TASK_ID);
+ final boolean isOccluded = isKeyguardOccluded(window);
// TODO Dialog window does not need to attach on activity, check
// window.mAttrs.type != TYPE_BASE_APPLICATION
if ((window.getParent().getChildCount() > 1
@@ -244,16 +245,24 @@
backType = BackNavigationInfo.TYPE_DIALOG_CLOSE;
removedWindowContainer = window;
} else if (prevActivity != null) {
- // We have another Activity in the same currentTask to go to
- backType = BackNavigationInfo.TYPE_CROSS_ACTIVITY;
- removedWindowContainer = currentActivity;
- prevTask = prevActivity.getTask();
+ if (!isOccluded || prevActivity.canShowWhenLocked()) {
+ // We have another Activity in the same currentTask to go to
+ backType = BackNavigationInfo.TYPE_CROSS_ACTIVITY;
+ removedWindowContainer = currentActivity;
+ prevTask = prevActivity.getTask();
+ } else {
+ backType = BackNavigationInfo.TYPE_CALLBACK;
+ }
} else if (currentTask.returnsToHomeRootTask()) {
- // Our Task should bring back to home
- removedWindowContainer = currentTask;
- prevTask = currentTask.getDisplayArea().getRootHomeTask();
- backType = BackNavigationInfo.TYPE_RETURN_TO_HOME;
- mShowWallpaper = true;
+ if (isOccluded) {
+ backType = BackNavigationInfo.TYPE_CALLBACK;
+ } else {
+ // Our Task should bring back to home
+ removedWindowContainer = currentTask;
+ prevTask = currentTask.getDisplayArea().getRootHomeTask();
+ backType = BackNavigationInfo.TYPE_RETURN_TO_HOME;
+ mShowWallpaper = true;
+ }
} else if (currentActivity.isRootOfTask()) {
// TODO(208789724): Create single source of truth for this, maybe in
// RootWindowContainer
@@ -267,7 +276,9 @@
backType = BackNavigationInfo.TYPE_CALLBACK;
} else {
prevActivity = prevTask.getTopNonFinishingActivity();
- if (prevTask.isActivityTypeHome()) {
+ if (prevActivity == null || (isOccluded && !prevActivity.canShowWhenLocked())) {
+ backType = BackNavigationInfo.TYPE_CALLBACK;
+ } else if (prevTask.isActivityTypeHome()) {
backType = BackNavigationInfo.TYPE_RETURN_TO_HOME;
mShowWallpaper = true;
} else {
@@ -323,6 +334,12 @@
return mAnimationTargets.mComposed && mAnimationTargets.mWaitTransition;
}
+ boolean isKeyguardOccluded(WindowState focusWindow) {
+ final KeyguardController kc = mWindowManagerService.mAtmService.mKeyguardController;
+ final int displayId = focusWindow.getDisplayId();
+ return kc.isKeyguardLocked(displayId) && kc.isDisplayOccluded(displayId);
+ }
+
// For legacy transition.
/**
* Once we find the transition targets match back animation targets, remove the target from
@@ -810,7 +827,7 @@
if (activity == null) {
return;
}
- if (!activity.mVisibleRequested) {
+ if (!activity.isVisibleRequested()) {
activity.setVisibility(true);
}
activity.mLaunchTaskBehind = true;
diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java
index 5ba70aa..d5802cf 100644
--- a/services/core/java/com/android/server/wm/DisplayContent.java
+++ b/services/core/java/com/android/server/wm/DisplayContent.java
@@ -876,11 +876,11 @@
final ActivityRecord activity = w.mActivityRecord;
if (gone) Slog.v(TAG, " GONE: mViewVisibility=" + w.mViewVisibility
+ " mRelayoutCalled=" + w.mRelayoutCalled + " visible=" + w.mToken.isVisible()
- + " visibleRequested=" + (activity != null && activity.mVisibleRequested)
+ + " visibleRequested=" + (activity != null && activity.isVisibleRequested())
+ " parentHidden=" + w.isParentWindowHidden());
else Slog.v(TAG, " VIS: mViewVisibility=" + w.mViewVisibility
+ " mRelayoutCalled=" + w.mRelayoutCalled + " visible=" + w.mToken.isVisible()
- + " visibleRequested=" + (activity != null && activity.mVisibleRequested)
+ + " visibleRequested=" + (activity != null && activity.isVisibleRequested())
+ " parentHidden=" + w.isParentWindowHidden());
}
@@ -1705,7 +1705,7 @@
.notifyTaskRequestedOrientationChanged(task.mTaskId, orientation);
}
// The orientation source may not be the top if it uses SCREEN_ORIENTATION_BEHIND.
- final ActivityRecord topCandidate = !r.mVisibleRequested ? topRunningActivity() : r;
+ final ActivityRecord topCandidate = !r.isVisibleRequested() ? topRunningActivity() : r;
if (handleTopActivityLaunchingInDifferentOrientation(
topCandidate, r, true /* checkOpening */)) {
// Display orientation should be deferred until the top fixed rotation is finished.
@@ -2101,13 +2101,13 @@
}
/**
- * @see DisplayWindowPolicyController#canShowTasksInRecents()
+ * @see DisplayWindowPolicyController#canShowTasksInHostDeviceRecents()
*/
- boolean canShowTasksInRecents() {
+ boolean canShowTasksInHostDeviceRecents() {
if (mDwpcHelper == null) {
return true;
}
- return mDwpcHelper.canShowTasksInRecents();
+ return mDwpcHelper.canShowTasksInHostDeviceRecents();
}
/**
@@ -2712,7 +2712,7 @@
mWmService.mWindowsChanged = true;
// If the transition finished callback cannot match the token for some reason, make sure the
// rotated state is cleared if it is already invisible.
- if (mFixedRotationLaunchingApp != null && !mFixedRotationLaunchingApp.mVisibleRequested
+ if (mFixedRotationLaunchingApp != null && !mFixedRotationLaunchingApp.isVisibleRequested()
&& !mFixedRotationLaunchingApp.isVisible()
&& !mDisplayRotation.isRotatingSeamlessly()) {
clearFixedRotationLaunchingApp();
diff --git a/services/core/java/com/android/server/wm/DisplayPolicy.java b/services/core/java/com/android/server/wm/DisplayPolicy.java
index b419f36..300deca 100644
--- a/services/core/java/com/android/server/wm/DisplayPolicy.java
+++ b/services/core/java/com/android/server/wm/DisplayPolicy.java
@@ -2128,15 +2128,10 @@
}
void updateSystemBarAttributes() {
- WindowState winCandidate = mFocusedWindow;
- if (winCandidate == null && mTopFullscreenOpaqueWindowState != null
- && (mTopFullscreenOpaqueWindowState.mAttrs.flags
- & WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE) == 0) {
- // Only focusable window can take system bar control.
- winCandidate = mTopFullscreenOpaqueWindowState;
- }
// If there is no window focused, there will be nobody to handle the events
// anyway, so just hang on in whatever state we're in until things settle down.
+ WindowState winCandidate = mFocusedWindow != null ? mFocusedWindow
+ : mTopFullscreenOpaqueWindowState;
if (winCandidate == null) {
return;
}
diff --git a/services/core/java/com/android/server/wm/DisplayWindowPolicyControllerHelper.java b/services/core/java/com/android/server/wm/DisplayWindowPolicyControllerHelper.java
index 6f821b5..69fd00c 100644
--- a/services/core/java/com/android/server/wm/DisplayWindowPolicyControllerHelper.java
+++ b/services/core/java/com/android/server/wm/DisplayWindowPolicyControllerHelper.java
@@ -153,13 +153,13 @@
}
/**
- * @see DisplayWindowPolicyController#canShowTasksInRecents()
+ * @see DisplayWindowPolicyController#canShowTasksInHostDeviceRecents()
*/
- public final boolean canShowTasksInRecents() {
+ public final boolean canShowTasksInHostDeviceRecents() {
if (mDisplayWindowPolicyController == null) {
return true;
}
- return mDisplayWindowPolicyController.canShowTasksInRecents();
+ return mDisplayWindowPolicyController.canShowTasksInHostDeviceRecents();
}
/**
diff --git a/services/core/java/com/android/server/wm/EnsureActivitiesVisibleHelper.java b/services/core/java/com/android/server/wm/EnsureActivitiesVisibleHelper.java
index 7bb036d..bd83794 100644
--- a/services/core/java/com/android/server/wm/EnsureActivitiesVisibleHelper.java
+++ b/services/core/java/com/android/server/wm/EnsureActivitiesVisibleHelper.java
@@ -191,7 +191,7 @@
if (!r.attachedToProcess()) {
makeVisibleAndRestartIfNeeded(mStarting, mConfigChanges, isTop,
resumeTopActivity && isTop, r);
- } else if (r.mVisibleRequested) {
+ } else if (r.isVisibleRequested()) {
// If this activity is already visible, then there is nothing to do here.
if (DEBUG_VISIBILITY) {
Slog.v(TAG_VISIBILITY, "Skipping: already visible at " + r);
@@ -244,7 +244,7 @@
// invisible. If the app is already visible, it must have died while it was visible. In this
// case, we'll show the dead window but will not restart the app. Otherwise we could end up
// thrashing.
- if (!isTop && r.mVisibleRequested && !r.isState(INITIALIZING)) {
+ if (!isTop && r.isVisibleRequested() && !r.isState(INITIALIZING)) {
return;
}
@@ -256,7 +256,7 @@
if (r != starting) {
r.startFreezingScreenLocked(configChanges);
}
- if (!r.mVisibleRequested || r.mLaunchTaskBehind) {
+ if (!r.isVisibleRequested() || r.mLaunchTaskBehind) {
if (DEBUG_VISIBILITY) {
Slog.v(TAG_VISIBILITY, "Starting and making visible: " + r);
}
diff --git a/services/core/java/com/android/server/wm/RecentTasks.java b/services/core/java/com/android/server/wm/RecentTasks.java
index 1fc061b..c827062 100644
--- a/services/core/java/com/android/server/wm/RecentTasks.java
+++ b/services/core/java/com/android/server/wm/RecentTasks.java
@@ -1393,7 +1393,7 @@
// Ignore the task if it is started on a display which is not allow to show its tasks on
// Recents.
if (task.getDisplayContent() != null
- && !task.getDisplayContent().canShowTasksInRecents()) {
+ && !task.getDisplayContent().canShowTasksInHostDeviceRecents()) {
return false;
}
diff --git a/services/core/java/com/android/server/wm/RecentsAnimation.java b/services/core/java/com/android/server/wm/RecentsAnimation.java
index ffe3374..be90588 100644
--- a/services/core/java/com/android/server/wm/RecentsAnimation.java
+++ b/services/core/java/com/android/server/wm/RecentsAnimation.java
@@ -112,7 +112,7 @@
mTargetActivityType);
ActivityRecord targetActivity = getTargetActivity(targetRootTask);
if (targetActivity != null) {
- if (targetActivity.mVisibleRequested || targetActivity.isTopRunningActivity()) {
+ if (targetActivity.isVisibleRequested() || targetActivity.isTopRunningActivity()) {
// The activity is ready.
return;
}
@@ -195,7 +195,7 @@
// Send launch hint if we are actually launching the target. If it's already visible
// (shouldn't happen in general) we don't need to send it.
- if (targetActivity == null || !targetActivity.mVisibleRequested) {
+ if (targetActivity == null || !targetActivity.isVisibleRequested()) {
mService.mRootWindowContainer.startPowerModeLaunchIfNeeded(
true /* forceSend */, targetActivity);
}
diff --git a/services/core/java/com/android/server/wm/RootWindowContainer.java b/services/core/java/com/android/server/wm/RootWindowContainer.java
index 64cca87..4c7f6fe 100644
--- a/services/core/java/com/android/server/wm/RootWindowContainer.java
+++ b/services/core/java/com/android/server/wm/RootWindowContainer.java
@@ -2099,6 +2099,7 @@
// from the client organizer, so the PIP activity can get the correct config
// from the Task, and prevent conflict with the PipTaskOrganizer.
tf.updateRequestedOverrideConfiguration(EMPTY);
+ tf.updateRelativeEmbeddedBounds();
}
});
rootTask.setWindowingMode(WINDOWING_MODE_PINNED);
@@ -2619,7 +2620,7 @@
final ArrayList<Task> addedTasks = new ArrayList<>();
forAllActivities((r) -> {
final Task task = r.getTask();
- if (r.mVisibleRequested && r.mStartingData == null && !addedTasks.contains(task)) {
+ if (r.isVisibleRequested() && r.mStartingData == null && !addedTasks.contains(task)) {
r.showStartingWindow(true /*taskSwitch*/);
addedTasks.add(task);
}
@@ -2644,7 +2645,7 @@
forAllLeafTasks(task -> {
final int oldRank = task.mLayerRank;
final ActivityRecord r = task.topRunningActivityLocked();
- if (r != null && r.mVisibleRequested) {
+ if (r != null && r.isVisibleRequested()) {
task.mLayerRank = ++mTmpTaskLayerRank;
} else {
task.mLayerRank = Task.LAYER_RANK_INVISIBLE;
diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java
index a93767e..6d4a526 100644
--- a/services/core/java/com/android/server/wm/Task.java
+++ b/services/core/java/com/android/server/wm/Task.java
@@ -1591,15 +1591,22 @@
removeChild(r, reason);
});
} else {
+ final ArrayList<ActivityRecord> finishingActivities = new ArrayList<>();
+ forAllActivities(r -> {
+ if (r.finishing || (excludingTaskOverlay && r.isTaskOverlay())) {
+ return;
+ }
+ finishingActivities.add(r);
+ });
+
// Finish or destroy apps from the bottom to ensure that all the other activity have
// been finished and the top task in another task gets resumed when a top activity is
// removed. Otherwise, the next top activity could be started while the top activity
// is removed, which is not necessary since the next top activity is on the same Task
// and should also be removed.
- forAllActivities((r) -> {
- if (r.finishing || (excludingTaskOverlay && r.isTaskOverlay())) {
- return;
- }
+ for (int i = finishingActivities.size() - 1; i >= 0; i--) {
+ final ActivityRecord r = finishingActivities.get(i);
+
// Prevent the transition from being executed too early if the top activity is
// resumed but the mVisibleRequested of any other activity is true, the transition
// should wait until next activity resumed.
@@ -1609,7 +1616,7 @@
} else {
r.destroyIfPossible(reason);
}
- }, false /* traverseTopToBottom */);
+ }
}
}
@@ -2167,7 +2174,7 @@
}
private boolean shouldStartChangeTransition(int prevWinMode, @NonNull Rect prevBounds) {
- if (!isLeafTask() || !canStartChangeTransition()) {
+ if (!(isLeafTask() || mCreatedByOrganizer) || !canStartChangeTransition()) {
return false;
}
final int newWinMode = getWindowingMode();
@@ -2455,7 +2462,7 @@
final String myReason = reason + " adjustFocusToNextFocusableTask";
final ActivityRecord top = focusableTask.topRunningActivity();
- if (focusableTask.isActivityTypeHome() && (top == null || !top.mVisibleRequested)) {
+ if (focusableTask.isActivityTypeHome() && (top == null || !top.isVisibleRequested())) {
// If we will be focusing on the root home task next and its current top activity isn't
// visible, then use the move the root home task to top to make the activity visible.
focusableTask.getDisplayArea().moveHomeActivityToTop(myReason);
@@ -2768,7 +2775,7 @@
*/
private static void getMaxVisibleBounds(ActivityRecord token, Rect out, boolean[] foundTop) {
// skip hidden (or about to hide) apps
- if (token.mIsExiting || !token.isClientVisible() || !token.mVisibleRequested) {
+ if (token.mIsExiting || !token.isClientVisible() || !token.isVisibleRequested()) {
return;
}
final WindowState win = token.findMainWindow();
@@ -3077,7 +3084,7 @@
* this activity.
*/
ActivityRecord getTopVisibleActivity() {
- return getActivity((r) -> !r.mIsExiting && r.isClientVisible() && r.mVisibleRequested);
+ return getActivity((r) -> !r.mIsExiting && r.isClientVisible() && r.isVisibleRequested());
}
/**
@@ -5726,7 +5733,7 @@
forAllActivities(r -> {
if (!r.info.packageName.equals(packageName)) return;
r.forceNewConfig = true;
- if (starting != null && r == starting && r.mVisibleRequested) {
+ if (starting != null && r == starting && r.isVisibleRequested()) {
r.startFreezingScreenLocked(CONFIG_SCREEN_LAYOUT);
}
});
diff --git a/services/core/java/com/android/server/wm/TaskFragment.java b/services/core/java/com/android/server/wm/TaskFragment.java
index d69d949..212a474 100644
--- a/services/core/java/com/android/server/wm/TaskFragment.java
+++ b/services/core/java/com/android/server/wm/TaskFragment.java
@@ -302,6 +302,14 @@
private final IBinder mFragmentToken;
/**
+ * The bounds of the embedded TaskFragment relative to the parent Task.
+ * {@code null} if it is not {@link #mIsEmbedded}
+ * TODO(b/261785978) cleanup with legacy app transition
+ */
+ @Nullable
+ private final Rect mRelativeEmbeddedBounds;
+
+ /**
* Whether to delay the call to {@link #updateOrganizedTaskFragmentSurface()} when there is a
* configuration change.
*/
@@ -346,7 +354,7 @@
}
void process(ActivityRecord start, boolean preserveWindow) {
- if (start == null || !start.mVisibleRequested) {
+ if (start == null || !start.isVisibleRequested()) {
return;
}
reset(preserveWindow);
@@ -383,6 +391,7 @@
mRootWindowContainer = mAtmService.mRootWindowContainer;
mCreatedByOrganizer = createdByOrganizer;
mIsEmbedded = isEmbedded;
+ mRelativeEmbeddedBounds = isEmbedded ? new Rect() : null;
mTaskFragmentOrganizerController =
mAtmService.mWindowOrganizerController.mTaskFragmentOrganizerController;
mFragmentToken = fragmentToken;
@@ -1356,7 +1365,7 @@
if (next.attachedToProcess()) {
if (DEBUG_SWITCH) {
Slog.v(TAG_SWITCH, "Resume running: " + next + " stopped=" + next.stopped
- + " visibleRequested=" + next.mVisibleRequested);
+ + " visibleRequested=" + next.isVisibleRequested());
}
// If the previous activity is translucent, force a visibility update of
@@ -1370,7 +1379,7 @@
|| mLastPausedActivity != null && !mLastPausedActivity.occludesParent();
// This activity is now becoming visible.
- if (!next.mVisibleRequested || next.stopped || lastActivityTranslucent) {
+ if (!next.isVisibleRequested() || next.stopped || lastActivityTranslucent) {
next.app.addToPendingTop();
next.setVisibility(true);
}
@@ -1421,7 +1430,7 @@
// Do over!
mTaskSupervisor.scheduleResumeTopActivities();
}
- if (!next.mVisibleRequested || next.stopped) {
+ if (!next.isVisibleRequested() || next.stopped) {
next.setVisibility(true);
}
next.completeResumeLocked();
@@ -1732,7 +1741,7 @@
} else if (prev.attachedToProcess()) {
ProtoLog.v(WM_DEBUG_STATES, "Enqueue pending stop if needed: %s "
+ "wasStopping=%b visibleRequested=%b", prev, wasStopping,
- prev.mVisibleRequested);
+ prev.isVisibleRequested());
if (prev.deferRelaunchUntilPaused) {
// Complete the deferred relaunch that was waiting for pause to complete.
ProtoLog.v(WM_DEBUG_STATES, "Re-launching after pause: %s", prev);
@@ -1742,7 +1751,7 @@
// We can't clobber it, because the stop confirmation will not be handled.
// We don't need to schedule another stop, we only need to let it happen.
prev.setState(STOPPING, "completePausedLocked");
- } else if (!prev.mVisibleRequested || shouldSleepOrShutDownActivities()) {
+ } else if (!prev.isVisibleRequested() || shouldSleepOrShutDownActivities()) {
// Clear out any deferred client hide we might currently have.
prev.setDeferHidingClient(false);
// If we were visible then resumeTopActivities will release resources before
@@ -2410,16 +2419,53 @@
}
}
- /** Whether we should prepare a transition for this {@link TaskFragment} bounds change. */
- boolean shouldStartChangeTransition(Rect startBounds) {
+ /**
+ * Gets the relative bounds of this embedded TaskFragment. This should only be called on
+ * embedded TaskFragment.
+ */
+ @NonNull
+ Rect getRelativeEmbeddedBounds() {
+ if (mRelativeEmbeddedBounds == null) {
+ throw new IllegalStateException("The TaskFragment is not embedded");
+ }
+ return mRelativeEmbeddedBounds;
+ }
+
+ /**
+ * Updates the record of the relative bounds of this embedded TaskFragment. This should only be
+ * called when the embedded TaskFragment's override bounds are changed.
+ * Returns {@code true} if the bounds is changed.
+ */
+ void updateRelativeEmbeddedBounds() {
+ // We only record the override bounds, which means it will not be changed when it is filling
+ // Task, and resize with the parent.
+ getRequestedOverrideBounds(mTmpBounds);
+ getRelativePosition(mTmpPoint);
+ mTmpBounds.offsetTo(mTmpPoint.x, mTmpPoint.y);
+ mRelativeEmbeddedBounds.set(mTmpBounds);
+ }
+
+ /**
+ * Updates the record of relative bounds of this embedded TaskFragment, and checks whether we
+ * should prepare a transition for the bounds change.
+ */
+ boolean shouldStartChangeTransition(@NonNull Rect absStartBounds,
+ @NonNull Rect relStartBounds) {
if (mTaskFragmentOrganizer == null || !canStartChangeTransition()) {
return false;
}
- // Only take snapshot if the bounds are resized.
- final Rect endBounds = getConfiguration().windowConfiguration.getBounds();
- return endBounds.width() != startBounds.width()
- || endBounds.height() != startBounds.height();
+ if (mTransitionController.isShellTransitionsEnabled()) {
+ // For Shell transition, the change will be collected anyway, so only take snapshot when
+ // the bounds are resized.
+ final Rect endBounds = getConfiguration().windowConfiguration.getBounds();
+ return endBounds.width() != absStartBounds.width()
+ || endBounds.height() != absStartBounds.height();
+ } else {
+ // For legacy transition, we need to trigger a change transition as long as the bounds
+ // is changed, even if it is not resized.
+ return !relStartBounds.equals(mRelativeEmbeddedBounds);
+ }
}
/** Records the starting bounds of the closing organized TaskFragment. */
diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java
index 9ca0439..2737456 100644
--- a/services/core/java/com/android/server/wm/Transition.java
+++ b/services/core/java/com/android/server/wm/Transition.java
@@ -992,7 +992,7 @@
// show here in the same way that we manually hide in finishTransaction.
for (int i = mParticipants.size() - 1; i >= 0; --i) {
final ActivityRecord ar = mParticipants.valueAt(i).asActivityRecord();
- if (ar == null || !ar.mVisibleRequested) continue;
+ if (ar == null || !ar.isVisibleRequested()) continue;
transaction.show(ar.getSurfaceControl());
// Also manually show any non-reported parents. This is necessary in a few cases
@@ -1246,7 +1246,7 @@
ArrayMap<WindowContainer, Integer> reasons = new ArrayMap<>();
for (int i = mParticipants.size() - 1; i >= 0; --i) {
ActivityRecord r = mParticipants.valueAt(i).asActivityRecord();
- if (r == null || !r.mVisibleRequested) continue;
+ if (r == null || !r.isVisibleRequested()) continue;
int transitionReason = APP_TRANSITION_WINDOWS_DRAWN;
// At this point, r is "ready", but if it's not "ALL ready" then it is probably only
// ready due to starting-window.
diff --git a/services/core/java/com/android/server/wm/TransitionController.java b/services/core/java/com/android/server/wm/TransitionController.java
index 99527b1..cf541fc 100644
--- a/services/core/java/com/android/server/wm/TransitionController.java
+++ b/services/core/java/com/android/server/wm/TransitionController.java
@@ -38,6 +38,7 @@
import android.util.ArrayMap;
import android.util.Slog;
import android.util.proto.ProtoOutputStream;
+import android.view.SurfaceControl;
import android.view.WindowManager;
import android.window.ITransitionMetricsReporter;
import android.window.ITransitionPlayer;
@@ -116,6 +117,8 @@
*/
boolean mBuildingFinishLayers = false;
+ private final SurfaceControl.Transaction mWakeT = new SurfaceControl.Transaction();
+
TransitionController(ActivityTaskManagerService atm,
TaskSnapshotController taskSnapshotController,
TransitionTracer transitionTracer) {
@@ -619,8 +622,16 @@
private void updateRunningRemoteAnimation(Transition transition, boolean isPlaying) {
if (mTransitionPlayerProc == null) return;
if (isPlaying) {
+ mWakeT.setEarlyWakeupStart();
+ mWakeT.apply();
+ // Usually transitions put quite a load onto the system already (with all the things
+ // happening in app), so pause task snapshot persisting to not increase the load.
+ mAtm.mWindowManager.mTaskSnapshotController.setPersisterPaused(true);
mTransitionPlayerProc.setRunningRemoteAnimation(true);
} else if (mPlayingTransitions.isEmpty()) {
+ mWakeT.setEarlyWakeupEnd();
+ mWakeT.apply();
+ mAtm.mWindowManager.mTaskSnapshotController.setPersisterPaused(false);
mTransitionPlayerProc.setRunningRemoteAnimation(false);
mRemotePlayer.clear();
return;
diff --git a/services/core/java/com/android/server/wm/WallpaperController.java b/services/core/java/com/android/server/wm/WallpaperController.java
index 3b30dd1..5de143d9 100644
--- a/services/core/java/com/android/server/wm/WallpaperController.java
+++ b/services/core/java/com/android/server/wm/WallpaperController.java
@@ -725,9 +725,9 @@
}
final boolean newTargetHidden = wallpaperTarget.mActivityRecord != null
- && !wallpaperTarget.mActivityRecord.mVisibleRequested;
+ && !wallpaperTarget.mActivityRecord.isVisibleRequested();
final boolean oldTargetHidden = prevWallpaperTarget.mActivityRecord != null
- && !prevWallpaperTarget.mActivityRecord.mVisibleRequested;
+ && !prevWallpaperTarget.mActivityRecord.isVisibleRequested();
ProtoLog.v(WM_DEBUG_WALLPAPER, "Animating wallpapers: "
+ "old: %s hidden=%b new: %s hidden=%b",
diff --git a/services/core/java/com/android/server/wm/WallpaperWindowToken.java b/services/core/java/com/android/server/wm/WallpaperWindowToken.java
index 6ee30bb..32cfb70 100644
--- a/services/core/java/com/android/server/wm/WallpaperWindowToken.java
+++ b/services/core/java/com/android/server/wm/WallpaperWindowToken.java
@@ -136,7 +136,7 @@
recentsAnimationController.linkFixedRotationTransformIfNeeded(this);
} else if ((wallpaperTarget.mActivityRecord == null
// Ignore invisible activity because it may be moving to background.
- || wallpaperTarget.mActivityRecord.mVisibleRequested)
+ || wallpaperTarget.mActivityRecord.isVisibleRequested())
&& wallpaperTarget.mToken.hasFixedRotationTransform()) {
// If the wallpaper target has a fixed rotation, we want the wallpaper to follow its
// rotation
diff --git a/services/core/java/com/android/server/wm/WindowContainer.java b/services/core/java/com/android/server/wm/WindowContainer.java
index 6e61071..df8886d 100644
--- a/services/core/java/com/android/server/wm/WindowContainer.java
+++ b/services/core/java/com/android/server/wm/WindowContainer.java
@@ -3025,13 +3025,16 @@
// When there are more than one changing containers, it may leave part of the
// screen empty. Show background color to cover that.
showBackdrop = getDisplayContent().mChangingContainers.size() > 1;
+ backdropColor = appTransition.getNextAppTransitionBackgroundColor();
} else {
// Check whether the app has requested to show backdrop for open/close
// transition.
final Animation a = appTransition.getNextAppRequestedAnimation(enter);
- showBackdrop = a != null && a.getShowBackdrop();
+ if (a != null) {
+ showBackdrop = a.getShowBackdrop();
+ backdropColor = a.getBackdropColor();
+ }
}
- backdropColor = appTransition.getNextAppTransitionBackgroundColor();
}
final Rect localBounds = new Rect(mTmpRect);
localBounds.offsetTo(mTmpPoint.x, mTmpPoint.y);
diff --git a/services/core/java/com/android/server/wm/WindowOrganizerController.java b/services/core/java/com/android/server/wm/WindowOrganizerController.java
index 738adc3..6c46451 100644
--- a/services/core/java/com/android/server/wm/WindowOrganizerController.java
+++ b/services/core/java/com/android/server/wm/WindowOrganizerController.java
@@ -148,7 +148,8 @@
@VisibleForTesting
final ArrayMap<IBinder, TaskFragment> mLaunchTaskFragments = new ArrayMap<>();
- private final Rect mTmpBounds = new Rect();
+ private final Rect mTmpBounds0 = new Rect();
+ private final Rect mTmpBounds1 = new Rect();
WindowOrganizerController(ActivityTaskManagerService atm) {
mService = atm;
@@ -797,14 +798,15 @@
// When the TaskFragment is resized, we may want to create a change transition for it, for
// which we want to defer the surface update until we determine whether or not to start
// change transition.
- mTmpBounds.set(taskFragment.getBounds());
+ mTmpBounds0.set(taskFragment.getBounds());
+ mTmpBounds1.set(taskFragment.getRelativeEmbeddedBounds());
taskFragment.deferOrganizedTaskFragmentSurfaceUpdate();
final int effects = applyChanges(taskFragment, c, errorCallbackToken);
- if (taskFragment.shouldStartChangeTransition(mTmpBounds)) {
- taskFragment.initializeChangeTransition(mTmpBounds);
+ taskFragment.updateRelativeEmbeddedBounds();
+ if (taskFragment.shouldStartChangeTransition(mTmpBounds0, mTmpBounds1)) {
+ taskFragment.initializeChangeTransition(mTmpBounds0);
}
taskFragment.continueOrganizedTaskFragmentSurfaceUpdate();
- mTmpBounds.set(0, 0, 0, 0);
return effects;
}
diff --git a/services/core/java/com/android/server/wm/WindowProcessController.java b/services/core/java/com/android/server/wm/WindowProcessController.java
index 8f63e93..d2cd8f8 100644
--- a/services/core/java/com/android/server/wm/WindowProcessController.java
+++ b/services/core/java/com/android/server/wm/WindowProcessController.java
@@ -765,7 +765,7 @@
// - no longer visible OR
// - not focusable (in PiP mode for instance)
if (topDisplay == null
- || !mPreQTopResumedActivity.mVisibleRequested
+ || !mPreQTopResumedActivity.isVisibleRequested()
|| !mPreQTopResumedActivity.isFocusable()) {
canUpdate = true;
}
@@ -874,7 +874,7 @@
// to those activities that are part of the package whose app-specific settings changed
if (packageName.equals(r.packageName)
&& r.applyAppSpecificConfig(nightMode, localesOverride)
- && r.mVisibleRequested) {
+ && r.isVisibleRequested()) {
r.ensureActivityConfiguration(0 /* globalChanges */, true /* preserveWindow */);
}
}
@@ -956,7 +956,7 @@
}
// Don't consider any activities that are currently not in a state where they
// can be destroyed.
- if (r.mVisibleRequested || !r.stopped || !r.hasSavedState() || !r.isDestroyable()
+ if (r.isVisibleRequested() || !r.stopped || !r.hasSavedState() || !r.isDestroyable()
|| r.isState(STARTED, RESUMED, PAUSING, PAUSED, STOPPING)) {
if (DEBUG_RELEASE) Slog.d(TAG_RELEASE, "Not releasing in-use activity: " + r);
continue;
@@ -1002,7 +1002,7 @@
final int displayId = r.getDisplayId();
final Context c = root.getDisplayUiContext(displayId);
- if (c != null && r.mVisibleRequested && !displayContexts.contains(c)) {
+ if (c != null && r.isVisibleRequested() && !displayContexts.contains(c)) {
displayContexts.add(c);
}
}
@@ -1070,7 +1070,7 @@
if (task != null && task.mLayerRank != Task.LAYER_RANK_INVISIBLE) {
stateFlags |= ACTIVITY_STATE_FLAG_HAS_ACTIVITY_IN_VISIBLE_TASK;
}
- if (r.mVisibleRequested) {
+ if (r.isVisibleRequested()) {
if (r.isState(RESUMED)) {
stateFlags |= ACTIVITY_STATE_FLAG_HAS_RESUMED;
}
@@ -1282,7 +1282,7 @@
}
for (int i = activities.size() - 1; i >= 0; i--) {
final ActivityRecord r = activities.get(i);
- if (r.mVisibleRequested || r.isVisible()) {
+ if (r.isVisibleRequested() || r.isVisible()) {
// While an activity launches a new activity, it's possible that the old activity
// is already requested to be hidden (mVisibleRequested=false), but this visibility
// is not yet committed, so isVisible()=true.
@@ -1503,7 +1503,7 @@
Configuration overrideConfig = new Configuration(r.getRequestedOverrideConfiguration());
overrideConfig.assetsSeq = assetSeq;
r.onRequestedOverrideConfigurationChanged(overrideConfig);
- if (r.mVisibleRequested) {
+ if (r.isVisibleRequested()) {
r.ensureActivityConfiguration(0, true);
}
}
diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java
index 73759d3..1b7bd9e 100644
--- a/services/core/java/com/android/server/wm/WindowState.java
+++ b/services/core/java/com/android/server/wm/WindowState.java
@@ -1957,7 +1957,7 @@
*/
// TODO: Can we consolidate this with #isVisible() or have a more appropriate name for this?
boolean isWinVisibleLw() {
- return (mActivityRecord == null || mActivityRecord.mVisibleRequested
+ return (mActivityRecord == null || mActivityRecord.isVisibleRequested()
|| mActivityRecord.isAnimating(TRANSITION | PARENTS)) && isVisible();
}
@@ -1994,7 +1994,7 @@
final ActivityRecord atoken = mActivityRecord;
return (mHasSurface || (!mRelayoutCalled && mViewVisibility == View.VISIBLE))
&& isVisibleByPolicy() && !isParentWindowHidden()
- && (atoken == null || atoken.mVisibleRequested)
+ && (atoken == null || atoken.isVisibleRequested())
&& !mAnimatingExit && !mDestroying;
}
@@ -2090,7 +2090,10 @@
} else {
final Task task = getTask();
final boolean canFromTask = task != null && task.canAffectSystemUiFlags();
- return canFromTask && mActivityRecord.isVisible();
+ return canFromTask && mActivityRecord.isVisible()
+ // Do not let snapshot window control the bar
+ && (mAttrs.type != TYPE_APPLICATION_STARTING
+ || !(mStartingData instanceof SnapshotStartingData));
}
}
@@ -2101,7 +2104,7 @@
boolean isDisplayed() {
final ActivityRecord atoken = mActivityRecord;
return isDrawn() && isVisibleByPolicy()
- && ((!isParentWindowHidden() && (atoken == null || atoken.mVisibleRequested))
+ && ((!isParentWindowHidden() && (atoken == null || atoken.isVisibleRequested()))
|| isAnimating(TRANSITION | PARENTS));
}
@@ -2123,7 +2126,7 @@
// a layout since they can request relayout when client visibility is false.
// TODO (b/157682066) investigate if we can clean up isVisible
|| (atoken == null && !(wouldBeVisibleIfPolicyIgnored() && isVisibleByPolicy()))
- || (atoken != null && !atoken.mVisibleRequested)
+ || (atoken != null && !atoken.isVisibleRequested())
|| isParentWindowGoneForLayout()
|| (mAnimatingExit && !isAnimatingLw())
|| mDestroying;
@@ -2170,7 +2173,7 @@
return;
}
if (mActivityRecord != null) {
- if (!mActivityRecord.mVisibleRequested) return;
+ if (!mActivityRecord.isVisibleRequested()) return;
if (mActivityRecord.allDrawn) {
// The allDrawn of activity is reset when the visibility is changed to visible, so
// the content should be ready if allDrawn is set.
@@ -2743,7 +2746,7 @@
+ " exiting=" + mAnimatingExit + " destroying=" + mDestroying);
if (mActivityRecord != null) {
Slog.i(TAG_WM, " mActivityRecord.visibleRequested="
- + mActivityRecord.mVisibleRequested);
+ + mActivityRecord.isVisibleRequested());
}
}
}
@@ -3219,7 +3222,7 @@
}
return !mActivityRecord.getTask().getRootTask().shouldIgnoreInput()
- && mActivityRecord.mVisibleRequested;
+ && mActivityRecord.isVisibleRequested();
}
/**
@@ -3875,7 +3878,7 @@
// the client erroneously accepting a configuration that would have otherwise caused an
// activity restart. We instead hand back the last reported {@link MergedConfiguration}.
if (useLatestConfig || (relayoutVisible && (mActivityRecord == null
- || mActivityRecord.mVisibleRequested))) {
+ || mActivityRecord.isVisibleRequested()))) {
final Configuration globalConfig = getProcessGlobalConfiguration();
final Configuration overrideConfig = getMergedOverrideConfiguration();
outMergedConfiguration.setConfiguration(globalConfig, overrideConfig);
@@ -4734,7 +4737,7 @@
+ " during animation: policyVis=" + isVisibleByPolicy()
+ " parentHidden=" + isParentWindowHidden()
+ " tok.visibleRequested="
- + (mActivityRecord != null && mActivityRecord.mVisibleRequested)
+ + (mActivityRecord != null && mActivityRecord.isVisibleRequested())
+ " tok.visible=" + (mActivityRecord != null && mActivityRecord.isVisible())
+ " animating=" + isAnimating(TRANSITION | PARENTS)
+ " tok animating="
@@ -5195,7 +5198,7 @@
+ " pv=" + isVisibleByPolicy()
+ " mDrawState=" + mWinAnimator.mDrawState
+ " ph=" + isParentWindowHidden()
- + " th=" + (mActivityRecord != null && mActivityRecord.mVisibleRequested)
+ + " th=" + (mActivityRecord != null && mActivityRecord.isVisibleRequested())
+ " a=" + isAnimating(TRANSITION | PARENTS));
}
}
diff --git a/services/core/jni/Android.bp b/services/core/jni/Android.bp
index e661688..07819b9 100644
--- a/services/core/jni/Android.bp
+++ b/services/core/jni/Android.bp
@@ -71,6 +71,7 @@
"com_android_server_PersistentDataBlockService.cpp",
"com_android_server_am_LowMemDetector.cpp",
"com_android_server_pm_PackageManagerShellCommandDataLoader.cpp",
+ "com_android_server_pm_Settings.cpp",
"com_android_server_sensor_SensorService.cpp",
"com_android_server_wm_TaskFpsCallbackController.cpp",
"onload.cpp",
@@ -152,6 +153,7 @@
"libpsi",
"libdataloader",
"libincfs",
+ "liblz4",
"android.hardware.audio.common@2.0",
"android.media.audio.common.types-V1-ndk",
"android.hardware.broadcastradio@1.0",
@@ -232,3 +234,26 @@
"com_android_server_app_GameManagerService.cpp",
],
}
+
+// Settings JNI library for unit tests.
+cc_library_shared {
+ name: "libservices.core.settings.testonly",
+ defaults: ["libservices.core-libs"],
+
+ cpp_std: "c++2a",
+ cflags: [
+ "-Wall",
+ "-Werror",
+ "-Wno-unused-parameter",
+ "-Wthread-safety",
+ ],
+
+ srcs: [
+ "com_android_server_pm_Settings.cpp",
+ "onload_settings.cpp",
+ ],
+
+ header_libs: [
+ "bionic_libc_platform_headers",
+ ],
+}
diff --git a/services/core/jni/com_android_server_input_InputManagerService.cpp b/services/core/jni/com_android_server_input_InputManagerService.cpp
index 969056e..145e088 100644
--- a/services/core/jni/com_android_server_input_InputManagerService.cpp
+++ b/services/core/jni/com_android_server_input_InputManagerService.cpp
@@ -1578,6 +1578,12 @@
return vec;
}
+static void nativeAddKeyRemapping(JNIEnv* env, jobject nativeImplObj, jint deviceId,
+ jint fromKeyCode, jint toKeyCode) {
+ NativeInputManager* im = getNativeInputManager(env, nativeImplObj);
+ im->getInputManager()->getReader().addKeyRemapping(deviceId, fromKeyCode, toKeyCode);
+}
+
static jboolean nativeHasKeys(JNIEnv* env, jobject nativeImplObj, jint deviceId, jint sourceMask,
jintArray keyCodes, jbooleanArray outFlags) {
NativeInputManager* im = getNativeInputManager(env, nativeImplObj);
@@ -2360,6 +2366,7 @@
{"getScanCodeState", "(III)I", (void*)nativeGetScanCodeState},
{"getKeyCodeState", "(III)I", (void*)nativeGetKeyCodeState},
{"getSwitchState", "(III)I", (void*)nativeGetSwitchState},
+ {"addKeyRemapping", "(III)V", (void*)nativeAddKeyRemapping},
{"hasKeys", "(II[I[Z)Z", (void*)nativeHasKeys},
{"getKeyCodeForKeyLocation", "(II)I", (void*)nativeGetKeyCodeForKeyLocation},
{"createInputChannel", "(Ljava/lang/String;)Landroid/view/InputChannel;",
diff --git a/services/core/jni/com_android_server_pm_Settings.cpp b/services/core/jni/com_android_server_pm_Settings.cpp
new file mode 100644
index 0000000..9633a11
--- /dev/null
+++ b/services/core/jni/com_android_server_pm_Settings.cpp
@@ -0,0 +1,161 @@
+/*
+ * 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.
+ */
+
+#define ATRACE_TAG ATRACE_TAG_ADB
+#define LOG_TAG "Settings-jni"
+#include <android-base/file.h>
+#include <android-base/logging.h>
+#include <android-base/no_destructor.h>
+#include <core_jni_helpers.h>
+#include <lz4frame.h>
+#include <nativehelper/JNIHelp.h>
+
+#include <vector>
+
+namespace android {
+
+namespace {
+
+struct LZ4FCContextDeleter {
+ void operator()(LZ4F_cctx* cctx) { LZ4F_freeCompressionContext(cctx); }
+};
+
+static constexpr int LZ4_BUFFER_SIZE = 64 * 1024;
+
+static bool writeToFile(std::vector<char>& outBuffer, int fdOut) {
+ if (!android::base::WriteFully(fdOut, outBuffer.data(), outBuffer.size())) {
+ PLOG(ERROR) << "Error to write to output file";
+ return false;
+ }
+ outBuffer.clear();
+ return true;
+}
+
+static bool compressAndWriteLz4(LZ4F_cctx* context, std::vector<char>& inBuffer,
+ std::vector<char>& outBuffer, int fdOut) {
+ auto inSize = inBuffer.size();
+ if (inSize > 0) {
+ auto prvSize = outBuffer.size();
+ auto outSize = LZ4F_compressBound(inSize, nullptr);
+ outBuffer.resize(prvSize + outSize);
+ auto rc = LZ4F_compressUpdate(context, outBuffer.data() + prvSize, outSize, inBuffer.data(),
+ inSize, nullptr);
+ if (LZ4F_isError(rc)) {
+ LOG(ERROR) << "LZ4F_compressUpdate failed: " << LZ4F_getErrorName(rc);
+ return false;
+ }
+ outBuffer.resize(prvSize + rc);
+ }
+
+ if (outBuffer.size() > LZ4_BUFFER_SIZE) {
+ return writeToFile(outBuffer, fdOut);
+ }
+
+ return true;
+}
+
+static jboolean nativeCompressLz4(JNIEnv* env, jclass klass, jint fdIn, jint fdOut) {
+ LZ4F_cctx* cctx;
+ if (LZ4F_createCompressionContext(&cctx, LZ4F_VERSION) != 0) {
+ LOG(ERROR) << "Failed to initialize LZ4 compression context.";
+ return false;
+ }
+ std::unique_ptr<LZ4F_cctx, LZ4FCContextDeleter> context(cctx);
+
+ std::vector<char> inBuffer, outBuffer;
+ inBuffer.reserve(LZ4_BUFFER_SIZE);
+ outBuffer.reserve(2 * LZ4_BUFFER_SIZE);
+
+ LZ4F_preferences_t prefs;
+
+ memset(&prefs, 0, sizeof(prefs));
+
+ // Set compression parameters.
+ prefs.autoFlush = 0;
+ prefs.compressionLevel = 0;
+ prefs.frameInfo.blockMode = LZ4F_blockLinked;
+ prefs.frameInfo.blockSizeID = LZ4F_default;
+ prefs.frameInfo.blockChecksumFlag = LZ4F_noBlockChecksum;
+ prefs.frameInfo.contentChecksumFlag = LZ4F_contentChecksumEnabled;
+ prefs.favorDecSpeed = 0;
+
+ struct stat sb;
+ if (fstat(fdIn, &sb) == -1) {
+ PLOG(ERROR) << "Failed to obtain input file size.";
+ return false;
+ }
+ prefs.frameInfo.contentSize = sb.st_size;
+
+ // Write header first.
+ outBuffer.resize(LZ4F_HEADER_SIZE_MAX);
+ auto rc = LZ4F_compressBegin(context.get(), outBuffer.data(), outBuffer.size(), &prefs);
+ if (LZ4F_isError(rc)) {
+ LOG(ERROR) << "LZ4F_compressBegin failed: " << LZ4F_getErrorName(rc);
+ return false;
+ }
+ outBuffer.resize(rc);
+
+ bool eof = false;
+ while (!eof) {
+ constexpr auto capacity = LZ4_BUFFER_SIZE;
+ inBuffer.resize(capacity);
+ auto read = TEMP_FAILURE_RETRY(::read(fdIn, inBuffer.data(), inBuffer.size()));
+ if (read < 0) {
+ PLOG(ERROR) << "Failed to read from input file.";
+ return false;
+ }
+
+ inBuffer.resize(read);
+
+ if (read == 0) {
+ eof = true;
+ }
+
+ if (!compressAndWriteLz4(context.get(), inBuffer, outBuffer, fdOut)) {
+ return false;
+ }
+ }
+
+ // Footer.
+ auto prvSize = outBuffer.size();
+ outBuffer.resize(outBuffer.capacity());
+ rc = LZ4F_compressEnd(context.get(), outBuffer.data() + prvSize, outBuffer.size() - prvSize,
+ nullptr);
+ if (LZ4F_isError(rc)) {
+ LOG(ERROR) << "LZ4F_compressEnd failed: " << LZ4F_getErrorName(rc);
+ return false;
+ }
+ outBuffer.resize(prvSize + rc);
+
+ if (!writeToFile(outBuffer, fdOut)) {
+ return false;
+ }
+
+ return true;
+}
+
+static const JNINativeMethod method_table[] = {
+ {"nativeCompressLz4", "(II)Z", (void*)nativeCompressLz4},
+};
+
+} // namespace
+
+int register_android_server_com_android_server_pm_Settings(JNIEnv* env) {
+ return jniRegisterNativeMethods(env, "com/android/server/pm/Settings", method_table,
+ NELEM(method_table));
+}
+
+} // namespace android
diff --git a/services/core/jni/onload.cpp b/services/core/jni/onload.cpp
index 00f851f..1845057 100644
--- a/services/core/jni/onload.cpp
+++ b/services/core/jni/onload.cpp
@@ -56,6 +56,7 @@
int register_com_android_server_soundtrigger_middleware_AudioSessionProviderImpl(JNIEnv* env);
int register_com_android_server_soundtrigger_middleware_ExternalCaptureStateTracker(JNIEnv* env);
int register_android_server_com_android_server_pm_PackageManagerShellCommandDataLoader(JNIEnv* env);
+int register_android_server_com_android_server_pm_Settings(JNIEnv* env);
int register_android_server_AdbDebuggingManager(JNIEnv* env);
int register_android_server_FaceService(JNIEnv* env);
int register_android_server_GpuService(JNIEnv* env);
@@ -114,6 +115,7 @@
register_com_android_server_soundtrigger_middleware_AudioSessionProviderImpl(env);
register_com_android_server_soundtrigger_middleware_ExternalCaptureStateTracker(env);
register_android_server_com_android_server_pm_PackageManagerShellCommandDataLoader(env);
+ register_android_server_com_android_server_pm_Settings(env);
register_android_server_AdbDebuggingManager(env);
register_android_server_FaceService(env);
register_android_server_GpuService(env);
diff --git a/services/core/jni/onload_settings.cpp b/services/core/jni/onload_settings.cpp
new file mode 100644
index 0000000..b21c34a
--- /dev/null
+++ b/services/core/jni/onload_settings.cpp
@@ -0,0 +1,39 @@
+/*
+ * 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.
+ */
+
+#include "jni.h"
+#include "utils/Log.h"
+
+namespace android {
+int register_android_server_com_android_server_pm_Settings(JNIEnv* env);
+};
+
+using namespace android;
+
+extern "C" jint JNI_OnLoad(JavaVM* vm, void* /* reserved */) {
+ JNIEnv* env = NULL;
+ jint result = -1;
+
+ if (vm->GetEnv((void**)&env, JNI_VERSION_1_4) != JNI_OK) {
+ ALOGE("GetEnv failed!");
+ return result;
+ }
+ ALOG_ASSERT(env, "Could not retrieve the env!");
+
+ register_android_server_com_android_server_pm_Settings(env);
+
+ return JNI_VERSION_1_4;
+}
diff --git a/services/core/jni/tvinput/JTvInputHal.cpp b/services/core/jni/tvinput/JTvInputHal.cpp
index 0f6ef03..3453cbd 100644
--- a/services/core/jni/tvinput/JTvInputHal.cpp
+++ b/services/core/jni/tvinput/JTvInputHal.cpp
@@ -42,8 +42,11 @@
std::shared_ptr<ITvInputWrapper>(new ITvInputWrapper(hidlITvInput)),
looper);
}
- ::ndk::SpAIBinder binder(AServiceManager_waitForService(TV_INPUT_AIDL_SERVICE_NAME));
- std::shared_ptr<AidlITvInput> aidlITvInput = AidlITvInput::fromBinder(binder);
+ std::shared_ptr<AidlITvInput> aidlITvInput = nullptr;
+ if (AServiceManager_isDeclared(TV_INPUT_AIDL_SERVICE_NAME)) {
+ ::ndk::SpAIBinder binder(AServiceManager_waitForService(TV_INPUT_AIDL_SERVICE_NAME));
+ aidlITvInput = AidlITvInput::fromBinder(binder);
+ }
if (aidlITvInput == nullptr) {
ALOGE("Couldn't get tv.input service.");
return nullptr;
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
index 775e3d8..1ec2438 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
@@ -718,6 +718,16 @@
private static final String ENABLE_COEXISTENCE_FLAG = "enable_coexistence";
private static final boolean DEFAULT_ENABLE_COEXISTENCE_FLAG = false;
+ // TODO(b/258425381) remove the flag after rollout.
+ private static final String KEEP_PROFILES_RUNNING_FLAG = "enable_keep_profiles_running";
+ private static final boolean DEFAULT_KEEP_PROFILES_RUNNING_FLAG = false;
+
+ /**
+ * This feature flag is checked once after boot and this value us used until the next reboot to
+ * avoid needing to handle the flag changing on the fly.
+ */
+ private final boolean mKeepProfilesRunning = isKeepProfilesRunningFlagEnabled();
+
/**
* For apps targeting U+
* Enable multiple admins to coexist on the same device.
@@ -9892,6 +9902,8 @@
(size == 1 ? "" : "s"));
}
pw.println();
+ pw.println("Keep profiles running: " + mKeepProfilesRunning);
+ pw.println();
mPolicyCache.dump(pw);
pw.println();
@@ -13533,6 +13545,11 @@
}
}
+ @Override
+ public boolean isKeepProfilesRunningEnabled() {
+ return mKeepProfilesRunning;
+ }
+
private @Mode int findInteractAcrossProfilesResetMode(String packageName) {
return getDefaultCrossProfilePackages().contains(packageName)
? AppOpsManager.MODE_ALLOWED
@@ -19167,4 +19184,11 @@
ENABLE_COEXISTENCE_FLAG,
DEFAULT_ENABLE_COEXISTENCE_FLAG);
}
+
+ private static boolean isKeepProfilesRunningFlagEnabled() {
+ return DeviceConfig.getBoolean(
+ NAMESPACE_DEVICE_POLICY_MANAGER,
+ KEEP_PROFILES_RUNNING_FLAG,
+ DEFAULT_KEEP_PROFILES_RUNNING_FLAG);
+ }
}
diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java
index c346b2f..e41e781 100644
--- a/services/java/com/android/server/SystemServer.java
+++ b/services/java/com/android/server/SystemServer.java
@@ -187,6 +187,7 @@
import com.android.server.security.FileIntegrityService;
import com.android.server.security.KeyAttestationApplicationIdProviderService;
import com.android.server.security.KeyChainSystemService;
+import com.android.server.security.rkp.RemoteProvisioningService;
import com.android.server.sensorprivacy.SensorPrivacyService;
import com.android.server.sensors.SensorService;
import com.android.server.signedconfig.SignedConfigService;
@@ -211,6 +212,7 @@
import com.android.server.utils.TimingsTraceAndSlog;
import com.android.server.vibrator.VibratorManagerService;
import com.android.server.vr.VrManagerService;
+import com.android.server.wearable.WearableSensingManagerService;
import com.android.server.webkit.WebViewUpdateService;
import com.android.server.wm.ActivityTaskManagerService;
import com.android.server.wm.WindowManagerGlobalLock;
@@ -1360,11 +1362,16 @@
mSystemServiceManager.startService(BugreportManagerService.class);
t.traceEnd();
- // Serivce for GPU and GPU driver.
+ // Service for GPU and GPU driver.
t.traceBegin("GpuService");
mSystemServiceManager.startService(GpuService.class);
t.traceEnd();
+ // Handles system process requests for remotely provisioned keys & data.
+ t.traceBegin("StartRemoteProvisioningService");
+ mSystemServiceManager.startService(RemoteProvisioningService.class);
+ t.traceEnd();
+
t.traceEnd(); // startCoreServices
}
@@ -1830,6 +1837,7 @@
startSystemCaptionsManagerService(context, t);
startTextToSpeechManagerService(context, t);
startAmbientContextService(t);
+ startWearableSensingService(t);
// System Speech Recognition Service
t.traceBegin("StartSpeechRecognitionManagerService");
@@ -3170,6 +3178,12 @@
t.traceEnd();
}
+ private void startWearableSensingService(@NonNull TimingsTraceAndSlog t) {
+ t.traceBegin("startWearableSensingService");
+ mSystemServiceManager.startService(WearableSensingManagerService.class);
+ t.traceEnd();
+ }
+
private static void startSystemUi(Context context, WindowManagerService windowManager) {
PackageManagerInternal pm = LocalServices.getService(PackageManagerInternal.class);
Intent intent = new Intent();
diff --git a/services/tests/InputMethodSystemServerTests/Android.bp b/services/tests/InputMethodSystemServerTests/Android.bp
index 939fb6a..70a5c3f 100644
--- a/services/tests/InputMethodSystemServerTests/Android.bp
+++ b/services/tests/InputMethodSystemServerTests/Android.bp
@@ -28,7 +28,7 @@
],
srcs: [
- "src/**/*.java",
+ "src/server/**/*.java",
],
static_libs: [
@@ -60,3 +60,52 @@
enabled: false,
},
}
+
+android_test {
+ name: "FrameworksImeTests",
+ defaults: [
+ "modules-utils-testable-device-config-defaults",
+ ],
+
+ srcs: [
+ "src/com/android/inputmethodservice/**/*.java",
+ ],
+
+ manifest: "src/com/android/inputmethodservice/AndroidManifest.xml",
+ test_config: "src/com/android/inputmethodservice/AndroidTest.xml",
+
+ static_libs: [
+ "androidx.test.core",
+ "androidx.test.runner",
+ "androidx.test.espresso.core",
+ "androidx.test.espresso.contrib",
+ "androidx.test.ext.truth",
+ "frameworks-base-testutils",
+ "mockito-target-extended-minus-junit4",
+ "platform-test-annotations",
+ "services.core",
+ "servicestests-core-utils",
+ "servicestests-utils-mockito-extended",
+ "truth-prebuilt",
+ "SimpleImeTestingLib",
+ "SimpleImeImsLib",
+ ],
+
+ libs: [
+ "android.test.mock",
+ "android.test.base",
+ "android.test.runner",
+ ],
+
+ data: [
+ ":SimpleTestIme",
+ ],
+
+ certificate: "platform",
+ platform_apis: true,
+ test_suites: ["device-tests"],
+
+ optimize: {
+ enabled: false,
+ },
+}
diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/inputmethodservice/AndroidManifest.xml b/services/tests/InputMethodSystemServerTests/src/com/android/inputmethodservice/AndroidManifest.xml
new file mode 100644
index 0000000..0104f71
--- /dev/null
+++ b/services/tests/InputMethodSystemServerTests/src/com/android/inputmethodservice/AndroidManifest.xml
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2022 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.inputmethod.imetests">
+
+ <uses-sdk android:targetSdkVersion="31" />
+
+ <!-- Permissions required for granting and logging -->
+ <uses-permission android:name="android.permission.LOG_COMPAT_CHANGE"/>
+ <uses-permission android:name="android.permission.READ_COMPAT_CHANGE_CONFIG"/>
+ <uses-permission android:name="android.permission.OVERRIDE_COMPAT_CHANGE_CONFIG"/>
+ <uses-permission android:name="android.permission.OVERRIDE_COMPAT_CHANGE_CONFIG_ON_RELEASE_BUILD"/>
+
+ <!-- Permissions for reading system info -->
+ <uses-permission android:name="android.permission.READ_DEVICE_CONFIG" />
+ <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS_FULL" />
+
+ <application android:debuggable="true">
+ <uses-library android:name="android.test.runner" />
+ </application>
+
+ <!-- The "targetPackage" reference the instruments APK package, which is the FakeImeApk, while
+ the test package is "com.android.inputmethod.imetests" (FrameworksImeTests.apk).-->
+ <instrumentation
+ android:name="androidx.test.runner.AndroidJUnitRunner"
+ android:targetPackage="com.android.apps.inputmethod.simpleime"
+ android:label="Frameworks IME Tests" />
+</manifest>
diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/inputmethodservice/AndroidTest.xml b/services/tests/InputMethodSystemServerTests/src/com/android/inputmethodservice/AndroidTest.xml
new file mode 100644
index 0000000..6c24d6d
--- /dev/null
+++ b/services/tests/InputMethodSystemServerTests/src/com/android/inputmethodservice/AndroidTest.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2022 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<configuration description="Runs Frameworks IME Tests.">
+ <option name="test-suite-tag" value="apct" />
+ <option name="test-suite-tag" value="apct-instrumentation" />
+
+ <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+ <option name="cleanup-apks" value="true" />
+ <option name="install-arg" value="-t" />
+ <option name="test-file-name" value="FrameworksImeTests.apk" />
+ <option name="test-file-name" value="SimpleTestIme.apk" />
+ </target_preparer>
+
+ <option name="test-tag" value="FrameworksImeTests" />
+
+ <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
+ <option name="package" value="com.android.inputmethod.imetests" />
+ <option name="runner" value="androidx.test.runner.AndroidJUnitRunner" />
+ <option name="hidden-api-checks" value="false"/>
+ </test>
+
+ <!-- Collect the files in the dump directory for debugging -->
+ <metrics_collector class="com.android.tradefed.device.metric.FilePullerLogCollector">
+ <option name="directory-keys" value="/sdcard/FrameworksImeTests/" />
+ <option name="collect-on-run-ended-only" value="true" />
+ </metrics_collector>
+</configuration>
diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/inputmethodservice/InputMethodServiceTest.java b/services/tests/InputMethodSystemServerTests/src/com/android/inputmethodservice/InputMethodServiceTest.java
new file mode 100644
index 0000000..16a9845
--- /dev/null
+++ b/services/tests/InputMethodSystemServerTests/src/com/android/inputmethodservice/InputMethodServiceTest.java
@@ -0,0 +1,276 @@
+/*
+ * 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.inputmethodservice;
+
+import static com.android.compatibility.common.util.SystemUtil.eventually;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.app.Instrumentation;
+import android.content.Context;
+import android.content.res.Configuration;
+import android.os.RemoteException;
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.UiDevice;
+import android.support.test.uiautomator.UiObject2;
+import android.support.test.uiautomator.Until;
+import android.util.Log;
+import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.InputMethodManager;
+import android.widget.EditText;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.MediumTest;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import com.android.apps.inputmethod.simpleime.ims.InputMethodServiceWrapper;
+import com.android.apps.inputmethod.simpleime.testing.TestActivity;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+@RunWith(AndroidJUnit4.class)
+@MediumTest
+public class InputMethodServiceTest {
+ private static final String TAG = "SimpleIMSTest";
+ private static final String INPUT_METHOD_SERVICE_NAME = ".SimpleInputMethodService";
+ private static final String EDIT_TEXT_DESC = "Input box";
+ private static final long TIMEOUT_IN_SECONDS = 3;
+
+ public Instrumentation mInstrumentation;
+ public UiDevice mUiDevice;
+ public Context mContext;
+ public String mTargetPackageName;
+ public TestActivity mActivity;
+ public EditText mEditText;
+ public InputMethodServiceWrapper mInputMethodService;
+ public String mInputMethodId;
+
+ @Before
+ public void setUp() throws Exception {
+ mInstrumentation = InstrumentationRegistry.getInstrumentation();
+ mUiDevice = UiDevice.getInstance(mInstrumentation);
+ mContext = mInstrumentation.getContext();
+ mTargetPackageName = mInstrumentation.getTargetContext().getPackageName();
+ mInputMethodId = getInputMethodId();
+ prepareIme();
+ prepareEditor();
+
+ // Waits for input binding ready.
+ eventually(
+ () -> {
+ mInputMethodService =
+ InputMethodServiceWrapper.getInputMethodServiceWrapperForTesting();
+ assertThat(mInputMethodService).isNotNull();
+
+ // The editor won't bring up keyboard by default.
+ assertThat(mInputMethodService.getCurrentInputStarted()).isTrue();
+ assertThat(mInputMethodService.getCurrentInputViewStarted()).isFalse();
+ });
+ }
+
+ @Test
+ public void testShowHideKeyboard_byUserAction() throws InterruptedException {
+ // Performs click on editor box to bring up the soft keyboard.
+ Log.i(TAG, "Click on EditText.");
+ verifyInputViewStatus(() -> clickOnEditorText(), true /* inputViewStarted */);
+
+ // Press back key to hide soft keyboard.
+ Log.i(TAG, "Press back");
+ verifyInputViewStatus(
+ () -> assertThat(mUiDevice.pressHome()).isTrue(), false /* inputViewStarted */);
+ }
+
+ @Test
+ public void testShowHideKeyboard_byApi() throws InterruptedException {
+ // Triggers to show IME via public API.
+ verifyInputViewStatus(
+ () -> assertThat(mActivity.showImeWithWindowInsetsController()).isTrue(),
+ true /* inputViewStarted */);
+
+ // Triggers to hide IME via public API.
+ // TODO(b/242838873): investigate why WIC#hide(ime()) does not work, likely related to
+ // triggered from IME process.
+ verifyInputViewStatusOnMainSync(
+ () -> assertThat(mActivity.hideImeWithInputMethodManager(0 /* flags */)).isTrue(),
+ false /* inputViewStarted */);
+ }
+
+ @Test
+ public void testShowHideSelf() throws InterruptedException {
+ // IME requests to show itself without any flags: expect shown.
+ Log.i(TAG, "Call IMS#requestShowSelf(0)");
+ verifyInputViewStatusOnMainSync(
+ () -> mInputMethodService.requestShowSelf(0), true /* inputViewStarted */);
+
+ // IME requests to hide itself with flag: HIDE_IMPLICIT_ONLY, expect not hide (shown).
+ Log.i(TAG, "Call IMS#requestHideSelf(InputMethodManager.HIDE_IMPLICIT_ONLY)");
+ verifyInputViewStatusOnMainSync(
+ () -> mInputMethodService.requestHideSelf(InputMethodManager.HIDE_IMPLICIT_ONLY),
+ true /* inputViewStarted */);
+
+ // IME request to hide itself without any flags: expect hidden.
+ Log.i(TAG, "Call IMS#requestHideSelf(0)");
+ verifyInputViewStatusOnMainSync(
+ () -> mInputMethodService.requestHideSelf(0), false /* inputViewStarted */);
+
+ // IME request to show itself with flag SHOW_IMPLICIT: expect shown.
+ Log.i(TAG, "Call IMS#requestShowSelf(InputMethodManager.SHOW_IMPLICIT)");
+ verifyInputViewStatusOnMainSync(
+ () -> mInputMethodService.requestShowSelf(InputMethodManager.SHOW_IMPLICIT),
+ true /* inputViewStarted */);
+
+ // IME request to hide itself with flag: HIDE_IMPLICIT_ONLY, expect hidden.
+ Log.i(TAG, "Call IMS#requestHideSelf(InputMethodManager.HIDE_IMPLICIT_ONLY)");
+ verifyInputViewStatusOnMainSync(
+ () -> mInputMethodService.requestHideSelf(InputMethodManager.HIDE_IMPLICIT_ONLY),
+ false /* inputViewStarted */);
+ }
+
+ private void verifyInputViewStatus(Runnable runnable, boolean inputViewStarted)
+ throws InterruptedException {
+ verifyInputViewStatusInternal(runnable, inputViewStarted, false /*runOnMainSync*/);
+ }
+
+ private void verifyInputViewStatusOnMainSync(Runnable runnable, boolean inputViewStarted)
+ throws InterruptedException {
+ verifyInputViewStatusInternal(runnable, inputViewStarted, true /*runOnMainSync*/);
+ }
+
+ private void verifyInputViewStatusInternal(
+ Runnable runnable, boolean inputViewStarted, boolean runOnMainSync)
+ throws InterruptedException {
+ CountDownLatch signal = new CountDownLatch(1);
+ mInputMethodService.setCountDownLatchForTesting(signal);
+ // Runnable to trigger onStartInputView()/ onFinishInputView()
+ if (runOnMainSync) {
+ mInstrumentation.runOnMainSync(runnable);
+ } else {
+ runnable.run();
+ }
+ // Waits for onStartInputView() to finish.
+ mInstrumentation.waitForIdleSync();
+ signal.await(TIMEOUT_IN_SECONDS, TimeUnit.SECONDS);
+ // Input is not finished.
+ assertThat(mInputMethodService.getCurrentInputStarted()).isTrue();
+ assertThat(mInputMethodService.getCurrentInputViewStarted()).isEqualTo(inputViewStarted);
+ }
+
+ @Test
+ public void testFullScreenMode() throws Exception {
+ Log.i(TAG, "Set orientation natural");
+ verifyFullscreenMode(() -> setOrientation(0), true /* orientationPortrait */);
+
+ Log.i(TAG, "Set orientation left");
+ verifyFullscreenMode(() -> setOrientation(1), false /* orientationPortrait */);
+
+ Log.i(TAG, "Set orientation right");
+ verifyFullscreenMode(() -> setOrientation(2), false /* orientationPortrait */);
+
+ mUiDevice.unfreezeRotation();
+ }
+
+ private void setOrientation(int orientation) {
+ // Simple wrapper for catching RemoteException.
+ try {
+ switch (orientation) {
+ case 1:
+ mUiDevice.setOrientationLeft();
+ break;
+ case 2:
+ mUiDevice.setOrientationRight();
+ break;
+ default:
+ mUiDevice.setOrientationNatural();
+ }
+ } catch (RemoteException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private void verifyFullscreenMode(Runnable runnable, boolean orientationPortrait)
+ throws InterruptedException {
+ CountDownLatch signal = new CountDownLatch(1);
+ mInputMethodService.setCountDownLatchForTesting(signal);
+
+ // Runnable to trigger onConfigurationChanged()
+ try {
+ runnable.run();
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ // Waits for onConfigurationChanged() to finish.
+ mInstrumentation.waitForIdleSync();
+ signal.await(TIMEOUT_IN_SECONDS, TimeUnit.SECONDS);
+
+ clickOnEditorText();
+ eventually(() -> assertThat(mInputMethodService.isInputViewShown()).isTrue());
+
+ assertThat(mInputMethodService.getResources().getConfiguration().orientation)
+ .isEqualTo(
+ orientationPortrait
+ ? Configuration.ORIENTATION_PORTRAIT
+ : Configuration.ORIENTATION_LANDSCAPE);
+ EditorInfo editorInfo = mInputMethodService.getCurrentInputEditorInfo();
+ assertThat(editorInfo.imeOptions & EditorInfo.IME_FLAG_NO_FULLSCREEN).isEqualTo(0);
+ assertThat(editorInfo.internalImeOptions & EditorInfo.IME_INTERNAL_FLAG_APP_WINDOW_PORTRAIT)
+ .isEqualTo(
+ orientationPortrait ? EditorInfo.IME_INTERNAL_FLAG_APP_WINDOW_PORTRAIT : 0);
+ assertThat(mInputMethodService.onEvaluateFullscreenMode()).isEqualTo(!orientationPortrait);
+ assertThat(mInputMethodService.isFullscreenMode()).isEqualTo(!orientationPortrait);
+
+ mUiDevice.pressBack();
+ }
+
+ private void prepareIme() throws Exception {
+ executeShellCommand("ime enable " + mInputMethodId);
+ executeShellCommand("ime set " + mInputMethodId);
+ mInstrumentation.waitForIdleSync();
+ Log.i(TAG, "Finish preparing IME");
+ }
+
+ private void prepareEditor() {
+ mActivity = TestActivity.start(mInstrumentation);
+ mEditText = mActivity.mEditText;
+ Log.i(TAG, "Finish preparing activity with editor.");
+ }
+
+ private String getInputMethodId() {
+ return mTargetPackageName + "/" + INPUT_METHOD_SERVICE_NAME;
+ }
+
+ private String executeShellCommand(String cmd) throws Exception {
+ Log.i(TAG, "Run command: " + cmd);
+ return UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
+ .executeShellCommand(cmd);
+ }
+
+ private void clickOnEditorText() {
+ // Find the editText and click it.
+ UiObject2 editTextUiObject =
+ mUiDevice.wait(
+ Until.findObject(By.desc(EDIT_TEXT_DESC)),
+ TimeUnit.SECONDS.toMillis(TIMEOUT_IN_SECONDS));
+ assertThat(editTextUiObject).isNotNull();
+ editTextUiObject.click();
+ mInstrumentation.waitForIdleSync();
+ }
+}
diff --git a/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/Android.bp b/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/Android.bp
new file mode 100644
index 0000000..ef50476
--- /dev/null
+++ b/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/Android.bp
@@ -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 {
+ // See: http://go/android-license-faq
+ // A large-scale-change added 'default_applicable_licenses' to import
+ // all of the 'license_kinds' from "frameworks_base_license"
+ // to get the below license kinds:
+ // SPDX-license-identifier-Apache-2.0
+ default_applicable_licenses: ["frameworks_base_license"],
+}
+
+android_test_helper_app {
+ name: "SimpleTestIme",
+
+ srcs: [
+ "src/com/android/apps/inputmethod/simpleime/*.java",
+ ],
+
+ static_libs: [
+ "SimpleImeImsLib",
+ "SimpleImeTestingLib",
+ ],
+ resource_dirs: ["res"],
+ manifest: "AndroidManifest.xml",
+
+ dex_preopt: {
+ enabled: false,
+ },
+ optimize: {
+ enabled: false,
+ },
+ export_package_resources: true,
+ sdk_version: "current",
+}
+
+android_library {
+ name: "SimpleImeImsLib",
+ srcs: [
+ "src/com/android/apps/inputmethod/simpleime/ims/*.java",
+ ],
+ sdk_version: "current",
+}
+
+android_library {
+ name: "SimpleImeTestingLib",
+ srcs: [
+ "src/com/android/apps/inputmethod/simpleime/testing/*.java",
+ ],
+ sdk_version: "current",
+}
diff --git a/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/AndroidManifest.xml b/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/AndroidManifest.xml
new file mode 100644
index 0000000..802caf1
--- /dev/null
+++ b/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/AndroidManifest.xml
@@ -0,0 +1,54 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2022 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.apps.inputmethod.simpleime">
+
+ <uses-sdk android:targetSdkVersion="31" />
+
+ <application android:debuggable="true"
+ android:label="@string/app_name">
+ <service
+ android:name="com.android.apps.inputmethod.simpleime.SimpleInputMethodService"
+ android:label="@string/app_name"
+ android:directBootAware="true"
+ android:permission="android.permission.BIND_INPUT_METHOD"
+ android:exported="true">
+
+ <meta-data
+ android:name="android.view.im"
+ android:resource="@xml/method"/>
+
+ <intent-filter>
+ <action android:name="android.view.InputMethod"/>
+ </intent-filter>
+ </service>
+
+ <!-- This is for test only. -->
+ <activity android:name="com.android.apps.inputmethod.simpleime.testing.TestActivity"
+ android:exported="false"
+ android:label="TestActivity"
+ android:launchMode="singleInstance"
+ android:excludeFromRecents="true"
+ android:noHistory="true"
+ android:taskAffinity="">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+ </intent-filter>
+ </activity>
+ </application>
+</manifest>
\ No newline at end of file
diff --git a/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/res/drawable/key_border.xml b/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/res/drawable/key_border.xml
new file mode 100644
index 0000000..dbfcc30
--- /dev/null
+++ b/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/res/drawable/key_border.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2022 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="rectangle" >
+ <solid
+ android:color="#FAFAFA" >
+ </solid>
+ <stroke
+ android:width="1dp"
+ android:color="#0F000000" >
+ </stroke>
+ <corners
+ android:radius="2dp" >
+ </corners>
+</shape>
\ No newline at end of file
diff --git a/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/res/layout/input_view.xml b/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/res/layout/input_view.xml
new file mode 100644
index 0000000..f229270
--- /dev/null
+++ b/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/res/layout/input_view.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2022 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<FrameLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/input"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"/>
\ No newline at end of file
diff --git a/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/res/layout/qwerty_10_9_9.xml b/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/res/layout/qwerty_10_9_9.xml
new file mode 100644
index 0000000..ee94ea9
--- /dev/null
+++ b/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/res/layout/qwerty_10_9_9.xml
@@ -0,0 +1,200 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2022 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ style="@style/KeyboardArea">
+
+ <View style="@style/KeyboardRow.Header"/>
+
+ <LinearLayout style="@style/KeyboardRow">
+ <TextView
+ android:id="@+id/key_pos_0_0"
+ android:text="q"
+ android:tag="KEYCODE_Q"
+ style="@style/SoftKey"/>
+ <TextView
+ android:id="@+id/key_pos_0_1"
+ android:text="w"
+ android:tag="KEYCODE_W"
+ style="@style/SoftKey"/>
+ <TextView
+ android:id="@+id/key_pos_0_2"
+ android:text="e"
+ android:tag="KEYCODE_E"
+ style="@style/SoftKey"/>
+ <TextView
+ android:id="@+id/key_pos_0_3"
+ android:text="r"
+ android:tag="KEYCODE_R"
+ style="@style/SoftKey"/>
+ <TextView
+ android:id="@+id/key_pos_0_4"
+ android:text="t"
+ android:tag="KEYCODE_T"
+ style="@style/SoftKey"/>
+ <TextView
+ android:id="@+id/key_pos_0_5"
+ android:text="y"
+ android:tag="KEYCODE_Y"
+ style="@style/SoftKey"/>
+ <TextView
+ android:id="@+id/key_pos_0_6"
+ android:text="u"
+ android:tag="KEYCODE_U"
+ style="@style/SoftKey"/>
+ <TextView
+ android:id="@+id/key_pos_0_7"
+ android:text="i"
+ android:tag="KEYCODE_I"
+ style="@style/SoftKey"/>
+ <TextView
+ android:id="@+id/key_pos_0_8"
+ android:text="o"
+ android:tag="KEYCODE_O"
+ style="@style/SoftKey"/>
+ <TextView
+ android:id="@+id/key_pos_0_9"
+ android:text="p"
+ android:tag="KEYCODE_P"
+ style="@style/SoftKey"/>
+ </LinearLayout>
+
+ <LinearLayout style="@style/KeyboardRow">
+ <TextView
+ android:id="@+id/key_pos_1_0"
+ android:text="a"
+ android:tag="KEYCODE_A"
+ style="@style/SoftKey"/>
+ <TextView
+ android:id="@+id/key_pos_1_1"
+ android:text="s"
+ android:tag="KEYCODE_S"
+ style="@style/SoftKey"/>
+ <TextView
+ android:id="@+id/key_pos_1_2"
+ android:text="d"
+ android:tag="KEYCODE_D"
+ style="@style/SoftKey"/>
+ <TextView
+ android:id="@+id/key_pos_1_3"
+ android:text="f"
+ android:tag="KEYCODE_F"
+ style="@style/SoftKey"/>
+ <TextView
+ android:id="@+id/key_pos_1_4"
+ android:text="g"
+ android:tag="KEYCODE_G"
+ style="@style/SoftKey"/>
+ <TextView
+ android:id="@+id/key_pos_1_5"
+ android:text="h"
+ android:tag="KEYCODE_H"
+ style="@style/SoftKey"/>
+ <TextView
+ android:id="@+id/key_pos_1_6"
+ android:text="j"
+ android:tag="KEYCODE_J"
+ style="@style/SoftKey"/>
+ <TextView
+ android:id="@+id/key_pos_1_7"
+ android:text="k"
+ android:tag="KEYCODE_K"
+ style="@style/SoftKey"/>
+ <TextView
+ android:id="@+id/key_pos_1_8"
+ android:text="l"
+ android:tag="KEYCODE_L"
+ style="@style/SoftKey"/>
+ </LinearLayout>
+
+ <LinearLayout style="@style/KeyboardRow">
+ <TextView
+ android:id="@+id/key_pos_shift"
+ android:text="SHI"
+ android:tag="KEYCODE_SHIFT"
+ style="@style/SoftKey.Function"/>
+ <TextView
+ android:id="@+id/key_pos_2_0"
+ android:text="z"
+ android:tag="KEYCODE_Z"
+ style="@style/SoftKey"/>
+ <TextView
+ android:id="@+id/key_pos_2_1"
+ android:text="x"
+ android:tag="KEYCODE_X"
+ style="@style/SoftKey"/>
+ <TextView
+ android:id="@+id/key_pos_2_2"
+ style="@style/SoftKey"
+ android:text="c"
+ android:tag="KEYCODE_C"/>
+ <TextView
+ android:id="@+id/key_pos_2_3"
+ android:text="v"
+ android:tag="KEYCODE_V"
+ style="@style/SoftKey"/>
+ <TextView
+ android:id="@+id/key_pos_2_4"
+ android:text="b"
+ android:tag="KEYCODE_B"
+ style="@style/SoftKey"/>
+ <TextView
+ android:id="@+id/key_pos_2_5"
+ android:text="n"
+ android:tag="KEYCODE_N"
+ style="@style/SoftKey"/>
+ <TextView
+ android:id="@+id/key_pos_2_6"
+ android:text="m"
+ android:tag="KEYCODE_M"
+ style="@style/SoftKey"/>
+ <TextView
+ android:id="@+id/key_pos_del"
+ android:text="DEL"
+ android:tag="KEYCODE_DEL"
+ style="@style/SoftKey.Function"/>
+ </LinearLayout>
+
+ <LinearLayout style="@style/KeyboardRow">
+ <TextView
+ android:id="@+id/key_pos_symbol"
+ android:text="TAB"
+ android:tag="KEYCODE_TAB"
+ style="@style/SoftKey.Function"/>
+ <TextView
+ android:id="@+id/key_pos_comma"
+ android:text=","
+ android:tag="KEYCODE_COMMA"
+ style="@style/SoftKey"/>
+ <TextView
+ android:id="@+id/key_pos_space"
+ android:text="SPACE"
+ android:tag="KEYCODE_SPACE"
+ style="@style/SoftKey.Space"/>
+ <TextView
+ android:id="@+id/key_pos_period"
+ android:text="."
+ android:tag="KEYCODE_PERIOD"
+ style="@style/SoftKey"/>
+ <TextView
+ android:id="@+id/key_pos_enter"
+ android:text="ENT"
+ android:tag="KEYCODE_ENTER"
+ style="@style/SoftKey.Function.Bottom"/>
+ </LinearLayout>
+</LinearLayout>
diff --git a/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/res/values/dimens.xml b/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/res/values/dimens.xml
new file mode 100644
index 0000000..1a4959e
--- /dev/null
+++ b/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/res/values/dimens.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2022 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<resources>
+ <dimen name="text_size_normal">24dp</dimen>
+ <dimen name="text_size_symbol">14dp</dimen>
+
+ <dimen name="keyboard_header_height">40dp</dimen>
+ <dimen name="keyboard_row_height">50dp</dimen>
+</resources>
\ No newline at end of file
diff --git a/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/res/values/strings.xml b/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/res/values/strings.xml
new file mode 100644
index 0000000..11377fa
--- /dev/null
+++ b/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/res/values/strings.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2022 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<resources>
+ <string name="app_name">Fake IME</string>
+</resources>
\ No newline at end of file
diff --git a/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/res/values/styles.xml b/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/res/values/styles.xml
new file mode 100644
index 0000000..83f7bc3
--- /dev/null
+++ b/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/res/values/styles.xml
@@ -0,0 +1,66 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2022 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<resources>
+ <style name="KeyboardArea">
+ <item name="android:layout_width">match_parent</item>
+ <item name="android:layout_height">wrap_content</item>
+ <item name="android:layout_gravity">bottom</item>
+ <item name="android:orientation">vertical</item>
+ <item name="android:background">#FFFFFFFF</item>
+ </style>
+
+ <style name="KeyboardRow">
+ <item name="android:layout_width">match_parent</item>
+ <item name="android:layout_height">@dimen/keyboard_row_height</item>
+ <item name="android:orientation">horizontal</item>
+ </style>
+
+ <style name="KeyboardRow.Header">
+ <item name="android:layout_height">@dimen/keyboard_header_height</item>
+ <item name="android:background">#FFEEEEEE</item>
+ </style>
+
+ <style name="SoftKey">
+ <item name="android:layout_width">0dp</item>
+ <item name="android:layout_height">match_parent</item>
+ <item name="android:layout_weight">2</item>
+ <item name="android:gravity">center</item>
+ <item name="android:textColor">#FF000000</item>
+ <item name="android:textSize">@dimen/text_size_normal</item>
+ <item name="android:fontFamily">roboto-regular</item>
+ <item name="android:background">@drawable/key_border</item>
+ </style>
+
+ <style name="SoftKey.Function">
+ <item name="android:layout_weight">3</item>
+ <item name="android:textColor">#FF333333</item>
+ <item name="android:textSize">@dimen/text_size_symbol</item>
+ </style>
+
+ <style name="SoftKey.Function.Bottom">
+ <item name="android:layout_weight">3</item>
+ <item name="android:textColor">#FF333333</item>
+ <item name="android:textSize">@dimen/text_size_symbol</item>
+ </style>
+
+ <style name="SoftKey.Space">
+ <item name="android:layout_weight">10</item>
+ <item name="android:textColor">#FF333333</item>
+ <item name="android:textSize">@dimen/text_size_symbol</item>
+ </style>
+</resources>
\ No newline at end of file
diff --git a/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/res/xml/method.xml b/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/res/xml/method.xml
new file mode 100644
index 0000000..872b068
--- /dev/null
+++ b/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/res/xml/method.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2022 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<input-method xmlns:android="http://schemas.android.com/apk/res/android">
+ <subtype
+ android:label="FakeIme"
+ android:imeSubtypeLocale="en_US"
+ android:imeSubtypeMode="keyboard"/>
+</input-method>
\ No newline at end of file
diff --git a/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/src/com/android/apps/inputmethod/simpleime/KeyCodeConstants.java b/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/src/com/android/apps/inputmethod/simpleime/KeyCodeConstants.java
new file mode 100644
index 0000000..990fa24
--- /dev/null
+++ b/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/src/com/android/apps/inputmethod/simpleime/KeyCodeConstants.java
@@ -0,0 +1,68 @@
+/*
+ * 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.apps.inputmethod.simpleime;
+
+import android.view.KeyEvent;
+
+import java.util.HashMap;
+
+/** Holder of key codes and their name. */
+public final class KeyCodeConstants {
+ private KeyCodeConstants() {}
+
+ static final HashMap<String, Integer> KEY_NAME_TO_CODE_MAP = new HashMap<>();
+
+ static {
+ KEY_NAME_TO_CODE_MAP.put("KEYCODE_A", KeyEvent.KEYCODE_A);
+ KEY_NAME_TO_CODE_MAP.put("KEYCODE_B", KeyEvent.KEYCODE_B);
+ KEY_NAME_TO_CODE_MAP.put("KEYCODE_C", KeyEvent.KEYCODE_C);
+ KEY_NAME_TO_CODE_MAP.put("KEYCODE_D", KeyEvent.KEYCODE_D);
+ KEY_NAME_TO_CODE_MAP.put("KEYCODE_E", KeyEvent.KEYCODE_E);
+ KEY_NAME_TO_CODE_MAP.put("KEYCODE_F", KeyEvent.KEYCODE_F);
+ KEY_NAME_TO_CODE_MAP.put("KEYCODE_G", KeyEvent.KEYCODE_G);
+ KEY_NAME_TO_CODE_MAP.put("KEYCODE_H", KeyEvent.KEYCODE_H);
+ KEY_NAME_TO_CODE_MAP.put("KEYCODE_I", KeyEvent.KEYCODE_I);
+ KEY_NAME_TO_CODE_MAP.put("KEYCODE_J", KeyEvent.KEYCODE_J);
+ KEY_NAME_TO_CODE_MAP.put("KEYCODE_K", KeyEvent.KEYCODE_K);
+ KEY_NAME_TO_CODE_MAP.put("KEYCODE_L", KeyEvent.KEYCODE_L);
+ KEY_NAME_TO_CODE_MAP.put("KEYCODE_M", KeyEvent.KEYCODE_M);
+ KEY_NAME_TO_CODE_MAP.put("KEYCODE_N", KeyEvent.KEYCODE_N);
+ KEY_NAME_TO_CODE_MAP.put("KEYCODE_O", KeyEvent.KEYCODE_O);
+ KEY_NAME_TO_CODE_MAP.put("KEYCODE_P", KeyEvent.KEYCODE_P);
+ KEY_NAME_TO_CODE_MAP.put("KEYCODE_Q", KeyEvent.KEYCODE_Q);
+ KEY_NAME_TO_CODE_MAP.put("KEYCODE_R", KeyEvent.KEYCODE_R);
+ KEY_NAME_TO_CODE_MAP.put("KEYCODE_S", KeyEvent.KEYCODE_S);
+ KEY_NAME_TO_CODE_MAP.put("KEYCODE_T", KeyEvent.KEYCODE_T);
+ KEY_NAME_TO_CODE_MAP.put("KEYCODE_U", KeyEvent.KEYCODE_U);
+ KEY_NAME_TO_CODE_MAP.put("KEYCODE_V", KeyEvent.KEYCODE_V);
+ KEY_NAME_TO_CODE_MAP.put("KEYCODE_W", KeyEvent.KEYCODE_W);
+ KEY_NAME_TO_CODE_MAP.put("KEYCODE_X", KeyEvent.KEYCODE_X);
+ KEY_NAME_TO_CODE_MAP.put("KEYCODE_Y", KeyEvent.KEYCODE_Y);
+ KEY_NAME_TO_CODE_MAP.put("KEYCODE_Z", KeyEvent.KEYCODE_Z);
+ KEY_NAME_TO_CODE_MAP.put("KEYCODE_SHIFT", KeyEvent.KEYCODE_SHIFT_LEFT);
+ KEY_NAME_TO_CODE_MAP.put("KEYCODE_DEL", KeyEvent.KEYCODE_DEL);
+ KEY_NAME_TO_CODE_MAP.put("KEYCODE_SPACE", KeyEvent.KEYCODE_SPACE);
+ KEY_NAME_TO_CODE_MAP.put("KEYCODE_ENTER", KeyEvent.KEYCODE_ENTER);
+ KEY_NAME_TO_CODE_MAP.put("KEYCODE_COMMA", KeyEvent.KEYCODE_COMMA);
+ KEY_NAME_TO_CODE_MAP.put("KEYCODE_PERIOD", KeyEvent.KEYCODE_PERIOD);
+ KEY_NAME_TO_CODE_MAP.put("KEYCODE_TAB", KeyEvent.KEYCODE_TAB);
+ }
+
+ public static boolean isAlphaKeyCode(int keyCode) {
+ return keyCode >= KeyEvent.KEYCODE_A && keyCode <= KeyEvent.KEYCODE_Z;
+ }
+}
diff --git a/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/src/com/android/apps/inputmethod/simpleime/SimpleInputMethodService.java b/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/src/com/android/apps/inputmethod/simpleime/SimpleInputMethodService.java
new file mode 100644
index 0000000..48942a3
--- /dev/null
+++ b/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/src/com/android/apps/inputmethod/simpleime/SimpleInputMethodService.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apps.inputmethod.simpleime;
+
+import android.inputmethodservice.InputMethodService;
+import android.os.SystemClock;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.InputConnection;
+import android.widget.FrameLayout;
+
+import com.android.apps.inputmethod.simpleime.ims.InputMethodServiceWrapper;
+
+/** The {@link InputMethodService} implementation for SimpeTestIme app. */
+public class SimpleInputMethodService extends InputMethodServiceWrapper {
+ private static final String TAG = "SimpleIMS";
+
+ private FrameLayout mInputView;
+
+ @Override
+ public View onCreateInputView() {
+ Log.i(TAG, "onCreateInputView()");
+ mInputView = (FrameLayout) LayoutInflater.from(this).inflate(R.layout.input_view, null);
+ return mInputView;
+ }
+
+ @Override
+ public void onStartInputView(EditorInfo info, boolean restarting) {
+ super.onStartInputView(info, restarting);
+ mInputView.removeAllViews();
+ SimpleKeyboard keyboard = new SimpleKeyboard(this, R.layout.qwerty_10_9_9);
+ mInputView.addView(keyboard.inflateKeyboardView(LayoutInflater.from(this), mInputView));
+ }
+
+ void handle(String data, int keyboardState) {
+ InputConnection inputConnection = getCurrentInputConnection();
+ Integer keyCode = KeyCodeConstants.KEY_NAME_TO_CODE_MAP.get(data);
+ Log.v(TAG, "keyCode: " + keyCode);
+ if (keyCode != null) {
+ inputConnection.sendKeyEvent(
+ new KeyEvent(
+ SystemClock.uptimeMillis(),
+ SystemClock.uptimeMillis(),
+ KeyEvent.ACTION_DOWN,
+ keyCode,
+ 0,
+ KeyCodeConstants.isAlphaKeyCode(keyCode) ? keyboardState : 0));
+ }
+ }
+}
diff --git a/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/src/com/android/apps/inputmethod/simpleime/SimpleKeyboard.java b/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/src/com/android/apps/inputmethod/simpleime/SimpleKeyboard.java
new file mode 100644
index 0000000..b16ec9eb
--- /dev/null
+++ b/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/src/com/android/apps/inputmethod/simpleime/SimpleKeyboard.java
@@ -0,0 +1,126 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apps.inputmethod.simpleime;
+
+import android.text.TextUtils;
+import android.util.Log;
+import android.util.SparseArray;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+
+/** Controls the visible virtual keyboard view. */
+final class SimpleKeyboard {
+ private static final String TAG = "SimpleKeyboard";
+
+ private static final int[] SOFT_KEY_IDS =
+ new int[] {
+ R.id.key_pos_0_0,
+ R.id.key_pos_0_1,
+ R.id.key_pos_0_2,
+ R.id.key_pos_0_3,
+ R.id.key_pos_0_4,
+ R.id.key_pos_0_5,
+ R.id.key_pos_0_6,
+ R.id.key_pos_0_7,
+ R.id.key_pos_0_8,
+ R.id.key_pos_0_9,
+ R.id.key_pos_1_0,
+ R.id.key_pos_1_1,
+ R.id.key_pos_1_2,
+ R.id.key_pos_1_3,
+ R.id.key_pos_1_4,
+ R.id.key_pos_1_5,
+ R.id.key_pos_1_6,
+ R.id.key_pos_1_7,
+ R.id.key_pos_1_8,
+ R.id.key_pos_2_0,
+ R.id.key_pos_2_1,
+ R.id.key_pos_2_2,
+ R.id.key_pos_2_3,
+ R.id.key_pos_2_4,
+ R.id.key_pos_2_5,
+ R.id.key_pos_2_6,
+ R.id.key_pos_shift,
+ R.id.key_pos_del,
+ R.id.key_pos_symbol,
+ R.id.key_pos_comma,
+ R.id.key_pos_space,
+ R.id.key_pos_period,
+ R.id.key_pos_enter,
+ };
+
+ private final SimpleInputMethodService mSimpleInputMethodService;
+ private final int mViewResId;
+ private final SparseArray<TextView> mSoftKeyViews = new SparseArray<>();
+ private View mKeyboardView;
+ private int mKeyboardState;
+
+ SimpleKeyboard(SimpleInputMethodService simpleInputMethodService, int viewResId) {
+ this.mSimpleInputMethodService = simpleInputMethodService;
+ this.mViewResId = viewResId;
+ this.mKeyboardState = 0;
+ }
+
+ View inflateKeyboardView(LayoutInflater inflater, ViewGroup inputView) {
+ mKeyboardView = inflater.inflate(mViewResId, inputView, false);
+ mapSoftKeys();
+ return mKeyboardView;
+ }
+
+ private void mapSoftKeys() {
+ for (int id : SOFT_KEY_IDS) {
+ TextView softKeyView = mKeyboardView.findViewById(id);
+ mSoftKeyViews.put(id, softKeyView);
+ String tagData = softKeyView.getTag() != null ? softKeyView.getTag().toString() : null;
+ softKeyView.setOnClickListener(v -> handle(tagData));
+ }
+ }
+
+ private void handle(String data) {
+ Log.i(TAG, "handle(): " + data);
+ if (TextUtils.isEmpty(data)) {
+ return;
+ }
+ if ("KEYCODE_SHIFT".equals(data)) {
+ handleShift();
+ return;
+ }
+
+ mSimpleInputMethodService.handle(data, mKeyboardState);
+ }
+
+ private void handleShift() {
+ mKeyboardState = toggleShiftState(mKeyboardState);
+ Log.v(TAG, "currentKeyboardState: " + mKeyboardState);
+ boolean isShiftOn = isShiftOn(mKeyboardState);
+ for (int i = 0; i < mSoftKeyViews.size(); i++) {
+ TextView softKeyView = mSoftKeyViews.valueAt(i);
+ softKeyView.setAllCaps(isShiftOn);
+ }
+ }
+
+ private static boolean isShiftOn(int state) {
+ return (state & KeyEvent.META_SHIFT_ON) == KeyEvent.META_SHIFT_ON;
+ }
+
+ private static int toggleShiftState(int state) {
+ return state ^ KeyEvent.META_SHIFT_ON;
+ }
+}
diff --git a/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/src/com/android/apps/inputmethod/simpleime/ims/InputMethodServiceWrapper.java b/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/src/com/android/apps/inputmethod/simpleime/ims/InputMethodServiceWrapper.java
new file mode 100644
index 0000000..b706a65
--- /dev/null
+++ b/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/src/com/android/apps/inputmethod/simpleime/ims/InputMethodServiceWrapper.java
@@ -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.apps.inputmethod.simpleime.ims;
+
+import android.content.res.Configuration;
+import android.inputmethodservice.InputMethodService;
+import android.util.Log;
+import android.view.inputmethod.EditorInfo;
+
+import java.util.concurrent.CountDownLatch;
+
+/** Wrapper of {@link InputMethodService} to expose interfaces for testing purpose. */
+public class InputMethodServiceWrapper extends InputMethodService {
+ private static final String TAG = "InputMethodServiceWrapper";
+
+ private static InputMethodServiceWrapper sInputMethodServiceWrapper;
+
+ public static InputMethodServiceWrapper getInputMethodServiceWrapperForTesting() {
+ return sInputMethodServiceWrapper;
+ }
+
+ private boolean mInputViewStarted;
+ private CountDownLatch mCountDownLatchForTesting;
+
+ public boolean getCurrentInputViewStarted() {
+ return mInputViewStarted;
+ }
+
+ public void setCountDownLatchForTesting(CountDownLatch countDownLatchForTesting) {
+ mCountDownLatchForTesting = countDownLatchForTesting;
+ }
+
+ @Override
+ public void onCreate() {
+ Log.i(TAG, "onCreate()");
+ super.onCreate();
+ sInputMethodServiceWrapper = this;
+ }
+
+ @Override
+ public void onStartInput(EditorInfo info, boolean restarting) {
+ Log.i(TAG, "onStartInput() editor=" + info + ", restarting=" + restarting);
+ super.onStartInput(info, restarting);
+ }
+
+ @Override
+ public void onStartInputView(EditorInfo info, boolean restarting) {
+ Log.i(TAG, "onStartInputView() editor=" + info + ", restarting=" + restarting);
+ super.onStartInputView(info, restarting);
+ mInputViewStarted = true;
+ if (mCountDownLatchForTesting != null) {
+ mCountDownLatchForTesting.countDown();
+ }
+ }
+
+ @Override
+ public void onFinishInput() {
+ Log.i(TAG, "onFinishInput()");
+ super.onFinishInput();
+ }
+
+ @Override
+ public void onFinishInputView(boolean finishingInput) {
+ Log.i(TAG, "onFinishInputView()");
+ super.onFinishInputView(finishingInput);
+ mInputViewStarted = false;
+
+ if (mCountDownLatchForTesting != null) {
+ mCountDownLatchForTesting.countDown();
+ }
+ }
+
+ @Override
+ public void requestHideSelf(int flags) {
+ Log.i(TAG, "requestHideSelf() " + flags);
+ super.requestHideSelf(flags);
+ }
+
+ @Override
+ public void onConfigurationChanged(Configuration newConfig) {
+ Log.i(TAG, "onConfigurationChanged() " + newConfig);
+ super.onConfigurationChanged(newConfig);
+
+ if (mCountDownLatchForTesting != null) {
+ mCountDownLatchForTesting.countDown();
+ }
+ }
+}
diff --git a/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/src/com/android/apps/inputmethod/simpleime/testing/TestActivity.java b/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/src/com/android/apps/inputmethod/simpleime/testing/TestActivity.java
new file mode 100644
index 0000000..0eec7e6
--- /dev/null
+++ b/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/src/com/android/apps/inputmethod/simpleime/testing/TestActivity.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apps.inputmethod.simpleime.testing;
+
+import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
+import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
+
+import android.app.Activity;
+import android.app.Instrumentation;
+import android.content.Intent;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.WindowInsets;
+import android.view.WindowInsetsController;
+import android.view.WindowManager;
+import android.view.inputmethod.InputMethodManager;
+import android.widget.EditText;
+import android.widget.LinearLayout;
+
+/**
+ * A special activity for testing purpose.
+ *
+ * <p>This is used when the instruments package is SimpleTestIme, as the Intent needs to be started
+ * in the instruments package. More details see {@link
+ * Instrumentation#startActivitySync(Intent)}.</>
+ */
+public class TestActivity extends Activity {
+ private static final String TAG = "TestActivity";
+
+ /**
+ * Start a new test activity with an editor and wait for it to begin running before returning.
+ *
+ * @param instrumentation application instrumentation
+ * @return the newly started activity
+ */
+ public static TestActivity start(Instrumentation instrumentation) {
+ Intent intent =
+ new Intent()
+ .setAction(Intent.ACTION_MAIN)
+ .setClass(instrumentation.getTargetContext(), TestActivity.class)
+ .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+ return (TestActivity) instrumentation.startActivitySync(intent);
+ }
+
+ public EditText mEditText;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN);
+ LinearLayout rootView = new LinearLayout(this);
+ mEditText = new EditText(this);
+ mEditText.setContentDescription("Input box");
+ rootView.addView(mEditText, new LinearLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT));
+ setContentView(rootView);
+ mEditText.requestFocus();
+ super.onCreate(savedInstanceState);
+ }
+
+ /** Shows soft keyboard via InputMethodManager. */
+ public boolean showImeWithInputMethodManager(int flags) {
+ InputMethodManager imm = getSystemService(InputMethodManager.class);
+ boolean result = imm.showSoftInput(mEditText, flags);
+ Log.i(TAG, "hideIme() via InputMethodManager, result=" + result);
+ return result;
+ }
+
+ /** Shows soft keyboard via WindowInsetsController. */
+ public boolean showImeWithWindowInsetsController() {
+ WindowInsetsController windowInsetsController = mEditText.getWindowInsetsController();
+ windowInsetsController.show(WindowInsets.Type.ime());
+ Log.i(TAG, "showIme() via WindowInsetsController");
+ return true;
+ }
+
+ /** Hides soft keyboard via InputMethodManager. */
+ public boolean hideImeWithInputMethodManager(int flags) {
+ InputMethodManager imm = getSystemService(InputMethodManager.class);
+ boolean result = imm.hideSoftInputFromWindow(mEditText.getWindowToken(), flags);
+ Log.i(TAG, "hideIme() via InputMethodManager, result=" + result);
+ return result;
+ }
+
+ /** Hides soft keyboard via WindowInsetsController. */
+ public boolean hideImeWithWindowInsetsController() {
+ WindowInsetsController windowInsetsController = mEditText.getWindowInsetsController();
+ windowInsetsController.hide(WindowInsets.Type.ime());
+ Log.i(TAG, "hideIme() via WindowInsetsController");
+ return true;
+ }
+}
diff --git a/services/tests/PackageManagerServiceTests/server/Android.bp b/services/tests/PackageManagerServiceTests/server/Android.bp
index ebd6b64..cc26593 100644
--- a/services/tests/PackageManagerServiceTests/server/Android.bp
+++ b/services/tests/PackageManagerServiceTests/server/Android.bp
@@ -94,6 +94,7 @@
"libunwindstack",
"libutils",
"netd_aidl_interface-V5-cpp",
+ "libservices.core.settings.testonly",
],
dxflags: ["--multi-dex"],
diff --git a/services/tests/PackageManagerServiceTests/server/src/com/android/server/PackageManagerSettingsTests.java b/services/tests/PackageManagerServiceTests/server/src/com/android/server/PackageManagerSettingsTests.java
index 3727d66..b3f64b6 100644
--- a/services/tests/PackageManagerServiceTests/server/src/com/android/server/PackageManagerSettingsTests.java
+++ b/services/tests/PackageManagerServiceTests/server/src/com/android/server/PackageManagerSettingsTests.java
@@ -111,6 +111,10 @@
private static final String PACKAGE_NAME_3 = "com.android.app3";
private static final int TEST_RESOURCE_ID = 2131231283;
+ static {
+ System.loadLibrary("services.core.settings.testonly");
+ }
+
@Mock
RuntimePermissionsPersistence mRuntimePermissionsPersistence;
@Mock
diff --git a/services/tests/mockingservicestests/assets/AppOpsUpgradeTest/appops-version-1.xml b/services/tests/mockingservicestests/assets/AppOpsUpgradeTest/appops-version-1.xml
new file mode 100644
index 0000000..1dde7dc
--- /dev/null
+++ b/services/tests/mockingservicestests/assets/AppOpsUpgradeTest/appops-version-1.xml
@@ -0,0 +1,781 @@
+<?xml version='1.0' encoding='UTF-8' standalone='yes' ?>
+<app-ops v="1">
+ <uid n="1001">
+ <op n="0" m="4" />
+ <op n="1" m="4" />
+ <op n="15" m="0" />
+ <op n="27" m="4" />
+ <op n="87" m="1" />
+ </uid>
+ <uid n="1002">
+ <op n="0" m="4" />
+ <op n="1" m="4" />
+ <op n="15" m="0" />
+ <op n="87" m="1" />
+ </uid>
+ <uid n="10077">
+ <op n="87" m="1" />
+ </uid>
+ <uid n="10079">
+ <op n="116" m="1" />
+ </uid>
+ <uid n="10080">
+ <op n="87" m="1" />
+ </uid>
+ <uid n="10081">
+ <op n="87" m="1" />
+ </uid>
+ <uid n="10086">
+ <op n="87" m="1" />
+ </uid>
+ <uid n="10087">
+ <op n="59" m="1" />
+ <op n="87" m="1" />
+ </uid>
+ <uid n="10090">
+ <op n="0" m="4" />
+ <op n="1" m="4" />
+ </uid>
+ <uid n="10096">
+ <op n="0" m="1" />
+ <op n="1" m="1" />
+ </uid>
+ <uid n="10112">
+ <op n="11" m="1" />
+ <op n="51" m="1" />
+ <op n="59" m="1" />
+ <op n="62" m="1" />
+ <op n="81" m="1" />
+ <op n="83" m="1" />
+ <op n="85" m="1" />
+ <op n="87" m="1" />
+ </uid>
+ <uid n="10113">
+ <op n="0" m="1" />
+ <op n="1" m="1" />
+ <op n="51" m="1" />
+ <op n="62" m="1" />
+ </uid>
+ <uid n="10114">
+ <op n="4" m="1" />
+ </uid>
+ <uid n="10115">
+ <op n="0" m="1" />
+ </uid>
+ <uid n="10116">
+ <op n="0" m="1" />
+ </uid>
+ <uid n="10117">
+ <op n="0" m="4" />
+ <op n="1" m="4" />
+ <op n="13" m="1" />
+ <op n="14" m="1" />
+ <op n="16" m="1" />
+ <op n="26" m="4" />
+ <op n="27" m="4" />
+ </uid>
+ <uid n="10118">
+ <op n="51" m="1" />
+ </uid>
+ <uid n="10119">
+ <op n="11" m="1" />
+ <op n="77" m="1" />
+ <op n="111" m="1" />
+ </uid>
+ <uid n="10120">
+ <op n="0" m="1" />
+ <op n="1" m="1" />
+ <op n="4" m="1" />
+ <op n="6" m="1" />
+ <op n="7" m="1" />
+ <op n="11" m="1" />
+ <op n="13" m="1" />
+ <op n="26" m="1" />
+ <op n="27" m="1" />
+ <op n="54" m="1" />
+ <op n="59" m="1" />
+ <op n="81" m="1" />
+ <op n="83" m="1" />
+ <op n="85" m="1" />
+ <op n="87" m="1" />
+ </uid>
+ <uid n="10121">
+ <op n="59" m="1" />
+ <op n="60" m="1" />
+ <op n="87" m="1" />
+ </uid>
+ <uid n="10122">
+ <op n="0" m="1" />
+ <op n="1" m="1" />
+ <op n="51" m="1" />
+ <op n="59" m="1" />
+ <op n="60" m="1" />
+ <op n="81" m="1" />
+ <op n="83" m="1" />
+ <op n="85" m="1" />
+ <op n="87" m="1" />
+ </uid>
+ <uid n="10123">
+ <op n="0" m="4" />
+ <op n="1" m="4" />
+ <op n="87" m="1" />
+ </uid>
+ <uid n="10124">
+ <op n="11" m="1" />
+ <op n="26" m="1" />
+ </uid>
+ <uid n="10125">
+ <op n="11" m="1" />
+ </uid>
+ <uid n="10127">
+ <op n="0" m="1" />
+ <op n="1" m="1" />
+ <op n="26" m="4" />
+ <op n="27" m="4" />
+ <op n="59" m="1" />
+ <op n="60" m="1" />
+ <op n="65" m="1" />
+ <op n="81" m="1" />
+ <op n="83" m="1" />
+ <op n="85" m="1" />
+ <op n="87" m="1" />
+ <op n="111" m="1" />
+ </uid>
+ <uid n="10129">
+ <op n="0" m="1" />
+ <op n="1" m="1" />
+ </uid>
+ <uid n="10130">
+ <op n="51" m="1" />
+ </uid>
+ <uid n="10131">
+ <op n="0" m="1" />
+ <op n="1" m="1" />
+ <op n="59" m="1" />
+ <op n="60" m="1" />
+ <op n="81" m="1" />
+ <op n="83" m="1" />
+ <op n="85" m="1" />
+ <op n="87" m="1" />
+ </uid>
+ <uid n="10132">
+ <op n="0" m="1" />
+ <op n="1" m="1" />
+ <op n="26" m="1" />
+ <op n="27" m="1" />
+ <op n="69" m="1" />
+ <op n="79" m="1" />
+ </uid>
+ <uid n="10133">
+ <op n="11" m="1" />
+ </uid>
+ <uid n="10136">
+ <op n="0" m="1" />
+ <op n="4" m="1" />
+ <op n="77" m="1" />
+ <op n="87" m="1" />
+ <op n="111" m="1" />
+ <op n="114" m="1" />
+ </uid>
+ <uid n="10137">
+ <op n="62" m="1" />
+ </uid>
+ <uid n="10138">
+ <op n="26" m="4" />
+ </uid>
+ <uid n="10140">
+ <op n="26" m="4" />
+ <op n="27" m="4" />
+ <op n="87" m="1" />
+ </uid>
+ <uid n="10141">
+ <op n="11" m="1" />
+ <op n="27" m="1" />
+ <op n="111" m="1" />
+ </uid>
+ <uid n="10142">
+ <op n="0" m="1" />
+ <op n="1" m="1" />
+ <op n="11" m="1" />
+ </uid>
+ <uid n="10144">
+ <op n="0" m="1" />
+ <op n="1" m="1" />
+ <op n="4" m="1" />
+ <op n="11" m="1" />
+ <op n="27" m="4" />
+ <op n="111" m="1" />
+ </uid>
+ <uid n="10145">
+ <op n="11" m="1" />
+ </uid>
+ <uid n="10149">
+ <op n="11" m="1" />
+ <op n="59" m="1" />
+ <op n="60" m="1" />
+ <op n="81" m="1" />
+ <op n="83" m="1" />
+ <op n="85" m="1" />
+ <op n="87" m="1" />
+ </uid>
+ <uid n="10150">
+ <op n="51" m="1" />
+ </uid>
+ <uid n="10151">
+ <op n="11" m="1" />
+ </uid>
+ <uid n="10152">
+ <op n="11" m="1" />
+ </uid>
+ <uid n="10154">
+ <op n="0" m="4" />
+ <op n="1" m="4" />
+ <op n="27" m="4" />
+ </uid>
+ <uid n="10155">
+ <op n="0" m="1" />
+ <op n="1" m="1" />
+ </uid>
+ <uid n="10157">
+ <op n="13" m="1" />
+ </uid>
+ <uid n="10158">
+ <op n="11" m="1" />
+ </uid>
+ <uid n="10160">
+ <op n="0" m="1" />
+ <op n="1" m="1" />
+ <op n="11" m="1" />
+ <op n="26" m="4" />
+ <op n="27" m="4" />
+ <op n="87" m="1" />
+ </uid>
+ <uid n="10161">
+ <op n="0" m="1" />
+ <op n="1" m="1" />
+ <op n="11" m="1" />
+ <op n="51" m="1" />
+ <op n="77" m="1" />
+ <op n="87" m="1" />
+ <op n="111" m="1" />
+ <op n="114" m="1" />
+ </uid>
+ <uid n="10162">
+ <op n="0" m="1" />
+ <op n="1" m="1" />
+ <op n="15" m="0" />
+ <op n="26" m="4" />
+ <op n="27" m="4" />
+ <op n="87" m="1" />
+ <op n="89" m="0" />
+ </uid>
+ <uid n="10163">
+ <op n="26" m="4" />
+ <op n="27" m="4" />
+ <op n="56" m="4" />
+ <op n="87" m="1" />
+ </uid>
+ <uid n="10164">
+ <op n="26" m="1" />
+ <op n="27" m="4" />
+ <op n="59" m="1" />
+ <op n="60" m="1" />
+ <op n="69" m="1" />
+ <op n="79" m="1" />
+ <op n="81" m="1" />
+ <op n="83" m="1" />
+ <op n="85" m="1" />
+ <op n="87" m="1" />
+ </uid>
+ <uid n="10169">
+ <op n="11" m="1" />
+ </uid>
+ <uid n="10170">
+ <op n="87" m="1" />
+ </uid>
+ <uid n="10171">
+ <op n="26" m="1" />
+ <op n="51" m="1" />
+ <op n="59" m="1" />
+ <op n="60" m="1" />
+ <op n="81" m="1" />
+ <op n="83" m="1" />
+ <op n="85" m="1" />
+ <op n="87" m="1" />
+ </uid>
+ <uid n="10172">
+ <op n="11" m="1" />
+ </uid>
+ <uid n="10173">
+ <op n="0" m="1" />
+ <op n="1" m="1" />
+ <op n="23" m="0" />
+ <op n="26" m="1" />
+ <op n="51" m="1" />
+ <op n="62" m="1" />
+ <op n="65" m="1" />
+ </uid>
+ <uid n="10175">
+ <op n="0" m="1" />
+ <op n="11" m="1" />
+ <op n="26" m="1" />
+ <op n="27" m="1" />
+ </uid>
+ <uid n="10178">
+ <op n="0" m="1" />
+ <op n="1" m="1" />
+ <op n="11" m="1" />
+ <op n="27" m="1" />
+ <op n="87" m="1" />
+ </uid>
+ <uid n="10179">
+ <op n="0" m="1" />
+ <op n="1" m="1" />
+ <op n="4" m="1" />
+ <op n="11" m="1" />
+ <op n="26" m="1" />
+ <op n="27" m="1" />
+ <op n="51" m="1" />
+ <op n="59" m="1" />
+ <op n="60" m="1" />
+ <op n="62" m="1" />
+ <op n="81" m="1" />
+ <op n="83" m="1" />
+ <op n="85" m="1" />
+ <op n="87" m="1" />
+ </uid>
+ <uid n="10180">
+ <op n="11" m="1" />
+ </uid>
+ <uid n="10181">
+ <op n="4" m="1" />
+ <op n="11" m="1" />
+ <op n="26" m="1" />
+ <op n="27" m="1" />
+ <op n="59" m="1" />
+ <op n="62" m="1" />
+ <op n="81" m="1" />
+ <op n="83" m="1" />
+ <op n="85" m="1" />
+ <op n="87" m="1" />
+ </uid>
+ <uid n="1110181">
+ <op n="4" m="1" />
+ <op n="11" m="1" />
+ <op n="26" m="1" />
+ <op n="27" m="1" />
+ <op n="81" m="1" />
+ <op n="83" m="1" />
+ <op n="85" m="1" />
+ <op n="87" m="1" />
+ <op n="107" m="2" />
+ </uid>
+ <uid n="10182">
+ <op n="27" m="4" />
+ </uid>
+ <uid n="10183">
+ <op n="11" m="1" />
+ <op n="26" m="4" />
+ </uid>
+ <uid n="10184">
+ <op n="4" m="1" />
+ <op n="11" m="1" />
+ <op n="13" m="1" />
+ <op n="26" m="1" />
+ <op n="27" m="4" />
+ </uid>
+ <uid n="10185">
+ <op n="8" m="1" />
+ <op n="59" m="1" />
+ <op n="81" m="1" />
+ <op n="83" m="1" />
+ <op n="85" m="1" />
+ <op n="87" m="1" />
+ </uid>
+ <uid n="10187">
+ <op n="11" m="1" />
+ </uid>
+ <uid n="10189">
+ <op n="11" m="1" />
+ <op n="26" m="1" />
+ <op n="27" m="1" />
+ <op n="51" m="1" />
+ </uid>
+ <uid n="10190">
+ <op n="0" m="1" />
+ <op n="13" m="1" />
+ </uid>
+ <uid n="10191">
+ <op n="11" m="1" />
+ </uid>
+ <uid n="10192">
+ <op n="11" m="1" />
+ <op n="13" m="1" />
+ <op n="26" m="1" />
+ <op n="27" m="1" />
+ <op n="51" m="1" />
+ <op n="59" m="1" />
+ <op n="60" m="1" />
+ <op n="81" m="1" />
+ <op n="83" m="1" />
+ <op n="85" m="1" />
+ <op n="87" m="1" />
+ </uid>
+ <uid n="10193">
+ <op n="11" m="1" />
+ </uid>
+ <uid n="10197">
+ <op n="11" m="1" />
+ </uid>
+ <uid n="10198">
+ <op n="0" m="1" />
+ <op n="1" m="1" />
+ <op n="4" m="1" />
+ <op n="11" m="1" />
+ <op n="26" m="1" />
+ <op n="27" m="1" />
+ <op n="59" m="1" />
+ <op n="60" m="1" />
+ <op n="77" m="1" />
+ <op n="81" m="1" />
+ <op n="83" m="1" />
+ <op n="85" m="1" />
+ <op n="87" m="1" />
+ <op n="90" m="1" />
+ <op n="107" m="0" />
+ <op n="111" m="1" />
+ </uid>
+ <uid n="10199">
+ <op n="4" m="1" />
+ <op n="11" m="1" />
+ <op n="26" m="1" />
+ <op n="59" m="1" />
+ <op n="60" m="1" />
+ <op n="62" m="1" />
+ <op n="81" m="1" />
+ <op n="83" m="1" />
+ <op n="85" m="1" />
+ <op n="87" m="1" />
+ </uid>
+ <uid n="10200">
+ <op n="11" m="1" />
+ <op n="65" m="1" />
+ <op n="107" m="1" />
+ </uid>
+ <uid n="1110200">
+ <op n="11" m="1" />
+ <op n="65" m="1" />
+ <op n="107" m="2" />
+ </uid>
+ <uid n="10201">
+ <op n="0" m="1" />
+ <op n="1" m="1" />
+ <op n="4" m="1" />
+ <op n="11" m="1" />
+ <op n="51" m="1" />
+ <op n="62" m="1" />
+ <op n="84" m="0" />
+ <op n="86" m="0" />
+ <op n="87" m="1" />
+ </uid>
+ <uid n="10206">
+ <op n="0" m="4" />
+ <op n="1" m="4" />
+ <op n="26" m="4" />
+ <op n="59" m="1" />
+ <op n="60" m="1" />
+ <op n="87" m="1" />
+ </uid>
+ <uid n="10209">
+ <op n="11" m="1" />
+ <op n="51" m="1" />
+ <op n="59" m="1" />
+ <op n="60" m="1" />
+ <op n="81" m="1" />
+ <op n="83" m="1" />
+ <op n="85" m="1" />
+ <op n="87" m="1" />
+ </uid>
+ <uid n="10210">
+ <op n="51" m="1" />
+ </uid>
+ <uid n="10212">
+ <op n="11" m="1" />
+ <op n="62" m="1" />
+ </uid>
+ <uid n="10214">
+ <op n="26" m="4" />
+ </uid>
+ <uid n="10216">
+ <op n="51" m="1" />
+ <op n="59" m="1" />
+ <op n="60" m="1" />
+ <op n="85" m="1" />
+ <op n="87" m="1" />
+ </uid>
+ <uid n="10225">
+ <op n="0" m="1" />
+ <op n="1" m="1" />
+ <op n="51" m="1" />
+ <op n="59" m="1" />
+ <op n="87" m="1" />
+ </uid>
+ <uid n="10229">
+ <op n="11" m="1" />
+ <op n="59" m="1" />
+ <op n="60" m="1" />
+ <op n="62" m="1" />
+ <op n="87" m="1" />
+ </uid>
+ <uid n="10231">
+ <op n="51" m="1" />
+ </uid>
+ <uid n="10232">
+ <op n="51" m="1" />
+ </uid>
+ <uid n="10234">
+ <op n="0" m="1" />
+ <op n="1" m="1" />
+ <op n="11" m="1" />
+ <op n="13" m="1" />
+ <op n="20" m="1" />
+ <op n="26" m="1" />
+ <op n="27" m="1" />
+ <op n="59" m="1" />
+ <op n="60" m="1" />
+ <op n="81" m="1" />
+ <op n="83" m="1" />
+ <op n="85" m="1" />
+ <op n="87" m="1" />
+ </uid>
+ <uid n="10235">
+ <op n="59" m="1" />
+ <op n="60" m="1" />
+ <op n="87" m="1" />
+ </uid>
+ <uid n="10237">
+ <op n="0" m="4" />
+ <op n="1" m="4" />
+ </uid>
+ <uid n="10238">
+ <op n="26" m="4" />
+ <op n="27" m="4" />
+ <op n="87" m="1" />
+ </uid>
+ <uid n="10240">
+ <op n="112" m="1" />
+ </uid>
+ <uid n="10241">
+ <op n="59" m="1" />
+ <op n="87" m="1" />
+ </uid>
+ <uid n="10245">
+ <op n="13" m="1" />
+ <op n="51" m="1" />
+ </uid>
+ <uid n="10247">
+ <op n="0" m="1" />
+ <op n="59" m="1" />
+ <op n="60" m="1" />
+ <op n="81" m="1" />
+ <op n="83" m="1" />
+ <op n="85" m="1" />
+ <op n="87" m="0" />
+ <op n="90" m="1" />
+ </uid>
+ <uid n="10254">
+ <op n="11" m="1" />
+ </uid>
+ <uid n="10255">
+ <op n="11" m="1" />
+ </uid>
+ <uid n="10256">
+ <op n="87" m="1" />
+ </uid>
+ <uid n="10258">
+ <op n="11" m="1" />
+ </uid>
+ <uid n="10260">
+ <op n="51" m="1" />
+ <op n="59" m="1" />
+ <op n="60" m="1" />
+ <op n="87" m="1" />
+ </uid>
+ <uid n="10262">
+ <op n="15" m="0" />
+ </uid>
+ <uid n="10266">
+ <op n="0" m="4" />
+ </uid>
+ <uid n="10267">
+ <op n="0" m="1" />
+ <op n="1" m="1" />
+ <op n="4" m="1" />
+ <op n="59" m="1" />
+ <op n="60" m="1" />
+ <op n="62" m="1" />
+ <op n="77" m="1" />
+ <op n="81" m="1" />
+ <op n="83" m="1" />
+ <op n="85" m="1" />
+ <op n="87" m="1" />
+ <op n="107" m="2" />
+ <op n="111" m="1" />
+ </uid>
+ <uid n="10268">
+ <op n="4" m="1" />
+ <op n="11" m="1" />
+ <op n="62" m="1" />
+ </uid>
+ <uid n="10269">
+ <op n="11" m="1" />
+ <op n="26" m="1" />
+ <op n="27" m="1" />
+ <op n="59" m="1" />
+ <op n="60" m="1" />
+ <op n="85" m="1" />
+ <op n="87" m="1" />
+ </uid>
+ <pkg n="com.google.android.iwlan">
+ <uid n="0">
+ <op n="1" />
+ <op n="75" m="0" />
+ </uid>
+ </pkg>
+ <pkg n="com.android.phone">
+ <uid n="0">
+ <op n="1" />
+ <op n="75" m="0" />
+ </uid>
+ </pkg>
+ <pkg n="android">
+ <uid n="1000">
+ <op n="0">
+ <st n="214748364801" t="1670287941040" />
+ </op>
+ <op n="4">
+ <st n="214748364801" t="1670289665522" />
+ </op>
+ <op n="6">
+ <st n="214748364801" t="1670287946650" />
+ </op>
+ <op n="8">
+ <st n="214748364801" t="1670289624396" />
+ </op>
+ <op n="14">
+ <st n="214748364801" t="1670287951031" />
+ </op>
+ <op n="40">
+ <st n="214748364801" t="1670291786337" d="156" />
+ </op>
+ <op n="41">
+ <st id="SensorNotificationService" n="214748364801" t="1670287585567" d="4251183" />
+ <st id="CountryDetector" n="214748364801" t="1670287583306" d="6700" />
+ </op>
+ <op n="43">
+ <st n="214748364801" r="1670291755062" />
+ </op>
+ <op n="61">
+ <st n="214748364801" r="1670291754997" />
+ </op>
+ <op n="105">
+ <st n="214748364801" r="1670291473903" />
+ <st id="GnssService" n="214748364801" r="1670288044920" />
+ </op>
+ <op n="111">
+ <st n="214748364801" t="1670291441554" />
+ </op>
+ </uid>
+ </pkg>
+ <pkg n="com.android.server.telecom">
+ <uid n="1000">
+ <op n="6">
+ <st n="214748364801" t="1670287609092" />
+ </op>
+ <op n="111">
+ <st n="214748364801" t="1670287583728" />
+ </op>
+ </uid>
+ </pkg>
+ <pkg n="com.android.settings">
+ <uid n="1000">
+ <op n="43">
+ <st n="214748364801" r="1670291447349" />
+ </op>
+ <op n="105">
+ <st n="214748364801" r="1670291399231" />
+ </op>
+ <op n="111">
+ <st n="214748364801" t="1670291756910" />
+ </op>
+ </uid>
+ </pkg>
+ <pkg n="com.android.phone">
+ <uid n="1001">
+ <op n="15">
+ <st n="214748364801" t="1670287951022" />
+ </op>
+ <op n="40">
+ <st n="214748364801" t="1670291786177" />
+ </op>
+ <op n="105">
+ <st n="214748364801" r="1670291756403" />
+ </op>
+ </uid>
+ </pkg>
+ <pkg n="com.android.bluetooth">
+ <uid n="1002">
+ <op n="4">
+ <st n="214748364801" t="1670289671076" />
+ </op>
+ <op n="40">
+ <st n="214748364801" t="1670287585676" d="8" />
+ </op>
+ <op n="43">
+ <st n="214748364801" r="1670287585818" />
+ </op>
+ <op n="77">
+ <st n="214748364801" t="1670288037629" />
+ </op>
+ <op n="111">
+ <st n="214748364801" t="1670287592081" />
+ </op>
+ </uid>
+ </pkg>
+ <pkg n="com.android.vending">
+ <uid n="10136">
+ <op n="40">
+ <st n="429496729601" t="1670289621210" d="114" />
+ <st n="858993459201" t="1670289879730" d="349" />
+ <st n="1288490188801" t="1670287942622" d="937" />
+ </op>
+ <op n="43">
+ <st n="429496729601" r="1670289755305" />
+ <st n="858993459201" r="1670288019246" />
+ <st n="1073741824001" r="1670289571783" />
+ <st n="1288490188801" r="1670289373336" />
+ </op>
+ <op n="76">
+ <st n="429496729601" t="1670289748735" d="15991" />
+ <st n="858993459201" t="1670291395180" d="79201" />
+ <st n="1073741824001" t="1670291395168" d="12" />
+ <st n="1288490188801" t="1670291526029" d="3" />
+ <st n="1503238553601" t="1670291526032" d="310718" />
+ </op>
+ <op n="105">
+ <st n="429496729601" r="1670289538910" />
+ <st n="858993459201" r="1670288054519" />
+ <st n="1073741824001" r="1670287599379" />
+ <st n="1288490188801" r="1670289526854" />
+ <st n="1503238553601" r="1670289528242" />
+ </op>
+ </uid>
+ </pkg>
+ <pkg n="com.android.nfc">
+ <uid n="1027">
+ <op n="40">
+ <st n="214748364801" t="1670291786330" d="22" />
+ </op>
+ </uid>
+ </pkg>
+</app-ops>
\ No newline at end of file
diff --git a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueModernImplTest.java b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueModernImplTest.java
index c87fd26..8d78cd6 100644
--- a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueModernImplTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueModernImplTest.java
@@ -195,6 +195,12 @@
false, null, false, null);
}
+ private void enqueueOrReplaceBroadcast(BroadcastProcessQueue queue,
+ BroadcastRecord record, int recordIndex, long enqueueTime) {
+ queue.enqueueOrReplaceBroadcast(record, recordIndex);
+ record.enqueueTime = enqueueTime;
+ }
+
@Test
public void testRunnableList_Simple() {
assertRunnableList(List.of(), mHead);
@@ -549,29 +555,32 @@
mConstants.MAX_CONSECUTIVE_URGENT_DISPATCHES = 2;
BroadcastProcessQueue queue = new BroadcastProcessQueue(mConstants,
PACKAGE_GREEN, getUidForPackage(PACKAGE_GREEN));
+ long timeCounter = 100;
// mix of broadcasts, with more than 2 fg/urgent
- queue.enqueueOrReplaceBroadcast(
- makeBroadcastRecord(new Intent(Intent.ACTION_TIMEZONE_CHANGED)), 0);
- queue.enqueueOrReplaceBroadcast(
- makeBroadcastRecord(new Intent(Intent.ACTION_ALARM_CHANGED)), 0);
- queue.enqueueOrReplaceBroadcast(
- makeBroadcastRecord(new Intent(Intent.ACTION_TIME_TICK)), 0);
- queue.enqueueOrReplaceBroadcast(
+ enqueueOrReplaceBroadcast(queue,
+ makeBroadcastRecord(new Intent(Intent.ACTION_TIMEZONE_CHANGED)),
+ 0, timeCounter++);
+ enqueueOrReplaceBroadcast(queue,
+ makeBroadcastRecord(new Intent(Intent.ACTION_ALARM_CHANGED)),
+ 0, timeCounter++);
+ enqueueOrReplaceBroadcast(queue,
+ makeBroadcastRecord(new Intent(Intent.ACTION_TIME_TICK)), 0, timeCounter++);
+ enqueueOrReplaceBroadcast(queue,
makeBroadcastRecord(new Intent(Intent.ACTION_LOCALE_CHANGED)
- .addFlags(Intent.FLAG_RECEIVER_FOREGROUND)), 0);
- queue.enqueueOrReplaceBroadcast(
+ .addFlags(Intent.FLAG_RECEIVER_FOREGROUND)), 0, timeCounter++);
+ enqueueOrReplaceBroadcast(queue,
makeBroadcastRecord(new Intent(Intent.ACTION_APPLICATION_PREFERENCES),
- optInteractive), 0);
- queue.enqueueOrReplaceBroadcast(
+ optInteractive), 0, timeCounter++);
+ enqueueOrReplaceBroadcast(queue,
makeBroadcastRecord(new Intent(AppWidgetManager.ACTION_APPWIDGET_UPDATE),
- optInteractive), 0);
- queue.enqueueOrReplaceBroadcast(
+ optInteractive), 0, timeCounter++);
+ enqueueOrReplaceBroadcast(queue,
makeBroadcastRecord(new Intent(Intent.ACTION_INPUT_METHOD_CHANGED)
- .addFlags(Intent.FLAG_RECEIVER_FOREGROUND)), 0);
- queue.enqueueOrReplaceBroadcast(
+ .addFlags(Intent.FLAG_RECEIVER_FOREGROUND)), 0, timeCounter++);
+ enqueueOrReplaceBroadcast(queue,
makeBroadcastRecord(new Intent(Intent.ACTION_NEW_OUTGOING_CALL),
- optInteractive), 0);
+ optInteractive), 0, timeCounter++);
queue.makeActiveNextPending();
assertEquals(Intent.ACTION_LOCALE_CHANGED, queue.getActive().intent.getAction());
@@ -592,6 +601,133 @@
}
/**
+ * Verify that offload broadcasts are not starved because of broadcasts in higher priority
+ * queues.
+ */
+ @Test
+ public void testOffloadStarvation() {
+ final BroadcastOptions optInteractive = BroadcastOptions.makeBasic();
+ optInteractive.setInteractive(true);
+
+ mConstants.MAX_CONSECUTIVE_URGENT_DISPATCHES = 1;
+ mConstants.MAX_CONSECUTIVE_NORMAL_DISPATCHES = 2;
+ final BroadcastProcessQueue queue = new BroadcastProcessQueue(mConstants,
+ PACKAGE_GREEN, getUidForPackage(PACKAGE_GREEN));
+ long timeCounter = 100;
+
+ // mix of broadcasts, with more than 2 normal
+ enqueueOrReplaceBroadcast(queue,
+ makeBroadcastRecord(new Intent(Intent.ACTION_BOOT_COMPLETED)
+ .addFlags(Intent.FLAG_RECEIVER_OFFLOAD)), 0, timeCounter++);
+ enqueueOrReplaceBroadcast(queue,
+ makeBroadcastRecord(new Intent(Intent.ACTION_TIMEZONE_CHANGED)),
+ 0, timeCounter++);
+ enqueueOrReplaceBroadcast(queue,
+ makeBroadcastRecord(new Intent(Intent.ACTION_PACKAGE_CHANGED)
+ .addFlags(Intent.FLAG_RECEIVER_OFFLOAD)), 0, timeCounter++);
+ enqueueOrReplaceBroadcast(queue,
+ makeBroadcastRecord(new Intent(Intent.ACTION_ALARM_CHANGED)),
+ 0, timeCounter++);
+ enqueueOrReplaceBroadcast(queue,
+ makeBroadcastRecord(new Intent(Intent.ACTION_TIME_TICK)), 0, timeCounter++);
+ enqueueOrReplaceBroadcast(queue,
+ makeBroadcastRecord(new Intent(Intent.ACTION_LOCALE_CHANGED)
+ .addFlags(Intent.FLAG_RECEIVER_FOREGROUND)), 0, timeCounter++);
+ enqueueOrReplaceBroadcast(queue,
+ makeBroadcastRecord(new Intent(Intent.ACTION_APPLICATION_PREFERENCES),
+ optInteractive), 0, timeCounter++);
+ enqueueOrReplaceBroadcast(queue,
+ makeBroadcastRecord(new Intent(AppWidgetManager.ACTION_APPWIDGET_UPDATE),
+ optInteractive), 0, timeCounter++);
+ enqueueOrReplaceBroadcast(queue,
+ makeBroadcastRecord(new Intent(Intent.ACTION_INPUT_METHOD_CHANGED)
+ .addFlags(Intent.FLAG_RECEIVER_FOREGROUND)), 0, timeCounter++);
+ enqueueOrReplaceBroadcast(queue,
+ makeBroadcastRecord(new Intent(Intent.ACTION_NEW_OUTGOING_CALL),
+ optInteractive), 0, timeCounter++);
+
+ queue.makeActiveNextPending();
+ assertEquals(Intent.ACTION_LOCALE_CHANGED, queue.getActive().intent.getAction());
+ // after MAX_CONSECUTIVE_URGENT_DISPATCHES expect an ordinary one next
+ queue.makeActiveNextPending();
+ assertEquals(Intent.ACTION_TIMEZONE_CHANGED, queue.getActive().intent.getAction());
+ // and then back to prioritizing urgent ones
+ queue.makeActiveNextPending();
+ assertEquals(Intent.ACTION_APPLICATION_PREFERENCES, queue.getActive().intent.getAction());
+ // after MAX_CONSECUTIVE_URGENT_DISPATCHES, again an ordinary one next
+ queue.makeActiveNextPending();
+ assertEquals(Intent.ACTION_ALARM_CHANGED, queue.getActive().intent.getAction());
+ // and then back to prioritizing urgent ones
+ queue.makeActiveNextPending();
+ assertEquals(AppWidgetManager.ACTION_APPWIDGET_UPDATE,
+ queue.getActive().intent.getAction());
+ // after MAX_CONSECUTIVE_URGENT_DISPATCHES and MAX_CONSECUTIVE_NORMAL_DISPATCHES,
+ // expect an offload one
+ queue.makeActiveNextPending();
+ assertEquals(Intent.ACTION_BOOT_COMPLETED, queue.getActive().intent.getAction());
+ // and then back to prioritizing urgent ones
+ queue.makeActiveNextPending();
+ assertEquals(Intent.ACTION_INPUT_METHOD_CHANGED, queue.getActive().intent.getAction());
+ }
+
+ /**
+ * Verify that BroadcastProcessQueue#setPrioritizeEarliest() works as expected.
+ */
+ @Test
+ public void testPrioritizeEarliest() {
+ final BroadcastOptions optInteractive = BroadcastOptions.makeBasic();
+ optInteractive.setInteractive(true);
+
+ BroadcastProcessQueue queue = new BroadcastProcessQueue(mConstants,
+ PACKAGE_GREEN, getUidForPackage(PACKAGE_GREEN));
+ queue.setPrioritizeEarliest(true);
+ long timeCounter = 100;
+
+ enqueueOrReplaceBroadcast(queue,
+ makeBroadcastRecord(new Intent(Intent.ACTION_BOOT_COMPLETED)
+ .addFlags(Intent.FLAG_RECEIVER_OFFLOAD)), 0, timeCounter++);
+ enqueueOrReplaceBroadcast(queue,
+ makeBroadcastRecord(new Intent(Intent.ACTION_TIMEZONE_CHANGED)),
+ 0, timeCounter++);
+ enqueueOrReplaceBroadcast(queue,
+ makeBroadcastRecord(new Intent(Intent.ACTION_PACKAGE_CHANGED)
+ .addFlags(Intent.FLAG_RECEIVER_OFFLOAD)), 0, timeCounter++);
+ enqueueOrReplaceBroadcast(queue,
+ makeBroadcastRecord(new Intent(Intent.ACTION_ALARM_CHANGED)),
+ 0, timeCounter++);
+ enqueueOrReplaceBroadcast(queue,
+ makeBroadcastRecord(new Intent(Intent.ACTION_TIME_TICK)),
+ 0, timeCounter++);
+ enqueueOrReplaceBroadcast(queue,
+ makeBroadcastRecord(new Intent(Intent.ACTION_LOCALE_CHANGED)
+ .addFlags(Intent.FLAG_RECEIVER_FOREGROUND)), 0, timeCounter++);
+ enqueueOrReplaceBroadcast(queue,
+ makeBroadcastRecord(new Intent(AppWidgetManager.ACTION_APPWIDGET_UPDATE),
+ optInteractive), 0, timeCounter++);
+
+ // When we mark BroadcastProcessQueue to prioritize earliest, we should
+ // expect to dispatch broadcasts in the order they were enqueued
+ queue.makeActiveNextPending();
+ assertEquals(Intent.ACTION_BOOT_COMPLETED, queue.getActive().intent.getAction());
+ queue.makeActiveNextPending();
+ assertEquals(Intent.ACTION_TIMEZONE_CHANGED, queue.getActive().intent.getAction());
+ // after MAX_CONSECUTIVE_URGENT_DISPATCHES expect an ordinary one next
+ queue.makeActiveNextPending();
+ assertEquals(Intent.ACTION_PACKAGE_CHANGED, queue.getActive().intent.getAction());
+ // and then back to prioritizing urgent ones
+ queue.makeActiveNextPending();
+ assertEquals(Intent.ACTION_ALARM_CHANGED, queue.getActive().intent.getAction());
+ queue.makeActiveNextPending();
+ assertEquals(Intent.ACTION_TIME_TICK, queue.getActive().intent.getAction());
+ queue.makeActiveNextPending();
+ assertEquals(Intent.ACTION_LOCALE_CHANGED, queue.getActive().intent.getAction());
+ // verify the reset-count-then-resume worked too
+ queue.makeActiveNextPending();
+ assertEquals(AppWidgetManager.ACTION_APPWIDGET_UPDATE,
+ queue.getActive().intent.getAction());
+ }
+
+ /**
* Verify that sending a broadcast that removes any matching pending
* broadcasts is applied as expected.
*/
diff --git a/services/tests/mockingservicestests/src/com/android/server/app/GameManagerServiceTests.java b/services/tests/mockingservicestests/src/com/android/server/app/GameManagerServiceTests.java
index a8d8945..d3fa92c 100644
--- a/services/tests/mockingservicestests/src/com/android/server/app/GameManagerServiceTests.java
+++ b/services/tests/mockingservicestests/src/com/android/server/app/GameManagerServiceTests.java
@@ -647,15 +647,15 @@
}
}
- private void checkReportedOptedInGameModes(GameManagerService gameManagerService,
- int... requiredOptedInModes) {
- Arrays.sort(requiredOptedInModes);
- // check GetModeInfo.getOptedInGameModes
+ private void checkReportedOverriddenGameModes(GameManagerService gameManagerService,
+ int... requiredOverriddenModes) {
+ Arrays.sort(requiredOverriddenModes);
+ // check GetModeInfo.getOverriddenGameModes
GameModeInfo info = gameManagerService.getGameModeInfo(mPackageName, USER_ID_1);
assertNotNull(info);
- int[] optedInModes = info.getOptedInGameModes();
- Arrays.sort(optedInModes);
- assertArrayEquals(requiredOptedInModes, optedInModes);
+ int[] overriddenModes = info.getOverriddenGameModes();
+ Arrays.sort(overriddenModes);
+ assertArrayEquals(requiredOverriddenModes, overriddenModes);
}
private void checkDownscaling(GameManagerService gameManagerService,
@@ -697,7 +697,7 @@
assertEquals(fps, config.getGameModeConfiguration(gameMode).getFps());
}
- private boolean checkOptedIn(GameManagerService gameManagerService, int gameMode) {
+ private boolean checkOverridden(GameManagerService gameManagerService, int gameMode) {
GameManagerService.GamePackageConfiguration config =
gameManagerService.getConfig(mPackageName, USER_ID_1);
return config.willGamePerformOptimizations(gameMode);
@@ -870,8 +870,8 @@
mTestLooper.getLooper());
startUser(gameManagerService, USER_ID_1);
- assertFalse(checkOptedIn(gameManagerService, GameManager.GAME_MODE_PERFORMANCE));
- assertFalse(checkOptedIn(gameManagerService, GameManager.GAME_MODE_BATTERY));
+ assertFalse(checkOverridden(gameManagerService, GameManager.GAME_MODE_PERFORMANCE));
+ assertFalse(checkOverridden(gameManagerService, GameManager.GAME_MODE_BATTERY));
checkFps(gameManagerService, GameManager.GAME_MODE_PERFORMANCE, 0);
gameManagerService.setGameModeConfigOverride(mPackageName, USER_ID_1, 3, "40",
@@ -884,9 +884,9 @@
mockInterventionsDisabledAllOptInFromXml();
gameManagerService.updateConfigsForUser(USER_ID_1, false, mPackageName);
- assertTrue(checkOptedIn(gameManagerService, GameManager.GAME_MODE_PERFORMANCE));
+ assertTrue(checkOverridden(gameManagerService, GameManager.GAME_MODE_PERFORMANCE));
// opt-in is still false for battery mode as override exists
- assertFalse(checkOptedIn(gameManagerService, GameManager.GAME_MODE_BATTERY));
+ assertFalse(checkOverridden(gameManagerService, GameManager.GAME_MODE_BATTERY));
}
/**
@@ -1310,7 +1310,7 @@
checkReportedAvailableGameModes(gameManagerService, GameManager.GAME_MODE_PERFORMANCE,
GameManager.GAME_MODE_BATTERY, GameManager.GAME_MODE_STANDARD,
GameManager.GAME_MODE_CUSTOM);
- checkReportedOptedInGameModes(gameManagerService);
+ checkReportedOverriddenGameModes(gameManagerService);
assertEquals(new GameModeConfiguration.Builder()
.setFpsOverride(30)
@@ -1337,7 +1337,7 @@
checkReportedAvailableGameModes(gameManagerService,
GameManager.GAME_MODE_BATTERY, GameManager.GAME_MODE_STANDARD,
GameManager.GAME_MODE_CUSTOM);
- checkReportedOptedInGameModes(gameManagerService);
+ checkReportedOverriddenGameModes(gameManagerService);
assertNotNull(gameModeInfo.getGameModeConfiguration(GameManager.GAME_MODE_BATTERY));
assertNull(gameModeInfo.getGameModeConfiguration(GameManager.GAME_MODE_PERFORMANCE));
@@ -1357,7 +1357,7 @@
checkReportedAvailableGameModes(gameManagerService,
GameManager.GAME_MODE_PERFORMANCE, GameManager.GAME_MODE_STANDARD,
GameManager.GAME_MODE_CUSTOM);
- checkReportedOptedInGameModes(gameManagerService);
+ checkReportedOverriddenGameModes(gameManagerService);
assertNotNull(gameModeInfo.getGameModeConfiguration(GameManager.GAME_MODE_PERFORMANCE));
assertNull(gameModeInfo.getGameModeConfiguration(GameManager.GAME_MODE_BATTERY));
@@ -1382,7 +1382,7 @@
}
@Test
- public void testGetGameModeInfoWithAllGameModesOptedIn_noDeviceConfig()
+ public void testGetGameModeInfoWithAllGameModesOverridden_noDeviceConfig()
throws Exception {
mockModifyGameModeGranted();
mockInterventionsEnabledAllOptInFromXml();
@@ -1390,14 +1390,14 @@
GameManagerService gameManagerService = createServiceAndStartUser(USER_ID_1);
GameModeInfo gameModeInfo = gameManagerService.getGameModeInfo(mPackageName, USER_ID_1);
assertEquals(GameManager.GAME_MODE_STANDARD, gameModeInfo.getActiveGameMode());
- verifyAllModesOptedInAndInterventionsAvailable(gameManagerService, gameModeInfo);
+ verifyAllModesOverriddenAndInterventionsAvailable(gameManagerService, gameModeInfo);
assertNull(gameModeInfo.getGameModeConfiguration(GameManager.GAME_MODE_BATTERY));
assertNull(gameModeInfo.getGameModeConfiguration(GameManager.GAME_MODE_PERFORMANCE));
}
@Test
- public void testGetGameModeInfoWithAllGameModesOptedIn_allDeviceConfig()
+ public void testGetGameModeInfoWithAllGameModesOverridden_allDeviceConfig()
throws Exception {
mockModifyGameModeGranted();
mockInterventionsEnabledAllOptInFromXml();
@@ -1405,26 +1405,26 @@
GameManagerService gameManagerService = createServiceAndStartUser(USER_ID_1);
GameModeInfo gameModeInfo = gameManagerService.getGameModeInfo(mPackageName, USER_ID_1);
assertEquals(GameManager.GAME_MODE_STANDARD, gameModeInfo.getActiveGameMode());
- verifyAllModesOptedInAndInterventionsAvailable(gameManagerService, gameModeInfo);
+ verifyAllModesOverriddenAndInterventionsAvailable(gameManagerService, gameModeInfo);
assertNull(gameModeInfo.getGameModeConfiguration(GameManager.GAME_MODE_BATTERY));
assertNull(gameModeInfo.getGameModeConfiguration(GameManager.GAME_MODE_PERFORMANCE));
}
- private void verifyAllModesOptedInAndInterventionsAvailable(
+ private void verifyAllModesOverriddenAndInterventionsAvailable(
GameManagerService gameManagerService,
GameModeInfo gameModeInfo) {
checkReportedAvailableGameModes(gameManagerService,
GameManager.GAME_MODE_PERFORMANCE, GameManager.GAME_MODE_BATTERY,
GameManager.GAME_MODE_STANDARD, GameManager.GAME_MODE_CUSTOM);
- checkReportedOptedInGameModes(gameManagerService,
+ checkReportedOverriddenGameModes(gameManagerService,
GameManager.GAME_MODE_PERFORMANCE, GameManager.GAME_MODE_BATTERY);
assertTrue(gameModeInfo.isFpsOverrideAllowed());
assertTrue(gameModeInfo.isDownscalingAllowed());
}
@Test
- public void testGetGameModeInfoWithBatteryModeOptedIn_withBatteryDeviceConfig()
+ public void testGetGameModeInfoWithBatteryModeOverridden_withBatteryDeviceConfig()
throws Exception {
mockModifyGameModeGranted();
mockInterventionsEnabledBatteryOptInFromXml();
@@ -1435,14 +1435,14 @@
checkReportedAvailableGameModes(gameManagerService, GameManager.GAME_MODE_BATTERY,
GameManager.GAME_MODE_STANDARD, GameManager.GAME_MODE_CUSTOM);
- checkReportedOptedInGameModes(gameManagerService, GameManager.GAME_MODE_BATTERY);
+ checkReportedOverriddenGameModes(gameManagerService, GameManager.GAME_MODE_BATTERY);
assertNull(gameModeInfo.getGameModeConfiguration(GameManager.GAME_MODE_BATTERY));
assertNull(gameModeInfo.getGameModeConfiguration(GameManager.GAME_MODE_PERFORMANCE));
}
@Test
- public void testGetGameModeInfoWithPerformanceModeOptedIn_withAllDeviceConfig()
+ public void testGetGameModeInfoWithPerformanceModeOverridden_withAllDeviceConfig()
throws Exception {
mockModifyGameModeGranted();
mockInterventionsEnabledPerformanceOptInFromXml();
@@ -1454,7 +1454,7 @@
checkReportedAvailableGameModes(gameManagerService, GameManager.GAME_MODE_PERFORMANCE,
GameManager.GAME_MODE_BATTERY, GameManager.GAME_MODE_STANDARD,
GameManager.GAME_MODE_CUSTOM);
- checkReportedOptedInGameModes(gameManagerService, GameManager.GAME_MODE_PERFORMANCE);
+ checkReportedOverriddenGameModes(gameManagerService, GameManager.GAME_MODE_PERFORMANCE);
assertNotNull(gameModeInfo.getGameModeConfiguration(GameManager.GAME_MODE_BATTERY));
assertNull(gameModeInfo.getGameModeConfiguration(GameManager.GAME_MODE_PERFORMANCE));
@@ -1993,7 +1993,7 @@
}
@Test
- public void testResetInterventions_onGameModeOptedIn() throws Exception {
+ public void testResetInterventions_onGameModeOverridden() throws Exception {
mockModifyGameModeGranted();
String configStringBefore =
"mode=2,downscaleFactor=1.0,fps=90";
diff --git a/services/tests/mockingservicestests/src/com/android/server/appop/AppOpsUpgradeTest.java b/services/tests/mockingservicestests/src/com/android/server/appop/AppOpsUpgradeTest.java
index 298dbf4..302fa0f 100644
--- a/services/tests/mockingservicestests/src/com/android/server/appop/AppOpsUpgradeTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/appop/AppOpsUpgradeTest.java
@@ -16,23 +16,32 @@
package com.android.server.appop;
+import static android.app.AppOpsManager.OP_SCHEDULE_EXACT_ALARM;
+
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.doAnswer;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
+
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.nullable;
import static org.mockito.Mockito.doNothing;
-import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
-import static org.mockito.Mockito.when;
import android.app.AppOpsManager;
import android.content.Context;
import android.content.pm.PackageManager;
+import android.content.pm.PackageManagerInternal;
import android.content.res.AssetManager;
import android.os.Handler;
-import android.os.HandlerThread;
+import android.os.UserHandle;
import android.util.Log;
import android.util.SparseArray;
import android.util.SparseIntArray;
@@ -42,11 +51,20 @@
import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;
+import com.android.internal.util.ArrayUtils;
import com.android.modules.utils.TypedXmlPullParser;
+import com.android.server.LocalServices;
+import com.android.server.SystemServerInitThreadPool;
+import com.android.server.pm.UserManagerInternal;
+import com.android.server.pm.permission.PermissionManagerServiceInternal;
+import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoSession;
+import org.mockito.quality.Strictness;
import org.xmlpull.v1.XmlPullParser;
import java.io.File;
@@ -64,29 +82,39 @@
private static final String TAG = AppOpsUpgradeTest.class.getSimpleName();
private static final String APP_OPS_UNVERSIONED_ASSET_PATH =
"AppOpsUpgradeTest/appops-unversioned.xml";
+ private static final String APP_OPS_VERSION_1_ASSET_PATH =
+ "AppOpsUpgradeTest/appops-version-1.xml";
private static final String APP_OPS_FILENAME = "appops-test.xml";
private static final int NON_DEFAULT_OPS_IN_FILE = 4;
- private static final int CURRENT_VERSION = 1;
- private File mAppOpsFile;
- private Context mContext;
+ private static final Context sContext = InstrumentationRegistry.getTargetContext();
+ private static final File sAppOpsFile = new File(sContext.getFilesDir(), APP_OPS_FILENAME);
+
+ private Context mTestContext;
+ private MockitoSession mMockitoSession;
+
+ @Mock
+ private PackageManagerInternal mPackageManagerInternal;
+ @Mock
+ private PackageManager mPackageManager;
+ @Mock
+ private UserManagerInternal mUserManagerInternal;
+ @Mock
+ private PermissionManagerServiceInternal mPermissionManagerInternal;
+ @Mock
private Handler mHandler;
- private void extractAppOpsFile() {
- mAppOpsFile.getParentFile().mkdirs();
- if (mAppOpsFile.exists()) {
- mAppOpsFile.delete();
- }
- try (FileOutputStream out = new FileOutputStream(mAppOpsFile);
- InputStream in = mContext.getAssets().open(APP_OPS_UNVERSIONED_ASSET_PATH,
- AssetManager.ACCESS_BUFFER)) {
+ private static void extractAppOpsFile(String assetPath) {
+ sAppOpsFile.getParentFile().mkdirs();
+ try (FileOutputStream out = new FileOutputStream(sAppOpsFile);
+ InputStream in = sContext.getAssets().open(assetPath, AssetManager.ACCESS_BUFFER)) {
byte[] buffer = new byte[4096];
int bytesRead;
while ((bytesRead = in.read(buffer)) >= 0) {
out.write(buffer, 0, bytesRead);
}
out.flush();
- Log.d(TAG, "Successfully copied xml to " + mAppOpsFile.getAbsolutePath());
+ Log.d(TAG, "Successfully copied xml to " + sAppOpsFile.getAbsolutePath());
} catch (IOException exc) {
Log.e(TAG, "Exception while copying appops xml", exc);
fail();
@@ -98,7 +126,7 @@
int numberOfNonDefaultOps = 0;
final int defaultModeOp1 = AppOpsManager.opToDefaultMode(op1);
final int defaultModeOp2 = AppOpsManager.opToDefaultMode(op2);
- for(int i = 0; i < uidStates.size(); i++) {
+ for (int i = 0; i < uidStates.size(); i++) {
final AppOpsServiceImpl.UidState uidState = uidStates.valueAt(i);
SparseIntArray opModes = uidState.getNonDefaultUidModes();
if (opModes != null) {
@@ -132,41 +160,191 @@
@Before
public void setUp() {
- mContext = InstrumentationRegistry.getTargetContext();
- mAppOpsFile = new File(mContext.getFilesDir(), APP_OPS_FILENAME);
- extractAppOpsFile();
- HandlerThread handlerThread = new HandlerThread(TAG);
- handlerThread.start();
- mHandler = new Handler(handlerThread.getLooper());
+ if (sAppOpsFile.exists()) {
+ sAppOpsFile.delete();
+ }
+
+ mMockitoSession = mockitoSession()
+ .initMocks(this)
+ .spyStatic(LocalServices.class)
+ .mockStatic(SystemServerInitThreadPool.class)
+ .strictness(Strictness.LENIENT)
+ .startMocking();
+
+ doReturn(mPermissionManagerInternal).when(
+ () -> LocalServices.getService(PermissionManagerServiceInternal.class));
+ doReturn(mUserManagerInternal).when(
+ () -> LocalServices.getService(UserManagerInternal.class));
+ doReturn(mPackageManagerInternal).when(
+ () -> LocalServices.getService(PackageManagerInternal.class));
+
+ mTestContext = spy(sContext);
+
+ // Pretend everybody has all permissions
+ doNothing().when(mTestContext).enforcePermission(anyString(), anyInt(), anyInt(),
+ nullable(String.class));
+
+ doReturn(mPackageManager).when(mTestContext).getPackageManager();
+
+ // Stub out package calls to disable AppOpsService#updatePermissionRevokedCompat
+ doReturn(null).when(mPackageManager).getPackagesForUid(anyInt());
+ }
+
+ @After
+ public void tearDown() {
+ mMockitoSession.finishMocking();
}
@Test
- public void testUpgradeFromNoVersion() throws Exception {
- AppOpsDataParser parser = new AppOpsDataParser(mAppOpsFile);
+ public void upgradeRunAnyInBackground() {
+ extractAppOpsFile(APP_OPS_UNVERSIONED_ASSET_PATH);
+
+ AppOpsServiceImpl testService = new AppOpsServiceImpl(sAppOpsFile, mHandler, mTestContext);
+
+ testService.upgradeRunAnyInBackgroundLocked();
+ assertSameModes(testService.mUidStates, AppOpsManager.OP_RUN_IN_BACKGROUND,
+ AppOpsManager.OP_RUN_ANY_IN_BACKGROUND);
+ }
+
+ private static int getModeInFile(int uid) {
+ switch (uid) {
+ case 10198:
+ return 0;
+ case 10200:
+ return 1;
+ case 1110200:
+ case 10267:
+ case 1110181:
+ return 2;
+ default:
+ return AppOpsManager.opToDefaultMode(OP_SCHEDULE_EXACT_ALARM);
+ }
+ }
+
+ @Test
+ public void upgradeScheduleExactAlarm() {
+ extractAppOpsFile(APP_OPS_VERSION_1_ASSET_PATH);
+
+ String[] packageNames = {"p1", "package2", "pkg3", "package.4", "pkg-5", "pkg.6"};
+ int[] appIds = {10267, 10181, 10198, 10199, 10200, 4213};
+ int[] userIds = {0, 10, 11};
+
+ doReturn(userIds).when(mUserManagerInternal).getUserIds();
+
+ doReturn(packageNames).when(mPermissionManagerInternal).getAppOpPermissionPackages(
+ AppOpsManager.opToPermission(OP_SCHEDULE_EXACT_ALARM));
+
+ doAnswer(invocation -> {
+ String pkg = invocation.getArgument(0);
+ int index = ArrayUtils.indexOf(packageNames, pkg);
+ if (index < 0) {
+ return index;
+ }
+ int userId = invocation.getArgument(2);
+ return UserHandle.getUid(userId, appIds[index]);
+ }).when(mPackageManagerInternal).getPackageUid(anyString(), anyLong(), anyInt());
+
+ AppOpsServiceImpl testService = new AppOpsServiceImpl(sAppOpsFile, mHandler, mTestContext);
+
+ testService.upgradeScheduleExactAlarmLocked();
+
+ for (int userId : userIds) {
+ for (int appId : appIds) {
+ final int uid = UserHandle.getUid(userId, appId);
+ final int previousMode = getModeInFile(uid);
+
+ final int expectedMode;
+ if (previousMode == AppOpsManager.opToDefaultMode(OP_SCHEDULE_EXACT_ALARM)) {
+ expectedMode = AppOpsManager.MODE_ALLOWED;
+ } else {
+ expectedMode = previousMode;
+ }
+ final AppOpsServiceImpl.UidState uidState = testService.mUidStates.get(uid);
+ assertEquals(expectedMode, uidState.getUidMode(OP_SCHEDULE_EXACT_ALARM));
+ }
+ }
+
+ // These uids don't even declare the permission. So should stay as default / empty.
+ int[] unrelatedUidsInFile = {10225, 10178};
+
+ for (int uid : unrelatedUidsInFile) {
+ final AppOpsServiceImpl.UidState uidState = testService.mUidStates.get(uid);
+ assertEquals(AppOpsManager.opToDefaultMode(OP_SCHEDULE_EXACT_ALARM),
+ uidState.getUidMode(OP_SCHEDULE_EXACT_ALARM));
+ }
+ }
+
+ @Test
+ public void upgradeFromNoFile() {
+ assertFalse(sAppOpsFile.exists());
+
+ AppOpsServiceImpl testService = spy(
+ new AppOpsServiceImpl(sAppOpsFile, mHandler, mTestContext));
+
+ doNothing().when(testService).upgradeRunAnyInBackgroundLocked();
+ doNothing().when(testService).upgradeScheduleExactAlarmLocked();
+
+ // trigger upgrade
+ testService.systemReady();
+
+ verify(testService, never()).upgradeRunAnyInBackgroundLocked();
+ verify(testService, never()).upgradeScheduleExactAlarmLocked();
+
+ testService.writeState();
+
+ assertTrue(sAppOpsFile.exists());
+
+ AppOpsDataParser parser = new AppOpsDataParser(sAppOpsFile);
+ assertTrue(parser.parse());
+ assertEquals(AppOpsServiceImpl.CURRENT_VERSION, parser.mVersion);
+ }
+
+ @Test
+ public void upgradeFromNoVersion() {
+ extractAppOpsFile(APP_OPS_UNVERSIONED_ASSET_PATH);
+ AppOpsDataParser parser = new AppOpsDataParser(sAppOpsFile);
assertTrue(parser.parse());
assertEquals(AppOpsDataParser.NO_VERSION, parser.mVersion);
- // Use mock context and package manager to fake permision package manager calls.
- Context testContext = spy(mContext);
-
- // Pretent everybody has all permissions
- doNothing().when(testContext).enforcePermission(anyString(), anyInt(), anyInt(),
- nullable(String.class));
-
- PackageManager testPM = mock(PackageManager.class);
- when(testContext.getPackageManager()).thenReturn(testPM);
-
- // Stub out package calls to disable AppOpsService#updatePermissionRevokedCompat
- when(testPM.getPackagesForUid(anyInt())).thenReturn(null);
-
AppOpsServiceImpl testService = spy(
- new AppOpsServiceImpl(mAppOpsFile, mHandler, testContext)); // trigger upgrade
- assertSameModes(testService.mUidStates, AppOpsManager.OP_RUN_IN_BACKGROUND,
- AppOpsManager.OP_RUN_ANY_IN_BACKGROUND);
- mHandler.removeCallbacks(testService.mWriteRunner);
+ new AppOpsServiceImpl(sAppOpsFile, mHandler, mTestContext));
+
+ doNothing().when(testService).upgradeRunAnyInBackgroundLocked();
+ doNothing().when(testService).upgradeScheduleExactAlarmLocked();
+
+ // trigger upgrade
+ testService.systemReady();
+
+ verify(testService).upgradeRunAnyInBackgroundLocked();
+ verify(testService).upgradeScheduleExactAlarmLocked();
+
testService.writeState();
assertTrue(parser.parse());
- assertEquals(CURRENT_VERSION, parser.mVersion);
+ assertEquals(AppOpsServiceImpl.CURRENT_VERSION, parser.mVersion);
+ }
+
+ @Test
+ public void upgradeFromVersion1() {
+ extractAppOpsFile(APP_OPS_VERSION_1_ASSET_PATH);
+ AppOpsDataParser parser = new AppOpsDataParser(sAppOpsFile);
+ assertTrue(parser.parse());
+ assertEquals(1, parser.mVersion);
+
+ AppOpsServiceImpl testService = spy(
+ new AppOpsServiceImpl(sAppOpsFile, mHandler, mTestContext));
+
+ doNothing().when(testService).upgradeRunAnyInBackgroundLocked();
+ doNothing().when(testService).upgradeScheduleExactAlarmLocked();
+
+ // trigger upgrade
+ testService.systemReady();
+
+ verify(testService, never()).upgradeRunAnyInBackgroundLocked();
+ verify(testService).upgradeScheduleExactAlarmLocked();
+
+ testService.writeState();
+ assertTrue(parser.parse());
+ assertEquals(AppOpsServiceImpl.CURRENT_VERSION, parser.mVersion);
}
/**
@@ -174,7 +352,7 @@
* Other fields may be added as and when required for testing.
*/
private static final class AppOpsDataParser {
- static final int NO_VERSION = -1;
+ static final int NO_VERSION = -123;
int mVersion;
private File mFile;
diff --git a/services/tests/mockingservicestests/src/com/android/server/display/LocalDisplayAdapterTest.java b/services/tests/mockingservicestests/src/com/android/server/display/LocalDisplayAdapterTest.java
index be32b79..5e5cbdc 100644
--- a/services/tests/mockingservicestests/src/com/android/server/display/LocalDisplayAdapterTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/display/LocalDisplayAdapterTest.java
@@ -212,7 +212,7 @@
SurfaceControl.DisplayMode displayMode = createFakeDisplayMode(0, 1920, 1080, 0);
displayMode.supportedHdrTypes = new int[0];
FakeDisplay display = new FakeDisplay(PORT_A, new SurfaceControl.DisplayMode[]{displayMode},
- 0);
+ 0, 0);
setUpDisplay(display);
updateAvailableDisplays();
mAdapter.registerLocked();
@@ -393,7 +393,7 @@
SurfaceControl.DisplayMode displayMode = createFakeDisplayMode(0, 1920, 1080, 60f);
SurfaceControl.DisplayMode[] modes =
new SurfaceControl.DisplayMode[]{displayMode};
- FakeDisplay display = new FakeDisplay(PORT_A, modes, 0);
+ FakeDisplay display = new FakeDisplay(PORT_A, modes, 0, displayMode.refreshRate);
setUpDisplay(display);
updateAvailableDisplays();
mAdapter.registerLocked();
@@ -449,7 +449,7 @@
SurfaceControl.DisplayMode displayMode = createFakeDisplayMode(0, 1920, 1080, 60f);
SurfaceControl.DisplayMode[] modes =
new SurfaceControl.DisplayMode[]{displayMode};
- FakeDisplay display = new FakeDisplay(PORT_A, modes, 0);
+ FakeDisplay display = new FakeDisplay(PORT_A, modes, 0, displayMode.refreshRate);
setUpDisplay(display);
updateAvailableDisplays();
mAdapter.registerLocked();
@@ -504,7 +504,7 @@
createFakeDisplayMode(0, 1920, 1080, 60f),
createFakeDisplayMode(1, 1920, 1080, 50f)
};
- FakeDisplay display = new FakeDisplay(PORT_A, modes, /* activeMode */ 0);
+ FakeDisplay display = new FakeDisplay(PORT_A, modes, /* activeMode */ 0, 60f);
setUpDisplay(display);
updateAvailableDisplays();
mAdapter.registerLocked();
@@ -538,6 +538,49 @@
}
@Test
+ public void testAfterDisplayChange_RenderFrameRateIsUpdated() throws Exception {
+ SurfaceControl.DisplayMode[] modes = new SurfaceControl.DisplayMode[]{
+ createFakeDisplayMode(0, 1920, 1080, 60f),
+ };
+ FakeDisplay display = new FakeDisplay(PORT_A, modes, /* activeMode */ 0,
+ /* renderFrameRate */30f);
+ setUpDisplay(display);
+ updateAvailableDisplays();
+ mAdapter.registerLocked();
+ waitForHandlerToComplete(mHandler, HANDLER_WAIT_MS);
+
+ assertThat(mListener.addedDisplays.size()).isEqualTo(1);
+ assertThat(mListener.changedDisplays).isEmpty();
+
+ DisplayDeviceInfo displayDeviceInfo = mListener.addedDisplays.get(0)
+ .getDisplayDeviceInfoLocked();
+
+ Display.Mode activeMode = getModeById(displayDeviceInfo, displayDeviceInfo.modeId);
+ assertThat(activeMode.matches(1920, 1080, 60f)).isTrue();
+ assertEquals(Float.floatToIntBits(30f),
+ Float.floatToIntBits(displayDeviceInfo.renderFrameRate));
+
+ // Change the render frame rate
+ display.dynamicInfo.renderFrameRate = 60f;
+ setUpDisplay(display);
+ mInjector.getTransmitter().sendHotplug(display, /* connected */ true);
+ waitForHandlerToComplete(mHandler, HANDLER_WAIT_MS);
+
+ assertTrue(mListener.traversalRequested);
+ assertThat(mListener.addedDisplays.size()).isEqualTo(1);
+ assertThat(mListener.changedDisplays.size()).isEqualTo(1);
+
+ DisplayDevice displayDevice = mListener.changedDisplays.get(0);
+ displayDevice.applyPendingDisplayDeviceInfoChangesLocked();
+ displayDeviceInfo = displayDevice.getDisplayDeviceInfoLocked();
+
+ activeMode = getModeById(displayDeviceInfo, displayDeviceInfo.modeId);
+ assertThat(activeMode.matches(1920, 1080, 60f)).isTrue();
+ assertEquals(Float.floatToIntBits(60f),
+ Float.floatToIntBits(displayDeviceInfo.renderFrameRate));
+ }
+
+ @Test
public void testAfterDisplayChange_HdrCapabilitiesAreUpdated() throws Exception {
FakeDisplay display = new FakeDisplay(PORT_A);
Display.HdrCapabilities initialHdrCapabilities = new Display.HdrCapabilities(new int[0],
@@ -722,7 +765,7 @@
createFakeDisplayMode(1, 1920, 1080, 50f)
};
final int activeMode = 0;
- FakeDisplay display = new FakeDisplay(PORT_A, modes, activeMode);
+ FakeDisplay display = new FakeDisplay(PORT_A, modes, activeMode, 60f);
display.desiredDisplayModeSpecs.defaultMode = 1;
setUpDisplay(display);
@@ -979,11 +1022,13 @@
dynamicInfo.activeDisplayModeId = 0;
}
- private FakeDisplay(int port, SurfaceControl.DisplayMode[] modes, int activeMode) {
+ private FakeDisplay(int port, SurfaceControl.DisplayMode[] modes, int activeMode,
+ float renderFrameRate) {
address = createDisplayAddress(port);
info = createFakeDisplayInfo();
dynamicInfo.supportedDisplayModes = modes;
dynamicInfo.activeDisplayModeId = activeMode;
+ dynamicInfo.renderFrameRate = renderFrameRate;
}
private FakeDisplay(int port, SurfaceControl.DisplayMode[] modes, int activeMode,
@@ -1001,9 +1046,9 @@
mAddresses.add(display.address);
when(mSurfaceControlProxy.getPhysicalDisplayToken(display.address.getPhysicalDisplayId()))
.thenReturn(display.token);
- when(mSurfaceControlProxy.getStaticDisplayInfo(display.token))
+ when(mSurfaceControlProxy.getStaticDisplayInfo(display.address.getPhysicalDisplayId()))
.thenReturn(display.info);
- when(mSurfaceControlProxy.getDynamicDisplayInfo(display.token))
+ when(mSurfaceControlProxy.getDynamicDisplayInfo(display.address.getPhysicalDisplayId()))
.thenReturn(display.dynamicInfo);
when(mSurfaceControlProxy.getDesiredDisplayModeSpecs(display.token))
.thenReturn(display.desiredDisplayModeSpecs);
diff --git a/services/tests/mockingservicestests/src/com/android/server/job/JobSchedulerServiceTest.java b/services/tests/mockingservicestests/src/com/android/server/job/JobSchedulerServiceTest.java
index fc737d0..8e48490 100644
--- a/services/tests/mockingservicestests/src/com/android/server/job/JobSchedulerServiceTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/job/JobSchedulerServiceTest.java
@@ -34,6 +34,8 @@
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.when;
import android.app.ActivityManager;
@@ -46,6 +48,7 @@
import android.app.usage.UsageStatsManagerInternal;
import android.content.ComponentName;
import android.content.Context;
+import android.content.PermissionChecker;
import android.content.pm.PackageManager;
import android.content.pm.PackageManagerInternal;
import android.content.res.Resources;
@@ -63,6 +66,7 @@
import com.android.server.LocalServices;
import com.android.server.PowerAllowlistInternal;
import com.android.server.SystemServiceManager;
+import com.android.server.job.controllers.ConnectivityController;
import com.android.server.job.controllers.JobStatus;
import com.android.server.pm.UserManagerInternal;
import com.android.server.usage.AppStandbyInternal;
@@ -102,6 +106,7 @@
.initMocks(this)
.strictness(Strictness.LENIENT)
.mockStatic(LocalServices.class)
+ .mockStatic(PermissionChecker.class)
.mockStatic(ServiceManager.class)
.startMocking();
@@ -193,6 +198,15 @@
jobInfoBuilder.build(), callingUid, "com.android.test", 0, testTag);
}
+ private void grantRunLongJobsPermission(boolean grant) {
+ final int permissionStatus = grant
+ ? PermissionChecker.PERMISSION_GRANTED : PermissionChecker.PERMISSION_HARD_DENIED;
+ doReturn(permissionStatus)
+ .when(() -> PermissionChecker.checkPermissionForPreflight(
+ any(), eq(android.Manifest.permission.RUN_LONG_JOBS),
+ anyInt(), anyInt(), anyString()));
+ }
+
@Test
public void testGetMinJobExecutionGuaranteeMs() {
JobStatus ejMax = createJobStatus("testGetMinJobExecutionGuaranteeMs",
@@ -207,6 +221,15 @@
createJobInfo(5).setPriority(JobInfo.PRIORITY_HIGH));
JobStatus jobDef = createJobStatus("testGetMinJobExecutionGuaranteeMs",
createJobInfo(6));
+ JobStatus jobDT = createJobStatus("testGetMinJobExecutionGuaranteeMs",
+ createJobInfo(7)
+ .setDataTransfer(true).setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY));
+ JobStatus jobUI = createJobStatus("testGetMinJobExecutionGuaranteeMs",
+ createJobInfo(8)); // TODO(255371817): add setUserInitiated(true)
+ JobStatus jobUIDT = createJobStatus("testGetMinJobExecutionGuaranteeMs",
+ // TODO(255371817): add setUserInitiated(true)
+ createJobInfo(9)
+ .setDataTransfer(true).setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY));
spyOn(ejMax);
spyOn(ejHigh);
@@ -214,6 +237,9 @@
spyOn(ejHighDowngraded);
spyOn(jobHigh);
spyOn(jobDef);
+ spyOn(jobDT);
+ spyOn(jobUI);
+ spyOn(jobUIDT);
when(ejMax.shouldTreatAsExpeditedJob()).thenReturn(true);
when(ejHigh.shouldTreatAsExpeditedJob()).thenReturn(true);
@@ -221,6 +247,16 @@
when(ejHighDowngraded.shouldTreatAsExpeditedJob()).thenReturn(false);
when(jobHigh.shouldTreatAsExpeditedJob()).thenReturn(false);
when(jobDef.shouldTreatAsExpeditedJob()).thenReturn(false);
+ when(jobUI.shouldTreatAsUserInitiated()).thenReturn(true);
+ when(jobUIDT.shouldTreatAsUserInitiated()).thenReturn(true);
+
+ ConnectivityController connectivityController = mService.getConnectivityController();
+ spyOn(connectivityController);
+ mService.mConstants.RUNTIME_MIN_DATA_TRANSFER_GUARANTEE_MS = 10 * MINUTE_IN_MILLIS;
+ mService.mConstants.RUNTIME_DATA_TRANSFER_LIMIT_MS = 60 * MINUTE_IN_MILLIS;
+ mService.mConstants.RUNTIME_MIN_USER_INITIATED_DATA_TRANSFER_GUARANTEE_BUFFER_FACTOR = 1.5f;
+ mService.mConstants.RUNTIME_MIN_USER_INITIATED_DATA_TRANSFER_GUARANTEE_MS = HOUR_IN_MILLIS;
+ mService.mConstants.RUNTIME_USER_INITIATED_DATA_TRANSFER_LIMIT_MS = 6 * HOUR_IN_MILLIS;
assertEquals(mService.mConstants.RUNTIME_MIN_EJ_GUARANTEE_MS,
mService.getMinJobExecutionGuaranteeMs(ejMax));
@@ -234,8 +270,81 @@
mService.getMinJobExecutionGuaranteeMs(jobHigh));
assertEquals(mService.mConstants.RUNTIME_MIN_GUARANTEE_MS,
mService.getMinJobExecutionGuaranteeMs(jobDef));
+ grantRunLongJobsPermission(false); // Without permission
+ assertEquals(mService.mConstants.RUNTIME_MIN_GUARANTEE_MS,
+ mService.getMinJobExecutionGuaranteeMs(jobDT));
+ grantRunLongJobsPermission(true); // With permission
+ doReturn(ConnectivityController.UNKNOWN_TIME)
+ .when(connectivityController).getEstimatedTransferTimeMs(any());
+ assertEquals(mService.mConstants.RUNTIME_MIN_DATA_TRANSFER_GUARANTEE_MS,
+ mService.getMinJobExecutionGuaranteeMs(jobDT));
+ doReturn(mService.mConstants.RUNTIME_MIN_DATA_TRANSFER_GUARANTEE_MS / 2)
+ .when(connectivityController).getEstimatedTransferTimeMs(any());
+ assertEquals(mService.mConstants.RUNTIME_MIN_DATA_TRANSFER_GUARANTEE_MS,
+ mService.getMinJobExecutionGuaranteeMs(jobDT));
+ doReturn(mService.mConstants.RUNTIME_MIN_DATA_TRANSFER_GUARANTEE_MS * 2)
+ .when(connectivityController).getEstimatedTransferTimeMs(any());
+ assertEquals(mService.mConstants.RUNTIME_MIN_DATA_TRANSFER_GUARANTEE_MS,
+ mService.getMinJobExecutionGuaranteeMs(jobDT));
+ doReturn(mService.mConstants.RUNTIME_DATA_TRANSFER_LIMIT_MS * 2)
+ .when(connectivityController).getEstimatedTransferTimeMs(any());
+ assertEquals(mService.mConstants.RUNTIME_MIN_DATA_TRANSFER_GUARANTEE_MS,
+ mService.getMinJobExecutionGuaranteeMs(jobDT));
+ // UserInitiated
+ assertEquals(mService.mConstants.RUNTIME_MIN_USER_INITIATED_GUARANTEE_MS,
+ mService.getMinJobExecutionGuaranteeMs(jobUI));
+ grantRunLongJobsPermission(false);
+ assertEquals(mService.mConstants.RUNTIME_MIN_USER_INITIATED_GUARANTEE_MS,
+ mService.getMinJobExecutionGuaranteeMs(jobUIDT));
+ grantRunLongJobsPermission(true); // With permission
+ doReturn(ConnectivityController.UNKNOWN_TIME)
+ .when(connectivityController).getEstimatedTransferTimeMs(any());
+ assertEquals(mService.mConstants.RUNTIME_MIN_USER_INITIATED_DATA_TRANSFER_GUARANTEE_MS,
+ mService.getMinJobExecutionGuaranteeMs(jobUIDT));
+ doReturn(mService.mConstants.RUNTIME_MIN_USER_INITIATED_DATA_TRANSFER_GUARANTEE_MS / 2)
+ .when(connectivityController).getEstimatedTransferTimeMs(any());
+ assertEquals(mService.mConstants.RUNTIME_MIN_USER_INITIATED_DATA_TRANSFER_GUARANTEE_MS,
+ mService.getMinJobExecutionGuaranteeMs(jobUIDT));
+ doReturn(mService.mConstants.RUNTIME_MIN_USER_INITIATED_DATA_TRANSFER_GUARANTEE_MS * 2)
+ .when(connectivityController).getEstimatedTransferTimeMs(any());
+ assertEquals(
+ (long) (mService.mConstants.RUNTIME_MIN_USER_INITIATED_DATA_TRANSFER_GUARANTEE_MS
+ * 2 * 1.5),
+ mService.getMinJobExecutionGuaranteeMs(jobUIDT));
+ doReturn(mService.mConstants.RUNTIME_USER_INITIATED_DATA_TRANSFER_LIMIT_MS * 2)
+ .when(connectivityController).getEstimatedTransferTimeMs(any());
+ assertEquals(mService.mConstants.RUNTIME_USER_INITIATED_DATA_TRANSFER_LIMIT_MS,
+ mService.getMinJobExecutionGuaranteeMs(jobUIDT));
}
+ @Test
+ public void testGetMaxJobExecutionTimeMs() {
+ JobStatus jobDT = createJobStatus("testGetMaxJobExecutionTimeMs",
+ createJobInfo(7)
+ .setDataTransfer(true).setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY));
+ JobStatus jobUI = createJobStatus("testGetMaxJobExecutionTimeMs",
+ createJobInfo(9)); // TODO(255371817): add setUserInitiated(true)
+ JobStatus jobUIDT = createJobStatus("testGetMaxJobExecutionTimeMs",
+ // TODO(255371817): add setUserInitiated(true)
+ createJobInfo(10)
+ .setDataTransfer(true).setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY));
+
+ spyOn(jobDT);
+ spyOn(jobUI);
+ spyOn(jobUIDT);
+
+ when(jobUI.shouldTreatAsUserInitiated()).thenReturn(true);
+ when(jobUIDT.shouldTreatAsUserInitiated()).thenReturn(true);
+
+ grantRunLongJobsPermission(true);
+
+ assertEquals(mService.mConstants.RUNTIME_DATA_TRANSFER_LIMIT_MS,
+ mService.getMaxJobExecutionTimeMs(jobDT));
+ assertEquals(mService.mConstants.RUNTIME_USER_INITIATED_LIMIT_MS,
+ mService.getMaxJobExecutionTimeMs(jobUI));
+ assertEquals(mService.mConstants.RUNTIME_USER_INITIATED_DATA_TRANSFER_LIMIT_MS,
+ mService.getMaxJobExecutionTimeMs(jobUIDT));
+ }
/**
* Confirm that {@link JobSchedulerService#getRescheduleJobForFailureLocked(JobStatus, int)}
diff --git a/services/tests/mockingservicestests/src/com/android/server/job/controllers/ConnectivityControllerTest.java b/services/tests/mockingservicestests/src/com/android/server/job/controllers/ConnectivityControllerTest.java
index 1f85f2c..42e22f3 100644
--- a/services/tests/mockingservicestests/src/com/android/server/job/controllers/ConnectivityControllerTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/job/controllers/ConnectivityControllerTest.java
@@ -24,6 +24,7 @@
import static android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED;
import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
import static android.net.NetworkCapabilities.TRANSPORT_VPN;
+import static android.text.format.DateUtils.SECOND_IN_MILLIS;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.doNothing;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
@@ -35,6 +36,7 @@
import static com.android.server.job.JobSchedulerService.RARE_INDEX;
import static com.android.server.job.JobSchedulerService.RESTRICTED_INDEX;
+import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
@@ -240,20 +242,20 @@
.setLinkDownstreamBandwidthKbps(1).build(), mConstants));
// Slow downstream
assertFalse(controller.isSatisfied(createJobStatus(job), net,
- createCapabilitiesBuilder().setLinkUpstreamBandwidthKbps(137)
+ createCapabilitiesBuilder().setLinkUpstreamBandwidthKbps(140)
.setLinkDownstreamBandwidthKbps(1).build(), mConstants));
// Slow upstream
assertFalse(controller.isSatisfied(createJobStatus(job), net,
createCapabilitiesBuilder().setLinkUpstreamBandwidthKbps(1)
- .setLinkDownstreamBandwidthKbps(137).build(), mConstants));
+ .setLinkDownstreamBandwidthKbps(140).build(), mConstants));
// Network good enough
assertTrue(controller.isSatisfied(createJobStatus(job), net,
- createCapabilitiesBuilder().setLinkUpstreamBandwidthKbps(137)
- .setLinkDownstreamBandwidthKbps(137).build(), mConstants));
+ createCapabilitiesBuilder().setLinkUpstreamBandwidthKbps(140)
+ .setLinkDownstreamBandwidthKbps(140).build(), mConstants));
// Network slightly too slow given reduced time
assertFalse(controller.isSatisfied(createJobStatus(job), net,
- createCapabilitiesBuilder().setLinkUpstreamBandwidthKbps(130)
- .setLinkDownstreamBandwidthKbps(130).build(), mConstants));
+ createCapabilitiesBuilder().setLinkUpstreamBandwidthKbps(139)
+ .setLinkDownstreamBandwidthKbps(139).build(), mConstants));
// Slow network is too slow, but device is charging and network is unmetered.
when(mService.isBatteryCharging()).thenReturn(true);
controller.onBatteryStateChangedLocked();
@@ -1188,6 +1190,78 @@
assertFalse(unnetworked.isConstraintSatisfied(JobStatus.CONSTRAINT_CONNECTIVITY));
}
+ @Test
+ public void testCalculateTransferTimeMs() {
+ assertEquals(ConnectivityController.UNKNOWN_TIME,
+ ConnectivityController.calculateTransferTimeMs(1, 0));
+ assertEquals(ConnectivityController.UNKNOWN_TIME,
+ ConnectivityController.calculateTransferTimeMs(JobInfo.NETWORK_BYTES_UNKNOWN, 512));
+ assertEquals(1, ConnectivityController.calculateTransferTimeMs(1, 8));
+ assertEquals(1000, ConnectivityController.calculateTransferTimeMs(1000, 8));
+ assertEquals(8, ConnectivityController.calculateTransferTimeMs(1024, 1024));
+ }
+
+ @Test
+ public void testGetEstimatedTransferTimeMs() {
+ final ArgumentCaptor<NetworkCallback> callbackCaptor =
+ ArgumentCaptor.forClass(NetworkCallback.class);
+ doNothing().when(mConnManager).registerNetworkCallback(any(), callbackCaptor.capture());
+
+ final JobStatus job = createJobStatus(createJob()
+ .setEstimatedNetworkBytes(DataUnit.MEBIBYTES.toBytes(10_000),
+ DataUnit.MEBIBYTES.toBytes(1_000))
+ .setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY));
+
+ final ConnectivityController controller = new ConnectivityController(mService,
+ mFlexibilityController);
+
+ final JobStatus jobNoEstimates = createJobStatus(createJob());
+ assertEquals(ConnectivityController.UNKNOWN_TIME,
+ controller.getEstimatedTransferTimeMs(jobNoEstimates));
+
+ // No network
+ job.network = null;
+ assertEquals(ConnectivityController.UNKNOWN_TIME,
+ controller.getEstimatedTransferTimeMs(job));
+
+ final NetworkCallback generalCallback = callbackCaptor.getValue();
+
+ // No capabilities
+ final Network network = mock(Network.class);
+ answerNetwork(generalCallback, null, null, network, null);
+ job.network = network;
+ assertEquals(ConnectivityController.UNKNOWN_TIME,
+ controller.getEstimatedTransferTimeMs(job));
+
+ // Capabilities don't have bandwidth values
+ NetworkCapabilities caps = createCapabilitiesBuilder().build();
+ answerNetwork(generalCallback, null, null, network, caps);
+ assertEquals(ConnectivityController.UNKNOWN_TIME,
+ controller.getEstimatedTransferTimeMs(job));
+
+ // Capabilities only has downstream bandwidth
+ caps = createCapabilitiesBuilder()
+ .setLinkDownstreamBandwidthKbps(1024)
+ .build();
+ answerNetwork(generalCallback, null, null, network, caps);
+ assertEquals(81920 * SECOND_IN_MILLIS, controller.getEstimatedTransferTimeMs(job));
+
+ // Capabilities only has upstream bandwidth
+ caps = createCapabilitiesBuilder()
+ .setLinkUpstreamBandwidthKbps(2 * 1024)
+ .build();
+ answerNetwork(generalCallback, null, null, network, caps);
+ assertEquals(4096 * SECOND_IN_MILLIS, controller.getEstimatedTransferTimeMs(job));
+
+ // Capabilities only both stream bandwidths
+ caps = createCapabilitiesBuilder()
+ .setLinkDownstreamBandwidthKbps(1024)
+ .setLinkUpstreamBandwidthKbps(2 * 1024)
+ .build();
+ answerNetwork(generalCallback, null, null, network, caps);
+ assertEquals((81920 + 4096) * SECOND_IN_MILLIS, controller.getEstimatedTransferTimeMs(job));
+ }
+
private void answerNetwork(@NonNull NetworkCallback generalCallback,
@Nullable NetworkCallback uidCallback, @Nullable Network lastNetwork,
@Nullable Network net, @Nullable NetworkCapabilities caps) {
@@ -1198,11 +1272,15 @@
}
} else {
generalCallback.onAvailable(net);
- generalCallback.onCapabilitiesChanged(net, caps);
+ if (caps != null) {
+ generalCallback.onCapabilitiesChanged(net, caps);
+ }
if (uidCallback != null) {
uidCallback.onAvailable(net);
uidCallback.onBlockedStatusChanged(net, ConnectivityManager.BLOCKED_REASON_NONE);
- uidCallback.onCapabilitiesChanged(net, caps);
+ if (caps != null) {
+ uidCallback.onCapabilitiesChanged(net, caps);
+ }
}
}
}
diff --git a/services/tests/servicestests/AndroidManifest.xml b/services/tests/servicestests/AndroidManifest.xml
index 9eb3b92..7149265 100644
--- a/services/tests/servicestests/AndroidManifest.xml
+++ b/services/tests/servicestests/AndroidManifest.xml
@@ -98,6 +98,7 @@
<uses-permission android:name="android.permission.MAINLINE_NETWORK_STACK"/>
<uses-permission
android:name="android.permission.OVERRIDE_COMPAT_CHANGE_CONFIG_ON_RELEASE_BUILD"/>
+ <uses-permission android:name="android.permission.OVERRIDE_COMPAT_CHANGE_CONFIG"/>
<uses-permission android:name="android.permission.ASSOCIATE_INPUT_DEVICE_TO_DISPLAY" />
<uses-permission android:name="android.permission.READ_NEARBY_STREAMING_POLICY" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_ROUTING" />
diff --git a/services/tests/servicestests/res/xml/power_profile_test_legacy_modem.xml b/services/tests/servicestests/res/xml/power_profile_test_legacy_modem.xml
new file mode 100644
index 0000000..5335f96
--- /dev/null
+++ b/services/tests/servicestests/res/xml/power_profile_test_legacy_modem.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2022 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<device name="Android">
+ <!-- Cellular modem related values. These constants are deprecated, but still supported and
+ need to be tested -->
+ <item name="radio.scanning">720</item>
+ <item name="modem.controller.sleep">70</item>
+ <item name="modem.controller.idle">360</item>
+ <item name="modem.controller.rx">1440</item>
+ <array name="modem.controller.tx"> <!-- Strength 0 to 4 -->
+ <value>720</value>
+ <value>1080</value>
+ <value>1440</value>
+ <value>1800</value>
+ <value>2160</value>
+ </array>
+</device>
\ No newline at end of file
diff --git a/services/tests/servicestests/res/xml/power_profile_test_modem_calculator.xml b/services/tests/servicestests/res/xml/power_profile_test_modem_calculator.xml
new file mode 100644
index 0000000..f57bc0f
--- /dev/null
+++ b/services/tests/servicestests/res/xml/power_profile_test_modem_calculator.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2022 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<device name="Android">
+ <modem>
+ <sleep>70</sleep>
+ <idle>360</idle>
+ <active rat="DEFAULT">
+ <receive>1440</receive>
+ <transmit level="0">720</transmit>
+ <transmit level="1">1080</transmit>
+ <transmit level="2">1440</transmit>
+ <transmit level="3">1800</transmit>
+ <transmit level="4">2160</transmit>
+ </active>
+ </modem>
+</device>
\ No newline at end of file
diff --git a/services/tests/servicestests/res/xml/power_profile_test_modem_calculator_multiactive.xml b/services/tests/servicestests/res/xml/power_profile_test_modem_calculator_multiactive.xml
new file mode 100644
index 0000000..4f5e674
--- /dev/null
+++ b/services/tests/servicestests/res/xml/power_profile_test_modem_calculator_multiactive.xml
@@ -0,0 +1,63 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2022 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<device name="Android">
+ <modem>
+ <sleep>70</sleep>
+ <idle>360</idle>
+ <active rat="DEFAULT">
+ <receive>1440</receive>
+ <transmit level="0">720</transmit>
+ <transmit level="1">1080</transmit>
+ <transmit level="2">1440</transmit>
+ <transmit level="3">1800</transmit>
+ <transmit level="4">2160</transmit>
+ </active>
+ <active rat="LTE">
+ <receive>2000</receive>
+ <transmit level="0">800</transmit>
+ <transmit level="1">1200</transmit>
+ <transmit level="2">1600</transmit>
+ <transmit level="3">2000</transmit>
+ <transmit level="4">2400</transmit>
+ </active>
+ <active rat="NR" nrFrequency="DEFAULT">
+ <receive>2222</receive>
+ <transmit level="0">999</transmit>
+ <transmit level="1">1333</transmit>
+ <transmit level="2">1888</transmit>
+ <transmit level="3">2222</transmit>
+ <transmit level="4">2666</transmit>
+ </active>
+ <active rat="NR" nrFrequency="HIGH">
+ <receive>2727</receive>
+ <transmit level="0">1818</transmit>
+ <transmit level="1">2727</transmit>
+ <transmit level="2">3636</transmit>
+ <transmit level="3">4545</transmit>
+ <transmit level="4">5454</transmit>
+ </active>
+ <active rat="NR" nrFrequency="MMWAVE">
+ <receive>3456</receive>
+ <transmit level="0">2345</transmit>
+ <transmit level="1">3456</transmit>
+ <transmit level="2">4567</transmit>
+ <transmit level="3">5678</transmit>
+ <transmit level="4">6789</transmit>
+ </active>
+ </modem>
+</device>
\ No newline at end of file
diff --git a/services/tests/servicestests/res/xml/power_profile_test_modem_default.xml b/services/tests/servicestests/res/xml/power_profile_test_modem_default.xml
new file mode 100644
index 0000000..ab016fb
--- /dev/null
+++ b/services/tests/servicestests/res/xml/power_profile_test_modem_default.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2022 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<device name="Android">
+ <modem>
+ <!-- Modem sleep drain current value in mA. -->
+ <sleep>10</sleep>
+ <!-- Modem idle drain current value in mA. -->
+ <idle>20</idle>
+ <active rat="DEFAULT">
+ <!-- Transmit current drain in mA. -->
+ <receive>30</receive>
+ <!-- Transmit current drains in mA. Must be defined for all levels (0 to 4) -->
+ <transmit level="0">40</transmit>
+ <transmit level="1">50</transmit>
+ <transmit level="2">60</transmit>
+ <transmit level="3">70</transmit>
+ <transmit level="4">80</transmit>
+ </active>
+ </modem>
+</device>
\ No newline at end of file
diff --git a/services/tests/servicestests/src/com/android/server/companion/virtual/audio/VirtualAudioControllerTest.java b/services/tests/servicestests/src/com/android/server/companion/virtual/audio/VirtualAudioControllerTest.java
index 3ca648c..aaa1351 100644
--- a/services/tests/servicestests/src/com/android/server/companion/virtual/audio/VirtualAudioControllerTest.java
+++ b/services/tests/servicestests/src/com/android/server/companion/virtual/audio/VirtualAudioControllerTest.java
@@ -16,7 +16,6 @@
package com.android.server.companion.virtual.audio;
-import static android.companion.AssociationRequest.DEVICE_PROFILE_APP_STREAMING;
import static android.media.AudioAttributes.FLAG_SECURE;
import static android.media.AudioPlaybackConfiguration.PLAYER_STATE_STARTED;
import static android.view.WindowManager.LayoutParams.SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS;
@@ -86,8 +85,9 @@
/* pipBlockedCallback= */ null,
/* activityBlockedCallback= */ null,
/* secureWindowCallback= */ null,
- /* deviceProfile= */ DEVICE_PROFILE_APP_STREAMING,
- /* displayCategories= */ new ArrayList<>());
+ /* displayCategories= */ new ArrayList<>(),
+ /* recentsPolicy= */
+ VirtualDeviceParams.RECENTS_POLICY_ALLOW_IN_HOST_DEVICE_RECENTS);
}
diff --git a/services/tests/servicestests/src/com/android/server/display/DisplayManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/display/DisplayManagerServiceTest.java
index 303b240..f676a3f 100644
--- a/services/tests/servicestests/src/com/android/server/display/DisplayManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/display/DisplayManagerServiceTest.java
@@ -214,7 +214,7 @@
.thenReturn(mMockDisplayToken);
SurfaceControl.StaticDisplayInfo staticDisplayInfo = new SurfaceControl.StaticDisplayInfo();
staticDisplayInfo.isInternal = true;
- when(mSurfaceControlProxy.getStaticDisplayInfo(mMockDisplayToken))
+ when(mSurfaceControlProxy.getStaticDisplayInfo(anyLong()))
.thenReturn(staticDisplayInfo);
SurfaceControl.DynamicDisplayInfo dynamicDisplayMode =
new SurfaceControl.DynamicDisplayInfo();
@@ -223,7 +223,7 @@
displayMode.height = 200;
displayMode.supportedHdrTypes = new int[]{1, 2};
dynamicDisplayMode.supportedDisplayModes = new SurfaceControl.DisplayMode[] {displayMode};
- when(mSurfaceControlProxy.getDynamicDisplayInfo(mMockDisplayToken))
+ when(mSurfaceControlProxy.getDynamicDisplayInfo(anyLong()))
.thenReturn(dynamicDisplayMode);
when(mSurfaceControlProxy.getDesiredDisplayModeSpecs(mMockDisplayToken))
.thenReturn(new SurfaceControl.DesiredDisplayModeSpecs());
@@ -1253,6 +1253,76 @@
}
/**
+ * Tests that there is a display change notification if the render frame rate is updated
+ */
+ @Test
+ public void testShouldNotifyChangeWhenDisplayInfoRenderFrameRateChanged() throws Exception {
+ DisplayManagerService displayManager =
+ new DisplayManagerService(mContext, mShortMockedInjector);
+ DisplayManagerService.BinderService displayManagerBinderService =
+ displayManager.new BinderService();
+ registerDefaultDisplays(displayManager);
+ displayManager.onBootPhase(SystemService.PHASE_WAIT_FOR_DEFAULT_DISPLAY);
+
+ FakeDisplayDevice displayDevice = createFakeDisplayDevice(displayManager, new float[]{60f});
+ FakeDisplayManagerCallback callback = registerDisplayListenerCallback(displayManager,
+ displayManagerBinderService, displayDevice);
+
+ updateRenderFrameRate(displayManager, displayDevice, 30f);
+ assertTrue(callback.mDisplayChangedCalled);
+ callback.clear();
+
+ updateRenderFrameRate(displayManager, displayDevice, 30f);
+ assertFalse(callback.mDisplayChangedCalled);
+
+ updateRenderFrameRate(displayManager, displayDevice, 20f);
+ assertTrue(callback.mDisplayChangedCalled);
+ callback.clear();
+ }
+
+ /**
+ * Tests that the DisplayInfo is updated correctly with a render frame rate
+ */
+ @Test
+ public void testDisplayInfoRenderFrameRate() throws Exception {
+ DisplayManagerService displayManager =
+ new DisplayManagerService(mContext, mShortMockedInjector);
+ DisplayManagerService.BinderService displayManagerBinderService =
+ displayManager.new BinderService();
+ registerDefaultDisplays(displayManager);
+ displayManager.onBootPhase(SystemService.PHASE_WAIT_FOR_DEFAULT_DISPLAY);
+
+ FakeDisplayDevice displayDevice = createFakeDisplayDevice(displayManager,
+ new float[]{60f, 30f, 20f});
+ int displayId = getDisplayIdForDisplayDevice(displayManager, displayManagerBinderService,
+ displayDevice);
+ DisplayInfo displayInfo = displayManagerBinderService.getDisplayInfo(displayId);
+ assertEquals(60f, displayInfo.getRefreshRate(), 0.01f);
+
+ updateRenderFrameRate(displayManager, displayDevice, 20f);
+ displayInfo = displayManagerBinderService.getDisplayInfo(displayId);
+ assertEquals(20f, displayInfo.getRefreshRate(), 0.01f);
+ }
+
+ /**
+ * Tests that the mode reflects the render frame rate is in compat mode
+ */
+ @Test
+ @DisableCompatChanges({DisplayManagerService.DISPLAY_MODE_RETURNS_PHYSICAL_REFRESH_RATE})
+ public void testDisplayInfoRenderFrameRateModeCompat() throws Exception {
+ testDisplayInfoRenderFrameRateModeCompat(/*compatChangeEnabled*/ false);
+ }
+
+ /**
+ * Tests that the mode reflects the physical display refresh rate when not in compat mode.
+ */
+ @Test
+ @EnableCompatChanges({DisplayManagerService.DISPLAY_MODE_RETURNS_PHYSICAL_REFRESH_RATE})
+ public void testDisplayInfoRenderFrameRateMode() throws Exception {
+ testDisplayInfoRenderFrameRateModeCompat(/*compatChangeEnabled*/ true);
+ }
+
+ /**
* Tests that EVENT_DISPLAY_ADDED is sent when a display is added.
*/
@Test
@@ -1472,6 +1542,34 @@
assertEquals(expectedMode, displayInfo.getMode());
}
+ private void testDisplayInfoRenderFrameRateModeCompat(boolean compatChangeEnabled)
+ throws Exception {
+ DisplayManagerService displayManager =
+ new DisplayManagerService(mContext, mShortMockedInjector);
+ DisplayManagerService.BinderService displayManagerBinderService =
+ displayManager.new BinderService();
+ registerDefaultDisplays(displayManager);
+ displayManager.onBootPhase(SystemService.PHASE_WAIT_FOR_DEFAULT_DISPLAY);
+
+ FakeDisplayDevice displayDevice = createFakeDisplayDevice(displayManager,
+ new float[]{60f, 30f, 20f});
+ int displayId = getDisplayIdForDisplayDevice(displayManager, displayManagerBinderService,
+ displayDevice);
+ DisplayInfo displayInfo = displayManagerBinderService.getDisplayInfo(displayId);
+ assertEquals(60f, displayInfo.getRefreshRate(), 0.01f);
+
+ updateRenderFrameRate(displayManager, displayDevice, 20f);
+ displayInfo = displayManagerBinderService.getDisplayInfo(displayId);
+ assertEquals(20f, displayInfo.getRefreshRate(), 0.01f);
+ Display.Mode expectedMode;
+ if (compatChangeEnabled) {
+ expectedMode = new Display.Mode(1, 100, 200, 60f);
+ } else {
+ expectedMode = new Display.Mode(3, 100, 200, 20f);
+ }
+ assertEquals(expectedMode, displayInfo.getMode());
+ }
+
private void testDisplayInfoNonNativeFrameRateOverrideMode(boolean compatChangeEnabled) {
DisplayManagerService displayManager =
new DisplayManagerService(mContext, mBasicInjector);
@@ -1541,6 +1639,15 @@
updateDisplayDeviceInfo(displayManager, displayDevice, displayDeviceInfo);
}
+ private void updateRenderFrameRate(DisplayManagerService displayManager,
+ FakeDisplayDevice displayDevice,
+ float renderFrameRate) {
+ DisplayDeviceInfo displayDeviceInfo = new DisplayDeviceInfo();
+ displayDeviceInfo.copyFrom(displayDevice.getDisplayDeviceInfoLocked());
+ displayDeviceInfo.renderFrameRate = renderFrameRate;
+ updateDisplayDeviceInfo(displayManager, displayDevice, displayDeviceInfo);
+ }
+
private void updateModeId(DisplayManagerService displayManager,
FakeDisplayDevice displayDevice,
int modeId) {
@@ -1580,6 +1687,7 @@
new Display.Mode(i + 1, width, height, refreshRates[i]);
}
displayDeviceInfo.modeId = 1;
+ displayDeviceInfo.renderFrameRate = displayDeviceInfo.supportedModes[0].getRefreshRate();
displayDeviceInfo.width = width;
displayDeviceInfo.height = height;
final Rect zeroRect = new Rect();
diff --git a/services/tests/servicestests/src/com/android/server/graphics/fonts/UpdatableFontDirTest.java b/services/tests/servicestests/src/com/android/server/graphics/fonts/UpdatableFontDirTest.java
index 9672085..68e5ebf 100644
--- a/services/tests/servicestests/src/com/android/server/graphics/fonts/UpdatableFontDirTest.java
+++ b/services/tests/servicestests/src/com/android/server/graphics/fonts/UpdatableFontDirTest.java
@@ -109,17 +109,16 @@
@Override
public boolean isFromTrustedProvider(String path, byte[] signature) {
- return mHasFsverityPaths.contains(path);
+ if (!mHasFsverityPaths.contains(path)) {
+ return false;
+ }
+ String fakeSignature = new String(signature, StandardCharsets.UTF_8);
+ return GOOD_SIGNATURE.equals(fakeSignature);
}
@Override
- public void setUpFsverity(String path, byte[] pkcs7Signature) throws IOException {
- String fakeSignature = new String(pkcs7Signature, StandardCharsets.UTF_8);
- if (GOOD_SIGNATURE.equals(fakeSignature)) {
- mHasFsverityPaths.add(path);
- } else {
- throw new IOException("Failed to set up fake fs-verity");
- }
+ public void setUpFsverity(String path) throws IOException {
+ mHasFsverityPaths.add(path);
}
@Override
@@ -813,8 +812,8 @@
}
@Override
- public void setUpFsverity(String path, byte[] pkcs7Signature) throws IOException {
- mFakeFsverityUtil.setUpFsverity(path, pkcs7Signature);
+ public void setUpFsverity(String path) throws IOException {
+ mFakeFsverityUtil.setUpFsverity(path);
}
@Override
diff --git a/services/tests/servicestests/src/com/android/server/input/KeyRemapperTests.kt b/services/tests/servicestests/src/com/android/server/input/KeyRemapperTests.kt
new file mode 100644
index 0000000..c22782c
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/input/KeyRemapperTests.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.android.server.input
+
+import android.content.Context
+import android.content.ContextWrapper
+import android.hardware.input.IInputManager
+import android.hardware.input.InputManager
+import android.os.test.TestLooper
+import android.platform.test.annotations.Presubmit
+import android.view.InputDevice
+import android.view.KeyEvent
+import androidx.test.core.app.ApplicationProvider
+import org.junit.After
+import org.junit.Assert.assertEquals
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.mockito.Mock
+import org.mockito.Mockito
+import org.mockito.junit.MockitoJUnit
+import java.io.FileNotFoundException
+import java.io.FileOutputStream
+import java.io.IOException
+import java.io.InputStream
+
+private fun createKeyboard(deviceId: Int): InputDevice =
+ InputDevice.Builder()
+ .setId(deviceId)
+ .setName("Device $deviceId")
+ .setDescriptor("descriptor $deviceId")
+ .setSources(InputDevice.SOURCE_KEYBOARD)
+ .setKeyboardType(InputDevice.KEYBOARD_TYPE_ALPHABETIC)
+ .setExternal(true)
+ .build()
+
+/**
+ * Tests for {@link KeyRemapper}.
+ *
+ * Build/Install/Run:
+ * atest FrameworksServicesTests:KeyRemapperTests
+ */
+@Presubmit
+class KeyRemapperTests {
+
+ companion object {
+ const val DEVICE_ID = 1
+ val REMAPPABLE_KEYS = intArrayOf(
+ KeyEvent.KEYCODE_CTRL_LEFT, KeyEvent.KEYCODE_CTRL_RIGHT,
+ KeyEvent.KEYCODE_META_LEFT, KeyEvent.KEYCODE_META_RIGHT,
+ KeyEvent.KEYCODE_ALT_LEFT, KeyEvent.KEYCODE_ALT_RIGHT,
+ KeyEvent.KEYCODE_SHIFT_LEFT, KeyEvent.KEYCODE_SHIFT_RIGHT,
+ KeyEvent.KEYCODE_CAPS_LOCK
+ )
+ }
+
+ @get:Rule
+ val rule = MockitoJUnit.rule()!!
+
+ @Mock
+ private lateinit var iInputManager: IInputManager
+ @Mock
+ private lateinit var native: NativeInputManagerService
+ private lateinit var mKeyRemapper: KeyRemapper
+ private lateinit var context: Context
+ private lateinit var dataStore: PersistentDataStore
+ private lateinit var testLooper: TestLooper
+
+ @Before
+ fun setup() {
+ context = Mockito.spy(ContextWrapper(ApplicationProvider.getApplicationContext()))
+ dataStore = PersistentDataStore(object : PersistentDataStore.Injector() {
+ override fun openRead(): InputStream? {
+ throw FileNotFoundException()
+ }
+
+ override fun startWrite(): FileOutputStream? {
+ throw IOException()
+ }
+
+ override fun finishWrite(fos: FileOutputStream?, success: Boolean) {}
+ })
+ testLooper = TestLooper()
+ mKeyRemapper = KeyRemapper(
+ context,
+ native,
+ dataStore,
+ testLooper.looper
+ )
+ val inputManager = InputManager.resetInstance(iInputManager)
+ Mockito.`when`(context.getSystemService(Mockito.eq(Context.INPUT_SERVICE)))
+ .thenReturn(inputManager)
+ Mockito.`when`(iInputManager.inputDeviceIds).thenReturn(intArrayOf(DEVICE_ID))
+ }
+
+ @After
+ fun tearDown() {
+ InputManager.clearInstance()
+ }
+
+ @Test
+ fun testKeyRemapping() {
+ val keyboard = createKeyboard(DEVICE_ID)
+ Mockito.`when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboard)
+
+ for (i in REMAPPABLE_KEYS.indices) {
+ val fromKeyCode = REMAPPABLE_KEYS[i]
+ val toKeyCode = REMAPPABLE_KEYS[(i + 1) % REMAPPABLE_KEYS.size]
+ mKeyRemapper.remapKey(fromKeyCode, toKeyCode)
+ testLooper.dispatchNext()
+ }
+
+ val remapping = mKeyRemapper.keyRemapping
+ val expectedSize = REMAPPABLE_KEYS.size
+ assertEquals("Remapping size should be $expectedSize", expectedSize, remapping.size)
+
+ for (i in REMAPPABLE_KEYS.indices) {
+ val fromKeyCode = REMAPPABLE_KEYS[i]
+ val toKeyCode = REMAPPABLE_KEYS[(i + 1) % REMAPPABLE_KEYS.size]
+ assertEquals(
+ "Remapping should include mapping from $fromKeyCode to $toKeyCode",
+ toKeyCode,
+ remapping.getOrDefault(fromKeyCode, -1)
+ )
+ }
+
+ mKeyRemapper.clearAllKeyRemappings()
+ testLooper.dispatchNext()
+
+ assertEquals(
+ "Remapping size should be 0 after clearAllModifierKeyRemappings",
+ 0,
+ mKeyRemapper.keyRemapping.size
+ )
+ }
+}
\ No newline at end of file
diff --git a/services/tests/servicestests/src/com/android/server/job/JobStoreTest.java b/services/tests/servicestests/src/com/android/server/job/JobStoreTest.java
index dc47b5e..0589b3a 100644
--- a/services/tests/servicestests/src/com/android/server/job/JobStoreTest.java
+++ b/services/tests/servicestests/src/com/android/server/job/JobStoreTest.java
@@ -8,6 +8,7 @@
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
@@ -22,6 +23,7 @@
import android.os.PersistableBundle;
import android.os.SystemClock;
import android.test.RenamingDelegatingContext;
+import android.util.ArraySet;
import android.util.Log;
import android.util.Pair;
@@ -38,9 +40,9 @@
import org.junit.Test;
import org.junit.runner.RunWith;
+import java.io.File;
import java.time.Clock;
import java.time.ZoneOffset;
-import java.util.Iterator;
/**
* Test reading and writing correctly from file.
@@ -93,11 +95,147 @@
mTaskStoreUnderTest.waitForWriteToCompleteForTesting(5_000L);
}
+ private void setUseSplitFiles(boolean useSplitFiles) throws Exception {
+ mTaskStoreUnderTest.setUseSplitFiles(useSplitFiles);
+ waitForPendingIo();
+ }
+
private void waitForPendingIo() throws Exception {
assertTrue("Timed out waiting for persistence I/O to complete",
mTaskStoreUnderTest.waitForWriteToCompleteForTesting(5_000L));
}
+ /** Test that we properly remove the last job of an app from the persisted file. */
+ @Test
+ public void testRemovingLastJob_singleFile() throws Exception {
+ setUseSplitFiles(false);
+ runRemovingLastJob();
+ }
+
+ /** Test that we properly remove the last job of an app from the persisted file. */
+ @Test
+ public void testRemovingLastJob_splitFiles() throws Exception {
+ setUseSplitFiles(true);
+ runRemovingLastJob();
+ }
+
+ private void runRemovingLastJob() throws Exception {
+ final JobInfo task1 = new Builder(8, mComponent)
+ .setRequiresDeviceIdle(true)
+ .setPeriodic(10000L)
+ .setRequiresCharging(true)
+ .setPersisted(true)
+ .build();
+ final JobInfo task2 = new Builder(12, mComponent)
+ .setMinimumLatency(5000L)
+ .setBackoffCriteria(15000L, JobInfo.BACKOFF_POLICY_LINEAR)
+ .setOverrideDeadline(30000L)
+ .setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED)
+ .setPersisted(true)
+ .build();
+ final int uid1 = SOME_UID;
+ final int uid2 = uid1 + 1;
+ final JobStatus JobStatus1 = JobStatus.createFromJobInfo(task1, uid1, null, -1, null);
+ final JobStatus JobStatus2 = JobStatus.createFromJobInfo(task2, uid2, null, -1, null);
+ runWritingJobsToDisk(JobStatus1, JobStatus2);
+
+ // Remove 1 job
+ mTaskStoreUnderTest.remove(JobStatus1, true);
+ waitForPendingIo();
+ JobSet jobStatusSet = new JobSet();
+ mTaskStoreUnderTest.readJobMapFromDisk(jobStatusSet, true);
+ assertEquals("Incorrect # of persisted tasks.", 1, jobStatusSet.size());
+ JobStatus loaded = jobStatusSet.getAllJobs().iterator().next();
+
+ assertJobsEqual(JobStatus2, loaded);
+ assertTrue("JobStore#contains invalid.", mTaskStoreUnderTest.containsJob(JobStatus2));
+
+ // Remove 2nd job
+ mTaskStoreUnderTest.remove(JobStatus2, true);
+ waitForPendingIo();
+ jobStatusSet = new JobSet();
+ mTaskStoreUnderTest.readJobMapFromDisk(jobStatusSet, true);
+ assertEquals("Incorrect # of persisted tasks.", 0, jobStatusSet.size());
+ }
+
+ /** Test that we properly clear the persisted file when all jobs are dropped. */
+ @Test
+ public void testClearJobs_singleFile() throws Exception {
+ setUseSplitFiles(false);
+ runClearJobs();
+ }
+
+ /** Test that we properly clear the persisted file when all jobs are dropped. */
+ @Test
+ public void testClearJobs_splitFiles() throws Exception {
+ setUseSplitFiles(true);
+ runClearJobs();
+ }
+
+ private void runClearJobs() throws Exception {
+ final JobInfo task1 = new Builder(8, mComponent)
+ .setRequiresDeviceIdle(true)
+ .setPeriodic(10000L)
+ .setRequiresCharging(true)
+ .setPersisted(true)
+ .build();
+ final JobInfo task2 = new Builder(12, mComponent)
+ .setMinimumLatency(5000L)
+ .setBackoffCriteria(15000L, JobInfo.BACKOFF_POLICY_LINEAR)
+ .setOverrideDeadline(30000L)
+ .setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED)
+ .setPersisted(true)
+ .build();
+ final int uid1 = SOME_UID;
+ final int uid2 = uid1 + 1;
+ final JobStatus JobStatus1 = JobStatus.createFromJobInfo(task1, uid1, null, -1, null);
+ final JobStatus JobStatus2 = JobStatus.createFromJobInfo(task2, uid2, null, -1, null);
+ runWritingJobsToDisk(JobStatus1, JobStatus2);
+
+ // Remove all jobs
+ mTaskStoreUnderTest.clear();
+ waitForPendingIo();
+ JobSet jobStatusSet = new JobSet();
+ mTaskStoreUnderTest.readJobMapFromDisk(jobStatusSet, true);
+ assertEquals("Incorrect # of persisted tasks.", 0, jobStatusSet.size());
+ }
+
+ @Test
+ public void testExtractUidFromJobFileName() {
+ File file = new File(mTestContext.getFilesDir(), "randomName");
+ assertEquals(JobStore.INVALID_UID, JobStore.extractUidFromJobFileName(file));
+
+ file = new File(mTestContext.getFilesDir(), "jobs.xml");
+ assertEquals(JobStore.INVALID_UID, JobStore.extractUidFromJobFileName(file));
+
+ file = new File(mTestContext.getFilesDir(), ".xml");
+ assertEquals(JobStore.INVALID_UID, JobStore.extractUidFromJobFileName(file));
+
+ file = new File(mTestContext.getFilesDir(), "1000.xml");
+ assertEquals(JobStore.INVALID_UID, JobStore.extractUidFromJobFileName(file));
+
+ file = new File(mTestContext.getFilesDir(), "10000");
+ assertEquals(JobStore.INVALID_UID, JobStore.extractUidFromJobFileName(file));
+
+ file = new File(mTestContext.getFilesDir(), JobStore.JOB_FILE_SPLIT_PREFIX);
+ assertEquals(JobStore.INVALID_UID, JobStore.extractUidFromJobFileName(file));
+
+ file = new File(mTestContext.getFilesDir(), JobStore.JOB_FILE_SPLIT_PREFIX + "text.xml");
+ assertEquals(JobStore.INVALID_UID, JobStore.extractUidFromJobFileName(file));
+
+ file = new File(mTestContext.getFilesDir(), JobStore.JOB_FILE_SPLIT_PREFIX + ".xml");
+ assertEquals(JobStore.INVALID_UID, JobStore.extractUidFromJobFileName(file));
+
+ file = new File(mTestContext.getFilesDir(), JobStore.JOB_FILE_SPLIT_PREFIX + "-10123.xml");
+ assertEquals(JobStore.INVALID_UID, JobStore.extractUidFromJobFileName(file));
+
+ file = new File(mTestContext.getFilesDir(), JobStore.JOB_FILE_SPLIT_PREFIX + "1.xml");
+ assertEquals(1, JobStore.extractUidFromJobFileName(file));
+
+ file = new File(mTestContext.getFilesDir(), JobStore.JOB_FILE_SPLIT_PREFIX + "101023.xml");
+ assertEquals(101023, JobStore.extractUidFromJobFileName(file));
+ }
+
@Test
public void testStringToIntArrayAndIntArrayToString() {
final int[] netCapabilitiesIntArray = { 1, 3, 5, 7, 9 };
@@ -144,13 +282,13 @@
@Test
public void testWritingTwoJobsToDisk_singleFile() throws Exception {
- mTaskStoreUnderTest.setUseSplitFiles(false);
+ setUseSplitFiles(false);
runWritingTwoJobsToDisk();
}
@Test
public void testWritingTwoJobsToDisk_splitFiles() throws Exception {
- mTaskStoreUnderTest.setUseSplitFiles(true);
+ setUseSplitFiles(true);
runWritingTwoJobsToDisk();
}
@@ -172,28 +310,44 @@
final int uid2 = uid1 + 1;
final JobStatus taskStatus1 = JobStatus.createFromJobInfo(task1, uid1, null, -1, null);
final JobStatus taskStatus2 = JobStatus.createFromJobInfo(task2, uid2, null, -1, null);
- mTaskStoreUnderTest.add(taskStatus1);
- mTaskStoreUnderTest.add(taskStatus2);
+
+ runWritingJobsToDisk(taskStatus1, taskStatus2);
+ }
+
+ private void runWritingJobsToDisk(JobStatus... jobStatuses) throws Exception {
+ ArraySet<JobStatus> expectedJobs = new ArraySet<>();
+ for (JobStatus jobStatus : jobStatuses) {
+ mTaskStoreUnderTest.add(jobStatus);
+ expectedJobs.add(jobStatus);
+ }
waitForPendingIo();
final JobSet jobStatusSet = new JobSet();
mTaskStoreUnderTest.readJobMapFromDisk(jobStatusSet, true);
- assertEquals("Incorrect # of persisted tasks.", 2, jobStatusSet.size());
- Iterator<JobStatus> it = jobStatusSet.getAllJobs().iterator();
- JobStatus loaded1 = it.next();
- JobStatus loaded2 = it.next();
+ assertEquals("Incorrect # of persisted tasks.", expectedJobs.size(), jobStatusSet.size());
+ int count = 0;
+ final int expectedCount = expectedJobs.size();
+ for (JobStatus loaded : jobStatusSet.getAllJobs()) {
+ count++;
+ for (int i = 0; i < expectedJobs.size(); ++i) {
+ JobStatus expected = expectedJobs.valueAt(i);
- // Reverse them so we know which comparison to make.
- if (loaded1.getJobId() != 8) {
- JobStatus tmp = loaded1;
- loaded1 = loaded2;
- loaded2 = tmp;
+ try {
+ assertJobsEqual(expected, loaded);
+ expectedJobs.remove(expected);
+ break;
+ } catch (AssertionError e) {
+ // Not equal. Move along.
+ }
+ }
}
-
- assertJobsEqual(taskStatus1, loaded1);
- assertJobsEqual(taskStatus2, loaded2);
- assertTrue("JobStore#contains invalid.", mTaskStoreUnderTest.containsJob(taskStatus1));
- assertTrue("JobStore#contains invalid.", mTaskStoreUnderTest.containsJob(taskStatus2));
+ assertEquals("Loaded more jobs than expected", expectedCount, count);
+ if (expectedJobs.size() > 0) {
+ fail("Not all expected jobs were restored");
+ }
+ for (JobStatus jobStatus : jobStatuses) {
+ assertTrue("JobStore#contains invalid.", mTaskStoreUnderTest.containsJob(jobStatus));
+ }
}
@Test
diff --git a/services/tests/servicestests/src/com/android/server/locksettings/BaseLockSettingsServiceTests.java b/services/tests/servicestests/src/com/android/server/locksettings/BaseLockSettingsServiceTests.java
index 55ab4a0..15eaf5d 100644
--- a/services/tests/servicestests/src/com/android/server/locksettings/BaseLockSettingsServiceTests.java
+++ b/services/tests/servicestests/src/com/android/server/locksettings/BaseLockSettingsServiceTests.java
@@ -36,7 +36,7 @@
import android.content.Context;
import android.content.pm.PackageManager;
import android.content.pm.UserInfo;
-import android.hardware.authsecret.V1_0.IAuthSecret;
+import android.hardware.authsecret.IAuthSecret;
import android.hardware.face.FaceManager;
import android.hardware.fingerprint.FingerprintManager;
import android.os.FileUtils;
diff --git a/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsServiceTestable.java b/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsServiceTestable.java
index eccfa06..d3b647d 100644
--- a/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsServiceTestable.java
+++ b/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsServiceTestable.java
@@ -22,7 +22,7 @@
import android.app.admin.DeviceStateCache;
import android.content.Context;
import android.content.pm.UserInfo;
-import android.hardware.authsecret.V1_0.IAuthSecret;
+import android.hardware.authsecret.IAuthSecret;
import android.os.Handler;
import android.os.Parcel;
import android.os.Process;
@@ -154,7 +154,7 @@
storageManager, spManager, gsiService, recoverableKeyStoreManager,
userManagerInternal, deviceStateCache));
mGateKeeperService = gatekeeper;
- mAuthSecretService = authSecretService;
+ mAuthSecretServiceAidl = authSecretService;
}
@Override
@@ -199,4 +199,4 @@
UserInfo userInfo = mUserManager.getUserInfo(userId);
return userInfo.isCloneProfile() || userInfo.isManagedProfile();
}
-}
\ No newline at end of file
+}
diff --git a/services/tests/servicestests/src/com/android/server/locksettings/SyntheticPasswordTests.java b/services/tests/servicestests/src/com/android/server/locksettings/SyntheticPasswordTests.java
index b9cafa4..a441824 100644
--- a/services/tests/servicestests/src/com/android/server/locksettings/SyntheticPasswordTests.java
+++ b/services/tests/servicestests/src/com/android/server/locksettings/SyntheticPasswordTests.java
@@ -59,6 +59,7 @@
import java.io.File;
import java.util.ArrayList;
+import java.util.Arrays;
/**
@@ -229,9 +230,11 @@
badPassword, PRIMARY_USER_ID, 0 /* flags */).getResponseCode());
// Check the same secret was passed each time
- ArgumentCaptor<ArrayList<Byte>> secret = ArgumentCaptor.forClass(ArrayList.class);
- verify(mAuthSecretService, atLeastOnce()).primaryUserCredential(secret.capture());
- assertEquals(1, secret.getAllValues().stream().distinct().count());
+ ArgumentCaptor<byte[]> secret = ArgumentCaptor.forClass(byte[].class);
+ verify(mAuthSecretService, atLeastOnce()).setPrimaryUserCredential(secret.capture());
+ for (byte[] val : secret.getAllValues()) {
+ assertArrayEquals(val, secret.getAllValues().get(0));
+ }
}
@Test
@@ -242,7 +245,7 @@
reset(mAuthSecretService);
assertEquals(VerifyCredentialResponse.RESPONSE_OK, mService.verifyCredential(
password, PRIMARY_USER_ID, 0 /* flags */).getResponseCode());
- verify(mAuthSecretService).primaryUserCredential(any(ArrayList.class));
+ verify(mAuthSecretService).setPrimaryUserCredential(any(byte[].class));
}
@Test
@@ -252,7 +255,7 @@
initializeCredential(password, SECONDARY_USER_ID);
assertEquals(VerifyCredentialResponse.RESPONSE_OK, mService.verifyCredential(
password, SECONDARY_USER_ID, 0 /* flags */).getResponseCode());
- verify(mAuthSecretService, never()).primaryUserCredential(any(ArrayList.class));
+ verify(mAuthSecretService, never()).setPrimaryUserCredential(any(byte[].class));
}
@Test
@@ -263,7 +266,7 @@
reset(mAuthSecretService);
mLocalService.unlockUserKeyIfUnsecured(PRIMARY_USER_ID);
- verify(mAuthSecretService).primaryUserCredential(any(ArrayList.class));
+ verify(mAuthSecretService).setPrimaryUserCredential(any(byte[].class));
}
@Test
@@ -591,7 +594,7 @@
initializeCredential(password, PRIMARY_USER_ID);
assertEquals(VerifyCredentialResponse.RESPONSE_OK, mService.verifyCredential(
password, PRIMARY_USER_ID, 0 /* flags */).getResponseCode());
- verify(mAuthSecretService, never()).primaryUserCredential(any(ArrayList.class));
+ verify(mAuthSecretService, never()).setPrimaryUserCredential(any(byte[].class));
}
@Test
diff --git a/services/tests/servicestests/src/com/android/server/power/stats/BatteryUsageStatsRule.java b/services/tests/servicestests/src/com/android/server/power/stats/BatteryUsageStatsRule.java
index 1815e2a..7c1962c 100644
--- a/services/tests/servicestests/src/com/android/server/power/stats/BatteryUsageStatsRule.java
+++ b/services/tests/servicestests/src/com/android/server/power/stats/BatteryUsageStatsRule.java
@@ -21,6 +21,7 @@
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.when;
+import android.annotation.XmlRes;
import android.content.Context;
import android.net.NetworkStats;
import android.os.BatteryConsumer;
@@ -50,6 +51,7 @@
.powerProfileModeledOnly()
.includePowerModels()
.build();
+ private final Context mContext;
private final PowerProfile mPowerProfile;
private final MockClock mMockClock = new MockClock();
@@ -67,14 +69,19 @@
}
public BatteryUsageStatsRule(long currentTime, File historyDir) {
- Context context = InstrumentationRegistry.getContext();
- mPowerProfile = spy(new PowerProfile(context, true /* forTest */));
+ mContext = InstrumentationRegistry.getContext();
+ mPowerProfile = spy(new PowerProfile(mContext, true /* forTest */));
mMockClock.currentTime = currentTime;
mBatteryStats = new MockBatteryStatsImpl(mMockClock, historyDir);
mBatteryStats.setPowerProfile(mPowerProfile);
mBatteryStats.onSystemReady();
}
+ public BatteryUsageStatsRule setTestPowerProfile(@XmlRes int xmlId) {
+ mPowerProfile.forceInitForTesting(mContext, xmlId);
+ return this;
+ }
+
public BatteryUsageStatsRule setAveragePower(String key, double value) {
when(mPowerProfile.getAveragePower(key)).thenReturn(value);
when(mPowerProfile.getAveragePowerOrDefault(eq(key), anyDouble())).thenReturn(value);
diff --git a/services/tests/servicestests/src/com/android/server/power/stats/MobileRadioPowerCalculatorTest.java b/services/tests/servicestests/src/com/android/server/power/stats/MobileRadioPowerCalculatorTest.java
index 8262fca..5bc73bd 100644
--- a/services/tests/servicestests/src/com/android/server/power/stats/MobileRadioPowerCalculatorTest.java
+++ b/services/tests/servicestests/src/com/android/server/power/stats/MobileRadioPowerCalculatorTest.java
@@ -27,6 +27,7 @@
import static org.mockito.Mockito.when;
import android.app.usage.NetworkStatsManager;
+import android.hardware.radio.V1_5.AccessNetwork;
import android.net.NetworkCapabilities;
import android.net.NetworkStats;
import android.os.BatteryConsumer;
@@ -34,6 +35,8 @@
import android.os.BatteryUsageStatsQuery;
import android.os.Process;
import android.os.UidBatteryConsumer;
+import android.telephony.ActivityStatsTechSpecificInfo;
+import android.telephony.CellSignalStrength;
import android.telephony.DataConnectionRealTimeInfo;
import android.telephony.ModemActivityInfo;
import android.telephony.ServiceState;
@@ -43,7 +46,7 @@
import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;
-import com.android.internal.os.PowerProfile;
+import com.android.frameworks.servicestests.R;
import com.google.common.collect.Range;
@@ -52,28 +55,25 @@
import org.junit.runner.RunWith;
import org.mockito.Mock;
+import java.util.ArrayList;
+
@RunWith(AndroidJUnit4.class)
@SmallTest
@SuppressWarnings("GuardedBy")
public class MobileRadioPowerCalculatorTest {
private static final double PRECISION = 0.00001;
private static final int APP_UID = Process.FIRST_APPLICATION_UID + 42;
+ private static final int APP_UID2 = Process.FIRST_APPLICATION_UID + 101;
@Mock
NetworkStatsManager mNetworkStatsManager;
@Rule
- public final BatteryUsageStatsRule mStatsRule = new BatteryUsageStatsRule()
- .setAveragePowerUnspecified(PowerProfile.POWER_RADIO_ACTIVE)
- .setAveragePowerUnspecified(PowerProfile.POWER_RADIO_ON)
- .setAveragePower(PowerProfile.POWER_MODEM_CONTROLLER_IDLE, 360.0)
- .setAveragePower(PowerProfile.POWER_RADIO_SCANNING, 720.0)
- .setAveragePower(PowerProfile.POWER_MODEM_CONTROLLER_RX, 1440.0)
- .setAveragePower(PowerProfile.POWER_MODEM_CONTROLLER_TX,
- new double[]{720.0, 1080.0, 1440.0, 1800.0, 2160.0})
- .initMeasuredEnergyStatsLocked();
+ public final BatteryUsageStatsRule mStatsRule = new BatteryUsageStatsRule();
@Test
public void testCounterBasedModel() {
+ mStatsRule.setTestPowerProfile(R.xml.power_profile_test_modem_calculator)
+ .initMeasuredEnergyStatsLocked();
BatteryStatsImpl stats = mStatsRule.getBatteryStats();
// Scan for a cell
@@ -85,9 +85,15 @@
stats.notePhoneStateLocked(ServiceState.STATE_IN_SERVICE, TelephonyManager.SIM_STATE_READY,
5000, 5000);
+ ArrayList<CellSignalStrength> perRatCellStrength = new ArrayList();
+ CellSignalStrength gsmSignalStrength = mock(CellSignalStrength.class);
+ when(gsmSignalStrength.getLevel()).thenReturn(
+ SignalStrength.SIGNAL_STRENGTH_NONE_OR_UNKNOWN);
+ perRatCellStrength.add(gsmSignalStrength);
+
// Note cell signal strength
SignalStrength signalStrength = mock(SignalStrength.class);
- when(signalStrength.getLevel()).thenReturn(SignalStrength.SIGNAL_STRENGTH_MODERATE);
+ when(signalStrength.getCellSignalStrengths()).thenReturn(perRatCellStrength);
stats.notePhoneSignalStrengthLocked(signalStrength, 5000, 5000);
stats.noteMobileRadioPowerStateLocked(DataConnectionRealTimeInfo.DC_POWER_STATE_HIGH,
@@ -97,10 +103,31 @@
stats.noteNetworkInterfaceForTransports("cellular",
new int[]{NetworkCapabilities.TRANSPORT_CELLULAR});
+ // Spend some time in each signal strength level. It doesn't matter how long.
+ // The ModemActivityInfo reported level residency should be trusted over the BatteryStats
+ // timers.
+ when(gsmSignalStrength.getLevel()).thenReturn(
+ SignalStrength.SIGNAL_STRENGTH_NONE_OR_UNKNOWN);
+ stats.notePhoneSignalStrengthLocked(signalStrength, 8111, 8111);
+ when(gsmSignalStrength.getLevel()).thenReturn(SignalStrength.SIGNAL_STRENGTH_POOR);
+ stats.notePhoneSignalStrengthLocked(signalStrength, 8333, 8333);
+ when(gsmSignalStrength.getLevel()).thenReturn(SignalStrength.SIGNAL_STRENGTH_MODERATE);
+ stats.notePhoneSignalStrengthLocked(signalStrength, 8666, 8666);
+ when(gsmSignalStrength.getLevel()).thenReturn(SignalStrength.SIGNAL_STRENGTH_GOOD);
+ stats.notePhoneSignalStrengthLocked(signalStrength, 9110, 9110);
+
+ stats.noteMobileRadioPowerStateLocked(DataConnectionRealTimeInfo.DC_POWER_STATE_HIGH,
+ 9_500_000_000L, APP_UID2, 9500, 9500);
+
+ when(gsmSignalStrength.getLevel()).thenReturn(SignalStrength.SIGNAL_STRENGTH_GREAT);
+ stats.notePhoneSignalStrengthLocked(signalStrength, 9665, 9665);
+
// Note application network activity
NetworkStats networkStats = new NetworkStats(10000, 1)
.addEntry(new NetworkStats.Entry("cellular", APP_UID, 0, 0,
- METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 1000, 100, 2000, 20, 100));
+ METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 1000, 150, 2000, 30, 100))
+ .addEntry(new NetworkStats.Entry("cellular", APP_UID2, 0, 0,
+ METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 500, 50, 300, 10, 111));
mStatsRule.setNetworkStats(networkStats);
ModemActivityInfo mai = new ModemActivityInfo(10000, 2000, 3000,
@@ -108,34 +135,331 @@
stats.noteModemControllerActivity(mai, POWER_DATA_UNAVAILABLE, 10000, 10000,
mNetworkStatsManager);
- mStatsRule.setTime(12_000, 12_000);
+ mStatsRule.setTime(10_000, 10_000);
MobileRadioPowerCalculator calculator =
new MobileRadioPowerCalculator(mStatsRule.getPowerProfile());
mStatsRule.apply(BatteryUsageStatsRule.POWER_PROFILE_MODEL_ONLY, calculator);
- UidBatteryConsumer uidConsumer = mStatsRule.getUidBatteryConsumer(APP_UID);
- assertThat(uidConsumer.getConsumedPower(BatteryConsumer.POWER_COMPONENT_MOBILE_RADIO))
- .isWithin(PRECISION).of(0.8);
- assertThat(uidConsumer.getPowerModel(BatteryConsumer.POWER_COMPONENT_MOBILE_RADIO))
- .isEqualTo(BatteryConsumer.POWER_MODEL_POWER_PROFILE);
-
BatteryConsumer deviceConsumer = mStatsRule.getDeviceBatteryConsumer();
+ // 720 mA * 100 ms (level 0 TX drain rate * level 0 TX duration)
+ // + 1080 mA * 200 ms (level 1 TX drain rate * level 1 TX duration)
+ // + 1440 mA * 300 ms (level 2 TX drain rate * level 2 TX duration)
+ // + 1800 mA * 400 ms (level 3 TX drain rate * level 3 TX duration)
+ // + 2160 mA * 500 ms (level 4 TX drain rate * level 4 TX duration)
+ // + 1440 mA * 600 ms (RX drain rate * RX duration)
+ // + 360 mA * 3000 ms (idle drain rate * idle duration)
+ // + 70 mA * 2000 ms (sleep drain rate * sleep duration)
+ // _________________
+ // = 4604000 mA-ms or 1.27888 mA-h
assertThat(deviceConsumer.getConsumedPower(BatteryConsumer.POWER_COMPONENT_MOBILE_RADIO))
- .isWithin(PRECISION).of(2.2444);
+ .isWithin(PRECISION).of(1.27888);
assertThat(deviceConsumer.getPowerModel(BatteryConsumer.POWER_COMPONENT_MOBILE_RADIO))
.isEqualTo(BatteryConsumer.POWER_MODEL_POWER_PROFILE);
+ // 720 mA * 100 ms (level 0 TX drain rate * level 0 TX duration)
+ // + 1080 mA * 200 ms (level 1 TX drain rate * level 1 TX duration)
+ // + 1440 mA * 300 ms (level 2 TX drain rate * level 2 TX duration)
+ // + 1800 mA * 400 ms (level 3 TX drain rate * level 3 TX duration)
+ // + 2160 mA * 500 ms (level 4 TX drain rate * level 4 TX duration)
+ // + 1440 mA * 600 ms (RX drain rate * RX duration)
+ // _________________
+ // = 3384000 mA-ms or 0.94 mA-h
BatteryConsumer appsConsumer = mStatsRule.getAppsBatteryConsumer();
assertThat(appsConsumer.getConsumedPower(BatteryConsumer.POWER_COMPONENT_MOBILE_RADIO))
- .isWithin(PRECISION).of(0.8);
+ .isWithin(PRECISION).of(0.94);
assertThat(appsConsumer.getPowerModel(BatteryConsumer.POWER_COMPONENT_MOBILE_RADIO))
.isEqualTo(BatteryConsumer.POWER_MODEL_POWER_PROFILE);
+
+ // 3/4 of total packets were sent by APP_UID so 75% of total
+ UidBatteryConsumer uidConsumer = mStatsRule.getUidBatteryConsumer(APP_UID);
+ assertThat(uidConsumer.getConsumedPower(BatteryConsumer.POWER_COMPONENT_MOBILE_RADIO))
+ .isWithin(PRECISION).of(0.705);
+ assertThat(uidConsumer.getPowerModel(BatteryConsumer.POWER_COMPONENT_MOBILE_RADIO))
+ .isEqualTo(BatteryConsumer.POWER_MODEL_POWER_PROFILE);
+
+ // Rest should go to the other app
+ UidBatteryConsumer uidConsumer2 = mStatsRule.getUidBatteryConsumer(APP_UID2);
+ assertThat(uidConsumer2.getConsumedPower(BatteryConsumer.POWER_COMPONENT_MOBILE_RADIO))
+ .isWithin(PRECISION).of(0.235);
+ assertThat(uidConsumer.getPowerModel(BatteryConsumer.POWER_COMPONENT_MOBILE_RADIO))
+ .isEqualTo(BatteryConsumer.POWER_MODEL_POWER_PROFILE);
+ }
+
+ @Test
+ public void testCounterBasedModel_multipleDefinedRat() {
+ mStatsRule.setTestPowerProfile(R.xml.power_profile_test_modem_calculator_multiactive)
+ .initMeasuredEnergyStatsLocked();
+ BatteryStatsImpl stats = mStatsRule.getBatteryStats();
+
+ // Scan for a cell
+ stats.notePhoneStateLocked(ServiceState.STATE_OUT_OF_SERVICE,
+ TelephonyManager.SIM_STATE_READY,
+ 2000, 2000);
+
+ // Found a cell
+ stats.notePhoneStateLocked(ServiceState.STATE_IN_SERVICE, TelephonyManager.SIM_STATE_READY,
+ 5000, 5000);
+
+ ArrayList<CellSignalStrength> perRatCellStrength = new ArrayList();
+ CellSignalStrength gsmSignalStrength = mock(CellSignalStrength.class);
+ when(gsmSignalStrength.getLevel()).thenReturn(
+ SignalStrength.SIGNAL_STRENGTH_NONE_OR_UNKNOWN);
+ perRatCellStrength.add(gsmSignalStrength);
+
+ // Note cell signal strength
+ SignalStrength signalStrength = mock(SignalStrength.class);
+ when(signalStrength.getCellSignalStrengths()).thenReturn(perRatCellStrength);
+ stats.notePhoneSignalStrengthLocked(signalStrength, 5000, 5000);
+
+ stats.noteMobileRadioPowerStateLocked(DataConnectionRealTimeInfo.DC_POWER_STATE_HIGH,
+ 8_000_000_000L, APP_UID, 8000, 8000);
+
+ // Note established network
+ stats.noteNetworkInterfaceForTransports("cellular",
+ new int[]{NetworkCapabilities.TRANSPORT_CELLULAR});
+
+ // Spend some time in each signal strength level. It doesn't matter how long.
+ // The ModemActivityInfo reported level residency should be trusted over the BatteryStats
+ // timers.
+ when(gsmSignalStrength.getLevel()).thenReturn(
+ SignalStrength.SIGNAL_STRENGTH_NONE_OR_UNKNOWN);
+ stats.notePhoneSignalStrengthLocked(signalStrength, 8111, 8111);
+ when(gsmSignalStrength.getLevel()).thenReturn(SignalStrength.SIGNAL_STRENGTH_POOR);
+ stats.notePhoneSignalStrengthLocked(signalStrength, 8333, 8333);
+ when(gsmSignalStrength.getLevel()).thenReturn(SignalStrength.SIGNAL_STRENGTH_MODERATE);
+ stats.notePhoneSignalStrengthLocked(signalStrength, 8666, 8666);
+ when(gsmSignalStrength.getLevel()).thenReturn(SignalStrength.SIGNAL_STRENGTH_GOOD);
+ stats.notePhoneSignalStrengthLocked(signalStrength, 9110, 9110);
+
+ stats.noteMobileRadioPowerStateLocked(DataConnectionRealTimeInfo.DC_POWER_STATE_HIGH,
+ 9_500_000_000L, APP_UID2, 9500, 9500);
+
+ when(gsmSignalStrength.getLevel()).thenReturn(SignalStrength.SIGNAL_STRENGTH_GREAT);
+ stats.notePhoneSignalStrengthLocked(signalStrength, 9665, 9665);
+
+ // Note application network activity
+ NetworkStats networkStats = new NetworkStats(10000, 1)
+ .addEntry(new NetworkStats.Entry("cellular", APP_UID, 0, 0,
+ METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 1000, 150, 2000, 30, 100))
+ .addEntry(new NetworkStats.Entry("cellular", APP_UID2, 0, 0,
+ METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 500, 50, 300, 10, 111));
+ mStatsRule.setNetworkStats(networkStats);
+
+ ActivityStatsTechSpecificInfo cdmaInfo = new ActivityStatsTechSpecificInfo(
+ AccessNetwork.CDMA2000, ServiceState.FREQUENCY_RANGE_UNKNOWN,
+ new int[]{10, 11, 12, 13, 14}, 15);
+ ActivityStatsTechSpecificInfo lteInfo = new ActivityStatsTechSpecificInfo(
+ AccessNetwork.EUTRAN, ServiceState.FREQUENCY_RANGE_UNKNOWN,
+ new int[]{20, 21, 22, 23, 24}, 25);
+ ActivityStatsTechSpecificInfo nrLowFreqInfo = new ActivityStatsTechSpecificInfo(
+ AccessNetwork.NGRAN, ServiceState.FREQUENCY_RANGE_LOW,
+ new int[]{30, 31, 32, 33, 34}, 35);
+ ActivityStatsTechSpecificInfo nrMidFreqInfo = new ActivityStatsTechSpecificInfo(
+ AccessNetwork.NGRAN, ServiceState.FREQUENCY_RANGE_MID,
+ new int[]{40, 41, 42, 43, 44}, 45);
+ ActivityStatsTechSpecificInfo nrHighFreqInfo = new ActivityStatsTechSpecificInfo(
+ AccessNetwork.NGRAN, ServiceState.FREQUENCY_RANGE_HIGH,
+ new int[]{50, 51, 52, 53, 54}, 55);
+ ActivityStatsTechSpecificInfo nrMmwaveFreqInfo = new ActivityStatsTechSpecificInfo(
+ AccessNetwork.NGRAN, ServiceState.FREQUENCY_RANGE_MMWAVE,
+ new int[]{60, 61, 62, 63, 64}, 65);
+
+ ActivityStatsTechSpecificInfo[] ratInfos =
+ new ActivityStatsTechSpecificInfo[]{cdmaInfo, lteInfo, nrLowFreqInfo, nrMidFreqInfo,
+ nrHighFreqInfo, nrMmwaveFreqInfo};
+
+ ModemActivityInfo mai = new ModemActivityInfo(10000, 2000, 3000, ratInfos);
+ stats.noteModemControllerActivity(mai, POWER_DATA_UNAVAILABLE, 10000, 10000,
+ mNetworkStatsManager);
+
+ mStatsRule.setTime(10_000, 10_000);
+
+ MobileRadioPowerCalculator calculator =
+ new MobileRadioPowerCalculator(mStatsRule.getPowerProfile());
+
+ mStatsRule.apply(BatteryUsageStatsRule.POWER_PROFILE_MODEL_ONLY, calculator);
+
+ BatteryConsumer deviceConsumer = mStatsRule.getDeviceBatteryConsumer();
+ // CDMA2000 [Tx0, Tx1, Tx2, Tx3, Tx4, Rx] drain * duration
+ // [720, 1080, 1440, 1800, 2160, 1440] mA . [10, 11, 12, 13, 14, 15] ms = 111600 mA-ms
+ // LTE [Tx0, Tx1, Tx2, Tx3, Tx4, Rx] drain * duration
+ // [800, 1200, 1600, 2000, 2400, 2000] mA . [20, 21, 22, 23, 24, 25] ms = 230000 mA-ms
+ // 5G Low Frequency [Tx0, Tx1, Tx2, Tx3, Tx4, Rx] drain * duration
+ // (nrFrequency="LOW" values was not defined so fall back to nrFrequency="DEFAULT")
+ // [999, 1333, 1888, 2222, 2666, 2222] mA . [30, 31, 32, 33, 34, 35] ms = 373449 mA-ms
+ // 5G Mid Frequency [Tx0, Tx1, Tx2, Tx3, Tx4, Rx] drain * duration
+ // (nrFrequency="MID" values was not defined so fall back to nrFrequency="DEFAULT")
+ // [999, 1333, 1888, 2222, 2666, 2222] mA . [40, 41, 42, 43, 44, 45] ms = 486749 mA-ms
+ // 5G High Frequency [Tx0, Tx1, Tx2, Tx3, Tx4, Rx] drain * duration
+ // [1818, 2727, 3636, 4545, 5454, 2727] mA . [50, 51, 52, 53, 54, 55] ms = 1104435 mA-ms
+ // 5G Mmwave Frequency [Tx0, Tx1, Tx2, Tx3, Tx4, Rx] drain * duration
+ // [2345, 3456, 4567, 5678, 6789, 3456] mA . [60, 61, 62, 63, 64, 65] ms = 1651520 mA-ms
+ // _________________
+ // = 3957753 mA-ms or 1.09938 mA-h active consumption
+ //
+ // Idle drain rate * idle duration
+ // 360 mA * 3000 ms = 1080000 mA-ms
+ // Sleep drain rate * sleep duration
+ // 70 mA * 2000 ms = 140000 mA-ms
+ // _________________
+ // = 5177753 mA-ms or 1.43826 mA-h total consumption
+ assertThat(deviceConsumer.getConsumedPower(BatteryConsumer.POWER_COMPONENT_MOBILE_RADIO))
+ .isWithin(PRECISION).of(1.43826);
+ assertThat(deviceConsumer.getPowerModel(BatteryConsumer.POWER_COMPONENT_MOBILE_RADIO))
+ .isEqualTo(BatteryConsumer.POWER_MODEL_POWER_PROFILE);
+
+ // 720 mA * 100 ms (level 0 TX drain rate * level 0 TX duration)
+ // + 1080 mA * 200 ms (level 1 TX drain rate * level 1 TX duration)
+ // + 1440 mA * 300 ms (level 2 TX drain rate * level 2 TX duration)
+ // + 1800 mA * 400 ms (level 3 TX drain rate * level 3 TX duration)
+ // + 2160 mA * 500 ms (level 4 TX drain rate * level 4 TX duration)
+ // + 1440 mA * 600 ms (RX drain rate * RX duration)
+ // _________________
+ // = 3384000 mA-ms or 0.94 mA-h
+ BatteryConsumer appsConsumer = mStatsRule.getAppsBatteryConsumer();
+ assertThat(appsConsumer.getConsumedPower(BatteryConsumer.POWER_COMPONENT_MOBILE_RADIO))
+ .isWithin(PRECISION).of(1.09938);
+ assertThat(appsConsumer.getPowerModel(BatteryConsumer.POWER_COMPONENT_MOBILE_RADIO))
+ .isEqualTo(BatteryConsumer.POWER_MODEL_POWER_PROFILE);
+
+ // 3/4 of total packets were sent by APP_UID so 75% of total
+ UidBatteryConsumer uidConsumer = mStatsRule.getUidBatteryConsumer(APP_UID);
+ assertThat(uidConsumer.getConsumedPower(BatteryConsumer.POWER_COMPONENT_MOBILE_RADIO))
+ .isWithin(PRECISION).of(0.82453);
+ assertThat(uidConsumer.getPowerModel(BatteryConsumer.POWER_COMPONENT_MOBILE_RADIO))
+ .isEqualTo(BatteryConsumer.POWER_MODEL_POWER_PROFILE);
+
+ // Rest should go to the other app
+ UidBatteryConsumer uidConsumer2 = mStatsRule.getUidBatteryConsumer(APP_UID2);
+ assertThat(uidConsumer2.getConsumedPower(BatteryConsumer.POWER_COMPONENT_MOBILE_RADIO))
+ .isWithin(PRECISION).of(0.27484);
+ assertThat(uidConsumer.getPowerModel(BatteryConsumer.POWER_COMPONENT_MOBILE_RADIO))
+ .isEqualTo(BatteryConsumer.POWER_MODEL_POWER_PROFILE);
+ }
+
+ @Test
+ public void testCounterBasedModel_legacyPowerProfile() {
+ mStatsRule.setTestPowerProfile(R.xml.power_profile_test_legacy_modem)
+ .initMeasuredEnergyStatsLocked();
+ BatteryStatsImpl stats = mStatsRule.getBatteryStats();
+
+ // Scan for a cell
+ stats.notePhoneStateLocked(ServiceState.STATE_OUT_OF_SERVICE,
+ TelephonyManager.SIM_STATE_READY,
+ 2000, 2000);
+
+ // Found a cell
+ stats.notePhoneStateLocked(ServiceState.STATE_IN_SERVICE, TelephonyManager.SIM_STATE_READY,
+ 5000, 5000);
+
+ ArrayList<CellSignalStrength> perRatCellStrength = new ArrayList();
+ CellSignalStrength gsmSignalStrength = mock(CellSignalStrength.class);
+ when(gsmSignalStrength.getLevel()).thenReturn(
+ SignalStrength.SIGNAL_STRENGTH_NONE_OR_UNKNOWN);
+ perRatCellStrength.add(gsmSignalStrength);
+
+ // Note cell signal strength
+ SignalStrength signalStrength = mock(SignalStrength.class);
+ when(signalStrength.getCellSignalStrengths()).thenReturn(perRatCellStrength);
+ stats.notePhoneSignalStrengthLocked(signalStrength, 5000, 5000);
+
+ stats.noteMobileRadioPowerStateLocked(DataConnectionRealTimeInfo.DC_POWER_STATE_HIGH,
+ 8_000_000_000L, APP_UID, 8000, 8000);
+
+ // Note established network
+ stats.noteNetworkInterfaceForTransports("cellular",
+ new int[]{NetworkCapabilities.TRANSPORT_CELLULAR});
+
+ // Spend some time in each signal strength level. It doesn't matter how long.
+ // The ModemActivityInfo reported level residency should be trusted over the BatteryStats
+ // timers.
+ when(gsmSignalStrength.getLevel()).thenReturn(
+ SignalStrength.SIGNAL_STRENGTH_NONE_OR_UNKNOWN);
+ stats.notePhoneSignalStrengthLocked(signalStrength, 8111, 8111);
+ when(gsmSignalStrength.getLevel()).thenReturn(SignalStrength.SIGNAL_STRENGTH_POOR);
+ stats.notePhoneSignalStrengthLocked(signalStrength, 8333, 8333);
+ when(gsmSignalStrength.getLevel()).thenReturn(SignalStrength.SIGNAL_STRENGTH_MODERATE);
+ stats.notePhoneSignalStrengthLocked(signalStrength, 8666, 8666);
+ when(gsmSignalStrength.getLevel()).thenReturn(SignalStrength.SIGNAL_STRENGTH_GOOD);
+ stats.notePhoneSignalStrengthLocked(signalStrength, 9110, 9110);
+
+ stats.noteMobileRadioPowerStateLocked(DataConnectionRealTimeInfo.DC_POWER_STATE_HIGH,
+ 9_500_000_000L, APP_UID2, 9500, 9500);
+
+ when(gsmSignalStrength.getLevel()).thenReturn(SignalStrength.SIGNAL_STRENGTH_GREAT);
+ stats.notePhoneSignalStrengthLocked(signalStrength, 9665, 9665);
+
+ // Note application network activity
+ NetworkStats networkStats = new NetworkStats(10000, 1)
+ .addEntry(new NetworkStats.Entry("cellular", APP_UID, 0, 0,
+ METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 1000, 150, 2000, 30, 100))
+ .addEntry(new NetworkStats.Entry("cellular", APP_UID2, 0, 0,
+ METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 500, 50, 300, 10, 111));
+ mStatsRule.setNetworkStats(networkStats);
+
+ ModemActivityInfo mai = new ModemActivityInfo(10000, 2000, 3000,
+ new int[]{100, 200, 300, 400, 500}, 600);
+ stats.noteModemControllerActivity(mai, POWER_DATA_UNAVAILABLE, 10000, 10000,
+ mNetworkStatsManager);
+
+ mStatsRule.setTime(10_000, 10_000);
+
+ MobileRadioPowerCalculator calculator =
+ new MobileRadioPowerCalculator(mStatsRule.getPowerProfile());
+
+ mStatsRule.apply(BatteryUsageStatsRule.POWER_PROFILE_MODEL_ONLY, calculator);
+
+ BatteryConsumer deviceConsumer = mStatsRule.getDeviceBatteryConsumer();
+ // 720 mA * 100 ms (level 0 TX drain rate * level 0 TX duration)
+ // + 1080 mA * 200 ms (level 1 TX drain rate * level 1 TX duration)
+ // + 1440 mA * 300 ms (level 2 TX drain rate * level 2 TX duration)
+ // + 1800 mA * 400 ms (level 3 TX drain rate * level 3 TX duration)
+ // + 2160 mA * 500 ms (level 4 TX drain rate * level 4 TX duration)
+ // + 1440 mA * 600 ms (RX drain rate * RX duration)
+ // + 360 mA * 3000 ms (idle drain rate * idle duration)
+ // + 70 mA * 2000 ms (sleep drain rate * sleep duration)
+ // _________________
+ // = 4604000 mA-ms or 1.27888 mA-h
+ assertThat(deviceConsumer.getConsumedPower(BatteryConsumer.POWER_COMPONENT_MOBILE_RADIO))
+ .isWithin(PRECISION).of(1.27888);
+ assertThat(deviceConsumer.getPowerModel(BatteryConsumer.POWER_COMPONENT_MOBILE_RADIO))
+ .isEqualTo(BatteryConsumer.POWER_MODEL_POWER_PROFILE);
+
+ // 720 mA * 100 ms (level 0 TX drain rate * level 0 TX duration)
+ // + 1080 mA * 200 ms (level 1 TX drain rate * level 1 TX duration)
+ // + 1440 mA * 300 ms (level 2 TX drain rate * level 2 TX duration)
+ // + 1800 mA * 400 ms (level 3 TX drain rate * level 3 TX duration)
+ // + 2160 mA * 500 ms (level 4 TX drain rate * level 4 TX duration)
+ // + 1440 mA * 600 ms (RX drain rate * RX duration)
+ // _________________
+ // = 3384000 mA-ms or 0.94 mA-h
+ BatteryConsumer appsConsumer = mStatsRule.getAppsBatteryConsumer();
+ assertThat(appsConsumer.getConsumedPower(BatteryConsumer.POWER_COMPONENT_MOBILE_RADIO))
+ .isWithin(PRECISION).of(0.94);
+ assertThat(appsConsumer.getPowerModel(BatteryConsumer.POWER_COMPONENT_MOBILE_RADIO))
+ .isEqualTo(BatteryConsumer.POWER_MODEL_POWER_PROFILE);
+
+ // 3/4 of total packets were sent by APP_UID so 75% of total
+ UidBatteryConsumer uidConsumer = mStatsRule.getUidBatteryConsumer(APP_UID);
+ assertThat(uidConsumer.getConsumedPower(BatteryConsumer.POWER_COMPONENT_MOBILE_RADIO))
+ .isWithin(PRECISION).of(0.705);
+ assertThat(uidConsumer.getPowerModel(BatteryConsumer.POWER_COMPONENT_MOBILE_RADIO))
+ .isEqualTo(BatteryConsumer.POWER_MODEL_POWER_PROFILE);
+
+ // Rest should go to the other app
+ UidBatteryConsumer uidConsumer2 = mStatsRule.getUidBatteryConsumer(APP_UID2);
+ assertThat(uidConsumer2.getConsumedPower(BatteryConsumer.POWER_COMPONENT_MOBILE_RADIO))
+ .isWithin(PRECISION).of(0.235);
+ assertThat(uidConsumer.getPowerModel(BatteryConsumer.POWER_COMPONENT_MOBILE_RADIO))
+ .isEqualTo(BatteryConsumer.POWER_MODEL_POWER_PROFILE);
}
@Test
public void testTimerBasedModel_byProcessState() {
+ mStatsRule.setTestPowerProfile(R.xml.power_profile_test_legacy_modem)
+ .initMeasuredEnergyStatsLocked();
BatteryStatsImpl stats = mStatsRule.getBatteryStats();
BatteryStatsImpl.Uid uid = stats.getUidStatsLocked(APP_UID);
@@ -148,13 +472,19 @@
TelephonyManager.SIM_STATE_READY,
2000, 2000);
+ ArrayList<CellSignalStrength> perRatCellStrength = new ArrayList();
+ CellSignalStrength gsmSignalStrength = mock(CellSignalStrength.class);
+ when(gsmSignalStrength.getLevel()).thenReturn(
+ SignalStrength.SIGNAL_STRENGTH_NONE_OR_UNKNOWN);
+ perRatCellStrength.add(gsmSignalStrength);
+
// Found a cell
stats.notePhoneStateLocked(ServiceState.STATE_IN_SERVICE, TelephonyManager.SIM_STATE_READY,
5000, 5000);
// Note cell signal strength
SignalStrength signalStrength = mock(SignalStrength.class);
- when(signalStrength.getLevel()).thenReturn(SignalStrength.SIGNAL_STRENGTH_MODERATE);
+ when(signalStrength.getCellSignalStrengths()).thenReturn(perRatCellStrength);
stats.notePhoneSignalStrengthLocked(signalStrength, 5000, 5000);
stats.noteMobileRadioPowerStateLocked(DataConnectionRealTimeInfo.DC_POWER_STATE_HIGH,
@@ -164,10 +494,25 @@
stats.noteNetworkInterfaceForTransports("cellular",
new int[]{NetworkCapabilities.TRANSPORT_CELLULAR});
+ // Spend some time in each signal strength level. It doesn't matter how long.
+ // The ModemActivityInfo reported level residency should be trusted over the BatteryStats
+ // timers.
+ when(gsmSignalStrength.getLevel()).thenReturn(
+ SignalStrength.SIGNAL_STRENGTH_NONE_OR_UNKNOWN);
+ stats.notePhoneSignalStrengthLocked(signalStrength, 8111, 8111);
+ when(gsmSignalStrength.getLevel()).thenReturn(SignalStrength.SIGNAL_STRENGTH_POOR);
+ stats.notePhoneSignalStrengthLocked(signalStrength, 8333, 8333);
+ when(gsmSignalStrength.getLevel()).thenReturn(SignalStrength.SIGNAL_STRENGTH_MODERATE);
+ stats.notePhoneSignalStrengthLocked(signalStrength, 8666, 8666);
+ when(gsmSignalStrength.getLevel()).thenReturn(SignalStrength.SIGNAL_STRENGTH_GOOD);
+ stats.notePhoneSignalStrengthLocked(signalStrength, 9110, 9110);
+ when(gsmSignalStrength.getLevel()).thenReturn(SignalStrength.SIGNAL_STRENGTH_GREAT);
+ stats.notePhoneSignalStrengthLocked(signalStrength, 9665, 9665);
+
// Note application network activity
mStatsRule.setNetworkStats(new NetworkStats(10000, 1)
.addEntry(new NetworkStats.Entry("cellular", APP_UID, 0, 0,
- METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 1000, 100, 2000, 20, 100)));
+ METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 1000, 100, 2000, 20, 100)));
stats.noteModemControllerActivity(null, POWER_DATA_UNAVAILABLE, 10000, 10000,
mNetworkStatsManager);
@@ -177,7 +522,7 @@
mStatsRule.setNetworkStats(new NetworkStats(12000, 1)
.addEntry(new NetworkStats.Entry("cellular", APP_UID, 0, 0,
- METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 1000, 250, 2000, 80, 200)));
+ METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 1000, 250, 2000, 80, 200)));
stats.noteModemControllerActivity(null, POWER_DATA_UNAVAILABLE, 12000, 12000,
mNetworkStatsManager);
@@ -199,6 +544,8 @@
MobileRadioPowerCalculator calculator =
new MobileRadioPowerCalculator(mStatsRule.getPowerProfile());
+ mStatsRule.setTime(12_000, 12_000);
+
mStatsRule.apply(new BatteryUsageStatsQuery.Builder()
.powerProfileModeledOnly()
.includePowerModels()
@@ -217,6 +564,10 @@
BatteryConsumer.POWER_COMPONENT_MOBILE_RADIO,
BatteryConsumer.PROCESS_STATE_FOREGROUND_SERVICE);
+
+ assertThat(uidConsumer.getConsumedPower(BatteryConsumer.POWER_COMPONENT_MOBILE_RADIO))
+ .isWithin(PRECISION).of(1.6);
+
assertThat(uidConsumer.getConsumedPower(foreground)).isWithin(PRECISION).of(1.2);
assertThat(uidConsumer.getConsumedPower(background)).isWithin(PRECISION).of(0.4);
assertThat(uidConsumer.getConsumedPower(fgs)).isWithin(PRECISION).of(0);
@@ -224,6 +575,8 @@
@Test
public void testMeasuredEnergyBasedModel() {
+ mStatsRule.setTestPowerProfile(R.xml.power_profile_test_legacy_modem)
+ .initMeasuredEnergyStatsLocked();
BatteryStatsImpl stats = mStatsRule.getBatteryStats();
// Scan for a cell
@@ -264,12 +617,6 @@
mStatsRule.apply(calculator);
- UidBatteryConsumer uidConsumer = mStatsRule.getUidBatteryConsumer(APP_UID);
- assertThat(uidConsumer.getConsumedPower(BatteryConsumer.POWER_COMPONENT_MOBILE_RADIO))
- .isWithin(PRECISION).of(1.53934);
- assertThat(uidConsumer.getPowerModel(BatteryConsumer.POWER_COMPONENT_MOBILE_RADIO))
- .isEqualTo(BatteryConsumer.POWER_MODEL_MEASURED_ENERGY);
-
BatteryConsumer deviceConsumer = mStatsRule.getDeviceBatteryConsumer();
// 10_000_000 micro-Coulomb * 1/1000 milli/micro * 1/3600 hour/second = 2.77778 mAh
assertThat(deviceConsumer.getConsumedPower(BatteryConsumer.POWER_COMPONENT_MOBILE_RADIO))
@@ -282,10 +629,18 @@
.isWithin(PRECISION).of(1.53934);
assertThat(appsConsumer.getPowerModel(BatteryConsumer.POWER_COMPONENT_MOBILE_RADIO))
.isEqualTo(BatteryConsumer.POWER_MODEL_MEASURED_ENERGY);
+
+ UidBatteryConsumer uidConsumer = mStatsRule.getUidBatteryConsumer(APP_UID);
+ assertThat(uidConsumer.getConsumedPower(BatteryConsumer.POWER_COMPONENT_MOBILE_RADIO))
+ .isWithin(PRECISION).of(1.53934);
+ assertThat(uidConsumer.getPowerModel(BatteryConsumer.POWER_COMPONENT_MOBILE_RADIO))
+ .isEqualTo(BatteryConsumer.POWER_MODEL_MEASURED_ENERGY);
}
@Test
public void testMeasuredEnergyBasedModel_byProcessState() {
+ mStatsRule.setTestPowerProfile(R.xml.power_profile_test_legacy_modem)
+ .initMeasuredEnergyStatsLocked();
BatteryStatsImpl stats = mStatsRule.getBatteryStats();
BatteryStatsImpl.Uid uid = stats.getUidStatsLocked(APP_UID);
@@ -317,7 +672,7 @@
// Note application network activity
mStatsRule.setNetworkStats(new NetworkStats(10000, 1)
.addEntry(new NetworkStats.Entry("cellular", APP_UID, 0, 0,
- METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 1000, 100, 2000, 20, 100)));
+ METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 1000, 100, 2000, 20, 100)));
stats.noteModemControllerActivity(null, 10_000_000, 10000, 10000, mNetworkStatsManager);
@@ -326,7 +681,7 @@
mStatsRule.setNetworkStats(new NetworkStats(12000, 1)
.addEntry(new NetworkStats.Entry("cellular", APP_UID, 0, 0,
- METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 1000, 250, 2000, 80, 200)));
+ METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 1000, 250, 2000, 80, 200)));
stats.noteModemControllerActivity(null, 15_000_000, 12000, 12000, mNetworkStatsManager);
diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityMetricsLaunchObserverTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityMetricsLaunchObserverTests.java
index 85c4975..4e001fe 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ActivityMetricsLaunchObserverTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ActivityMetricsLaunchObserverTests.java
@@ -106,7 +106,7 @@
mTransition.mParticipants.add(mTopActivity);
mTopActivity.mTransitionController.moveToCollecting(mTransition);
// becomes invisible when covered by mTopActivity
- mTrampolineActivity.mVisibleRequested = false;
+ mTrampolineActivity.setVisibleRequested(false);
}
private <T> T verifyAsync(T mock) {
@@ -235,7 +235,7 @@
public void testOnActivityLaunchCancelled_hasDrawn() {
onActivityLaunched(mTopActivity);
- mTopActivity.mVisibleRequested = true;
+ mTopActivity.setVisibleRequested(true);
doReturn(true).when(mTopActivity).isReportedDrawn();
// Cannot time already-visible activities.
@@ -258,7 +258,7 @@
notifyActivityLaunching(noDrawnActivity.intent);
notifyAndVerifyActivityLaunched(noDrawnActivity);
- noDrawnActivity.mVisibleRequested = false;
+ noDrawnActivity.setVisibleRequested(false);
mActivityMetricsLogger.notifyVisibilityChanged(noDrawnActivity);
verifyAsync(mLaunchObserver).onActivityLaunchCancelled(eqLastStartedId(noDrawnActivity));
@@ -286,7 +286,7 @@
clearInvocations(mLaunchObserver);
mLaunchTopByTrampoline = true;
- mTopActivity.mVisibleRequested = false;
+ mTopActivity.setVisibleRequested(false);
notifyActivityLaunching(mTopActivity.intent);
// It should schedule a message with UNKNOWN_VISIBILITY_CHECK_DELAY_MS to check whether
// the launch event is still valid.
@@ -314,7 +314,7 @@
// Create an invisible event that should be cancelled after the next event starts.
final ActivityRecord prev = new ActivityBuilder(mAtm).setCreateTask(true).build();
onActivityLaunched(prev);
- prev.mVisibleRequested = false;
+ prev.setVisibleRequested(false);
mActivityOptions = ActivityOptions.makeBasic();
mActivityOptions.setSourceInfo(SourceInfo.TYPE_LAUNCHER, SystemClock.uptimeMillis() - 10);
@@ -561,7 +561,7 @@
@Test
public void testConsecutiveLaunchWithDifferentWindowingMode() {
mTopActivity.setWindowingMode(WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW);
- mTrampolineActivity.mVisibleRequested = true;
+ mTrampolineActivity.setVisibleRequested(true);
onActivityLaunched(mTrampolineActivity);
mActivityMetricsLogger.notifyActivityLaunching(mTopActivity.intent,
mTrampolineActivity /* caller */, mTrampolineActivity.getUid());
diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
index a410eed..f983fa9 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
@@ -917,7 +917,7 @@
// Prepare the activity record to be ready for immediate removal. It should be invisible and
// have no process. Otherwise, request to finish it will send a message to client first.
activity.setState(STOPPED, "test");
- activity.mVisibleRequested = false;
+ activity.setVisibleRequested(false);
activity.nowVisible = false;
// Set process to 'null' to allow immediate removal, but don't call mActivity.setProcess() -
// this will cause NPE when updating task's process.
@@ -927,7 +927,7 @@
// next activity reports idle to destroy it.
final ActivityRecord topActivity = new ActivityBuilder(mAtm)
.setTask(activity.getTask()).build();
- topActivity.mVisibleRequested = true;
+ topActivity.setVisibleRequested(true);
topActivity.nowVisible = true;
topActivity.setState(RESUMED, "test");
@@ -1082,7 +1082,7 @@
final ActivityRecord activity = createActivityWithTask();
clearInvocations(activity.mDisplayContent);
activity.finishing = false;
- activity.mVisibleRequested = true;
+ activity.setVisibleRequested(true);
activity.setState(RESUMED, "test");
activity.finishIfPossible("test", false /* oomAdj */);
@@ -1099,7 +1099,7 @@
final ActivityRecord activity = createActivityWithTask();
clearInvocations(activity.mDisplayContent);
activity.finishing = false;
- activity.mVisibleRequested = true;
+ activity.setVisibleRequested(true);
activity.setState(PAUSED, "test");
activity.finishIfPossible("test", false /* oomAdj */);
@@ -1118,7 +1118,7 @@
// Put an activity on top of test activity to make it invisible and prevent us from
// accidentally resuming the topmost one again.
new ActivityBuilder(mAtm).build();
- activity.mVisibleRequested = false;
+ activity.setVisibleRequested(false);
activity.setState(STOPPED, "test");
activity.finishIfPossible("test", false /* oomAdj */);
@@ -1136,7 +1136,7 @@
final TestTransitionPlayer testPlayer = registerTestTransitionPlayer();
final ActivityRecord activity = createActivityWithTask();
activity.finishing = false;
- activity.mVisibleRequested = true;
+ activity.setVisibleRequested(true);
activity.setState(RESUMED, "test");
activity.finishIfPossible("test", false /* oomAdj */);
@@ -1273,7 +1273,7 @@
final ActivityRecord currentTop = createActivityWithTask();
final Task task = currentTop.getTask();
- currentTop.mVisibleRequested = currentTop.nowVisible = true;
+ currentTop.setVisibleRequested(currentTop.nowVisible = true);
// Simulates that {@code currentTop} starts an existing activity from background (so its
// state is stopped) and the starting flow just goes to place it at top.
@@ -1300,7 +1300,7 @@
final ActivityRecord bottomActivity = createActivityWithTask();
final ActivityRecord topActivity = new ActivityBuilder(mAtm)
.setTask(bottomActivity.getTask()).build();
- topActivity.mVisibleRequested = true;
+ topActivity.setVisibleRequested(true);
// simulating bottomActivity as a trampoline activity.
bottomActivity.setState(RESUMED, "test");
bottomActivity.finishIfPossible("test", false);
@@ -1316,13 +1316,13 @@
final ActivityRecord activity = createActivityWithTask();
final ActivityRecord topActivity = new ActivityBuilder(mAtm)
.setTask(activity.getTask()).build();
- topActivity.mVisibleRequested = true;
+ topActivity.setVisibleRequested(true);
topActivity.nowVisible = true;
topActivity.finishing = true;
topActivity.setState(PAUSED, "true");
// Mark the bottom activity as not visible, so that we will wait for it before removing
// the top one.
- activity.mVisibleRequested = false;
+ activity.setVisibleRequested(false);
activity.nowVisible = false;
activity.setState(STOPPED, "test");
@@ -1346,13 +1346,13 @@
final ActivityRecord topActivity = createActivityWithTask();
mDisplayContent.setIsSleeping(true);
doReturn(true).when(activity).shouldBeVisible();
- topActivity.mVisibleRequested = false;
+ topActivity.setVisibleRequested(false);
topActivity.nowVisible = false;
topActivity.finishing = true;
topActivity.setState(STOPPED, "true");
// Mark the activity behind (on a separate task) as not visible
- activity.mVisibleRequested = false;
+ activity.setVisibleRequested(false);
activity.nowVisible = false;
activity.setState(STOPPED, "test");
@@ -1370,13 +1370,13 @@
final ActivityRecord activity = createActivityWithTask();
final ActivityRecord topActivity = new ActivityBuilder(mAtm)
.setTask(activity.getTask()).build();
- topActivity.mVisibleRequested = false;
+ topActivity.setVisibleRequested(false);
topActivity.nowVisible = false;
topActivity.finishing = true;
topActivity.setState(STOPPED, "true");
// Mark the bottom activity as not visible, so that we would wait for it before removing
// the top one.
- activity.mVisibleRequested = false;
+ activity.setVisibleRequested(false);
activity.nowVisible = false;
activity.setState(STOPPED, "test");
@@ -1394,12 +1394,12 @@
final ActivityRecord activity = createActivityWithTask();
final ActivityRecord topActivity = new ActivityBuilder(mAtm)
.setTask(activity.getTask()).build();
- topActivity.mVisibleRequested = true;
+ topActivity.setVisibleRequested(true);
topActivity.nowVisible = true;
topActivity.finishing = true;
topActivity.setState(PAUSED, "true");
// Mark the bottom activity as already visible, so that there is no need to wait for it.
- activity.mVisibleRequested = true;
+ activity.setVisibleRequested(true);
activity.nowVisible = true;
activity.setState(RESUMED, "test");
@@ -1417,12 +1417,12 @@
final ActivityRecord activity = createActivityWithTask();
final ActivityRecord topActivity = new ActivityBuilder(mAtm)
.setTask(activity.getTask()).build();
- topActivity.mVisibleRequested = false;
+ topActivity.setVisibleRequested(false);
topActivity.nowVisible = false;
topActivity.finishing = true;
topActivity.setState(STOPPED, "true");
// Mark the bottom activity as already visible, so that there is no need to wait for it.
- activity.mVisibleRequested = true;
+ activity.setVisibleRequested(true);
activity.nowVisible = true;
activity.setState(RESUMED, "test");
@@ -1440,12 +1440,12 @@
final ActivityRecord activity = createActivityWithTask();
final ActivityRecord topActivity = new ActivityBuilder(mAtm)
.setTask(activity.getTask()).build();
- topActivity.mVisibleRequested = true;
+ topActivity.setVisibleRequested(true);
topActivity.nowVisible = true;
topActivity.finishing = true;
topActivity.setState(PAUSED, "true");
// Mark the bottom activity as already visible, so that there is no need to wait for it.
- activity.mVisibleRequested = true;
+ activity.setVisibleRequested(true);
activity.nowVisible = true;
activity.setState(RESUMED, "test");
@@ -1454,7 +1454,7 @@
final Task stack = new TaskBuilder(mSupervisor).setCreateActivity(true).build();
final ActivityRecord focusedActivity = stack.getTopMostActivity();
focusedActivity.nowVisible = true;
- focusedActivity.mVisibleRequested = true;
+ focusedActivity.setVisibleRequested(true);
focusedActivity.setState(RESUMED, "test");
stack.setResumedActivity(focusedActivity, "test");
@@ -1476,7 +1476,7 @@
int displayId = activity.getDisplayId();
doReturn(true).when(keyguardController).isKeyguardLocked(eq(displayId));
final ActivityRecord topActivity = new ActivityBuilder(mAtm).setTask(task).build();
- topActivity.mVisibleRequested = true;
+ topActivity.setVisibleRequested(true);
topActivity.nowVisible = true;
topActivity.setState(RESUMED, "true");
doCallRealMethod().when(mRootWindowContainer).ensureActivitiesVisible(
@@ -1515,12 +1515,12 @@
final ActivityRecord activity = createActivityWithTask();
final Task task = activity.getTask();
final ActivityRecord firstActivity = new ActivityBuilder(mAtm).setTask(task).build();
- firstActivity.mVisibleRequested = false;
+ firstActivity.setVisibleRequested(false);
firstActivity.nowVisible = false;
firstActivity.setState(STOPPED, "test");
final ActivityRecord secondActivity = new ActivityBuilder(mAtm).setTask(task).build();
- secondActivity.mVisibleRequested = true;
+ secondActivity.setVisibleRequested(true);
secondActivity.nowVisible = true;
secondActivity.setState(secondActivityState, "test");
@@ -1530,7 +1530,7 @@
} else {
translucentActivity = new ActivityBuilder(mAtm).setTask(task).build();
}
- translucentActivity.mVisibleRequested = true;
+ translucentActivity.setVisibleRequested(true);
translucentActivity.nowVisible = true;
translucentActivity.setState(RESUMED, "test");
@@ -1546,7 +1546,7 @@
// Finish the first activity
firstActivity.finishing = true;
- firstActivity.mVisibleRequested = true;
+ firstActivity.setVisibleRequested(true);
firstActivity.completeFinishing("test");
verify(firstActivity.mDisplayContent, times(2)).ensureActivitiesVisible(null /* starting */,
0 /* configChanges */ , false /* preserveWindows */,
@@ -1614,7 +1614,7 @@
}, true /* traverseTopToBottom */);
activity.setState(STARTED, "test");
activity.finishing = true;
- activity.mVisibleRequested = true;
+ activity.setVisibleRequested(true);
// Try to finish the last activity above the home stack.
activity.completeFinishing("test");
@@ -1909,7 +1909,7 @@
// Simulate that the activity requests the same orientation as display.
activity.setOrientation(display.getConfiguration().orientation);
// Skip the real freezing.
- activity.mVisibleRequested = false;
+ activity.setVisibleRequested(false);
clearInvocations(activity);
activity.onCancelFixedRotationTransform(originalRotation);
// The implementation of cancellation must be executed.
@@ -2535,7 +2535,7 @@
activity.setOccludesParent(true);
activity.setVisible(false);
- activity.mVisibleRequested = false;
+ activity.setVisibleRequested(false);
// Can not specify orientation if app isn't visible even though it occludes parent.
assertEquals(SCREEN_ORIENTATION_UNSET, activity.getOrientation());
// Can specify orientation if the current orientation candidate is orientation behind.
@@ -2910,7 +2910,7 @@
task.addChild(taskFragment2, POSITION_TOP);
final ActivityRecord activity2 = new ActivityBuilder(mAtm)
.setResizeMode(ActivityInfo.RESIZE_MODE_UNRESIZEABLE).build();
- activity2.mVisibleRequested = true;
+ activity2.setVisibleRequested(true);
taskFragment2.addChild(activity2);
assertTrue(activity2.isResizeable());
activity1.reparent(taskFragment1, POSITION_TOP);
@@ -3055,7 +3055,7 @@
.setCreateTask(true).build();
// By default, activity is visible.
assertTrue(activity.isVisible());
- assertTrue(activity.mVisibleRequested);
+ assertTrue(activity.isVisibleRequested());
assertTrue(activity.mDisplayContent.mOpeningApps.contains(activity));
assertFalse(activity.mDisplayContent.mClosingApps.contains(activity));
@@ -3064,7 +3064,7 @@
// until we verify no logic relies on this behavior, we'll keep this as is.
activity.setVisibility(true);
assertTrue(activity.isVisible());
- assertTrue(activity.mVisibleRequested);
+ assertTrue(activity.isVisibleRequested());
assertTrue(activity.mDisplayContent.mOpeningApps.contains(activity));
assertFalse(activity.mDisplayContent.mClosingApps.contains(activity));
}
@@ -3075,7 +3075,7 @@
.setCreateTask(true).build();
// By default, activity is visible.
assertTrue(activity.isVisible());
- assertTrue(activity.mVisibleRequested);
+ assertTrue(activity.isVisibleRequested());
assertTrue(activity.mDisplayContent.mOpeningApps.contains(activity));
assertFalse(activity.mDisplayContent.mClosingApps.contains(activity));
@@ -3083,7 +3083,7 @@
// animation should be applied on this activity.
activity.setVisibility(false);
assertTrue(activity.isVisible());
- assertFalse(activity.mVisibleRequested);
+ assertFalse(activity.isVisibleRequested());
assertFalse(activity.mDisplayContent.mOpeningApps.contains(activity));
assertTrue(activity.mDisplayContent.mClosingApps.contains(activity));
}
@@ -3095,7 +3095,7 @@
// Activiby is invisible. However ATMS requests it to become visible, since this is a top
// activity.
assertFalse(activity.isVisible());
- assertTrue(activity.mVisibleRequested);
+ assertTrue(activity.isVisibleRequested());
assertTrue(activity.mDisplayContent.mOpeningApps.contains(activity));
assertFalse(activity.mDisplayContent.mClosingApps.contains(activity));
@@ -3103,7 +3103,7 @@
// animation should be applied on this activity.
activity.setVisibility(true);
assertFalse(activity.isVisible());
- assertTrue(activity.mVisibleRequested);
+ assertTrue(activity.isVisibleRequested());
assertTrue(activity.mDisplayContent.mOpeningApps.contains(activity));
assertFalse(activity.mDisplayContent.mClosingApps.contains(activity));
@@ -3126,7 +3126,7 @@
// Activiby is invisible. However ATMS requests it to become visible, since this is a top
// activity.
assertFalse(activity.isVisible());
- assertTrue(activity.mVisibleRequested);
+ assertTrue(activity.isVisibleRequested());
assertTrue(activity.mDisplayContent.mOpeningApps.contains(activity));
assertFalse(activity.mDisplayContent.mClosingApps.contains(activity));
@@ -3134,7 +3134,7 @@
// transition should be applied on this activity.
activity.setVisibility(false);
assertFalse(activity.isVisible());
- assertFalse(activity.mVisibleRequested);
+ assertFalse(activity.isVisibleRequested());
assertFalse(activity.mDisplayContent.mOpeningApps.contains(activity));
assertFalse(activity.mDisplayContent.mClosingApps.contains(activity));
}
@@ -3549,12 +3549,12 @@
activity.reparent(taskFragment, POSITION_TOP);
// Ensure the activity visibility is updated even it is not shown to current user.
- activity.mVisibleRequested = true;
+ activity.setVisibleRequested(true);
doReturn(false).when(activity).showToCurrentUser();
spyOn(taskFragment);
doReturn(false).when(taskFragment).shouldBeVisible(any());
display.ensureActivitiesVisible(null, 0, false, false);
- assertFalse(activity.mVisibleRequested);
+ assertFalse(activity.isVisibleRequested());
}
@Test
diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java
index 0743ef4..ee31748 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java
@@ -514,7 +514,9 @@
.setCreateActivity(true)
.build()
.getTopMostActivity();
- splitPrimaryActivity.mVisibleRequested = splitSecondActivity.mVisibleRequested = true;
+
+ splitPrimaryActivity.setVisibleRequested(true);
+ splitSecondActivity.setVisibleRequested(true);
assertEquals(splitOrg.mPrimary, splitPrimaryActivity.getRootTask());
assertEquals(splitOrg.mSecondary, splitSecondActivity.getRootTask());
@@ -527,7 +529,7 @@
.setCreateActivity(true).build().getTopMostActivity();
final ActivityRecord translucentActivity = new TaskBuilder(mSupervisor)
.setCreateActivity(true).build().getTopMostActivity();
- assertTrue(activity.mVisibleRequested);
+ assertTrue(activity.isVisibleRequested());
final ActivityStarter starter = prepareStarter(FLAG_ACTIVITY_NEW_TASK,
false /* mockGetRootTask */);
@@ -1050,7 +1052,7 @@
ACTIVITY_TYPE_STANDARD, false /* onTop */));
// Activity should start invisible since we are bringing it to front.
singleTaskActivity.setVisible(false);
- singleTaskActivity.mVisibleRequested = false;
+ singleTaskActivity.setVisibleRequested(false);
// Create another activity on top of the secondary display.
final Task topStack = secondaryTaskContainer.createRootTask(WINDOWING_MODE_FULLSCREEN,
@@ -1268,7 +1270,7 @@
final ActivityStarter starter = prepareStarter(0 /* flags */);
final ActivityRecord target = new ActivityBuilder(mAtm).setCreateTask(true).build();
starter.mStartActivity = target;
- target.mVisibleRequested = false;
+ target.setVisibleRequested(false);
target.setTurnScreenOn(true);
// Assume the flag was consumed by relayout.
target.setCurrentLaunchCanTurnScreenOn(false);
@@ -1589,10 +1591,10 @@
final ActivityRecord activityTop = new ActivityBuilder(mAtm).setTask(task).build();
activityBot.setVisible(false);
- activityBot.mVisibleRequested = false;
+ activityBot.setVisibleRequested(false);
assertTrue(activityTop.isVisible());
- assertTrue(activityTop.mVisibleRequested);
+ assertTrue(activityTop.isVisibleRequested());
final ActivityStarter starter = prepareStarter(FLAG_ACTIVITY_REORDER_TO_FRONT
| FLAG_ACTIVITY_NEW_TASK, false /* mockGetRootTask */);
diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityTaskManagerServiceTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityTaskManagerServiceTests.java
index 2fccd64..368b750 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ActivityTaskManagerServiceTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ActivityTaskManagerServiceTests.java
@@ -344,7 +344,7 @@
// Assume the activity is finishing and hidden because it was crashed.
activity.finishing = true;
- activity.mVisibleRequested = false;
+ activity.setVisibleRequested(false);
activity.setVisible(false);
activity.getTask().setPausingActivity(activity);
homeActivity.setState(PAUSED, "test");
diff --git a/services/tests/wmtests/src/com/android/server/wm/AppTransitionControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/AppTransitionControllerTest.java
index fb94147c..c73237e 100644
--- a/services/tests/wmtests/src/com/android/server/wm/AppTransitionControllerTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/AppTransitionControllerTest.java
@@ -122,7 +122,7 @@
final ActivityRecord top = createActivityRecord(task);
top.setState(ActivityRecord.State.RESUMED, "test");
behind.setState(ActivityRecord.State.STARTED, "test");
- behind.mVisibleRequested = true;
+ behind.setVisibleRequested(true);
task.removeActivities("test", false /* excludingTaskOverlay */);
assertFalse(mDisplayContent.mAppTransition.isReady());
@@ -294,7 +294,7 @@
final ActivityRecord activity2 = createActivityRecord(mDisplayContent);
activity2.setVisible(false);
- activity2.mVisibleRequested = false;
+ activity2.setVisibleRequested(false);
final ArraySet<ActivityRecord> opening = new ArraySet<>();
opening.add(activity1);
@@ -319,12 +319,12 @@
// +- [Task2] - [ActivityRecord2] (opening, visible)
final ActivityRecord activity1 = createActivityRecord(mDisplayContent);
activity1.setVisible(true);
- activity1.mVisibleRequested = true;
+ activity1.setVisibleRequested(true);
activity1.mRequestForceTransition = true;
final ActivityRecord activity2 = createActivityRecord(mDisplayContent);
activity2.setVisible(false);
- activity2.mVisibleRequested = false;
+ activity2.setVisibleRequested(false);
activity2.mRequestForceTransition = true;
final ArraySet<ActivityRecord> opening = new ArraySet<>();
@@ -391,7 +391,7 @@
final ActivityRecord activity2 = createActivityRecord(mDisplayContent);
activity2.setVisible(false);
- activity2.mVisibleRequested = false;
+ activity2.setVisibleRequested(false);
attrs.setTitle("AppWindow2");
final TestWindowState appWindow2 = createWindowState(attrs, activity2);
appWindow2.mWillReplaceWindow = true;
@@ -424,17 +424,17 @@
// +- [ActivityRecord4] (invisible)
final ActivityRecord activity1 = createActivityRecord(mDisplayContent);
activity1.setVisible(false);
- activity1.mVisibleRequested = true;
+ activity1.setVisibleRequested(true);
final ActivityRecord activity2 = createActivityRecord(mDisplayContent,
activity1.getTask());
activity2.setVisible(false);
- activity2.mVisibleRequested = false;
+ activity2.setVisibleRequested(false);
final ActivityRecord activity3 = createActivityRecord(mDisplayContent);
final ActivityRecord activity4 = createActivityRecord(mDisplayContent,
activity3.getTask());
activity4.setVisible(false);
- activity4.mVisibleRequested = false;
+ activity4.setVisibleRequested(false);
final ArraySet<ActivityRecord> opening = new ArraySet<>();
opening.add(activity1);
@@ -459,7 +459,7 @@
// +- [ActivityRecord2] (closing, visible)
final ActivityRecord activity1 = createActivityRecord(mDisplayContent);
activity1.setVisible(false);
- activity1.mVisibleRequested = true;
+ activity1.setVisibleRequested(true);
final ActivityRecord activity2 = createActivityRecord(mDisplayContent,
activity1.getTask());
@@ -490,7 +490,7 @@
final ActivityRecord activity1 = createActivityRecord(mDisplayContent);
activity1.setVisible(false);
- activity1.mVisibleRequested = true;
+ activity1.setVisibleRequested(true);
activity1.setOccludesParent(false);
final ActivityRecord activity2 = createActivityRecord(mDisplayContent,
@@ -528,13 +528,13 @@
final ActivityRecord activity1 = createActivityRecord(mDisplayContent);
activity1.setVisible(false);
- activity1.mVisibleRequested = true;
+ activity1.setVisibleRequested(true);
activity1.setOccludesParent(false);
final ActivityRecord activity2 = createActivityRecord(mDisplayContent,
activity1.getTask());
activity2.setVisible(false);
- activity2.mVisibleRequested = true;
+ activity2.setVisibleRequested(true);
final ActivityRecord activity3 = createActivityRecord(mDisplayContent);
activity3.setOccludesParent(false);
@@ -567,7 +567,7 @@
final Task parentTask = createTask(mDisplayContent);
final ActivityRecord activity1 = createActivityRecordWithParentTask(parentTask);
activity1.setVisible(false);
- activity1.mVisibleRequested = true;
+ activity1.setVisibleRequested(true);
final ActivityRecord activity2 = createActivityRecordWithParentTask(parentTask);
final ArraySet<ActivityRecord> opening = new ArraySet<>();
@@ -600,10 +600,10 @@
splitRoot1.setAdjacentTaskFragment(splitRoot2);
final ActivityRecord activity1 = createActivityRecordWithParentTask(splitRoot1);
activity1.setVisible(false);
- activity1.mVisibleRequested = true;
+ activity1.setVisibleRequested(true);
final ActivityRecord activity2 = createActivityRecordWithParentTask(splitRoot2);
activity2.setVisible(false);
- activity2.mVisibleRequested = true;
+ activity2.setVisibleRequested(true);
final ArraySet<ActivityRecord> opening = new ArraySet<>();
opening.add(activity1);
@@ -625,12 +625,12 @@
final TaskFragment taskFragment1 = createTaskFragmentWithActivity(parentTask);
final ActivityRecord activity1 = taskFragment1.getTopMostActivity();
activity1.setVisible(false);
- activity1.mVisibleRequested = true;
+ activity1.setVisibleRequested(true);
final TaskFragment taskFragment2 = createTaskFragmentWithActivity(parentTask);
final ActivityRecord activity2 = taskFragment2.getTopMostActivity();
activity2.setVisible(true);
- activity2.mVisibleRequested = false;
+ activity2.setVisibleRequested(false);
final ArraySet<ActivityRecord> opening = new ArraySet<>();
opening.add(activity1);
@@ -654,11 +654,11 @@
final TaskFragment taskFragment1 = createTaskFragmentWithActivity(task1);
final ActivityRecord activity1 = taskFragment1.getTopMostActivity();
activity1.setVisible(false);
- activity1.mVisibleRequested = true;
+ activity1.setVisibleRequested(true);
final ActivityRecord activity2 = createActivityRecord(mDisplayContent);
activity2.setVisible(true);
- activity2.mVisibleRequested = false;
+ activity2.setVisibleRequested(false);
final ArraySet<ActivityRecord> opening = new ArraySet<>();
opening.add(activity1);
@@ -683,11 +683,11 @@
final TaskFragment taskFragment1 = createTaskFragmentWithActivity(task1);
final ActivityRecord activity1 = taskFragment1.getTopMostActivity();
activity1.setVisible(true);
- activity1.mVisibleRequested = false;
+ activity1.setVisibleRequested(false);
final ActivityRecord activity2 = createActivityRecord(mDisplayContent);
activity2.setVisible(false);
- activity2.mVisibleRequested = true;
+ activity2.setVisibleRequested(true);
final ArraySet<ActivityRecord> opening = new ArraySet<>();
opening.add(activity2);
@@ -710,13 +710,13 @@
// +- [Task2] (embedded) - [ActivityRecord2] (opening, invisible)
final ActivityRecord activity1 = createActivityRecord(mDisplayContent);
activity1.setVisible(false);
- activity1.mVisibleRequested = true;
+ activity1.setVisibleRequested(true);
final Task task2 = createTask(mDisplayContent);
task2.mRemoveWithTaskOrganizer = true;
final ActivityRecord activity2 = createActivityRecord(task2);
activity2.setVisible(false);
- activity2.mVisibleRequested = true;
+ activity2.setVisibleRequested(true);
final ArraySet<ActivityRecord> opening = new ArraySet<>();
opening.add(activity1);
@@ -744,7 +744,7 @@
final ActivityRecord activity1 = createActivityRecord(task);
activity1.setVisible(false);
- activity1.mVisibleRequested = true;
+ activity1.setVisibleRequested(true);
final ActivityRecord activity2 = createActivityRecord(task);
final ArraySet<ActivityRecord> opening = new ArraySet<>();
@@ -1260,6 +1260,8 @@
@Test
public void testTransitionGoodToGoForTaskFragments_detachedApp() {
final TaskFragmentOrganizer organizer = new TaskFragmentOrganizer(Runnable::run);
+ final ITaskFragmentOrganizer iOrganizer = getITaskFragmentOrganizer(organizer);
+ mAtm.mTaskFragmentOrganizerController.registerOrganizer(iOrganizer);
final Task task = createTask(mDisplayContent);
final TaskFragment changeTaskFragment =
createTaskFragmentWithEmbeddedActivity(task, organizer);
diff --git a/services/tests/wmtests/src/com/android/server/wm/AppTransitionTests.java b/services/tests/wmtests/src/com/android/server/wm/AppTransitionTests.java
index 437eeb1..6b814e6 100644
--- a/services/tests/wmtests/src/com/android/server/wm/AppTransitionTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/AppTransitionTests.java
@@ -385,11 +385,11 @@
// Simulate activity1 launches activity2.
final ActivityRecord activity1 = createActivityRecord(task);
activity1.setVisible(true);
- activity1.mVisibleRequested = false;
+ activity1.setVisibleRequested(false);
activity1.allDrawn = true;
final ActivityRecord activity2 = createActivityRecord(task);
activity2.setVisible(false);
- activity2.mVisibleRequested = true;
+ activity2.setVisibleRequested(true);
activity2.allDrawn = true;
dc.mClosingApps.add(activity1);
diff --git a/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java
index 3bce860..1b77c95 100644
--- a/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java
@@ -20,24 +20,23 @@
import static android.view.WindowManager.LayoutParams.FIRST_APPLICATION_WINDOW;
import static android.window.BackNavigationInfo.typeToString;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
+
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doAnswer;
-import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.annotation.NonNull;
import android.annotation.Nullable;
-import android.hardware.HardwareBuffer;
import android.os.RemoteException;
import android.platform.test.annotations.Presubmit;
import android.view.WindowManager;
@@ -48,7 +47,6 @@
import android.window.OnBackInvokedCallback;
import android.window.OnBackInvokedCallbackInfo;
import android.window.OnBackInvokedDispatcher;
-import android.window.TaskSnapshot;
import android.window.WindowOnBackInvokedDispatcher;
import com.android.server.LocalServices;
@@ -67,6 +65,7 @@
private BackNavigationController mBackNavigationController;
private WindowManagerInternal mWindowManagerInternal;
private BackAnimationAdapter mBackAnimationAdapter;
+ private Task mRootHomeTask;
@Before
public void setUp() throws Exception {
@@ -76,6 +75,7 @@
LocalServices.addService(WindowManagerInternal.class, mWindowManagerInternal);
mBackNavigationController.setWindowManager(mWm);
mBackAnimationAdapter = mock(BackAnimationAdapter.class);
+ mRootHomeTask = initHomeActivity();
}
@Test
@@ -101,7 +101,8 @@
ActivityRecord recordA = createActivityRecord(taskA);
Mockito.doNothing().when(recordA).reparentSurfaceControl(any(), any());
- withSystemCallback(createTopTaskWithActivity());
+ final Task topTask = createTopTaskWithActivity();
+ withSystemCallback(topTask);
BackNavigationInfo backNavigationInfo = startBackNavigation();
assertWithMessage("BackNavigationInfo").that(backNavigationInfo).isNotNull();
assertThat(typeToString(backNavigationInfo.getType()))
@@ -111,6 +112,20 @@
verify(mBackNavigationController).scheduleAnimationLocked(
eq(BackNavigationInfo.TYPE_CROSS_TASK), any(), eq(mBackAnimationAdapter),
any());
+
+ // reset drawning status
+ topTask.forAllWindows(w -> {
+ makeWindowVisibleAndDrawn(w);
+ }, true);
+ setupKeyguardOccluded();
+ backNavigationInfo = startBackNavigation();
+ assertThat(typeToString(backNavigationInfo.getType()))
+ .isEqualTo(typeToString(BackNavigationInfo.TYPE_CALLBACK));
+
+ doReturn(true).when(recordA).canShowWhenLocked();
+ backNavigationInfo = startBackNavigation();
+ assertThat(typeToString(backNavigationInfo.getType()))
+ .isEqualTo(typeToString(BackNavigationInfo.TYPE_CROSS_TASK));
}
@Test
@@ -137,6 +152,20 @@
assertThat(backNavigationInfo.getOnBackInvokedCallback()).isEqualTo(callback);
assertThat(typeToString(backNavigationInfo.getType()))
.isEqualTo(typeToString(BackNavigationInfo.TYPE_CROSS_ACTIVITY));
+
+ // reset drawing status
+ testCase.recordFront.forAllWindows(w -> {
+ makeWindowVisibleAndDrawn(w);
+ }, true);
+ setupKeyguardOccluded();
+ backNavigationInfo = startBackNavigation();
+ assertThat(typeToString(backNavigationInfo.getType()))
+ .isEqualTo(typeToString(BackNavigationInfo.TYPE_CALLBACK));
+
+ doReturn(true).when(testCase.recordBack).canShowWhenLocked();
+ backNavigationInfo = startBackNavigation();
+ assertThat(typeToString(backNavigationInfo.getType()))
+ .isEqualTo(typeToString(BackNavigationInfo.TYPE_CROSS_ACTIVITY));
}
@Test
@@ -164,12 +193,17 @@
@Test
public void preparesForBackToHome() {
- Task task = createTopTaskWithActivity();
- withSystemCallback(task);
+ final Task topTask = createTopTaskWithActivity();
+ withSystemCallback(topTask);
BackNavigationInfo backNavigationInfo = startBackNavigation();
assertThat(typeToString(backNavigationInfo.getType()))
.isEqualTo(typeToString(BackNavigationInfo.TYPE_RETURN_TO_HOME));
+
+ setupKeyguardOccluded();
+ backNavigationInfo = startBackNavigation();
+ assertThat(typeToString(backNavigationInfo.getType()))
+ .isEqualTo(typeToString(BackNavigationInfo.TYPE_CALLBACK));
}
@Test
@@ -302,14 +336,22 @@
};
}
- @NonNull
- private TaskSnapshotController createMockTaskSnapshotController() {
- TaskSnapshotController taskSnapshotController = mock(TaskSnapshotController.class);
- TaskSnapshot taskSnapshot = mock(TaskSnapshot.class);
- when(taskSnapshot.getHardwareBuffer()).thenReturn(mock(HardwareBuffer.class));
- when(taskSnapshotController.getSnapshot(anyInt(), anyInt(), anyBoolean(), anyBoolean()))
- .thenReturn(taskSnapshot);
- return taskSnapshotController;
+ private Task initHomeActivity() {
+ final Task task = mDisplayContent.getDefaultTaskDisplayArea().getRootHomeTask();
+ task.forAllLeafTasks((t) -> {
+ if (t.getTopMostActivity() == null) {
+ final ActivityRecord r = createActivityRecord(t);
+ Mockito.doNothing().when(t).reparentSurfaceControl(any(), any());
+ Mockito.doNothing().when(r).reparentSurfaceControl(any(), any());
+ }
+ }, true);
+ return task;
+ }
+
+ private void setupKeyguardOccluded() {
+ final KeyguardController kc = mRootHomeTask.mTaskSupervisor.getKeyguardController();
+ doReturn(true).when(kc).isKeyguardLocked(anyInt());
+ doReturn(true).when(kc).isDisplayOccluded(anyInt());
}
@NonNull
diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java
index d151188..bc23fa3 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java
@@ -590,7 +590,7 @@
assertEquals(window1, mWm.mRoot.getTopFocusedDisplayContent().mCurrentFocus);
// Make sure top focused display not changed if there is a focused app.
- window1.mActivityRecord.mVisibleRequested = false;
+ window1.mActivityRecord.setVisibleRequested(false);
window1.getDisplayContent().setFocusedApp(window1.mActivityRecord);
updateFocusedWindow();
assertTrue(!window1.isFocused());
@@ -1106,7 +1106,7 @@
public void testOrientationBehind() {
final ActivityRecord prev = new ActivityBuilder(mAtm).setCreateTask(true)
.setScreenOrientation(getRotatedOrientation(mDisplayContent)).build();
- prev.mVisibleRequested = false;
+ prev.setVisibleRequested(false);
final ActivityRecord top = new ActivityBuilder(mAtm).setCreateTask(true)
.setScreenOrientation(SCREEN_ORIENTATION_BEHIND).build();
assertNotEquals(WindowConfiguration.ROTATION_UNDEFINED,
diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayWindowPolicyControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayWindowPolicyControllerTests.java
index db1d15a..ba68a25 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DisplayWindowPolicyControllerTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DisplayWindowPolicyControllerTests.java
@@ -243,7 +243,7 @@
}
@Override
- public boolean canShowTasksInRecents() {
+ public boolean canShowTasksInHostDeviceRecents() {
return true;
}
diff --git a/services/tests/wmtests/src/com/android/server/wm/RecentTasksTest.java b/services/tests/wmtests/src/com/android/server/wm/RecentTasksTest.java
index ac2df62..6f2e3f2 100644
--- a/services/tests/wmtests/src/com/android/server/wm/RecentTasksTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/RecentTasksTest.java
@@ -819,7 +819,7 @@
@Test
public void testVisibleTask_displayCanNotShowTaskFromRecents_expectNotVisible() {
final DisplayContent displayContent = addNewDisplayContentAt(DisplayContent.POSITION_TOP);
- doReturn(false).when(displayContent).canShowTasksInRecents();
+ doReturn(false).when(displayContent).canShowTasksInHostDeviceRecents();
final Task task = displayContent.getDefaultTaskDisplayArea().createRootTask(
WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD, false /* onTop */);
mRecentTasks.add(task);
diff --git a/services/tests/wmtests/src/com/android/server/wm/RecentsAnimationControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/RecentsAnimationControllerTest.java
index 88e58ea..08635ab 100644
--- a/services/tests/wmtests/src/com/android/server/wm/RecentsAnimationControllerTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/RecentsAnimationControllerTest.java
@@ -172,12 +172,12 @@
// executed.
final ActivityRecord activity1 = createActivityRecord(task);
activity1.setVisible(true);
- activity1.mVisibleRequested = false;
+ activity1.setVisibleRequested(false);
activity1.addWindow(createWindowState(new LayoutParams(TYPE_BASE_APPLICATION), activity1));
final ActivityRecord activity2 = createActivityRecord(task);
activity2.setVisible(false);
- activity2.mVisibleRequested = true;
+ activity2.setVisibleRequested(true);
mDefaultDisplay.getConfiguration().windowConfiguration.setRotation(
mDefaultDisplay.getRotation());
diff --git a/services/tests/wmtests/src/com/android/server/wm/RecentsAnimationTest.java b/services/tests/wmtests/src/com/android/server/wm/RecentsAnimationTest.java
index a2b4cb8..de3a526 100644
--- a/services/tests/wmtests/src/com/android/server/wm/RecentsAnimationTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/RecentsAnimationTest.java
@@ -111,7 +111,7 @@
RecentsAnimationCallbacks recentsAnimation = startRecentsActivity(
mRecentsComponent, true /* getRecentsAnimation */);
// The launch-behind state should make the recents activity visible.
- assertTrue(recentActivity.mVisibleRequested);
+ assertTrue(recentActivity.isVisibleRequested());
assertEquals(ActivityTaskManagerService.DEMOTE_TOP_REASON_ANIMATING_RECENTS,
mAtm.mDemoteTopAppReasons);
assertFalse(mAtm.mInternal.useTopSchedGroupForTopProcess());
@@ -119,7 +119,7 @@
// Simulate the animation is cancelled without changing the stack order.
recentsAnimation.onAnimationFinished(REORDER_KEEP_IN_PLACE, false /* sendUserLeaveHint */);
// The non-top recents activity should be invisible by the restored launch-behind state.
- assertFalse(recentActivity.mVisibleRequested);
+ assertFalse(recentActivity.isVisibleRequested());
assertEquals(0, mAtm.mDemoteTopAppReasons);
}
@@ -164,7 +164,7 @@
// The activity is started in background so it should be invisible and will be stopped.
assertThat(recentsActivity).isNotNull();
assertThat(mSupervisor.mStoppingActivities).contains(recentsActivity);
- assertFalse(recentsActivity.mVisibleRequested);
+ assertFalse(recentsActivity.isVisibleRequested());
// Assume it is stopped to test next use case.
recentsActivity.activityStopped(null /* newIcicle */, null /* newPersistentState */,
@@ -360,7 +360,7 @@
true);
// Ensure we find the task for the right user and it is made visible
- assertTrue(otherUserHomeActivity.mVisibleRequested);
+ assertTrue(otherUserHomeActivity.isVisibleRequested());
}
private void startRecentsActivity() {
diff --git a/services/tests/wmtests/src/com/android/server/wm/RootTaskTests.java b/services/tests/wmtests/src/com/android/server/wm/RootTaskTests.java
index 881cc5f..fb29d3a 100644
--- a/services/tests/wmtests/src/com/android/server/wm/RootTaskTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/RootTaskTests.java
@@ -1068,7 +1068,7 @@
activity.app = null;
overlayActivity.app = null;
// Simulate the process is dead
- activity.mVisibleRequested = false;
+ activity.setVisibleRequested(false);
activity.setState(DESTROYED, "Test");
assertEquals(2, task.getChildCount());
@@ -1205,7 +1205,7 @@
// There is still an activity1 in rootTask1 so the activity2 should be added to finishing
// list that will be destroyed until idle.
- rootTask2.getTopNonFinishingActivity().mVisibleRequested = true;
+ rootTask2.getTopNonFinishingActivity().setVisibleRequested(true);
final ActivityRecord activity2 = finishTopActivity(rootTask2);
assertEquals(STOPPING, activity2.getState());
assertThat(mSupervisor.mStoppingActivities).contains(activity2);
@@ -1410,7 +1410,7 @@
new ActivityBuilder(mAtm).setTask(task).build();
// The scenario we are testing is when the app isn't visible yet.
nonTopVisibleActivity.setVisible(false);
- nonTopVisibleActivity.mVisibleRequested = false;
+ nonTopVisibleActivity.setVisibleRequested(false);
doReturn(false).when(nonTopVisibleActivity).attachedToProcess();
doReturn(true).when(nonTopVisibleActivity).shouldBeVisibleUnchecked();
doNothing().when(mSupervisor).startSpecificActivity(any(), anyBoolean(),
diff --git a/services/tests/wmtests/src/com/android/server/wm/RootWindowContainerTests.java b/services/tests/wmtests/src/com/android/server/wm/RootWindowContainerTests.java
index f84865b..a17e124 100644
--- a/services/tests/wmtests/src/com/android/server/wm/RootWindowContainerTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/RootWindowContainerTests.java
@@ -174,7 +174,7 @@
final Task rootTask = new TaskBuilder(mSupervisor).build();
final Task task1 = new TaskBuilder(mSupervisor).setParentTask(rootTask).build();
final ActivityRecord activity1 = new ActivityBuilder(mAtm).setTask(task1).build();
- activity1.mVisibleRequested = true;
+ activity1.setVisibleRequested(true);
mWm.mRoot.rankTaskLayers();
assertEquals(1, task1.mLayerRank);
@@ -183,7 +183,7 @@
final Task task2 = new TaskBuilder(mSupervisor).build();
final ActivityRecord activity2 = new ActivityBuilder(mAtm).setTask(task2).build();
- activity2.mVisibleRequested = true;
+ activity2.setVisibleRequested(true);
mWm.mRoot.rankTaskLayers();
// Note that ensureActivitiesVisible is disabled in SystemServicesTestRule, so both the
@@ -200,8 +200,8 @@
assertEquals(2, task2.mLayerRank);
// The rank should be updated to invisible when device went to sleep.
- activity1.mVisibleRequested = false;
- activity2.mVisibleRequested = false;
+ activity1.setVisibleRequested(false);
+ activity2.setVisibleRequested(false);
doReturn(true).when(mAtm).isSleepingOrShuttingDownLocked();
doReturn(true).when(mRootWindowContainer).putTasksToSleep(anyBoolean(), anyBoolean());
mSupervisor.mGoingToSleepWakeLock = mock(PowerManager.WakeLock.class);
diff --git a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
index e65610f..13ea99a 100644
--- a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
@@ -169,7 +169,7 @@
public void testRestartProcessIfVisible() {
setUpDisplaySizeWithApp(1000, 2500);
doNothing().when(mSupervisor).scheduleRestartTimeout(mActivity);
- mActivity.mVisibleRequested = true;
+ mActivity.setVisibleRequested(true);
mActivity.setSavedState(null /* savedState */);
mActivity.setState(RESUMED, "testRestart");
prepareUnresizable(mActivity, 1.5f /* maxAspect */, SCREEN_ORIENTATION_UNSPECIFIED);
@@ -553,7 +553,7 @@
resizeDisplay(display, 900, 1800);
mActivity.setState(STOPPED, "testSizeCompatMode");
- mActivity.mVisibleRequested = false;
+ mActivity.setVisibleRequested(false);
mActivity.visibleIgnoringKeyguard = false;
mActivity.app.setReportedProcState(ActivityManager.PROCESS_STATE_CACHED_ACTIVITY);
mActivity.app.computeProcessActivityState();
@@ -605,7 +605,7 @@
// Make the activity resizable again by restarting it
clearInvocations(mTask);
mActivity.info.resizeMode = RESIZE_MODE_RESIZEABLE;
- mActivity.mVisibleRequested = true;
+ mActivity.setVisibleRequested(true);
mActivity.restartProcessIfVisible();
// The full lifecycle isn't hooked up so manually set state to resumed
mActivity.setState(RESUMED, "testHandleActivitySizeCompatModeChanged");
@@ -3188,7 +3188,7 @@
task.mResizeMode = activity.info.resizeMode;
task.getRootActivity().info.resizeMode = activity.info.resizeMode;
}
- activity.mVisibleRequested = true;
+ activity.setVisibleRequested(true);
if (maxAspect >= 0) {
activity.info.setMaxAspectRatio(maxAspect);
}
diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskDisplayAreaTests.java b/services/tests/wmtests/src/com/android/server/wm/TaskDisplayAreaTests.java
index d5fb1a8..91f8d8d 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TaskDisplayAreaTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TaskDisplayAreaTests.java
@@ -398,7 +398,7 @@
.setParentTask(rootHomeTask).setCreateTask(true).build();
}
homeActivity.setVisible(false);
- homeActivity.mVisibleRequested = true;
+ homeActivity.setVisibleRequested(true);
assertFalse(rootHomeTask.isVisible());
assertEquals(defaultTaskDisplayArea.getOrientation(), rootHomeTask.getOrientation());
diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java
index 2b49314..834302e 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java
@@ -370,7 +370,8 @@
mController.onActivityReparentedToTask(activity);
mController.dispatchPendingEvents();
- assertTaskFragmentParentInfoChangedTransaction(task);
+ // There will not be TaskFragmentParentInfoChanged because Task visible request is changed
+ // before the organized TaskFragment is added to the Task.
assertActivityReparentedToTaskTransaction(task.mTaskId, activity.intent, activity.token);
}
@@ -1159,6 +1160,7 @@
doReturn(false).when(task).shouldBeVisible(any());
// Dispatch the initial event in the Task to update the Task visibility to the organizer.
+ clearInvocations(mOrganizer);
mController.onTaskFragmentAppeared(mIOrganizer, taskFragment);
mController.dispatchPendingEvents();
verify(mOrganizer).onTransactionReady(any());
diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java
index 8fda191..3ced84f 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java
@@ -107,18 +107,49 @@
}
@Test
+ public void testShouldStartChangeTransition_relativePositionChange() {
+ mockSurfaceFreezerSnapshot(mTaskFragment.mSurfaceFreezer);
+ final Rect startBounds = new Rect(0, 0, 500, 1000);
+ final Rect endBounds = new Rect(500, 0, 1000, 1000);
+ mTaskFragment.setBounds(startBounds);
+ mTaskFragment.updateRelativeEmbeddedBounds();
+ doReturn(true).when(mTaskFragment).isVisible();
+ doReturn(true).when(mTaskFragment).isVisibleRequested();
+
+ // Do not resize, just change the relative position.
+ final Rect relStartBounds = new Rect(mTaskFragment.getRelativeEmbeddedBounds());
+ mTaskFragment.setBounds(endBounds);
+ mTaskFragment.updateRelativeEmbeddedBounds();
+ spyOn(mDisplayContent.mTransitionController);
+
+ // For Shell transition, we don't want to take snapshot when the bounds are not resized
+ doReturn(true).when(mDisplayContent.mTransitionController)
+ .isShellTransitionsEnabled();
+ assertFalse(mTaskFragment.shouldStartChangeTransition(startBounds, relStartBounds));
+
+ // For legacy transition, we want to request a change transition even if it is just relative
+ // bounds change.
+ doReturn(false).when(mDisplayContent.mTransitionController)
+ .isShellTransitionsEnabled();
+ assertTrue(mTaskFragment.shouldStartChangeTransition(startBounds, relStartBounds));
+ }
+
+ @Test
public void testStartChangeTransition_resetSurface() {
mockSurfaceFreezerSnapshot(mTaskFragment.mSurfaceFreezer);
final Rect startBounds = new Rect(0, 0, 1000, 1000);
final Rect endBounds = new Rect(500, 500, 1000, 1000);
mTaskFragment.setBounds(startBounds);
+ mTaskFragment.updateRelativeEmbeddedBounds();
doReturn(true).when(mTaskFragment).isVisible();
doReturn(true).when(mTaskFragment).isVisibleRequested();
clearInvocations(mTransaction);
+ final Rect relStartBounds = new Rect(mTaskFragment.getRelativeEmbeddedBounds());
mTaskFragment.deferOrganizedTaskFragmentSurfaceUpdate();
mTaskFragment.setBounds(endBounds);
- assertTrue(mTaskFragment.shouldStartChangeTransition(startBounds));
+ mTaskFragment.updateRelativeEmbeddedBounds();
+ assertTrue(mTaskFragment.shouldStartChangeTransition(startBounds, relStartBounds));
mTaskFragment.initializeChangeTransition(startBounds);
mTaskFragment.continueOrganizedTaskFragmentSurfaceUpdate();
@@ -157,17 +188,20 @@
final Rect startBounds = new Rect(0, 0, 1000, 1000);
final Rect endBounds = new Rect(500, 500, 1000, 1000);
mTaskFragment.setBounds(startBounds);
+ mTaskFragment.updateRelativeEmbeddedBounds();
doReturn(true).when(mTaskFragment).isVisible();
doReturn(true).when(mTaskFragment).isVisibleRequested();
+ final Rect relStartBounds = new Rect(mTaskFragment.getRelativeEmbeddedBounds());
final DisplayPolicy displayPolicy = mDisplayContent.getDisplayPolicy();
displayPolicy.screenTurnedOff();
assertFalse(mTaskFragment.okToAnimate());
mTaskFragment.setBounds(endBounds);
+ mTaskFragment.updateRelativeEmbeddedBounds();
- assertFalse(mTaskFragment.shouldStartChangeTransition(startBounds));
+ assertFalse(mTaskFragment.shouldStartChangeTransition(startBounds, relStartBounds));
}
/**
diff --git a/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java b/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java
index 59a31b1..aaf07b9 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java
@@ -135,8 +135,8 @@
changes.put(closing, new Transition.ChangeInfo(true /* vis */, true /* exChg */));
fillChangeMap(changes, newTask);
// End states.
- closing.mVisibleRequested = false;
- opening.mVisibleRequested = true;
+ closing.setVisibleRequested(false);
+ opening.setVisibleRequested(true);
final int transit = transition.mType;
int flags = 0;
@@ -199,9 +199,9 @@
changes.put(closing, new Transition.ChangeInfo(true /* vis */, true /* exChg */));
fillChangeMap(changes, newTask);
// End states.
- closing.mVisibleRequested = false;
- opening.mVisibleRequested = true;
- opening2.mVisibleRequested = true;
+ closing.setVisibleRequested(false);
+ opening.setVisibleRequested(true);
+ opening2.setVisibleRequested(true);
final int transit = transition.mType;
int flags = 0;
@@ -250,8 +250,8 @@
fillChangeMap(changes, tda);
// End states.
- showing.mVisibleRequested = true;
- showing2.mVisibleRequested = true;
+ showing.setVisibleRequested(true);
+ showing2.setVisibleRequested(true);
final int transit = transition.mType;
int flags = 0;
@@ -286,16 +286,16 @@
final Task openTask = createTask(mDisplayContent);
final ActivityRecord opening = createActivityRecord(openTask);
- opening.mVisibleRequested = false; // starts invisible
+ opening.setVisibleRequested(false); // starts invisible
final Task closeTask = createTask(mDisplayContent);
final ActivityRecord closing = createActivityRecord(closeTask);
- closing.mVisibleRequested = true; // starts visible
+ closing.setVisibleRequested(true); // starts visible
transition.collectExistenceChange(openTask);
transition.collect(opening);
transition.collect(closing);
- opening.mVisibleRequested = true;
- closing.mVisibleRequested = false;
+ opening.setVisibleRequested(true);
+ closing.setVisibleRequested(false);
ArrayList<WindowContainer> targets = Transition.calculateTargets(
transition.mParticipants, transition.mChanges);
@@ -323,7 +323,7 @@
WINDOWING_MODE_FREEFORM, ACTIVITY_TYPE_STANDARD);
final ActivityRecord act = createActivityRecord(tasks[i]);
// alternate so that the transition doesn't get promoted to the display area
- act.mVisibleRequested = (i % 2) == 0; // starts invisible
+ act.setVisibleRequested((i % 2) == 0); // starts invisible
}
// doesn't matter which order collected since participants is a set
@@ -331,7 +331,7 @@
transition.collectExistenceChange(tasks[i]);
final ActivityRecord act = tasks[i].getTopMostActivity();
transition.collect(act);
- tasks[i].getTopMostActivity().mVisibleRequested = (i % 2) != 0;
+ tasks[i].getTopMostActivity().setVisibleRequested((i % 2) != 0);
}
ArrayList<WindowContainer> targets = Transition.calculateTargets(
@@ -360,7 +360,7 @@
WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD);
final ActivityRecord act = createActivityRecord(tasks[i]);
// alternate so that the transition doesn't get promoted to the display area
- act.mVisibleRequested = (i % 2) == 0; // starts invisible
+ act.setVisibleRequested((i % 2) == 0); // starts invisible
act.visibleIgnoringKeyguard = (i % 2) == 0;
if (i == showWallpaperTask) {
doReturn(true).when(act).showWallpaper();
@@ -381,7 +381,7 @@
transition.collectExistenceChange(tasks[i]);
final ActivityRecord act = tasks[i].getTopMostActivity();
transition.collect(act);
- tasks[i].getTopMostActivity().mVisibleRequested = (i % 2) != 0;
+ tasks[i].getTopMostActivity().setVisibleRequested((i % 2) != 0);
}
ArrayList<WindowContainer> targets = Transition.calculateTargets(
@@ -417,9 +417,9 @@
changes.put(closing, new Transition.ChangeInfo(true /* vis */, true /* exChg */));
fillChangeMap(changes, topTask);
// End states.
- showing.mVisibleRequested = true;
- closing.mVisibleRequested = false;
- hiding.mVisibleRequested = false;
+ showing.setVisibleRequested(true);
+ closing.setVisibleRequested(false);
+ hiding.setVisibleRequested(false);
participants.add(belowTask);
participants.add(hiding);
@@ -449,9 +449,9 @@
changes.put(closing, new Transition.ChangeInfo(true /* vis */, false /* exChg */));
fillChangeMap(changes, topTask);
// End states.
- showing.mVisibleRequested = true;
- opening.mVisibleRequested = true;
- closing.mVisibleRequested = false;
+ showing.setVisibleRequested(true);
+ opening.setVisibleRequested(true);
+ closing.setVisibleRequested(false);
participants.add(belowTask);
participants.add(showing);
@@ -531,19 +531,19 @@
@Test
public void testOpenActivityInTheSameTaskWithDisplayChange() {
final ActivityRecord closing = createActivityRecord(mDisplayContent);
- closing.mVisibleRequested = true;
+ closing.setVisibleRequested(true);
final Task task = closing.getTask();
makeTaskOrganized(task);
final ActivityRecord opening = createActivityRecord(task);
- opening.mVisibleRequested = false;
+ opening.setVisibleRequested(false);
makeDisplayAreaOrganized(mDisplayContent.getDefaultTaskDisplayArea(), mDisplayContent);
final WindowContainer<?>[] wcs = { closing, opening, task, mDisplayContent };
final Transition transition = createTestTransition(TRANSIT_OPEN);
for (WindowContainer<?> wc : wcs) {
transition.collect(wc);
}
- closing.mVisibleRequested = false;
- opening.mVisibleRequested = true;
+ closing.setVisibleRequested(false);
+ opening.setVisibleRequested(true);
final int newRotation = mDisplayContent.getWindowConfiguration().getRotation() + 1;
for (WindowContainer<?> wc : wcs) {
wc.getWindowConfiguration().setRotation(newRotation);
@@ -586,9 +586,9 @@
changes.put(changeInChange, new Transition.ChangeInfo(true /* vis */, false /* exChg */));
fillChangeMap(changes, openTask);
// End states.
- changeInChange.mVisibleRequested = true;
- openInOpen.mVisibleRequested = true;
- openInChange.mVisibleRequested = true;
+ changeInChange.setVisibleRequested(true);
+ openInOpen.setVisibleRequested(true);
+ openInChange.setVisibleRequested(true);
final int transit = transition.mType;
int flags = 0;
@@ -644,8 +644,8 @@
changes.put(closing, new Transition.ChangeInfo(true /* vis */, false /* exChg */));
fillChangeMap(changes, newTask);
// End states.
- closing.mVisibleRequested = true;
- opening.mVisibleRequested = true;
+ closing.setVisibleRequested(true);
+ opening.setVisibleRequested(true);
final int transit = transition.mType;
int flags = 0;
@@ -685,8 +685,8 @@
changes.put(closing, new Transition.ChangeInfo(true /* vis */, false /* exChg */));
fillChangeMap(changes, newTask);
// End states.
- closing.mVisibleRequested = true;
- opening.mVisibleRequested = true;
+ closing.setVisibleRequested(true);
+ opening.setVisibleRequested(true);
final int transit = transition.mType;
int flags = 0;
@@ -962,7 +962,7 @@
home.mTransitionController.requestStartTransition(transition, home.getTask(),
null /* remoteTransition */, null /* displayChange */);
transition.collectExistenceChange(home);
- home.mVisibleRequested = true;
+ home.setVisibleRequested(true);
mDisplayContent.setFixedRotationLaunchingAppUnchecked(home);
doReturn(true).when(home).hasFixedRotationTransform(any());
player.startTransition();
@@ -998,12 +998,12 @@
// Start out with task2 visible and set up a transition that closes task2 and opens task1
final Task task1 = createTask(mDisplayContent);
final ActivityRecord activity1 = createActivityRecord(task1);
- activity1.mVisibleRequested = false;
+ activity1.setVisibleRequested(false);
activity1.setVisible(false);
final Task task2 = createTask(mDisplayContent);
makeTaskOrganized(task1, task2);
final ActivityRecord activity2 = createActivityRecord(task1);
- activity2.mVisibleRequested = true;
+ activity2.setVisibleRequested(true);
activity2.setVisible(true);
openTransition.collectExistenceChange(task1);
@@ -1011,9 +1011,9 @@
openTransition.collectExistenceChange(task2);
openTransition.collectExistenceChange(activity2);
- activity1.mVisibleRequested = true;
+ activity1.setVisibleRequested(true);
activity1.setVisible(true);
- activity2.mVisibleRequested = false;
+ activity2.setVisibleRequested(false);
// Using abort to force-finish the sync (since we can't wait for drawing in unit test).
// We didn't call abort on the transition itself, so it will still run onTransactionReady
@@ -1029,8 +1029,8 @@
closeTransition.collectExistenceChange(task2);
closeTransition.collectExistenceChange(activity2);
- activity1.mVisibleRequested = false;
- activity2.mVisibleRequested = true;
+ activity1.setVisibleRequested(false);
+ activity2.setVisibleRequested(true);
openTransition.finishTransition();
@@ -1072,12 +1072,12 @@
// Start out with task2 visible and set up a transition that closes task2 and opens task1
final Task task1 = createTask(mDisplayContent);
final ActivityRecord activity1 = createActivityRecord(task1);
- activity1.mVisibleRequested = false;
+ activity1.setVisibleRequested(false);
activity1.setVisible(false);
final Task task2 = createTask(mDisplayContent);
makeTaskOrganized(task1, task2);
final ActivityRecord activity2 = createActivityRecord(task2);
- activity2.mVisibleRequested = true;
+ activity2.setVisibleRequested(true);
activity2.setVisible(true);
openTransition.collectExistenceChange(task1);
@@ -1085,9 +1085,9 @@
openTransition.collectExistenceChange(task2);
openTransition.collectExistenceChange(activity2);
- activity1.mVisibleRequested = true;
+ activity1.setVisibleRequested(true);
activity1.setVisible(true);
- activity2.mVisibleRequested = false;
+ activity2.setVisibleRequested(false);
// Using abort to force-finish the sync (since we can't wait for drawing in unit test).
// We didn't call abort on the transition itself, so it will still run onTransactionReady
@@ -1107,8 +1107,8 @@
closeTransition.collectExistenceChange(activity2);
closeTransition.setTransientLaunch(activity2, null /* restoreBelow */);
- activity1.mVisibleRequested = false;
- activity2.mVisibleRequested = true;
+ activity1.setVisibleRequested(false);
+ activity2.setVisibleRequested(true);
activity2.setVisible(true);
// Using abort to force-finish the sync (since we obviously can't wait for drawing).
@@ -1166,8 +1166,8 @@
changes.put(activity0, new Transition.ChangeInfo(true /* vis */, false /* exChg */));
changes.put(activity1, new Transition.ChangeInfo(false /* vis */, false /* exChg */));
// End states.
- activity0.mVisibleRequested = false;
- activity1.mVisibleRequested = true;
+ activity0.setVisibleRequested(false);
+ activity1.setVisibleRequested(true);
participants.add(activity0);
participants.add(activity1);
@@ -1210,9 +1210,9 @@
changes.put(nonEmbeddedActivity, new Transition.ChangeInfo(true /* vis */,
false /* exChg */));
// End states.
- closingActivity.mVisibleRequested = false;
- openingActivity.mVisibleRequested = true;
- nonEmbeddedActivity.mVisibleRequested = false;
+ closingActivity.setVisibleRequested(false);
+ openingActivity.setVisibleRequested(true);
+ nonEmbeddedActivity.setVisibleRequested(false);
participants.add(closingActivity);
participants.add(openingActivity);
@@ -1255,8 +1255,8 @@
false /* exChg */));
changes.put(embeddedTf, new Transition.ChangeInfo(false /* vis */, true /* exChg */));
// End states.
- nonEmbeddedActivity.mVisibleRequested = false;
- embeddedActivity.mVisibleRequested = true;
+ nonEmbeddedActivity.setVisibleRequested(false);
+ embeddedActivity.setVisibleRequested(true);
embeddedTf.setBounds(new Rect(0, 0, 500, 500));
participants.add(nonEmbeddedActivity);
@@ -1285,11 +1285,11 @@
final ActivityRecord activity = createActivityRecord(task);
// Start states: set bounds to make sure the start bounds is ignored if it is not visible.
activity.getConfiguration().windowConfiguration.setBounds(new Rect(0, 0, 250, 500));
- activity.mVisibleRequested = false;
+ activity.setVisibleRequested(false);
changes.put(activity, new Transition.ChangeInfo(activity));
// End states: reset bounds to fill Task.
activity.getConfiguration().windowConfiguration.setBounds(taskBounds);
- activity.mVisibleRequested = true;
+ activity.setVisibleRequested(true);
participants.add(activity);
final ArrayList<WindowContainer> targets = Transition.calculateTargets(
@@ -1313,11 +1313,11 @@
task.getConfiguration().windowConfiguration.setBounds(taskBounds);
final ActivityRecord activity = createActivityRecord(task);
// Start states: fills Task without override.
- activity.mVisibleRequested = true;
+ activity.setVisibleRequested(true);
changes.put(activity, new Transition.ChangeInfo(activity));
// End states: set bounds to make sure the start bounds is ignored if it is not visible.
activity.getConfiguration().windowConfiguration.setBounds(new Rect(0, 0, 250, 500));
- activity.mVisibleRequested = false;
+ activity.setVisibleRequested(false);
participants.add(activity);
final ArrayList<WindowContainer> targets = Transition.calculateTargets(
@@ -1340,12 +1340,12 @@
final Task lastParent = createTask(mDisplayContent);
final Task newParent = createTask(mDisplayContent);
final ActivityRecord activity = createActivityRecord(lastParent);
- activity.mVisibleRequested = true;
+ activity.setVisibleRequested(true);
// Skip manipulate the SurfaceControl.
doNothing().when(activity).setDropInputMode(anyInt());
changes.put(activity, new Transition.ChangeInfo(activity));
activity.reparent(newParent, POSITION_TOP);
- activity.mVisibleRequested = false;
+ activity.setVisibleRequested(false);
participants.add(activity);
final ArrayList<WindowContainer> targets = Transition.calculateTargets(
@@ -1365,7 +1365,7 @@
final Task task = createTask(mDisplayContent);
task.setBounds(new Rect(0, 0, 2000, 1000));
final ActivityRecord activity = createActivityRecord(task);
- activity.mVisibleRequested = true;
+ activity.setVisibleRequested(true);
// Skip manipulate the SurfaceControl.
doNothing().when(activity).setDropInputMode(anyInt());
final TaskFragmentOrganizer organizer = new TaskFragmentOrganizer(Runnable::run);
@@ -1413,13 +1413,13 @@
task.setTaskDescription(taskDescription);
// Start states:
- embeddedActivity.mVisibleRequested = true;
- nonEmbeddedActivity.mVisibleRequested = false;
+ embeddedActivity.setVisibleRequested(true);
+ nonEmbeddedActivity.setVisibleRequested(false);
changes.put(embeddedTf, new Transition.ChangeInfo(embeddedTf));
changes.put(nonEmbeddedActivity, new Transition.ChangeInfo(nonEmbeddedActivity));
// End states:
- embeddedActivity.mVisibleRequested = false;
- nonEmbeddedActivity.mVisibleRequested = true;
+ embeddedActivity.setVisibleRequested(false);
+ nonEmbeddedActivity.setVisibleRequested(true);
participants.add(embeddedTf);
participants.add(nonEmbeddedActivity);
@@ -1532,7 +1532,7 @@
final ActivityRecord activity = createActivityRecord(lastParent);
doReturn(true).when(lastParent).shouldRemoveSelfOnLastChildRemoval();
doNothing().when(activity).setDropInputMode(anyInt());
- activity.mVisibleRequested = true;
+ activity.setVisibleRequested(true);
final Transition transition = new Transition(TRANSIT_CHANGE, 0 /* flags */,
activity.mTransitionController, mWm.mSyncEngine);
diff --git a/services/tests/wmtests/src/com/android/server/wm/UnknownAppVisibilityControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/UnknownAppVisibilityControllerTest.java
index 45e1141..2fccb88a 100644
--- a/services/tests/wmtests/src/com/android/server/wm/UnknownAppVisibilityControllerTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/UnknownAppVisibilityControllerTest.java
@@ -95,7 +95,7 @@
final ActivityRecord activity = createNonAttachedActivityRecord(mDisplayContent);
mDisplayContent.mUnknownAppVisibilityController.notifyLaunched(activity);
activity.finishing = true;
- activity.mVisibleRequested = true;
+ activity.setVisibleRequested(true);
activity.setVisibility(false, false);
assertTrue(mDisplayContent.mUnknownAppVisibilityController.allResolved());
}
diff --git a/services/tests/wmtests/src/com/android/server/wm/WallpaperControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/WallpaperControllerTests.java
index 94b5b93..a100b9a 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WallpaperControllerTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WallpaperControllerTests.java
@@ -313,12 +313,12 @@
r.applyFixedRotationTransform(mDisplayContent.getDisplayInfo(),
mDisplayContent.mDisplayFrames, mDisplayContent.getConfiguration());
// Invisible requested activity should not share its rotation transform.
- r.mVisibleRequested = false;
+ r.setVisibleRequested(false);
mDisplayContent.mWallpaperController.adjustWallpaperWindows();
assertFalse(wallpaperToken.hasFixedRotationTransform());
// Wallpaper should link the transform of its target.
- r.mVisibleRequested = true;
+ r.setVisibleRequested(true);
mDisplayContent.mWallpaperController.adjustWallpaperWindows();
assertEquals(appWin, mDisplayContent.mWallpaperController.getWallpaperTarget());
assertTrue(r.hasFixedRotationTransform());
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java
index 871030f..3d777f8 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java
@@ -205,7 +205,7 @@
win.mViewVisibility = View.VISIBLE;
win.mHasSurface = true;
win.mActivityRecord.mAppStopped = true;
- win.mActivityRecord.mVisibleRequested = false;
+ win.mActivityRecord.setVisibleRequested(false);
win.mActivityRecord.setVisible(false);
mWm.mWindowMap.put(win.mClient.asBinder(), win);
final int w = 100;
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java
index c73e237..e5e9f54 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java
@@ -992,7 +992,7 @@
final Task task = createTask(rootTaskController1);
final WindowState w = createAppWindow(task, TYPE_APPLICATION, "Enlightened Window");
- w.mActivityRecord.mVisibleRequested = true;
+ w.mActivityRecord.setVisibleRequested(true);
w.mActivityRecord.setVisible(true);
BLASTSyncEngine bse = new BLASTSyncEngine(mWm);
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowProcessControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowProcessControllerTests.java
index 3abf7ce..8bd4148 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowProcessControllerTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowProcessControllerTests.java
@@ -324,7 +324,7 @@
@Test
public void testComputeOomAdjFromActivities() {
final ActivityRecord activity = createActivityRecord(mWpc);
- activity.mVisibleRequested = true;
+ activity.setVisibleRequested(true);
final int[] callbackResult = { 0 };
final int visible = 1;
final int paused = 2;
@@ -359,7 +359,7 @@
assertEquals(visible, callbackResult[0]);
callbackResult[0] = 0;
- activity.mVisibleRequested = false;
+ activity.setVisibleRequested(false);
activity.setState(PAUSED, "test");
mWpc.computeOomAdjFromActivities(callback);
assertEquals(paused, callbackResult[0]);
@@ -380,7 +380,7 @@
final VisibleActivityProcessTracker tracker = mAtm.mVisibleActivityProcessTracker;
spyOn(tracker);
final ActivityRecord activity = createActivityRecord(mWpc);
- activity.mVisibleRequested = true;
+ activity.setVisibleRequested(true);
activity.setState(STARTED, "test");
verify(tracker).onAnyActivityVisible(mWpc);
@@ -398,7 +398,7 @@
assertTrue(mWpc.hasForegroundActivities());
activity.setVisibility(false);
- activity.mVisibleRequested = false;
+ activity.setVisibleRequested(false);
activity.setState(STOPPED, "test");
verify(tracker).onAllActivitiesInvisible(mWpc);
@@ -413,7 +413,7 @@
@Test
public void testTopActivityUiModeChangeScheduleConfigChange() {
final ActivityRecord activity = createActivityRecord(mWpc);
- activity.mVisibleRequested = true;
+ activity.setVisibleRequested(true);
doReturn(true).when(activity).applyAppSpecificConfig(anyInt(), any());
mWpc.updateAppSpecificSettingsForAllActivitiesInPackage(DEFAULT_COMPONENT_PACKAGE_NAME,
Configuration.UI_MODE_NIGHT_YES, LocaleList.forLanguageTags("en-XA"));
@@ -423,7 +423,7 @@
@Test
public void testTopActivityUiModeChangeForDifferentPackage_noScheduledConfigChange() {
final ActivityRecord activity = createActivityRecord(mWpc);
- activity.mVisibleRequested = true;
+ activity.setVisibleRequested(true);
mWpc.updateAppSpecificSettingsForAllActivitiesInPackage("com.different.package",
Configuration.UI_MODE_NIGHT_YES, LocaleList.forLanguageTags("en-XA"));
verify(activity, never()).applyAppSpecificConfig(anyInt(), any());
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java
index 1b79dd3..69e3244 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java
@@ -264,7 +264,7 @@
// Verify that app window can still be IME target as long as it is visible (even if
// it is going to become invisible).
- appWindow.mActivityRecord.mVisibleRequested = false;
+ appWindow.mActivityRecord.setVisibleRequested(false);
assertTrue(appWindow.canBeImeTarget());
// Make windows invisible
@@ -413,6 +413,16 @@
}
@Test
+ public void testCanAffectSystemUiFlags_starting() {
+ final WindowState app = createWindow(null, TYPE_APPLICATION_STARTING, "app");
+ app.mActivityRecord.setVisible(true);
+ app.mStartingData = new SnapshotStartingData(mWm, null, 0);
+ assertFalse(app.canAffectSystemUiFlags());
+ app.mStartingData = new SplashScreenStartingData(mWm, 0, 0);
+ assertTrue(app.canAffectSystemUiFlags());
+ }
+
+ @Test
public void testCanAffectSystemUiFlags_disallow() {
final WindowState app = createWindow(null, TYPE_APPLICATION, "app");
app.mActivityRecord.setVisible(true);
@@ -720,7 +730,7 @@
// No need to wait for a window of invisible activity even if the window has surface.
final WindowState invisibleApp = mAppWindow;
- invisibleApp.mActivityRecord.mVisibleRequested = false;
+ invisibleApp.mActivityRecord.setVisibleRequested(false);
invisibleApp.mActivityRecord.allDrawn = false;
outWaitingForDrawn.clear();
invisibleApp.requestDrawIfNeeded(outWaitingForDrawn);
@@ -738,7 +748,7 @@
assertFalse(startingApp.getOrientationChanging());
// Even if the display is frozen, invisible requested window should not be affected.
- startingApp.mActivityRecord.mVisibleRequested = false;
+ startingApp.mActivityRecord.setVisibleRequested(false);
mWm.startFreezingDisplay(0, 0, mDisplayContent);
doReturn(true).when(mWm.mPolicy).isScreenOn();
startingApp.getWindowFrames().setInsetsChanged(true);
@@ -813,7 +823,7 @@
final WindowState win = createWindow(null /* parent */, TYPE_APPLICATION, embeddedActivity,
"App window");
doReturn(true).when(embeddedActivity).isVisible();
- embeddedActivity.mVisibleRequested = true;
+ embeddedActivity.setVisibleRequested(true);
makeWindowVisible(win);
win.mLayoutSeq = win.getDisplayContent().mLayoutSeq;
// Set the bounds twice:
@@ -838,7 +848,7 @@
@Test
public void testCantReceiveTouchWhenAppTokenHiddenRequested() {
final WindowState win0 = createWindow(null, TYPE_APPLICATION, "win0");
- win0.mActivityRecord.mVisibleRequested = false;
+ win0.mActivityRecord.setVisibleRequested(false);
assertFalse(win0.canReceiveTouchInput());
}
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java b/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java
index 5a261bc65..019b14d 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java
@@ -738,7 +738,7 @@
activity.onDisplayChanged(dc);
activity.setOccludesParent(true);
activity.setVisible(true);
- activity.mVisibleRequested = true;
+ activity.setVisibleRequested(true);
}
/**
@@ -1240,7 +1240,7 @@
mTask.moveToFront("createActivity");
}
if (mVisible) {
- activity.mVisibleRequested = true;
+ activity.setVisibleRequested(true);
activity.setVisible(true);
}
}
diff --git a/services/usage/java/com/android/server/usage/UsageStatsService.java b/services/usage/java/com/android/server/usage/UsageStatsService.java
index 4fd2b78..b3a1f2b 100644
--- a/services/usage/java/com/android/server/usage/UsageStatsService.java
+++ b/services/usage/java/com/android/server/usage/UsageStatsService.java
@@ -1864,6 +1864,21 @@
mResponseStatsTracker.dump(idpw);
}
return;
+ } else if ("app-component-usage".equals(arg)) {
+ final IndentingPrintWriter ipw = new IndentingPrintWriter(pw, " ");
+ synchronized (mLock) {
+ if (!mLastTimeComponentUsedGlobal.isEmpty()) {
+ ipw.println("App Component Usages:");
+ ipw.increaseIndent();
+ for (String pkg : mLastTimeComponentUsedGlobal.keySet()) {
+ ipw.println("package=" + pkg
+ + " lastUsed=" + UserUsageStatsService.formatDateTime(
+ mLastTimeComponentUsedGlobal.get(pkg), true));
+ }
+ ipw.decreaseIndent();
+ }
+ }
+ return;
} else if (arg != null && !arg.startsWith("-")) {
// Anything else that doesn't start with '-' is a pkg to filter
pkgs.add(arg);
diff --git a/telephony/java/android/telephony/ServiceState.java b/telephony/java/android/telephony/ServiceState.java
index 6be2f77..7c600b1 100644
--- a/telephony/java/android/telephony/ServiceState.java
+++ b/telephony/java/android/telephony/ServiceState.java
@@ -138,6 +138,12 @@
*/
public static final int FREQUENCY_RANGE_MMWAVE = 4;
+ /**
+ * Number of frequency ranges.
+ * @hide
+ */
+ public static final int FREQUENCY_RANGE_COUNT = 5;
+
/** @hide */
@Retention(RetentionPolicy.SOURCE)
@IntDef(prefix = "DUPLEX_MODE_",
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/BaseTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/BaseTest.kt
index c100a9c..a4609f7 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/BaseTest.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/BaseTest.kt
@@ -174,14 +174,18 @@
open fun cujCompleted() {
entireScreenCovered()
- navBarLayerIsVisibleAtStartAndEnd()
- navBarWindowIsAlwaysVisible()
- taskBarLayerIsVisibleAtStartAndEnd()
- taskBarWindowIsAlwaysVisible()
statusBarLayerIsVisibleAtStartAndEnd()
statusBarLayerPositionAtStartAndEnd()
statusBarWindowIsAlwaysVisible()
visibleLayersShownMoreThanOneConsecutiveEntry()
visibleWindowsShownMoreThanOneConsecutiveEntry()
+
+ if (flicker.scenario.isTablet) {
+ taskBarLayerIsVisibleAtStartAndEnd()
+ taskBarWindowIsAlwaysVisible()
+ } else {
+ navBarLayerIsVisibleAtStartAndEnd()
+ navBarWindowIsAlwaysVisible()
+ }
}
}
diff --git a/tests/utils/testutils/java/com/android/internal/util/test/BroadcastInterceptingContext.java b/tests/utils/testutils/java/com/android/internal/util/test/BroadcastInterceptingContext.java
index 133c176..cc3781a 100644
--- a/tests/utils/testutils/java/com/android/internal/util/test/BroadcastInterceptingContext.java
+++ b/tests/utils/testutils/java/com/android/internal/util/test/BroadcastInterceptingContext.java
@@ -246,6 +246,12 @@
}
@Override
+ public void sendBroadcastAsUser(Intent intent, UserHandle user,
+ String receiverPermission, Bundle options) {
+ sendBroadcast(intent);
+ }
+
+ @Override
public void sendStickyBroadcast(Intent intent) {
sendBroadcast(intent);
}