Merge "Add unit tests for DisposableBroadcastReceiverAsUser"
diff --git a/Android.bp b/Android.bp
index c0a2abb..3d0188a 100644
--- a/Android.bp
+++ b/Android.bp
@@ -108,7 +108,7 @@
":android.security.legacykeystore-java-source",
":android.security.maintenance-java-source",
":android.security.metrics-java-source",
- ":android.system.keystore2-V1-java-source",
+ ":android.system.keystore2-V3-java-source",
":credstore_aidl",
":dumpstate_aidl",
":framework_native_aidl",
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/JobSchedulerImpl.java b/apex/jobscheduler/framework/java/android/app/JobSchedulerImpl.java
index 0c65b99..2d3201a 100644
--- a/apex/jobscheduler/framework/java/android/app/JobSchedulerImpl.java
+++ b/apex/jobscheduler/framework/java/android/app/JobSchedulerImpl.java
@@ -36,7 +36,7 @@
* Note android.app.job is the better package to put this class, but we can't move it there
* because that'd break robolectric. Grr.
*
- * @hide
+ * @hide
*/
public class JobSchedulerImpl extends JobScheduler {
IJobScheduler mBinder;
@@ -109,6 +109,15 @@
}
@Override
+ public int getPendingJobReason(int jobId) {
+ try {
+ return mBinder.getPendingJobReason(jobId);
+ } catch (RemoteException e) {
+ return PENDING_JOB_REASON_UNDEFINED;
+ }
+ }
+
+ @Override
public boolean canRunLongJobs() {
try {
return mBinder.canRunLongJobs(mContext.getOpPackageName());
diff --git a/apex/jobscheduler/framework/java/android/app/job/IJobCallback.aidl b/apex/jobscheduler/framework/java/android/app/job/IJobCallback.aidl
index a3390b7..96494ec 100644
--- a/apex/jobscheduler/framework/java/android/app/job/IJobCallback.aidl
+++ b/apex/jobscheduler/framework/java/android/app/job/IJobCallback.aidl
@@ -16,6 +16,7 @@
package android.app.job;
+import android.app.Notification;
import android.app.job.JobWorkItem;
/**
@@ -104,4 +105,17 @@
*/
void updateTransferredNetworkBytes(int jobId, in JobWorkItem item,
long transferredDownloadBytes, long transferredUploadBytes);
+ /**
+ * Provide JobScheduler with a notification to post and tie to this job's
+ * lifecycle.
+ * This is required for all user-initiated job and optional for other jobs.
+ *
+ * @param jobId Unique integer used to identify this job.
+ * @param notificationId The ID for this notification, as per
+ * {@link android.app.NotificationManager#notify(int, Notification)}.
+ * @param notification The notification to be displayed.
+ * @param jobEndNotificationPolicy The policy to apply to the notification when the job stops.
+ */
+ void setNotification(int jobId, int notificationId,
+ in Notification notification, int jobEndNotificationPolicy);
}
diff --git a/apex/jobscheduler/framework/java/android/app/job/IJobScheduler.aidl b/apex/jobscheduler/framework/java/android/app/job/IJobScheduler.aidl
index d2be32e..bf29dc9 100644
--- a/apex/jobscheduler/framework/java/android/app/job/IJobScheduler.aidl
+++ b/apex/jobscheduler/framework/java/android/app/job/IJobScheduler.aidl
@@ -33,6 +33,7 @@
void cancelAll();
ParceledListSlice getAllPendingJobs();
JobInfo getPendingJob(int jobId);
+ int getPendingJobReason(int jobId);
boolean canRunLongJobs(String packageName);
boolean hasRunLongJobsPermission(String packageName, int userId);
List<JobInfo> getStartedJobs();
diff --git a/apex/jobscheduler/framework/java/android/app/job/JobScheduler.java b/apex/jobscheduler/framework/java/android/app/job/JobScheduler.java
index 13b6652..659db9f 100644
--- a/apex/jobscheduler/framework/java/android/app/job/JobScheduler.java
+++ b/apex/jobscheduler/framework/java/android/app/job/JobScheduler.java
@@ -23,10 +23,14 @@
import android.annotation.SystemApi;
import android.annotation.SystemService;
import android.annotation.UserIdInt;
+import android.app.ActivityManager;
+import android.app.usage.UsageStatsManager;
import android.compat.annotation.ChangeId;
import android.compat.annotation.EnabledAfter;
import android.content.ClipData;
import android.content.Context;
+import android.content.pm.PackageManager;
+import android.net.NetworkRequest;
import android.os.Build;
import android.os.Bundle;
import android.os.PersistableBundle;
@@ -133,6 +137,132 @@
*/
public static final int RESULT_SUCCESS = 1;
+ /** The job doesn't exist. */
+ public static final int PENDING_JOB_REASON_INVALID_JOB_ID = -2;
+ /** The job is currently running and is therefore not pending. */
+ public static final int PENDING_JOB_REASON_EXECUTING = -1;
+ /**
+ * There is no known reason why the job is pending.
+ * If additional reasons are added on newer Android versions, the system may return this reason
+ * to apps whose target SDK is not high enough to expect that reason.
+ */
+ public static final int PENDING_JOB_REASON_UNDEFINED = 0;
+ /**
+ * The app is in a state that prevents the job from running
+ * (eg. the {@link JobService} component is disabled).
+ */
+ public static final int PENDING_JOB_REASON_APP = 1;
+ /**
+ * The current standby bucket prevents the job from running.
+ *
+ * @see UsageStatsManager#STANDBY_BUCKET_RESTRICTED
+ */
+ public static final int PENDING_JOB_REASON_APP_STANDBY = 2;
+ /**
+ * The app is restricted from running in the background.
+ *
+ * @see ActivityManager#isBackgroundRestricted()
+ * @see PackageManager#isInstantApp()
+ */
+ public static final int PENDING_JOB_REASON_BACKGROUND_RESTRICTION = 3;
+ /**
+ * The requested battery-not-low constraint is not satisfied.
+ *
+ * @see JobInfo.Builder#setRequiresBatteryNotLow(boolean)
+ */
+ public static final int PENDING_JOB_REASON_CONSTRAINT_BATTERY_NOT_LOW = 4;
+ /**
+ * The requested charging constraint is not satisfied.
+ *
+ * @see JobInfo.Builder#setRequiresCharging(boolean)
+ */
+ public static final int PENDING_JOB_REASON_CONSTRAINT_CHARGING = 5;
+ /**
+ * The requested connectivity constraint is not satisfied.
+ *
+ * @see JobInfo.Builder#setRequiredNetwork(NetworkRequest)
+ * @see JobInfo.Builder#setRequiredNetworkType(int)
+ */
+ public static final int PENDING_JOB_REASON_CONSTRAINT_CONNECTIVITY = 6;
+ /**
+ * The requested content trigger constraint is not satisfied.
+ *
+ * @see JobInfo.Builder#addTriggerContentUri(JobInfo.TriggerContentUri)
+ */
+ public static final int PENDING_JOB_REASON_CONSTRAINT_CONTENT_TRIGGER = 7;
+ /**
+ * The requested idle constraint is not satisfied.
+ *
+ * @see JobInfo.Builder#setRequiresDeviceIdle(boolean)
+ */
+ public static final int PENDING_JOB_REASON_CONSTRAINT_DEVICE_IDLE = 8;
+ /**
+ * The minimum latency has not transpired.
+ *
+ * @see JobInfo.Builder#setMinimumLatency(long)
+ */
+ public static final int PENDING_JOB_REASON_CONSTRAINT_MINIMUM_LATENCY = 9;
+ /**
+ * The system's estimate of when the app will be launched is far away enough to warrant delaying
+ * this job.
+ *
+ * @see JobInfo#isPrefetch()
+ * @see JobInfo.Builder#setPrefetch(boolean)
+ */
+ public static final int PENDING_JOB_REASON_CONSTRAINT_PREFETCH = 10;
+ /**
+ * The requested storage-not-low constraint is not satisfied.
+ *
+ * @see JobInfo.Builder#setRequiresStorageNotLow(boolean)
+ */
+ public static final int PENDING_JOB_REASON_CONSTRAINT_STORAGE_NOT_LOW = 11;
+ /**
+ * The job is being deferred due to the device state (eg. Doze, battery saver, memory usage,
+ * thermal status, etc.).
+ */
+ public static final int PENDING_JOB_REASON_DEVICE_STATE = 12;
+ /**
+ * JobScheduler thinks it can defer this job to a more optimal running time.
+ */
+ public static final int PENDING_JOB_REASON_JOB_SCHEDULER_OPTIMIZATION = 13;
+ /**
+ * The app has consumed all of its current quota.
+ *
+ * @see UsageStatsManager#getAppStandbyBucket()
+ * @see JobParameters#STOP_REASON_QUOTA
+ */
+ public static final int PENDING_JOB_REASON_QUOTA = 14;
+ /**
+ * JobScheduler is respecting one of the user's actions (eg. force stop or adb shell commands)
+ * to defer this job.
+ */
+ public static final int PENDING_JOB_REASON_USER = 15;
+
+ /** @hide */
+ @IntDef(prefix = {"PENDING_JOB_REASON_"}, value = {
+ PENDING_JOB_REASON_UNDEFINED,
+ PENDING_JOB_REASON_APP,
+ PENDING_JOB_REASON_APP_STANDBY,
+ PENDING_JOB_REASON_BACKGROUND_RESTRICTION,
+ PENDING_JOB_REASON_CONSTRAINT_BATTERY_NOT_LOW,
+ PENDING_JOB_REASON_CONSTRAINT_CHARGING,
+ PENDING_JOB_REASON_CONSTRAINT_CONNECTIVITY,
+ PENDING_JOB_REASON_CONSTRAINT_CONTENT_TRIGGER,
+ PENDING_JOB_REASON_CONSTRAINT_DEVICE_IDLE,
+ PENDING_JOB_REASON_CONSTRAINT_MINIMUM_LATENCY,
+ PENDING_JOB_REASON_CONSTRAINT_PREFETCH,
+ PENDING_JOB_REASON_CONSTRAINT_STORAGE_NOT_LOW,
+ PENDING_JOB_REASON_DEVICE_STATE,
+ PENDING_JOB_REASON_EXECUTING,
+ PENDING_JOB_REASON_INVALID_JOB_ID,
+ PENDING_JOB_REASON_JOB_SCHEDULER_OPTIMIZATION,
+ PENDING_JOB_REASON_QUOTA,
+ PENDING_JOB_REASON_USER,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface PendingJobReason {
+ }
+
/**
* Schedule a job to be executed. Will replace any currently scheduled job with the same
* ID with the new information in the {@link JobInfo}. If a job with the given ID is currently
@@ -250,6 +380,15 @@
public abstract @Nullable JobInfo getPendingJob(int jobId);
/**
+ * Returns a reason why the job is pending and not currently executing. If there are multiple
+ * reasons why a job may be pending, this will only return one of them.
+ */
+ @PendingJobReason
+ public int getPendingJobReason(int jobId) {
+ return PENDING_JOB_REASON_UNDEFINED;
+ }
+
+ /**
* Returns {@code true} if the calling app currently holds the
* {@link android.Manifest.permission#RUN_LONG_JOBS} permission, allowing it to run long jobs.
*/
diff --git a/apex/jobscheduler/framework/java/android/app/job/JobService.java b/apex/jobscheduler/framework/java/android/app/job/JobService.java
index bad641c..e88e979 100644
--- a/apex/jobscheduler/framework/java/android/app/job/JobService.java
+++ b/apex/jobscheduler/framework/java/android/app/job/JobService.java
@@ -19,13 +19,18 @@
import static android.app.job.JobScheduler.THROW_ON_INVALID_DATA_TRANSFER_IMPLEMENTATION;
import android.annotation.BytesLong;
+import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.app.Notification;
import android.app.Service;
import android.compat.Compatibility;
import android.content.Intent;
import android.os.IBinder;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
/**
* <p>Entry point for the callback from the {@link android.app.job.JobScheduler}.</p>
* <p>This is the base class that handles asynchronous requests that were previously scheduled. You
@@ -63,6 +68,32 @@
public static final String PERMISSION_BIND =
"android.permission.BIND_JOB_SERVICE";
+ /**
+ * Detach the notification supplied to
+ * {@link #setNotification(JobParameters, int, Notification, int)} when the job ends.
+ * The notification will remain shown even after JobScheduler stops the job.
+ *
+ * @hide
+ */
+ public static final int JOB_END_NOTIFICATION_POLICY_DETACH = 0;
+ /**
+ * Cancel and remove the notification supplied to
+ * {@link #setNotification(JobParameters, int, Notification, int)} when the job ends.
+ * The notification will be removed from the notification shade.
+ *
+ * @hide
+ */
+ public static final int JOB_END_NOTIFICATION_POLICY_REMOVE = 1;
+
+ /** @hide */
+ @IntDef(prefix = {"JOB_END_NOTIFICATION_POLICY_"}, value = {
+ JOB_END_NOTIFICATION_POLICY_DETACH,
+ JOB_END_NOTIFICATION_POLICY_REMOVE,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface JobEndNotificationPolicy {
+ }
+
private JobServiceEngine mEngine;
/** @hide */
@@ -84,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);
}
}
@@ -95,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);
}
}
};
@@ -274,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.
@@ -298,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.
@@ -324,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
@@ -353,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
@@ -364,4 +397,37 @@
}
return 0;
}
+
+ /**
+ * Provide JobScheduler with a notification to post and tie to this job's lifecycle.
+ * This is required for all user-initiated jobs
+ * (scheduled via {link JobInfo.Builder#setUserInitiated(boolean)}) and optional for
+ * other jobs. If the app does not call this method for a required notification within
+ * 10 seconds after {@link #onStartJob(JobParameters)} is called,
+ * the system will trigger an ANR and stop this job.
+ *
+ * <p>
+ * Note that certain types of jobs
+ * (e.g. {@link JobInfo.Builder#setDataTransfer data transfer jobs}) may require the
+ * notification to have certain characteristics and their documentation will state
+ * any such requirements.
+ *
+ * <p>
+ * JobScheduler will not remember this notification after the job has finished running,
+ * so apps must call this every time the job is started (if required or desired).
+ *
+ * @param params The parameters identifying this job, as supplied to
+ * the job in the {@link #onStartJob(JobParameters)} callback.
+ * @param notificationId The ID for this notification, as per
+ * {@link android.app.NotificationManager#notify(int,
+ * Notification)}.
+ * @param notification The notification to be displayed.
+ * @param jobEndNotificationPolicy The policy to apply to the notification when the job stops.
+ * @hide
+ */
+ public final void setNotification(@NonNull JobParameters params, int notificationId,
+ @NonNull Notification notification,
+ @JobEndNotificationPolicy int jobEndNotificationPolicy) {
+ mEngine.setNotification(params, notificationId, notification, jobEndNotificationPolicy);
+ }
}
diff --git a/apex/jobscheduler/framework/java/android/app/job/JobServiceEngine.java b/apex/jobscheduler/framework/java/android/app/job/JobServiceEngine.java
index 83296a6..53e452f 100644
--- a/apex/jobscheduler/framework/java/android/app/job/JobServiceEngine.java
+++ b/apex/jobscheduler/framework/java/android/app/job/JobServiceEngine.java
@@ -21,6 +21,7 @@
import android.annotation.BytesLong;
import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.app.Notification;
import android.app.Service;
import android.compat.Compatibility;
import android.content.Intent;
@@ -73,6 +74,8 @@
private static final int MSG_UPDATE_TRANSFERRED_NETWORK_BYTES = 5;
/** Message that the client wants to update JobScheduler of the estimated transfer size. */
private static final int MSG_UPDATE_ESTIMATED_NETWORK_BYTES = 6;
+ /** Message that the client wants to give JobScheduler a notification to tie to the job. */
+ private static final int MSG_SET_NOTIFICATION = 7;
private final IJobService mBinder;
@@ -250,6 +253,24 @@
args.recycle();
break;
}
+ case MSG_SET_NOTIFICATION: {
+ final SomeArgs args = (SomeArgs) msg.obj;
+ final JobParameters params = (JobParameters) args.arg1;
+ final Notification notification = (Notification) args.arg2;
+ IJobCallback callback = params.getCallback();
+ if (callback != null) {
+ try {
+ callback.setNotification(params.getJobId(),
+ args.argi1, notification, args.argi2);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Error providing notification: binder has gone away.");
+ }
+ } else {
+ Log.e(TAG, "setNotification() called for a nonexistent job.");
+ }
+ args.recycle();
+ break;
+ }
default:
Log.e(TAG, "Unrecognised message received.");
break;
@@ -432,4 +453,27 @@
args.argl2 = uploadBytes;
mHandler.obtainMessage(MSG_UPDATE_ESTIMATED_NETWORK_BYTES, args).sendToTarget();
}
-}
\ No newline at end of file
+
+ /**
+ * Give JobScheduler a notification to tie to this job's lifecycle.
+ *
+ * @hide
+ * @see JobService#setNotification(JobParameters, int, Notification, int)
+ */
+ public void setNotification(@NonNull JobParameters params, int notificationId,
+ @NonNull Notification notification,
+ @JobService.JobEndNotificationPolicy int jobEndNotificationPolicy) {
+ if (params == null) {
+ throw new NullPointerException("params");
+ }
+ if (notification == null) {
+ throw new NullPointerException("notification");
+ }
+ SomeArgs args = SomeArgs.obtain();
+ args.arg1 = params;
+ args.arg2 = notification;
+ args.argi1 = notificationId;
+ args.argi2 = jobEndNotificationPolicy;
+ mHandler.obtainMessage(MSG_SET_NOTIFICATION, args).sendToTarget();
+ }
+}
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 f9dd0b3..47f6890 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/JobConcurrencyManager.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/JobConcurrencyManager.java
@@ -1175,7 +1175,7 @@
if (jobStatus != null && !jsc.isWithinExecutionGuaranteeTime()
&& restriction.isJobRestricted(jobStatus)) {
- jsc.cancelExecutingJobLocked(restriction.getReason(),
+ jsc.cancelExecutingJobLocked(restriction.getStopReason(),
restriction.getInternalReason(),
JobParameters.getInternalReasonCodeDescription(
restriction.getInternalReason()));
@@ -1208,7 +1208,7 @@
final JobRestriction restriction = mService.checkIfRestricted(running);
if (restriction != null) {
final int internalReasonCode = restriction.getInternalReason();
- serviceContext.cancelExecutingJobLocked(restriction.getReason(),
+ serviceContext.cancelExecutingJobLocked(restriction.getStopReason(),
internalReasonCode,
"restricted due to "
+ JobParameters.getInternalReasonCodeDescription(
@@ -1324,6 +1324,7 @@
mActivePkgStats.add(
jobStatus.getSourceUserId(), jobStatus.getSourcePackageName(),
packageStats);
+ mService.resetPendingJobReasonCache(jobStatus);
}
if (mService.getPendingJobQueue().remove(jobStatus)) {
mService.mJobPackageTracker.noteNonpending(jobStatus);
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 ee08f85..9fb2af7 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java
@@ -363,6 +363,9 @@
@GuardedBy("mLock")
private final ArraySet<JobStatus> mChangedJobList = new ArraySet<>();
+ @GuardedBy("mPendingJobReasonCache") // Use its own lock to avoid blocking JS processing
+ private final SparseArray<SparseIntArray> mPendingJobReasonCache = new SparseArray<>();
+
/**
* Named indices into standby bucket arrays, for clarity in referring to
* specific buckets' bookkeeping.
@@ -1357,6 +1360,134 @@
}
}
+ @JobScheduler.PendingJobReason
+ private int getPendingJobReason(int uid, int jobId) {
+ int reason;
+ // Some apps may attempt to query this frequently, so cache the reason under a separate lock
+ // so that the rest of JS processing isn't negatively impacted.
+ synchronized (mPendingJobReasonCache) {
+ SparseIntArray jobIdToReason = mPendingJobReasonCache.get(uid);
+ if (jobIdToReason != null) {
+ reason = jobIdToReason.get(jobId, JobScheduler.PENDING_JOB_REASON_UNDEFINED);
+ if (reason != JobScheduler.PENDING_JOB_REASON_UNDEFINED) {
+ return reason;
+ }
+ }
+ }
+ synchronized (mLock) {
+ reason = getPendingJobReasonLocked(uid, jobId);
+ if (DEBUG) {
+ Slog.v(TAG, "getPendingJobReason(" + uid + "," + jobId + ")=" + reason);
+ }
+ }
+ synchronized (mPendingJobReasonCache) {
+ SparseIntArray jobIdToReason = mPendingJobReasonCache.get(uid);
+ if (jobIdToReason == null) {
+ jobIdToReason = new SparseIntArray();
+ mPendingJobReasonCache.put(uid, jobIdToReason);
+ }
+ jobIdToReason.put(jobId, reason);
+ }
+ return reason;
+ }
+
+ @JobScheduler.PendingJobReason
+ @GuardedBy("mLock")
+ private int getPendingJobReasonLocked(int uid, int jobId) {
+ // Very similar code to isReadyToBeExecutedLocked.
+
+ JobStatus job = mJobs.getJobByUidAndJobId(uid, jobId);
+ if (job == null) {
+ // Job doesn't exist.
+ return JobScheduler.PENDING_JOB_REASON_INVALID_JOB_ID;
+ }
+
+ if (isCurrentlyRunningLocked(job)) {
+ return JobScheduler.PENDING_JOB_REASON_EXECUTING;
+ }
+
+ final boolean jobReady = job.isReady();
+
+ if (DEBUG) {
+ Slog.v(TAG, "getPendingJobReasonLocked: " + job.toShortString()
+ + " ready=" + jobReady);
+ }
+
+ if (!jobReady) {
+ return job.getPendingJobReason();
+ }
+
+ final boolean userStarted = areUsersStartedLocked(job);
+
+ if (DEBUG) {
+ Slog.v(TAG, "getPendingJobReasonLocked: " + job.toShortString()
+ + " userStarted=" + userStarted);
+ }
+ if (!userStarted) {
+ return JobScheduler.PENDING_JOB_REASON_USER;
+ }
+
+ final boolean backingUp = mBackingUpUids.get(job.getSourceUid());
+ if (DEBUG) {
+ Slog.v(TAG, "getPendingJobReasonLocked: " + job.toShortString()
+ + " backingUp=" + backingUp);
+ }
+
+ if (backingUp) {
+ // TODO: Should we make a special reason for this?
+ return JobScheduler.PENDING_JOB_REASON_APP;
+ }
+
+ JobRestriction restriction = checkIfRestricted(job);
+ if (DEBUG) {
+ Slog.v(TAG, "getPendingJobReasonLocked: " + job.toShortString()
+ + " restriction=" + restriction);
+ }
+ if (restriction != null) {
+ return restriction.getPendingReason();
+ }
+
+ // The following can be a little more expensive (especially jobActive, since we need to
+ // go through the array of all potentially active jobs), so we are doing them
+ // later... but still before checking with the package manager!
+ final boolean jobPending = mPendingJobQueue.contains(job);
+
+
+ if (DEBUG) {
+ Slog.v(TAG, "getPendingJobReasonLocked: " + job.toShortString()
+ + " pending=" + jobPending);
+ }
+
+ if (jobPending) {
+ // We haven't started the job for some reason. Presumably, there are too many jobs
+ // running.
+ return JobScheduler.PENDING_JOB_REASON_DEVICE_STATE;
+ }
+
+ final boolean jobActive = mConcurrencyManager.isJobRunningLocked(job);
+
+ if (DEBUG) {
+ Slog.v(TAG, "getPendingJobReasonLocked: " + job.toShortString()
+ + " active=" + jobActive);
+ }
+ if (jobActive) {
+ return JobScheduler.PENDING_JOB_REASON_UNDEFINED;
+ }
+
+ // Validate that the defined package+service is still present & viable.
+ final boolean componentUsable = isComponentUsable(job);
+
+ if (DEBUG) {
+ Slog.v(TAG, "getPendingJobReasonLocked: " + job.toShortString()
+ + " componentUsable=" + componentUsable);
+ }
+ if (!componentUsable) {
+ return JobScheduler.PENDING_JOB_REASON_APP;
+ }
+
+ return JobScheduler.PENDING_JOB_REASON_UNDEFINED;
+ }
+
public JobInfo getPendingJob(int uid, int jobId) {
synchronized (mLock) {
ArraySet<JobStatus> jobs = mJobs.getJobsByUid(uid);
@@ -1389,6 +1520,9 @@
synchronized (mLock) {
mJobs.removeJobsOfUnlistedUsers(umi.getUserIds());
}
+ synchronized (mPendingJobReasonCache) {
+ mPendingJobReasonCache.clear();
+ }
}
private void cancelJobsForPackageAndUidLocked(String pkgName, int uid,
@@ -1874,6 +2008,8 @@
jobStatus.enqueueTime = sElapsedRealtimeClock.millis();
final boolean update = lastJob != null;
mJobs.add(jobStatus);
+ // Clear potentially cached INVALID_JOB_ID reason.
+ resetPendingJobReasonCache(jobStatus);
if (mReadyToRock) {
for (int i = 0; i < mControllers.size(); i++) {
StateController controller = mControllers.get(i);
@@ -1895,6 +2031,13 @@
// Deal with any remaining work items in the old job.
jobStatus.stopTrackingJobLocked(incomingJob);
+ synchronized (mPendingJobReasonCache) {
+ SparseIntArray reasonCache = mPendingJobReasonCache.get(jobStatus.getUid());
+ if (reasonCache != null) {
+ reasonCache.delete(jobStatus.getJobId());
+ }
+ }
+
// Remove from store as well as controllers.
final boolean removed = mJobs.remove(jobStatus, removeFromPersisted);
if (!removed) {
@@ -1917,6 +2060,16 @@
return removed;
}
+ /** Remove the pending job reason for this job from the cache. */
+ void resetPendingJobReasonCache(@NonNull JobStatus jobStatus) {
+ synchronized (mPendingJobReasonCache) {
+ final SparseIntArray reasons = mPendingJobReasonCache.get(jobStatus.getUid());
+ if (reasons != null) {
+ reasons.delete(jobStatus.getJobId());
+ }
+ }
+ }
+
/** Return {@code true} if the specified job is currently executing. */
@GuardedBy("mLock")
public boolean isCurrentlyRunningLocked(JobStatus job) {
@@ -2210,11 +2363,20 @@
public void onControllerStateChanged(@Nullable ArraySet<JobStatus> changedJobs) {
if (changedJobs == null) {
mHandler.obtainMessage(MSG_CHECK_JOB).sendToTarget();
+ synchronized (mPendingJobReasonCache) {
+ mPendingJobReasonCache.clear();
+ }
} else if (changedJobs.size() > 0) {
synchronized (mLock) {
mChangedJobList.addAll(changedJobs);
}
mHandler.obtainMessage(MSG_CHECK_CHANGED_JOB_LIST).sendToTarget();
+ synchronized (mPendingJobReasonCache) {
+ for (int i = changedJobs.size() - 1; i >= 0; --i) {
+ final JobStatus job = changedJobs.valueAt(i);
+ resetPendingJobReasonCache(job);
+ }
+ }
}
}
@@ -2568,6 +2730,23 @@
if (DEBUG) {
Slog.d(TAG, "maybeQueueReadyJobsForExecutionLocked: Not running anything.");
}
+ final int numRunnableJobs = runnableJobs.size();
+ if (numRunnableJobs > 0) {
+ synchronized (mPendingJobReasonCache) {
+ for (int i = 0; i < numRunnableJobs; ++i) {
+ final JobStatus job = runnableJobs.get(i);
+ SparseIntArray reasons = mPendingJobReasonCache.get(job.getUid());
+ if (reasons == null) {
+ reasons = new SparseIntArray();
+ mPendingJobReasonCache.put(job.getUid(), reasons);
+ }
+ // We're force batching these jobs, so consider it an optimization
+ // policy reason.
+ reasons.put(job.getJobId(),
+ JobScheduler.PENDING_JOB_REASON_JOB_SCHEDULER_OPTIMIZATION);
+ }
+ }
+ }
}
// Be ready for next time
@@ -3370,6 +3549,18 @@
}
@Override
+ public int getPendingJobReason(int jobId) throws RemoteException {
+ final int uid = Binder.getCallingUid();
+
+ final long ident = Binder.clearCallingIdentity();
+ try {
+ return JobSchedulerService.this.getPendingJobReason(uid, jobId);
+ } finally {
+ Binder.restoreCallingIdentity(ident);
+ }
+ }
+
+ @Override
public JobInfo getPendingJob(int jobId) throws RemoteException {
final int uid = Binder.getCallingUid();
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 9aa6b1c..b20eedc 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java
@@ -17,6 +17,8 @@
package com.android.server.job;
import static android.app.job.JobInfo.getPriorityString;
+import static android.app.job.JobService.JOB_END_NOTIFICATION_POLICY_DETACH;
+import static android.app.job.JobService.JOB_END_NOTIFICATION_POLICY_REMOVE;
import static com.android.server.job.JobConcurrencyManager.WORK_TYPE_NONE;
import static com.android.server.job.JobSchedulerService.sElapsedRealtimeClock;
@@ -24,6 +26,7 @@
import android.annotation.BytesLong;
import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.app.Notification;
import android.app.job.IJobCallback;
import android.app.job.IJobService;
import android.app.job.JobInfo;
@@ -58,6 +61,7 @@
import com.android.server.EventLogTags;
import com.android.server.LocalServices;
import com.android.server.job.controllers.JobStatus;
+import com.android.server.notification.NotificationManagerInternal;
import com.android.server.tare.EconomicPolicy;
import com.android.server.tare.EconomyManagerInternal;
import com.android.server.tare.JobSchedulerEconomicPolicy;
@@ -117,6 +121,7 @@
private final EconomyManagerInternal mEconomyManagerInternal;
private final JobPackageTracker mJobPackageTracker;
private final PowerManager mPowerManager;
+ private final NotificationManagerInternal mNotificationManagerInternal;
private PowerManager.WakeLock mWakeLock;
// Execution state.
@@ -169,6 +174,11 @@
/** The absolute maximum amount of time the job can run */
private long mMaxExecutionTimeMillis;
+ private int mNotificationId;
+ private Notification mNotification;
+ private int mNotificationPid;
+ private int mNotificationJobStopPolicy;
+
/**
* The stop reason for a pending cancel. If there's not pending cancel, then the value should be
* {@link JobParameters#STOP_REASON_UNDEFINED}.
@@ -235,6 +245,12 @@
long downloadBytes, long uploadBytes) {
doUpdateTransferredNetworkBytes(this, jobId, item, downloadBytes, uploadBytes);
}
+
+ @Override
+ public void setNotification(int jobId, int notificationId,
+ Notification notification, int jobEndNotificationPolicy) {
+ doSetNotification(this, jobId, notificationId, notification, jobEndNotificationPolicy);
+ }
}
JobServiceContext(JobSchedulerService service, JobConcurrencyManager concurrencyManager,
@@ -244,6 +260,7 @@
mService = service;
mBatteryStats = batteryStats;
mEconomyManagerInternal = LocalServices.getService(EconomyManagerInternal.class);
+ mNotificationManagerInternal = LocalServices.getService(NotificationManagerInternal.class);
mJobPackageTracker = tracker;
mCallbackHandler = new JobServiceHandler(looper);
mJobConcurrencyManager = concurrencyManager;
@@ -593,6 +610,49 @@
}
}
+ private void doSetNotification(JobCallback cb, int jodId, int notificationId,
+ Notification notification, int jobEndNotificationPolicy) {
+ final int callingPid = Binder.getCallingPid();
+ final int callingUid = Binder.getCallingUid();
+ final long ident = Binder.clearCallingIdentity();
+ try {
+ synchronized (mLock) {
+ if (!verifyCallerLocked(cb)) {
+ return;
+ }
+ if (callingUid != mRunningJob.getUid()) {
+ Slog.wtfStack(TAG, "Calling UID isn't the same as running job's UID...");
+ throw new SecurityException("Can't post notification on behalf of another app");
+ }
+ if (notification == null) {
+ throw new NullPointerException("notification");
+ }
+ if (notification.getSmallIcon() == null) {
+ throw new IllegalArgumentException("small icon required");
+ }
+ final String callingPkgName = mRunningJob.getServiceComponent().getPackageName();
+ if (null == mNotificationManagerInternal.getNotificationChannel(
+ callingPkgName, callingUid, notification.getChannelId())) {
+ throw new IllegalArgumentException("invalid notification channel");
+ }
+ if (jobEndNotificationPolicy != JOB_END_NOTIFICATION_POLICY_DETACH
+ && jobEndNotificationPolicy != JOB_END_NOTIFICATION_POLICY_REMOVE) {
+ throw new IllegalArgumentException("invalid job end notification policy");
+ }
+ // TODO(260848384): ensure apps can't cancel the notification for user-initiated job
+ mNotificationManagerInternal.enqueueNotification(
+ callingPkgName, callingPkgName, callingUid, callingPid, /* tag */ null,
+ notificationId, notification, UserHandle.getUserId(callingUid));
+ mNotificationId = notificationId;
+ mNotification = notification;
+ mNotificationPid = callingPid;
+ mNotificationJobStopPolicy = jobEndNotificationPolicy;
+ }
+ } finally {
+ Binder.restoreCallingIdentity(ident);
+ }
+ }
+
private void doUpdateTransferredNetworkBytes(JobCallback jobCallback, int jobId,
@Nullable JobWorkItem item, long downloadBytes, long uploadBytes) {
// TODO(255393346): Make sure apps call this appropriately and monitor for abuse
@@ -1116,6 +1176,13 @@
JobSchedulerEconomicPolicy.ACTION_JOB_TIMEOUT,
String.valueOf(mRunningJob.getJobId()));
}
+ if (mNotification != null
+ && mNotificationJobStopPolicy == JOB_END_NOTIFICATION_POLICY_REMOVE) {
+ final String callingPkgName = completedJob.getServiceComponent().getPackageName();
+ mNotificationManagerInternal.cancelNotification(
+ callingPkgName, callingPkgName, completedJob.getUid(), mNotificationPid,
+ /* tag */ null, mNotificationId, UserHandle.getUserId(completedJob.getUid()));
+ }
if (mWakeLock != null) {
mWakeLock.release();
}
@@ -1133,6 +1200,7 @@
mPendingStopReason = JobParameters.STOP_REASON_UNDEFINED;
mPendingInternalStopReason = 0;
mPendingDebugStopReason = null;
+ mNotification = null;
removeOpTimeOutLocked();
mCompletedListener.onJobCompletedLocked(completedJob, internalStopReason, reschedule);
mJobConcurrencyManager.onJobCompletedLocked(this, completedJob, workType);
@@ -1157,6 +1225,7 @@
private void scheduleOpTimeOutLocked() {
removeOpTimeOutLocked();
+ // TODO(260848384): enforce setNotification timeout for user-initiated jobs
final long timeoutMillis;
switch (mVerb) {
case VERB_EXECUTING:
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/JobStatus.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java
index da20684..af8e727 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
@@ -32,6 +32,7 @@
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.content.ClipData;
import android.content.ComponentName;
@@ -106,6 +107,13 @@
static final int CONSTRAINT_BACKGROUND_NOT_RESTRICTED = 1 << 22; // Implicit constraint
static final int CONSTRAINT_FLEXIBLE = 1 << 21; // Implicit constraint
+ private static final int IMPLICIT_CONSTRAINTS = 0
+ | CONSTRAINT_BACKGROUND_NOT_RESTRICTED
+ | CONSTRAINT_DEVICE_NOT_DOZING
+ | CONSTRAINT_FLEXIBLE
+ | CONSTRAINT_TARE_WEALTH
+ | CONSTRAINT_WITHIN_QUOTA;
+
// The following set of dynamic constraints are for specific use cases (as explained in their
// relative naming and comments). Right now, they apply different constraints, which is fine,
// but if in the future, we have overlapping dynamic constraint sets, removing one constraint
@@ -1613,6 +1621,101 @@
}
}
+ /**
+ * If {@link #isReady()} returns false, this will return a single reason why the job isn't
+ * ready. If {@link #isReady()} returns true, this will return
+ * {@link JobScheduler#PENDING_JOB_REASON_UNDEFINED}.
+ */
+ @JobScheduler.PendingJobReason
+ public int getPendingJobReason() {
+ final int unsatisfiedConstraints = ~satisfiedConstraints
+ & (requiredConstraints | mDynamicConstraints | IMPLICIT_CONSTRAINTS);
+ if ((CONSTRAINT_BACKGROUND_NOT_RESTRICTED & unsatisfiedConstraints) != 0) {
+ // The BACKGROUND_NOT_RESTRICTED constraint could be unsatisfied either because
+ // the app is background restricted, or because we're restricting background work
+ // in battery saver. Assume that background restriction is the reason apps that
+ // jobs are not ready, and battery saver otherwise.
+ // This has the benefit of being consistent for background restricted apps
+ // (they'll always get BACKGROUND_RESTRICTION) as the reason, regardless of
+ // battery saver state.
+ if (mIsUserBgRestricted) {
+ return JobScheduler.PENDING_JOB_REASON_BACKGROUND_RESTRICTION;
+ }
+ return JobScheduler.PENDING_JOB_REASON_DEVICE_STATE;
+ }
+ if ((CONSTRAINT_BATTERY_NOT_LOW & unsatisfiedConstraints) != 0) {
+ if ((CONSTRAINT_BATTERY_NOT_LOW & requiredConstraints) != 0) {
+ // The developer requested this constraint, so it makes sense to return the
+ // explicit constraint reason.
+ return JobScheduler.PENDING_JOB_REASON_CONSTRAINT_BATTERY_NOT_LOW;
+ }
+ // Hard-coding right now since the current dynamic constraint sets don't overlap
+ // TODO: return based on active dynamic constraint sets when they start overlapping
+ return JobScheduler.PENDING_JOB_REASON_APP_STANDBY;
+ }
+ if ((CONSTRAINT_CHARGING & unsatisfiedConstraints) != 0) {
+ if ((CONSTRAINT_CHARGING & requiredConstraints) != 0) {
+ // The developer requested this constraint, so it makes sense to return the
+ // explicit constraint reason.
+ return JobScheduler.PENDING_JOB_REASON_CONSTRAINT_CHARGING;
+ }
+ // Hard-coding right now since the current dynamic constraint sets don't overlap
+ // TODO: return based on active dynamic constraint sets when they start overlapping
+ return JobScheduler.PENDING_JOB_REASON_APP_STANDBY;
+ }
+ if ((CONSTRAINT_CONNECTIVITY & unsatisfiedConstraints) != 0) {
+ return JobScheduler.PENDING_JOB_REASON_CONSTRAINT_CONNECTIVITY;
+ }
+ if ((CONSTRAINT_CONTENT_TRIGGER & unsatisfiedConstraints) != 0) {
+ return JobScheduler.PENDING_JOB_REASON_CONSTRAINT_CONTENT_TRIGGER;
+ }
+ if ((CONSTRAINT_DEVICE_NOT_DOZING & unsatisfiedConstraints) != 0) {
+ return JobScheduler.PENDING_JOB_REASON_DEVICE_STATE;
+ }
+ if ((CONSTRAINT_FLEXIBLE & unsatisfiedConstraints) != 0) {
+ return JobScheduler.PENDING_JOB_REASON_JOB_SCHEDULER_OPTIMIZATION;
+ }
+ if ((CONSTRAINT_IDLE & unsatisfiedConstraints) != 0) {
+ if ((CONSTRAINT_IDLE & requiredConstraints) != 0) {
+ // The developer requested this constraint, so it makes sense to return the
+ // explicit constraint reason.
+ return JobScheduler.PENDING_JOB_REASON_CONSTRAINT_DEVICE_IDLE;
+ }
+ // Hard-coding right now since the current dynamic constraint sets don't overlap
+ // TODO: return based on active dynamic constraint sets when they start overlapping
+ return JobScheduler.PENDING_JOB_REASON_APP_STANDBY;
+ }
+ if ((CONSTRAINT_PREFETCH & unsatisfiedConstraints) != 0) {
+ return JobScheduler.PENDING_JOB_REASON_CONSTRAINT_PREFETCH;
+ }
+ if ((CONSTRAINT_STORAGE_NOT_LOW & unsatisfiedConstraints) != 0) {
+ return JobScheduler.PENDING_JOB_REASON_CONSTRAINT_STORAGE_NOT_LOW;
+ }
+ if ((CONSTRAINT_TARE_WEALTH & unsatisfiedConstraints) != 0) {
+ return JobScheduler.PENDING_JOB_REASON_QUOTA;
+ }
+ if ((CONSTRAINT_TIMING_DELAY & unsatisfiedConstraints) != 0) {
+ return JobScheduler.PENDING_JOB_REASON_CONSTRAINT_MINIMUM_LATENCY;
+ }
+ if ((CONSTRAINT_WITHIN_QUOTA & unsatisfiedConstraints) != 0) {
+ return JobScheduler.PENDING_JOB_REASON_QUOTA;
+ }
+
+ if (getEffectiveStandbyBucket() == NEVER_INDEX) {
+ Slog.wtf(TAG, "App in NEVER bucket querying pending job reason");
+ // The user hasn't officially launched this app.
+ return JobScheduler.PENDING_JOB_REASON_USER;
+ }
+ if (serviceProcessName != null) {
+ return JobScheduler.PENDING_JOB_REASON_APP;
+ }
+
+ if (!isReady()) {
+ Slog.wtf(TAG, "Unknown reason job isn't ready");
+ }
+ return JobScheduler.PENDING_JOB_REASON_UNDEFINED;
+ }
+
/** @return whether or not the @param constraint is satisfied */
public boolean isConstraintSatisfied(int constraint) {
return (satisfiedConstraints&constraint) != 0;
diff --git a/apex/jobscheduler/service/java/com/android/server/job/restrictions/JobRestriction.java b/apex/jobscheduler/service/java/com/android/server/job/restrictions/JobRestriction.java
index 4067541..7aab67a 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/restrictions/JobRestriction.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/restrictions/JobRestriction.java
@@ -18,6 +18,7 @@
import android.app.job.JobInfo;
import android.app.job.JobParameters;
+import android.app.job.JobScheduler;
import android.util.IndentingPrintWriter;
import android.util.proto.ProtoOutputStream;
@@ -28,20 +29,23 @@
* Used by {@link JobSchedulerService} to impose additional restrictions regarding whether jobs
* should be scheduled or not based on the state of the system/device.
* Every restriction is associated with exactly one stop reason, which could be retrieved using
- * {@link #getReason()} (and the internal reason via {@link #getInternalReason()}).
+ * {@link #getStopReason()}, one pending reason (retrievable via {@link #getPendingReason()},
+ * (and the internal reason via {@link #getInternalReason()}).
* Note, that this is not taken into account for the jobs that have
* {@link JobInfo#BIAS_FOREGROUND_SERVICE} bias or higher.
*/
public abstract class JobRestriction {
final JobSchedulerService mService;
- private final int mReason;
+ private final int mStopReason;
+ private final int mPendingReason;
private final int mInternalReason;
- JobRestriction(JobSchedulerService service, @JobParameters.StopReason int reason,
- int internalReason) {
+ protected JobRestriction(JobSchedulerService service, @JobParameters.StopReason int stopReason,
+ @JobScheduler.PendingJobReason int pendingReason, int internalReason) {
mService = service;
- mReason = reason;
+ mPendingReason = pendingReason;
+ mStopReason = stopReason;
mInternalReason = internalReason;
}
@@ -70,10 +74,15 @@
public void dumpConstants(ProtoOutputStream proto) {
}
- /** @return reason code for the Restriction. */
+ @JobScheduler.PendingJobReason
+ public final int getPendingReason() {
+ return mPendingReason;
+ }
+
+ /** @return stop reason code for the Restriction. */
@JobParameters.StopReason
- public final int getReason() {
- return mReason;
+ public final int getStopReason() {
+ return mStopReason;
}
public final int getInternalReason() {
diff --git a/apex/jobscheduler/service/java/com/android/server/job/restrictions/ThermalStatusRestriction.java b/apex/jobscheduler/service/java/com/android/server/job/restrictions/ThermalStatusRestriction.java
index ca2fd60..830031e 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/restrictions/ThermalStatusRestriction.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/restrictions/ThermalStatusRestriction.java
@@ -18,6 +18,7 @@
import android.app.job.JobInfo;
import android.app.job.JobParameters;
+import android.app.job.JobScheduler;
import android.os.PowerManager;
import android.os.PowerManager.OnThermalStatusChangedListener;
import android.util.IndentingPrintWriter;
@@ -42,6 +43,7 @@
public ThermalStatusRestriction(JobSchedulerService service) {
super(service, JobParameters.STOP_REASON_DEVICE_STATE,
+ JobScheduler.PENDING_JOB_REASON_DEVICE_STATE,
JobParameters.INTERNAL_STOP_REASON_DEVICE_THERMAL);
}
diff --git a/core/api/current.txt b/core/api/current.txt
index 0f34e42..434b60d 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -8485,7 +8485,26 @@
method public abstract int enqueue(@NonNull android.app.job.JobInfo, @NonNull android.app.job.JobWorkItem);
method @NonNull public abstract java.util.List<android.app.job.JobInfo> getAllPendingJobs();
method @Nullable public abstract android.app.job.JobInfo getPendingJob(int);
+ method public int getPendingJobReason(int);
method public abstract int schedule(@NonNull android.app.job.JobInfo);
+ field public static final int PENDING_JOB_REASON_APP = 1; // 0x1
+ field public static final int PENDING_JOB_REASON_APP_STANDBY = 2; // 0x2
+ field public static final int PENDING_JOB_REASON_BACKGROUND_RESTRICTION = 3; // 0x3
+ field public static final int PENDING_JOB_REASON_CONSTRAINT_BATTERY_NOT_LOW = 4; // 0x4
+ field public static final int PENDING_JOB_REASON_CONSTRAINT_CHARGING = 5; // 0x5
+ field public static final int PENDING_JOB_REASON_CONSTRAINT_CONNECTIVITY = 6; // 0x6
+ field public static final int PENDING_JOB_REASON_CONSTRAINT_CONTENT_TRIGGER = 7; // 0x7
+ field public static final int PENDING_JOB_REASON_CONSTRAINT_DEVICE_IDLE = 8; // 0x8
+ field public static final int PENDING_JOB_REASON_CONSTRAINT_MINIMUM_LATENCY = 9; // 0x9
+ field public static final int PENDING_JOB_REASON_CONSTRAINT_PREFETCH = 10; // 0xa
+ field public static final int PENDING_JOB_REASON_CONSTRAINT_STORAGE_NOT_LOW = 11; // 0xb
+ field public static final int PENDING_JOB_REASON_DEVICE_STATE = 12; // 0xc
+ field public static final int PENDING_JOB_REASON_EXECUTING = -1; // 0xffffffff
+ field public static final int PENDING_JOB_REASON_INVALID_JOB_ID = -2; // 0xfffffffe
+ field public static final int PENDING_JOB_REASON_JOB_SCHEDULER_OPTIMIZATION = 13; // 0xd
+ field public static final int PENDING_JOB_REASON_QUOTA = 14; // 0xe
+ field public static final int PENDING_JOB_REASON_UNDEFINED = 0; // 0x0
+ field public static final int PENDING_JOB_REASON_USER = 15; // 0xf
field public static final int RESULT_FAILURE = 0; // 0x0
field public static final int RESULT_SUCCESS = 1; // 0x1
}
@@ -10347,6 +10366,7 @@
field @Deprecated @RequiresPermission("android.permission.BROADCAST_CLOSE_SYSTEM_DIALOGS") public static final String ACTION_CLOSE_SYSTEM_DIALOGS = "android.intent.action.CLOSE_SYSTEM_DIALOGS";
field public static final String ACTION_CONFIGURATION_CHANGED = "android.intent.action.CONFIGURATION_CHANGED";
field public static final String ACTION_CREATE_DOCUMENT = "android.intent.action.CREATE_DOCUMENT";
+ field public static final String ACTION_CREATE_NOTE = "android.intent.action.CREATE_NOTE";
field public static final String ACTION_CREATE_REMINDER = "android.intent.action.CREATE_REMINDER";
field public static final String ACTION_CREATE_SHORTCUT = "android.intent.action.CREATE_SHORTCUT";
field public static final String ACTION_DATE_CHANGED = "android.intent.action.DATE_CHANGED";
@@ -10595,6 +10615,7 @@
field public static final String EXTRA_UID = "android.intent.extra.UID";
field public static final String EXTRA_USER = "android.intent.extra.USER";
field public static final String EXTRA_USER_INITIATED = "android.intent.extra.USER_INITIATED";
+ field public static final String EXTRA_USE_STYLUS_MODE = "android.intent.extra.USE_STYLUS_MODE";
field public static final int FILL_IN_ACTION = 1; // 0x1
field public static final int FILL_IN_CATEGORIES = 4; // 0x4
field public static final int FILL_IN_CLIP_DATA = 128; // 0x80
@@ -11669,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();
@@ -20181,6 +20202,15 @@
}
+package android.location.altitude {
+
+ public final class AltitudeConverter {
+ ctor public AltitudeConverter();
+ method @WorkerThread public void addMslAltitude(@NonNull android.content.Context, @NonNull android.location.Location) throws java.io.IOException;
+ }
+
+}
+
package android.location.provider {
public final class ProviderProperties implements android.os.Parcelable {
@@ -38126,6 +38156,7 @@
field public static final int ERROR_PERMISSION_DENIED = 5; // 0x5
field public static final int ERROR_UNIMPLEMENTED = 12; // 0xc
field public static final int ERROR_USER_AUTHENTICATION_REQUIRED = 2; // 0x2
+ field public static final int RETRY_AFTER_NEXT_REBOOT = 4; // 0x4
field public static final int RETRY_NEVER = 1; // 0x1
field public static final int RETRY_WHEN_CONNECTIVITY_AVAILABLE = 3; // 0x3
field public static final int RETRY_WITH_EXPONENTIAL_BACKOFF = 2; // 0x2
diff --git a/core/api/module-lib-current.txt b/core/api/module-lib-current.txt
index b9182bc..3efb0c6 100644
--- a/core/api/module-lib-current.txt
+++ b/core/api/module-lib-current.txt
@@ -419,10 +419,38 @@
}
public final class DeviceConfig {
+ field public static final String NAMESPACE_ACTIVITY_MANAGER_COMPONENT_ALIAS = "activity_manager_ca";
field public static final String NAMESPACE_ALARM_MANAGER = "alarm_manager";
field public static final String NAMESPACE_APP_CLONING = "app_cloning";
field public static final String NAMESPACE_APP_STANDBY = "app_standby";
+ field public static final String NAMESPACE_ARC_APP_COMPAT = "arc_app_compat";
+ field public static final String NAMESPACE_CONFIGURATION = "configuration";
+ field public static final String NAMESPACE_CONNECTIVITY_THERMAL_POWER_MANAGER = "connectivity_thermal_power_manager";
+ field public static final String NAMESPACE_CONTACTS_PROVIDER = "contacts_provider";
field public static final String NAMESPACE_DEVICE_IDLE = "device_idle";
+ field public static final String NAMESPACE_DEVICE_POLICY_MANAGER = "device_policy_manager";
+ field public static final String NAMESPACE_GAME_OVERLAY = "game_overlay";
+ field public static final String NAMESPACE_INTELLIGENCE_CONTENT_SUGGESTIONS = "intelligence_content_suggestions";
+ field public static final String NAMESPACE_INTERACTION_JANK_MONITOR = "interaction_jank_monitor";
+ field public static final String NAMESPACE_LATENCY_TRACKER = "latency_tracker";
+ field public static final String NAMESPACE_MEMORY_SAFETY_NATIVE = "memory_safety_native";
+ field public static final String NAMESPACE_MGLRU_NATIVE = "mglru_native";
+ field public static final String NAMESPACE_REMOTE_KEY_PROVISIONING_NATIVE = "remote_key_provisioning_native";
+ field public static final String NAMESPACE_ROTATION_RESOLVER = "rotation_resolver";
+ field public static final String NAMESPACE_SETTINGS_STATS = "settings_stats";
+ field public static final String NAMESPACE_SETTINGS_UI = "settings_ui";
+ field public static final String NAMESPACE_TARE = "tare";
+ field public static final String NAMESPACE_VENDOR_SYSTEM_NATIVE = "vendor_system_native";
+ field public static final String NAMESPACE_VENDOR_SYSTEM_NATIVE_BOOT = "vendor_system_native_boot";
+ field public static final String NAMESPACE_VIRTUALIZATION_FRAMEWORK_NATIVE = "virtualization_framework_native";
+ field public static final String NAMESPACE_VOICE_INTERACTION = "voice_interaction";
+ field public static final String NAMESPACE_WEAR = "wear";
+ field public static final String NAMESPACE_WIDGET = "widget";
+ field public static final String NAMESPACE_WINDOW_MANAGER = "window_manager";
+ }
+
+ public static class DeviceConfig.Properties {
+ ctor public DeviceConfig.Properties(@NonNull String, @Nullable java.util.Map<java.lang.String,java.lang.String>);
}
public final class Settings {
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index 3d5139b..2e2ee23 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -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);
@@ -10704,7 +10707,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);
@@ -10712,17 +10715,22 @@
method @RequiresPermission(android.Manifest.permission.READ_DEVICE_CONFIG) public static long getLong(@NonNull String, @NonNull String, long);
method @NonNull @RequiresPermission(android.Manifest.permission.READ_DEVICE_CONFIG) public static android.provider.DeviceConfig.Properties getProperties(@NonNull String, @NonNull java.lang.String...);
method @RequiresPermission(android.Manifest.permission.READ_DEVICE_CONFIG) public static String getProperty(@NonNull String, @NonNull String);
+ method @NonNull public static java.util.List<java.lang.String> getPublicNamespaces();
method @RequiresPermission(android.Manifest.permission.READ_DEVICE_CONFIG) public static String getString(@NonNull String, @NonNull String, @Nullable String);
+ method @RequiresPermission(android.Manifest.permission.WRITE_DEVICE_CONFIG) public static int getSyncDisabledMode();
method public static void removeOnPropertiesChangedListener(@NonNull android.provider.DeviceConfig.OnPropertiesChangedListener);
method @RequiresPermission(android.Manifest.permission.WRITE_DEVICE_CONFIG) public static void resetToDefaults(int, @Nullable String);
method @RequiresPermission(android.Manifest.permission.WRITE_DEVICE_CONFIG) public static boolean setProperties(@NonNull android.provider.DeviceConfig.Properties) throws android.provider.DeviceConfig.BadConfigException;
method @RequiresPermission(android.Manifest.permission.WRITE_DEVICE_CONFIG) public static boolean setProperty(@NonNull String, @NonNull String, @Nullable String, boolean);
+ method @RequiresPermission(android.Manifest.permission.WRITE_DEVICE_CONFIG) public static void setSyncDisabledMode(int);
field public static final String NAMESPACE_ACTIVITY_MANAGER = "activity_manager";
field public static final String NAMESPACE_ACTIVITY_MANAGER_NATIVE_BOOT = "activity_manager_native_boot";
field public static final String NAMESPACE_ADSERVICES = "adservices";
field public static final String NAMESPACE_AMBIENT_CONTEXT_MANAGER_SERVICE = "ambient_context_manager_service";
+ field public static final String NAMESPACE_ANDROID = "android";
field public static final String NAMESPACE_APPSEARCH = "appsearch";
field public static final String NAMESPACE_APP_COMPAT = "app_compat";
+ field public static final String NAMESPACE_APP_COMPAT_OVERRIDES = "app_compat_overrides";
field public static final String NAMESPACE_APP_HIBERNATION = "app_hibernation";
field public static final String NAMESPACE_ATTENTION_MANAGER_SERVICE = "attention_manager_service";
field public static final String NAMESPACE_AUTOFILL = "autofill";
@@ -10734,13 +10742,16 @@
field public static final String NAMESPACE_CAPTIVEPORTALLOGIN = "captive_portal_login";
field public static final String NAMESPACE_CLIPBOARD = "clipboard";
field public static final String NAMESPACE_CONNECTIVITY = "connectivity";
+ field public static final String NAMESPACE_CONSTRAIN_DISPLAY_APIS = "constrain_display_apis";
field public static final String NAMESPACE_CONTENT_CAPTURE = "content_capture";
field @Deprecated public static final String NAMESPACE_DEX_BOOT = "dex_boot";
field public static final String NAMESPACE_DISPLAY_MANAGER = "display_manager";
field public static final String NAMESPACE_GAME_DRIVER = "game_driver";
field public static final String NAMESPACE_HDMI_CONTROL = "hdmi_control";
+ field public static final String NAMESPACE_INPUT_METHOD_MANAGER = "input_method_manager";
field public static final String NAMESPACE_INPUT_NATIVE_BOOT = "input_native_boot";
field public static final String NAMESPACE_INTELLIGENCE_ATTENTION = "intelligence_attention";
+ field public static final String NAMESPACE_JOB_SCHEDULER = "jobscheduler";
field public static final String NAMESPACE_LMKD_NATIVE = "lmkd_native";
field public static final String NAMESPACE_LOCATION = "location";
field public static final String NAMESPACE_MEDIA = "media";
@@ -10762,6 +10773,7 @@
field public static final String NAMESPACE_RUNTIME_NATIVE_BOOT = "runtime_native_boot";
field public static final String NAMESPACE_SCHEDULER = "scheduler";
field public static final String NAMESPACE_SDK_SANDBOX = "sdk_sandbox";
+ field public static final String NAMESPACE_SELECTION_TOOLBAR = "selection_toolbar";
field public static final String NAMESPACE_STATSD_JAVA = "statsd_java";
field public static final String NAMESPACE_STATSD_JAVA_BOOT = "statsd_java_boot";
field public static final String NAMESPACE_STATSD_NATIVE = "statsd_native";
diff --git a/core/api/test-current.txt b/core/api/test-current.txt
index 25c4652..43d4530 100644
--- a/core/api/test-current.txt
+++ b/core/api/test-current.txt
@@ -1511,13 +1511,33 @@
method public static boolean isEncodingLinearPcm(int);
}
+ public final class AudioHalVersionInfo implements java.lang.Comparable<android.media.AudioHalVersionInfo> android.os.Parcelable {
+ method public int compareTo(@NonNull android.media.AudioHalVersionInfo);
+ method public int describeContents();
+ method public int getHalType();
+ method public int getMajorVersion();
+ method public int getMinorVersion();
+ method public void writeToParcel(@NonNull android.os.Parcel, int);
+ field @NonNull public static final android.media.AudioHalVersionInfo AIDL_1_0;
+ field public static final int AUDIO_HAL_TYPE_AIDL = 1; // 0x1
+ field public static final int AUDIO_HAL_TYPE_HIDL = 0; // 0x0
+ field @NonNull public static final android.os.Parcelable.Creator<android.media.AudioHalVersionInfo> CREATOR;
+ field @NonNull public static final android.media.AudioHalVersionInfo HIDL_2_0;
+ field @NonNull public static final android.media.AudioHalVersionInfo HIDL_4_0;
+ field @NonNull public static final android.media.AudioHalVersionInfo HIDL_5_0;
+ field @NonNull public static final android.media.AudioHalVersionInfo HIDL_6_0;
+ field @NonNull public static final android.media.AudioHalVersionInfo HIDL_7_0;
+ field @NonNull public static final android.media.AudioHalVersionInfo HIDL_7_1;
+ field @NonNull public static final java.util.List<android.media.AudioHalVersionInfo> VERSIONS;
+ }
+
public class AudioManager {
method @RequiresPermission("android.permission.QUERY_AUDIO_STATE") public int abandonAudioFocusForTest(@NonNull android.media.AudioFocusRequest, @NonNull String);
method @NonNull @RequiresPermission(android.Manifest.permission.CALL_AUDIO_INTERCEPTION) public android.media.AudioRecord getCallDownlinkExtractionAudioRecord(@NonNull android.media.AudioFormat);
method @NonNull @RequiresPermission(android.Manifest.permission.CALL_AUDIO_INTERCEPTION) public android.media.AudioTrack getCallUplinkInjectionAudioTrack(@NonNull android.media.AudioFormat);
method @Nullable public static android.media.AudioDeviceInfo getDeviceInfoFromType(int);
method @IntRange(from=0) @RequiresPermission("android.permission.QUERY_AUDIO_STATE") public long getFadeOutDurationOnFocusLossMillis(@NonNull android.media.AudioAttributes);
- method @Nullable public static String getHalVersion();
+ method @Nullable public static android.media.AudioHalVersionInfo getHalVersion();
method public static final int[] getPublicStreamTypes();
method @NonNull public java.util.List<java.lang.Integer> getReportedSurroundFormats();
method public int getStreamMinVolumeInt(int);
@@ -1719,7 +1739,6 @@
public class Build {
method public static boolean is64BitAbi(String);
method public static boolean isDebuggable();
- method public static boolean isSecure();
field public static final boolean IS_EMULATOR;
}
@@ -2191,17 +2210,6 @@
field public static final android.net.Uri CORP_CONTENT_URI;
}
- public final class DeviceConfig {
- field public static final String NAMESPACE_ALARM_MANAGER = "alarm_manager";
- field public static final String NAMESPACE_ANDROID = "android";
- field public static final String NAMESPACE_APP_COMPAT_OVERRIDES = "app_compat_overrides";
- field public static final String NAMESPACE_CONSTRAIN_DISPLAY_APIS = "constrain_display_apis";
- field public static final String NAMESPACE_DEVICE_IDLE = "device_idle";
- field public static final String NAMESPACE_INPUT_METHOD_MANAGER = "input_method_manager";
- field public static final String NAMESPACE_JOB_SCHEDULER = "jobscheduler";
- field public static final String NAMESPACE_SELECTION_TOOLBAR = "selection_toolbar";
- }
-
public interface InputMethodManagerDeviceConfig {
field public static final String KEY_HIDE_IME_WHEN_NO_EDITOR_FOCUS = "hide_ime_when_no_editor_focus";
}
diff --git a/core/java/Android.bp b/core/java/Android.bp
index a4a12d7..128e3de 100644
--- a/core/java/Android.bp
+++ b/core/java/Android.bp
@@ -424,6 +424,7 @@
"android/os/IInterface.java",
"android/os/Binder.java",
"android/os/IBinder.java",
+ "android/os/Parcelable.java",
],
}
diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java
index 2dccd4d..31cbe28 100644
--- a/core/java/android/app/ActivityThread.java
+++ b/core/java/android/app/ActivityThread.java
@@ -6717,6 +6717,13 @@
ii = null;
}
+ final IActivityManager mgr = ActivityManager.getService();
+ try {
+ mgr.finishAttachApplication(mStartSeq);
+ } catch (RemoteException ex) {
+ throw ex.rethrowFromSystemServer();
+ }
+
final ContextImpl appContext = ContextImpl.createAppContext(this, data.info);
mConfigurationController.updateLocaleListFromAppContext(appContext);
@@ -6785,13 +6792,6 @@
final StrictMode.ThreadPolicy savedPolicy = StrictMode.allowThreadDiskWrites();
final StrictMode.ThreadPolicy writesAllowedPolicy = StrictMode.getThreadPolicy();
- final IActivityManager mgr = ActivityManager.getService();
- try {
- mgr.finishAttachApplication(mStartSeq);
- } catch (RemoteException ex) {
- throw ex.rethrowFromSystemServer();
- }
-
// Wait for debugger after we have notified the system to finish attach application
if (data.debugMode != ApplicationThreadConstants.DEBUG_OFF) {
// XXX should have option to change the port.
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/content/Intent.java b/core/java/android/content/Intent.java
index 34c4ef3..86a672f 100644
--- a/core/java/android/content/Intent.java
+++ b/core/java/android/content/Intent.java
@@ -5107,6 +5107,21 @@
public static final String ACTION_VIEW_LOCUS = "android.intent.action.VIEW_LOCUS";
/**
+ * Activity Action: Starts a note-taking activity that can be used to create a note. This action
+ * can be used to start an activity on the lock screen. Activity should ensure to appropriately
+ * handle privacy sensitive data and features when launched on the lock screen. See
+ * {@link android.app.KeyguardManager} for lock screen checks.
+ */
+ @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
+ public static final String ACTION_CREATE_NOTE = "android.intent.action.CREATE_NOTE";
+
+ /**
+ * A boolean extra used with {@link #ACTION_CREATE_NOTE} indicating whether the launched
+ * note-taking activity should show a UI that is suitable to use with stylus input.
+ */
+ public static final String EXTRA_USE_STYLUS_MODE = "android.intent.extra.USE_STYLUS_MODE";
+
+ /**
* Broadcast Action: Sent to the integrity component when a package
* needs to be verified. The data contains the package URI along with other relevant
* information.
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/os/Binder.java b/core/java/android/os/Binder.java
index 4c9ae37..def0cbd 100644
--- a/core/java/android/os/Binder.java
+++ b/core/java/android/os/Binder.java
@@ -366,7 +366,7 @@
*/
public static final int getCallingUidOrWtf() {
if (!isDirectlyHandlingTransaction() && !hasExplicitIdentity()) {
- Log.wtf(TAG,
+ Log.wtfStack(TAG,
"Thread is not in a binder transaction, "
+ "and the calling identity has not been "
+ "explicitly set with clearCallingIdentity");
diff --git a/core/java/android/os/Build.java b/core/java/android/os/Build.java
index 44a1fa5..249f486 100755
--- a/core/java/android/os/Build.java
+++ b/core/java/android/os/Build.java
@@ -1446,28 +1446,6 @@
return IS_DEBUGGABLE;
}
-
- /**
- * Returns true if the device is running a secure build, such as "user" or "userdebug".
- *
- * Secure builds drop adbd privileges by default, though debuggable builds still allow users
- * to gain root access via local shell. See should_drop_privileges() in adb for details.
- * @hide
- */
- private static final boolean IS_SECURE =
- SystemProperties.getBoolean("ro.secure", true);
- /**
- * Returns true if the device is running a secure build, such as "user" or "userdebug".
- *
- * Secure builds drop adbd privileges by default, though debuggable builds still allow users
- * to gain root access via local shell. See should_drop_privileges() in adb for details.
- * @hide
- */
- @TestApi
- public static boolean isSecure() {
- return IS_SECURE;
- }
-
/** {@hide} */
public static final boolean IS_ENG = "eng".equals(TYPE);
/** {@hide} */
diff --git a/core/java/android/provider/DeviceConfig.java b/core/java/android/provider/DeviceConfig.java
index 27f4d08..7df9290 100644
--- a/core/java/android/provider/DeviceConfig.java
+++ b/core/java/android/provider/DeviceConfig.java
@@ -23,9 +23,8 @@
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.RequiresPermission;
+import android.annotation.SuppressLint;
import android.annotation.SystemApi;
-import android.annotation.TestApi;
-import android.content.pm.PackageManager;
import android.database.ContentObserver;
import android.net.Uri;
import android.provider.Settings.Config.SyncDisabledMode;
@@ -68,6 +67,7 @@
* different namespace in order to avoid phonetype from resetting it.
* @hide
*/
+ @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
public static final String NAMESPACE_ACTIVITY_MANAGER_COMPONENT_ALIAS = "activity_manager_ca";
/**
@@ -86,7 +86,6 @@
* @hide
*/
@SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
- @TestApi
public static final String NAMESPACE_ALARM_MANAGER = "alarm_manager";
/**
@@ -224,7 +223,6 @@
* @hide
*/
@SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
- @TestApi
public static final String NAMESPACE_DEVICE_IDLE = "device_idle";
/**
@@ -284,6 +282,7 @@
*
* @hide
*/
+ @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
public static final String NAMESPACE_INTELLIGENCE_CONTENT_SUGGESTIONS =
"intelligence_content_suggestions";
@@ -291,7 +290,7 @@
* Namespace for JobScheduler configurations.
* @hide
*/
- @TestApi
+ @SystemApi
public static final String NAMESPACE_JOB_SCHEDULER = "jobscheduler";
/**
@@ -331,6 +330,7 @@
*
* @hide
*/
+ @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
public static final String NAMESPACE_MGLRU_NATIVE = "mglru_native";
/**
@@ -386,6 +386,7 @@
*
* @hide
*/
+ @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
public static final String NAMESPACE_REMOTE_KEY_PROVISIONING_NATIVE =
"remote_key_provisioning_native";
@@ -410,6 +411,7 @@
*
* @hide
*/
+ @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
public static final String NAMESPACE_ROTATION_RESOLVER = "rotation_resolver";
/**
@@ -461,6 +463,7 @@
*
* @hide
*/
+ @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
public static final String NAMESPACE_SETTINGS_STATS = "settings_stats";
/**
@@ -560,6 +563,7 @@
*
* @hide
*/
+ @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
public static final String NAMESPACE_TARE = "tare";
/**
@@ -584,6 +588,7 @@
*
* @hide
*/
+ @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
public static final String NAMESPACE_CONTACTS_PROVIDER = "contacts_provider";
/**
@@ -591,6 +596,7 @@
*
* @hide
*/
+ @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
public static final String NAMESPACE_SETTINGS_UI = "settings_ui";
/**
@@ -601,7 +607,7 @@
*
* @hide
*/
- @TestApi
+ @SystemApi
public static final String NAMESPACE_ANDROID = "android";
/**
@@ -609,6 +615,7 @@
*
* @hide
*/
+ @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
public static final String NAMESPACE_WINDOW_MANAGER = "window_manager";
/**
@@ -625,7 +632,7 @@
*
* @hide
*/
- @TestApi
+ @SystemApi
public static final String NAMESPACE_SELECTION_TOOLBAR = "selection_toolbar";
/**
@@ -633,6 +640,7 @@
*
* @hide
*/
+ @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
public static final String NAMESPACE_VOICE_INTERACTION = "voice_interaction";
/**
@@ -640,6 +648,7 @@
*
* @hide
*/
+ @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
public static final String NAMESPACE_DEVICE_POLICY_MANAGER =
"device_policy_manager";
@@ -690,6 +699,7 @@
*
* @hide
*/
+ @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
public static final String NAMESPACE_WIDGET = "widget";
/**
@@ -697,6 +707,7 @@
*
* @hide
*/
+ @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
public static final String NAMESPACE_CONNECTIVITY_THERMAL_POWER_MANAGER =
"connectivity_thermal_power_manager";
@@ -705,6 +716,7 @@
*
* @hide
*/
+ @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
public static final String NAMESPACE_CONFIGURATION = "configuration";
/**
@@ -712,6 +724,7 @@
*
* @hide
*/
+ @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
public static final String NAMESPACE_LATENCY_TRACKER = "latency_tracker";
/**
@@ -719,6 +732,8 @@
*
* @hide
*/
+ @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
+ @SuppressLint("IntentName")
public static final String NAMESPACE_INTERACTION_JANK_MONITOR = "interaction_jank_monitor";
/**
@@ -726,6 +741,7 @@
*
* @hide
*/
+ @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
public static final String NAMESPACE_GAME_OVERLAY = "game_overlay";
/**
@@ -733,6 +749,7 @@
*
* @hide
*/
+ @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
public static final String NAMESPACE_VIRTUALIZATION_FRAMEWORK_NATIVE =
"virtualization_framework_native";
@@ -741,7 +758,7 @@
*
* @hide
*/
- @TestApi
+ @SystemApi
public static final String NAMESPACE_CONSTRAIN_DISPLAY_APIS = "constrain_display_apis";
/**
@@ -749,7 +766,7 @@
*
* @hide
*/
- @TestApi
+ @SystemApi
public static final String NAMESPACE_APP_COMPAT_OVERRIDES = "app_compat_overrides";
/**
@@ -783,6 +800,7 @@
*
* @hide
*/
+ @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
public static final String NAMESPACE_VENDOR_SYSTEM_NATIVE = "vendor_system_native";
/**
@@ -790,6 +808,7 @@
*
* @hide
*/
+ @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
public static final String NAMESPACE_VENDOR_SYSTEM_NATIVE_BOOT = "vendor_system_native_boot";
/**
@@ -797,6 +816,7 @@
*
* @hide
*/
+ @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
public static final String NAMESPACE_MEMORY_SAFETY_NATIVE = "memory_safety_native";
/**
@@ -804,6 +824,7 @@
*
* @hide
*/
+ @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
public static final String NAMESPACE_WEAR = "wear";
/**
@@ -819,7 +840,7 @@
*
* @hide
*/
- @TestApi
+ @SystemApi
public static final String NAMESPACE_INPUT_METHOD_MANAGER = "input_method_manager";
/**
@@ -835,6 +856,7 @@
*
* @hide
*/
+ @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
public static final String NAMESPACE_ARC_APP_COMPAT = "arc_app_compat";
private static final Object sLock = new Object();
@@ -1112,6 +1134,7 @@
* @see #getSyncDisabledMode()
* @hide
*/
+ @SystemApi
@RequiresPermission(WRITE_DEVICE_CONFIG)
public static void setSyncDisabledMode(@SyncDisabledMode int syncDisabledMode) {
Settings.Config.setSyncDisabledMode(syncDisabledMode);
@@ -1123,6 +1146,7 @@
* @see #setSyncDisabledMode(int)
* @hide
*/
+ @SystemApi
@RequiresPermission(WRITE_DEVICE_CONFIG)
public static @SyncDisabledMode int getSyncDisabledMode() {
return Settings.Config.getSyncDisabledMode();
@@ -1143,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) {
@@ -1271,23 +1293,10 @@
}
/**
- * Enforces READ_DEVICE_CONFIG permission if namespace is not one of public namespaces.
- * @hide
- */
- public static void enforceReadPermission(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
*/
+ @SystemApi
public static @NonNull List<String> getPublicNamespaces() {
return PUBLIC_NAMESPACES;
}
@@ -1344,6 +1353,7 @@
* @param keyValueMap A map between property names and property values.
* @hide
*/
+ @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
public Properties(@NonNull String namespace, @Nullable Map<String, String> keyValueMap) {
Preconditions.checkNotNull(namespace);
mNamespace = namespace;
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index f3e8b7c..03846db 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;
@@ -18370,6 +18370,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/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/SurfaceControl.java b/core/java/android/view/SurfaceControl.java
index 30e7a7a..89a0c05 100644
--- a/core/java/android/view/SurfaceControl.java
+++ b/core/java/android/view/SurfaceControl.java
@@ -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);
}
}
diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java
index 09a9d46..bfb489e 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -77,6 +77,7 @@
import static android.view.WindowManager.LayoutParams.SOFT_INPUT_MASK_ADJUST;
import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_STARTING;
import static android.view.WindowManager.LayoutParams.TYPE_INPUT_METHOD;
+import static android.view.WindowManager.LayoutParams.TYPE_NOTIFICATION_SHADE;
import static android.view.WindowManager.LayoutParams.TYPE_STATUS_BAR_ADDITIONAL;
import static android.view.WindowManager.LayoutParams.TYPE_SYSTEM_ALERT;
import static android.view.WindowManager.LayoutParams.TYPE_TOAST;
@@ -505,6 +506,13 @@
Region mTouchableRegion;
Region mPreviousTouchableRegion;
+ private int mMeasuredWidth;
+ private int mMeasuredHeight;
+
+ // This indicates that we've already known the window size but without measuring the views.
+ // If this is true, we must measure the views before laying out them.
+ private boolean mViewMeasureDeferred;
+
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
int mWidth;
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
@@ -2593,7 +2601,8 @@
}
private boolean measureHierarchy(final View host, final WindowManager.LayoutParams lp,
- final Resources res, final int desiredWindowWidth, final int desiredWindowHeight) {
+ final Resources res, final int desiredWindowWidth, final int desiredWindowHeight,
+ boolean forRootSizeOnly) {
int childWidthMeasureSpec;
int childHeightMeasureSpec;
boolean windowSizeMayChange = false;
@@ -2649,7 +2658,15 @@
lp.privateFlags);
childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height,
lp.privateFlags);
- performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
+ if (!forRootSizeOnly || !setMeasuredRootSizeFromSpec(
+ childWidthMeasureSpec, childHeightMeasureSpec)) {
+ performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
+ } else {
+ // We already know how big the window should be before measuring the views.
+ // We can measure the views before laying out them. This is to avoid unnecessary
+ // measure.
+ mViewMeasureDeferred = true;
+ }
if (mWidth != host.getMeasuredWidth() || mHeight != host.getMeasuredHeight()) {
windowSizeMayChange = true;
}
@@ -2665,6 +2682,25 @@
}
/**
+ * Sets the measured root size for requesting the window frame.
+ *
+ * @param widthMeasureSpec contains the size and the mode of the width.
+ * @param heightMeasureSpec contains the size and the mode of the height.
+ * @return {@code true} if we actually set the measured size; {@code false} otherwise.
+ */
+ private boolean setMeasuredRootSizeFromSpec(int widthMeasureSpec, int heightMeasureSpec) {
+ final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
+ final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
+ if (widthMode != MeasureSpec.EXACTLY || heightMode != MeasureSpec.EXACTLY) {
+ // We don't know the exact size. We need to measure the hierarchy to know that.
+ return false;
+ }
+ mMeasuredWidth = MeasureSpec.getSize(widthMeasureSpec);
+ mMeasuredHeight = MeasureSpec.getSize(heightMeasureSpec);
+ return true;
+ }
+
+ /**
* Modifies the input matrix such that it maps view-local coordinates to
* on-screen coordinates.
*
@@ -2751,6 +2787,14 @@
|| lp.type == TYPE_VOLUME_OVERLAY;
}
+ /**
+ * @return {@code true} if we should reduce unnecessary measure for the window.
+ * TODO(b/260382739): Apply this to all windows.
+ */
+ private static boolean shouldOptimizeMeasure(final WindowManager.LayoutParams lp) {
+ return lp.type == TYPE_NOTIFICATION_SHADE;
+ }
+
private Rect getWindowBoundsInsetSystemBars() {
final Rect bounds = new Rect(
mContext.getResources().getConfiguration().windowConfiguration.getBounds());
@@ -2801,6 +2845,7 @@
mAppVisibilityChanged = false;
final boolean viewUserVisibilityChanged = !mFirst &&
((mViewVisibility == View.VISIBLE) != (viewVisibility == View.VISIBLE));
+ final boolean shouldOptimizeMeasure = shouldOptimizeMeasure(lp);
WindowManager.LayoutParams params = null;
CompatibilityInfo compatibilityInfo =
@@ -2922,7 +2967,7 @@
// Ask host how big it wants to be
windowSizeMayChange |= measureHierarchy(host, lp, mView.getContext().getResources(),
- desiredWindowWidth, desiredWindowHeight);
+ desiredWindowWidth, desiredWindowHeight, shouldOptimizeMeasure);
}
if (collectViewAttributes()) {
@@ -2962,8 +3007,8 @@
// we don't need to go through two layout passes when things
// change due to fitting system windows, which can happen a lot.
windowSizeMayChange |= measureHierarchy(host, lp,
- mView.getContext().getResources(),
- desiredWindowWidth, desiredWindowHeight);
+ mView.getContext().getResources(), desiredWindowWidth, desiredWindowHeight,
+ shouldOptimizeMeasure);
}
}
@@ -3383,6 +3428,13 @@
maybeHandleWindowMove(frame);
}
+ if (mViewMeasureDeferred) {
+ // It's time to measure the views since we are going to layout them.
+ performMeasure(
+ MeasureSpec.makeMeasureSpec(frame.width(), MeasureSpec.EXACTLY),
+ MeasureSpec.makeMeasureSpec(frame.height(), MeasureSpec.EXACTLY));
+ }
+
if (!mRelayoutRequested && mCheckIfCanDraw) {
// We had a sync previously, but we didn't call IWindowSession#relayout in this
// traversal. So we don't know if the sync is complete that we can continue to draw.
@@ -3973,6 +4025,9 @@
} finally {
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
+ mMeasuredWidth = mView.getMeasuredWidth();
+ mMeasuredHeight = mView.getMeasuredHeight();
+ mViewMeasureDeferred = false;
}
/**
@@ -4068,7 +4123,7 @@
view.requestLayout();
}
measureHierarchy(host, lp, mView.getContext().getResources(),
- desiredWindowWidth, desiredWindowHeight);
+ desiredWindowWidth, desiredWindowHeight, false /* forRootSizeOnly */);
mInLayout = true;
host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
@@ -8167,8 +8222,8 @@
final WindowConfiguration winConfigFromWm =
mLastReportedMergedConfiguration.getGlobalConfiguration().windowConfiguration;
final WindowConfiguration winConfig = getCompatWindowConfiguration();
- final int measuredWidth = mView.getMeasuredWidth();
- final int measuredHeight = mView.getMeasuredHeight();
+ final int measuredWidth = mMeasuredWidth;
+ final int measuredHeight = mMeasuredHeight;
final boolean relayoutAsync;
if (LOCAL_LAYOUT
&& (mViewFrameInfo.flags & FrameInfo.FLAG_WINDOW_VISIBILITY_CHANGED) == 0
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/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 ebfc4f7..db288c0 100644
--- a/core/java/com/android/internal/statusbar/IStatusBar.aidl
+++ b/core/java/com/android/internal/statusbar/IStatusBar.aidl
@@ -327,4 +327,14 @@
/** Shows rear display educational dialog */
void showRearDisplayDialog(int currentBaseState);
+
+ /** 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_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..1ed3555 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;
@@ -1184,6 +1185,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) {
@@ -2174,6 +2176,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/Android.bp b/core/res/Android.bp
index 7e17840..3c4bac8 100644
--- a/core/res/Android.bp
+++ b/core/res/Android.bp
@@ -107,7 +107,9 @@
sdk_version: "core_platform",
certificate: "platform",
- srcs: [":remote-color-resources-arsc"],
+ srcs: [
+ ":remote-color-resources-arsc",
+ ],
// Disable dexpreopt and verify_uses_libraries check as the app
// contains no Java code to be dexpreopted.
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index 8da91ee..fb451dd 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -3327,7 +3327,9 @@
<p>Protection level: normal
-->
<permission android:name="android.permission.REQUEST_COMPANION_START_FOREGROUND_SERVICES_FROM_BACKGROUND"
- android:protectionLevel="normal"/>
+ android:label="@string/permlab_startForegroundServicesFromBackground"
+ android:description="@string/permdesc_startForegroundServicesFromBackground"
+ android:protectionLevel="normal"/>
<!-- Allows a companion app to use data in the background.
<p>Protection level: normal
@@ -3343,6 +3345,8 @@
<p>Protection level: normal
-->
<permission android:name="android.permission.REQUEST_COMPANION_PROFILE_WATCH"
+ android:label="@string/permlab_companionProfileWatch"
+ android:description="@string/permdesc_companionProfileWatch"
android:protectionLevel="normal" />
<!-- Allows application to request to be associated with a virtual display capable of streaming
@@ -4914,11 +4918,15 @@
of their associated companion device
-->
<permission android:name="android.permission.REQUEST_OBSERVE_COMPANION_DEVICE_PRESENCE"
+ android:label="@string/permlab_observeCompanionDevicePresence"
+ android:description="@string/permdesc_observeCompanionDevicePresence"
android:protectionLevel="normal" />
<!-- Allows an application to deliver companion messages to system
-->
<permission android:name="android.permission.DELIVER_COMPANION_MESSAGES"
+ android:label="@string/permlab_deliverCompanionMessages"
+ android:description="@string/permdesc_deliverCompanionMessages"
android:protectionLevel="normal" />
<!-- Allows an application to create new companion device associations.
diff --git a/core/res/assets/geoid_height_map/README.md b/core/res/assets/geoid_height_map/README.md
new file mode 100644
index 0000000..849d32e
--- /dev/null
+++ b/core/res/assets/geoid_height_map/README.md
@@ -0,0 +1,2 @@
+These binary protos are generated at runtime from the text protos in ../../geoid_height_map_assets
+and using aprotoc.
\ No newline at end of file
diff --git a/core/res/assets/geoid_height_map/map-params.pb b/core/res/assets/geoid_height_map/map-params.pb
new file mode 100644
index 0000000..6fd4022
--- /dev/null
+++ b/core/res/assets/geoid_height_map/map-params.pb
Binary files differ
diff --git a/core/res/assets/geoid_height_map/tile-1.pb b/core/res/assets/geoid_height_map/tile-1.pb
new file mode 100644
index 0000000..546dd0d
--- /dev/null
+++ b/core/res/assets/geoid_height_map/tile-1.pb
Binary files differ
diff --git a/core/res/assets/geoid_height_map/tile-3.pb b/core/res/assets/geoid_height_map/tile-3.pb
new file mode 100644
index 0000000..eb3fe46
--- /dev/null
+++ b/core/res/assets/geoid_height_map/tile-3.pb
Binary files differ
diff --git a/core/res/assets/geoid_height_map/tile-5.pb b/core/res/assets/geoid_height_map/tile-5.pb
new file mode 100644
index 0000000..0243d6d0
--- /dev/null
+++ b/core/res/assets/geoid_height_map/tile-5.pb
Binary files differ
diff --git a/core/res/assets/geoid_height_map/tile-7.pb b/core/res/assets/geoid_height_map/tile-7.pb
new file mode 100644
index 0000000..3c2f777
--- /dev/null
+++ b/core/res/assets/geoid_height_map/tile-7.pb
Binary files differ
diff --git a/core/res/assets/geoid_height_map/tile-9.pb b/core/res/assets/geoid_height_map/tile-9.pb
new file mode 100644
index 0000000..5e9a480
--- /dev/null
+++ b/core/res/assets/geoid_height_map/tile-9.pb
Binary files differ
diff --git a/core/res/assets/geoid_height_map/tile-b.pb b/core/res/assets/geoid_height_map/tile-b.pb
new file mode 100644
index 0000000..c57e873
--- /dev/null
+++ b/core/res/assets/geoid_height_map/tile-b.pb
Binary files differ
diff --git a/core/res/geoid_height_map_assets/README.md b/core/res/geoid_height_map_assets/README.md
new file mode 100644
index 0000000..800b3e5
--- /dev/null
+++ b/core/res/geoid_height_map_assets/README.md
@@ -0,0 +1,8 @@
+These text protos contain composite JPEG/PNG images representing the EGM2008 Earth Gravitational
+Model[^1] published by the National Geospatial-Intelligence Agency.[^2]
+
+[^1]: Pavlis, Nikolaos K., et al. "The development and evaluation of the Earth Gravitational Model
+2008 (EGM2008)." Journal of geophysical research: solid earth 117.B4 (2012).
+
+[^2]: National Geospatial-Intelligence Agency. “Office of Geomatics.” 2022.
+URL: https://earth-info.nga.mil.
\ No newline at end of file
diff --git a/core/res/geoid_height_map_assets/map-params.textpb b/core/res/geoid_height_map_assets/map-params.textpb
new file mode 100644
index 0000000..3f504d4
--- /dev/null
+++ b/core/res/geoid_height_map_assets/map-params.textpb
@@ -0,0 +1,6 @@
+map_s2_level: 9
+cache_tile_s2_level: 5
+disk_tile_s2_level: 0
+model_a_meters: 255.0
+model_b_meters: -128.0
+model_rmse_meters: 0.36
diff --git a/core/res/geoid_height_map_assets/tile-1.textpb b/core/res/geoid_height_map_assets/tile-1.textpb
new file mode 100644
index 0000000..7fac234
--- /dev/null
+++ b/core/res/geoid_height_map_assets/tile-1.textpb
@@ -0,0 +1,3 @@
+tile_key: "1"
+byte_jpeg: "\377\330\377\340\000\020JFIF\000\001\002\000\000\001\000\001\000\000\377\333\000C\000\004\003\003\004\003\003\004\004\003\004\005\004\004\005\006\n\007\006\006\006\006\r\t\n\010\n\017\r\020\020\017\r\017\016\021\023\030\024\021\022\027\022\016\017\025\034\025\027\031\031\033\033\033\020\024\035\037\035\032\037\030\032\033\032\377\300\000\013\010\002\000\002\000\001\001\021\000\377\304\000\037\000\000\001\005\001\001\001\001\001\001\000\000\000\000\000\000\000\000\001\002\003\004\005\006\007\010\t\n\013\377\304\000\265\020\000\002\001\003\003\002\004\003\005\005\004\004\000\000\001}\001\002\003\000\004\021\005\022!1A\006\023Qa\007\"q\0242\201\221\241\010#B\261\301\025R\321\360$3br\202\t\n\026\027\030\031\032%&\'()*456789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz\203\204\205\206\207\210\211\212\222\223\224\225\226\227\230\231\232\242\243\244\245\246\247\250\251\252\262\263\264\265\266\267\270\271\272\302\303\304\305\306\307\310\311\312\322\323\324\325\326\327\330\331\332\341\342\343\344\345\346\347\350\351\352\361\362\363\364\365\366\367\370\371\372\377\332\000\010\001\001\000\000?\000\244i(\242\224R\216\264\361OZ\221je\251V\246Z\225jE\251V\245Z\225jE\247\216\324\361N\317\024f\214\322\023I\2323I\232L\322f\220\232LdqM\316:\321E\024\354\344sJ)\364\340i\340S\200\315(\030\243\255:\224\032p4\264\340i\300\324\213\315:\232EFz\320\0174\244\323I\246\223M-F\3527R\026\244\rR+S\353\314\r%\024QN\247\212z\324\253R\255J\2652\324\253R\255H*U\251\024\324\213O\006\244\024\264QM4RRRt\244$\342\220{\322a\201\312\363M,I\344P\017\255;\214\360h\245\006\234\r<5(4\365cO\006\226\227\024\264\nPiisNSS)\245&\232M0\232J3Fj65\031j7Rn\244\335F\352\225\033\212\224\036+\314\215!8\242\212)\303\245<S\326\245Z\225jU\251\226\245Z\221jU5\"\324\202\244\006\236)\342\235\2323HNh\2434\204\322SI\244\3154\322\t\n\364\241e\301\346\234]X\360(\371{Ss\315(4\340i\340\323\305<S\251A\365\247\216zQE.i3J\017\255(8\251U\251\305\251\204\323wRg4f\226\243sQ\023M\335I\272\223u\033\252x\232\236\322W\232\346\201\315)\351H\r-8S\305<\n\221jU\251V\246Z\225MJ\265\"\232\221jQO\024\360i\340\323\263I\232;\321\232Bi3IHO\024\302i3M4\334R\216)E-:\214\323\301\251\024\323\301\247\203K\326\201\221\305(4\273\251C\003KE&j@iwSKSKR\006\247f\226\230\325\003\034Te\251\245\250\335K\272\244G\300\241\236\274\374\212J\t\245\024\264\341O\002\244\025 \251TT\252*Q\305H\246\245SR\255<T\200\342\236\r<\032x4\271\2434f\214\322g\336\214\322n\3074\326bi\224\322i3I\232PiE:\235J\0058S\205<\032x4\340psN\316\356\264\224\240f\214R\200{sN\036\364\204zP\033\024\026\246\346\220\265\000\323\324\323\211\246\267J\253!\346\241-M\335I\276\215\364\345\227\212S%p\331\244\315!4\240\342\2349\251\000\251\022\244\002\236\005H\016*E\004\367\251\000>\265\"\232\225MJ\246\244SR\003N\006\236\r?4f\215\324\002)3Fh\315!>\264\204\323M7\223M9\024f\226\224S\201\247\212p\024\340)\324\242\234)\302\235\232\0058\014\323\302\346\244\000\001H\334\216*\022\r\' \323\261\232i\024\332L\322\356\3058=\014\374UYZ\2533sM-M-I\276\220\311\203K\346W\036\335i\271\245\315-<T\212jd\000\324\300\nv\332]\264\365\251\024\323\305H\246\244V\251\025\252P\324\360\324\340\324\354\322\346\214\322n\244\315\031\2434\231\367\2439\242\233\315&\t\240\nZu(\024\341\322\236)\340\323\207ZZu--(\251\024T\200S\361M \322\021\212\215\215\013\232v3\326\243u\305Bi\271\243v)\013UyZ\253\026\246\026\244\335HZ\243g\305\n\371\025\313\223\223E\'J\003T\201\263N\rS\306\370\251\321\352]\364\006\247\n\221i\300\323\301\251\026\244\025 4\360i\340\323\263J\r)4\334\321\232L\321E(\243\036\224}h\3074c\326\224\016)\300R\342\226\234:R\212p\247\212u:\212P*t\\u\251\024sO\307\2454\203M*M\"\305\334\323\304`\016\224\205)\214\271\025VE\301\250I\346\232M4\265V\225\252\271j\214\265&\352\013Tl\324\212Nk\235u\301\244\006\226\232G4\345\034S\226\246SR\251\251T\324\200\323\301\251\001\342\224\032p5 5\"\265H\032\236\032\236\032\234\r(4\354\322\032i4QE8R\343\024\244n?-(\030\034\322\355\357J)\301sF)qK\212p\024\242\235N\002\236\005(\024\365^jQ\351R\016*D\030\0314\270\024\234f\220\221J\016)O\"\242u\300\252\317\212\251/\025\\\2651\232\240\220\346\2533TE\2517PZ\230Z\244OZ\304u\315W*E%\031\346\2274\340jE5*\232\221MJ\032\244\016)CS\203S\203T\212\325 jxjxjxjx4\340i\331\2434Rd\032>\224\242\224\032p\247t\373\264\240f\234\247\035E.=*D\311\030\244\307<\322\342\214S\200\245\3058\np\247\201N\002\244Q\3058\nxS\326\235\272\223u!jM\334\346\202\371\251#\177Z\212y;\n\250\354j\274\247+UKb\242f\250]\262\rUc\223Q\261\246\346\220\2657v[\025aH\305d\221Q\262\203Q\264~\225\021\0304\242\234\005<T\200\323\301\247\203O\rN\rO\rO\006\236\rH\r<\032\220\032x4\360i\300\323\301\245&\222\212)E(\247\212p4\341N\034\365\247\004=\251\300`\323\331r3M\242\234\0058\nu\002\244Z\221Fi\346\225E<\275FZ\224P\302\231\2323NV\3051\2335VV\305V\221\370\252\214\334\324.\325\013>\005BMFM4\232ajj\037\232\246\316*\211\031\246\021M<\036j)S\270\250\207\006\236)\300\323\201\247\003O\rN\rN\rO\rR+T\201\252@i\340\323\301\247\203R\003O\006\234\r;4f\226\212QN\035i\302\234)\302\234*@i\300R\266q\326\220R\322\212Zu(\251\026\245N\224\340(\346\2028\250\363\315.\361H\322SKSKR\027\244.1Tg\227\223U\336O\226\253\226\250\335\270\252\356\325\021zc50\2651\232\225\033\034\323\214\225X7\024\3074\314\322\365\025\t\034\320)E:\235J\r(4\340i\341\251\341\252Uj\2205<\032\2205<\032\220\032x5 4\354\321J)iE8R\216\264\361\326\234)\300\323\205<\032x\346\215\270\240\nZ)A\245\025*\324\240b\244QI\'\025\031~\324\322j6ni\244\323wTm.)\215!=*3)\031\315S\222M\315QH\374T%\351\217\'\025Y\236\241g\244-\221Q\231)\245\350\3631F\372\256\257\212\035\371\247+\002)H\300\372\324\r\301\240\002FiA\247\003N\024\341KE(4\360\325\"\265H\255R+T\200\324\200\324\212j@j@i\324\240\322\322\203J\r8S\2058S\251\300\323\201\247\203OS\212\220`\365\247l\006\243<\032(\240\032\225\rN\247\232\225M6S\232\256i\245\2526j\214\265F\317P\273\032h|S\036@{\325f\340\325y_\232\204\265E$\225]\244\250\313\324m\'\2753}&\374\232\031\251w\361\212\210\032\010\315 8\342\245\r\221H\311\270\344S\261\260u\241B\261\347\212xU\'\031\240\250\354h\002\212(\245\006\234\rH\246\244SR\253T\212\325*\265H\246\244SO\3158\034\321\232Pis\212p4\340i\300\323\301\247\003N\024\340i\340\324\213R\251\300\250\336\243\315\031\247\n\231*PjE4\217U\337\203Q\263b\242g\250\331\252\"\325\0335B\315Q3TM \025\003\2605\003\266*\273\266j\274\204\212\207\314$\323Y\351\273\351CsAl\232pj\211Z\244\315;\002\224qR)\024\222\214\257\035\2521N\025 \247\216:R\032\000\024\231\242\224\032x5\"\232\221MJ\246\244SR\251\251T\324\200\322\212u\024\240\322\203N\006\234)\324\341N\006\236\r<\032\221Z\244\335Lza\3068\244\024\3455:\036*@i\340\342\225\216EWaP\311\305Vf\346\232\315Q3Te\251\214j\007\252\316\rC\236\271\250]\372\324\005\361Lr\030Uv\030\250\230\323\013b\205|\323\225\275i\302Nh\024\240\346\236\265 \024\003@\2239\030\315\"\343w\315\300\247\034\003\3058\032x4\264\204SM&i\300\323\201\251\026\244Z\225jU\251\026\245SR)\247\322\322\321J)E<\032p4\271\247\003N\006\236\r<\032xj]\324\323I\364\247\n\224\034\npj\2205!|To \305V\221\352\2635F\317\305D\315Q3\324fJa|\323\030\344Ug\030\315Tr9\252\356\324\315\370\250\344q\332\243$\032\206F\3051_\013I\347`\322\371\340\016\265p\232\007Z\221H\247\223@\246\276\027\225<\323U\311\353R\016i\302\234\016)\300\321\232Bi(\024\365\251V\245QR-J\242\244Z\221jU\247\322\212\\\321Fi\324\341N\024\np4\271\245\rO\rO\rK\272\234\034R\202\r9y\247\223\232Pi\013\342\232d\250\231\252\027j\254[\232\215\232\230Z\240v#\245@\322Sw\344\3224\230\025RisU\267TNs\322\253\273\021Q\026\246\371\225\034\255\221\232\214\036)\214\334\324l\325\262M74\240\234\324\212r9\251\007\000\324\017\311\241x\251T\323\301\3158R\321\232)3J*E\251EH\265*\324\242\244\024\361R\003N\006\235N\242\2123N\006\236\r.h\315\024\271\245\rN\rN\335I\272\2245N\217\305H\264\342x\250\\\324d\323\031\252\031\033\212\256Z\230MD\307\024\322\303\034\325Iz\234TFP\243\336\253IrI\300\250\231\311\034\232\207q&\220\266\006*\0275\013TD\323$o\227\024\302p*&jh\311<V\306\354\323\t\251\243\373\274\323\324\323\363L#\212h\340\323\301\251\026\236)h\242\212QR\n\220T\213R\251\251EH*@i\302\234)\331\245\006\235E\024\242\234\r-\024f\2274f\2245.\3523OSS\257\"\236\246\225\215B\315L&\242f\250\035\252\026j\210\2654\265B\315P\263UIO\245W\315F\315@\340d\324,\331\311\250\213Td\323\r@\347\006\241g\315\0107\034S\300\330s\236\225\177\'4\365\251T\366\247\322\364\024v\244\305(\251\026\236)sFiiE8u\247\n\220T\212*U\251V\244\025 \247\001N\024\264\341J)\331\030\351IJ)h\242\2123K\2323@9\245\247\255N\215\305H\0174\3622*\026\025\021\025\013T/U\331\252&5\0335D\315P;TM\316j\244\200\255F\0334\331\037\013P\253dT.\374\361Q\226\244\335QHj#\3159\030(>\265\033=l\200\r4\360i\350y\251A\241\216\005,|\2574\273M \353O\006\244\007\024f\200iE:\234)\342\244Z\225jU\025*\212\221E<S\205(\247S\201\315(\245\243\351E\024Q\2323Fh\245\006\224\034S\251EJ\246\246\034\214\323\201\241\205B\343\025]\352\007<UF=j&j\214\265D\355U\334\323\013T/\363US\36256^V\241\034-B\375j3Gj\216R\002\324!\276Z\214\2754\265n\253\323\261\232p\030\251\001\247\021\305:1\201O\307\345L<\032)\331\245\006\224S\251E<T\213R\250\251TT\252*@*@*AKE(\247\np4\354\203\332\222\214\372\322\023I\2323Fh\315\024\271\247\003N\024\361O\034T\250\335\251\343\031\247\023U\345`*\243\275Ww\252\356j\0064\306\346\242cPHs\322\241-\353M&\253\3102i$\000\'\275@zsQ=ELv\300\252\222K\232a~*2\324\205\253l5L\215S)\315H\026\236\005=W\025 \034SY8\250\250\247\nu:\234)\342\244QS(\251\224T\252*@*E\024\360)\333h\305&)\302\224\nZ)\246\212L\014u\244\242\214\321\232Pi\300\323\203S\203S\301\251\024\323\303\363C>\005S\226L\232\256\355P;T,\325ZG\346\243\3631Q\273\347\2450\236*\273\2674\200\346\220\214\232c&[\223\300\250g\340qU\230\361P\261\305C/CU\030b\243&\233\232ij\334\006\244SV#j\264\234\212x\342\244Z\220\nR\274Ug\034\361M\245\025 \024\243\255<S\300\251\026\246AS\255L\242\244\002\236\005H\242\237\212LQ\266\227m\030\243\024b\223m\030\246\220;SN1M\242\214\321\232]\324\340\324\340\324\360\365\"\267\024\026\346\221\334m\252r?5\0135@\355P\263\324\016rj\007\342\242\3630y\241\233\212\205\201\355LRKb\207s\273\002\230M5\210\306\rVu\306qP7Z\205\370\252\322T\004\323\t\246\223[\240\324\212j\304f\255!\251\001\251Tb\246ZRj\263\016i1@\024\360)\300S\200\251\000\251\024T\312*e\0252\324\242\244\025\"\212~(\305&\3321K\212LQ\2121AZiZa\\\347\024\302)\244RQF}\350\335N\rN\004\324\212\330\034\323\032L\232Fo\226\252\273TL\334Uy\036\253\264\225\023I\3151\233\212\254\347\232M\365 \341y\250\207\031\367\246\232k\032\201\330\223M \221PH*\263\236*\263\234\325v<\323\t\246\223[\252je5<ue\017J\235je\247\203J\307\212\212\214R\201J\0058\nx\025\"\255H\242\245QS-L\242\245\002\244\002\236\253O\002\235\2121HE!\036\224b\226\214Q\212\n\324l\246\242a\212a\024\332C\326\233\2323N\006\246Zk\266*\271~i\314\374T\016\325\004\217U]\352\274\215U\313\234\322o4\3269\246/Z\224\266\0056\243s\212\215\232\243=i\340qPH9\252N9\301\252\257P7Z\210\232a5\274\225:\212\236:\262\274b\254!\315J)CzP\\\343\030\244\034\322\201OQN\333N\002\236\005<\n\221EJ\242\245QS(\251\224S\300\251\000\247\201N\305\030\244+M\305\030\244\245\006\226\202)\244TN9\310\250\230S\010\246\232i\024\224\245\200\245\022\340To&\352\217<\3223\361U\336LUg\222\240y*\006z\205\233&\233\272\215\331\247(\346\234\313\315\025\024\234\232\204\365\245\305/j\205\272\223T\346\0309\252\255\311\252\362pj\003Q\265t\010*\302\234T\250j\312\363V\022\236\030\032\025\261\320\323\363\221\3159O\030\247v\247(\247\201J\005H\005<\n\221EH\242\245Z\231jU\251@\247\201R\001K\212)\r%&(\305\030\243\024\021HEF\313Q\262\324ei\204S\010\246\261\305G\313R1\300\342\233\232kTL\330\252\262\2775Y\332\240f\250\331\261Q\026\346\233\236i\353SD2j}\234\234\324.1P=DFi@8\305(\004\214\nc\256\336\265NnsU\010\353U$\250MF\325\321\"\032\231S5*\200*\302\234\nw\231\330S\303\032r\232\225NjAO\024\361O\024\360*@)\341i\340T\212*E\025*\212\231jU\025 \024\3608\245\"\223\024Q\212m\024QK\232Ji\024\302\276\225\031J\214\2550\255DS&\221\206\321Q\221\232\002b\230\342\252\313T\344\252\256j\022\365\013\276M34\n\231*\314?xU\2228&\253\311\336\25352\227\034qD|\212\216j\243%@zU\031\206\r@M1\253\250\351J[\002\205j\231Z\245S\353S)\030\251\027\232\221V\244U\251\025\rJ\261\232\221b&\236#5 B)\352)\340T\200\n\221EJ\253R*\324\200\021\326\236\265 \024QHE%!\024\224QKIE4\212i\024\302\275\351\205i\206<sP\310\274\323U;\320\303\002\240\220\3259\0175NS\212\246\3475]\215DM%*\324\351V\242\030\"\255\0221Ue\252\315M\357OQH\027nj\031\252\224\225]\370\252\322\256j\243\014\032\214\327LOzhl\232x52\232\225MJ\2652\232\235MJ\246\246Z\225EL\2652\212xZxZ\\{P\024}*@\247\352)\340\343\255L\2305(\024\270\245\315\031\245\242\220\212Ji\030\242\212L\321\2323IHi1\232k\014Tl{T/M\350)\215Ue5Q\352\224\334\232\252\374T\014)\204S)\351S\'QV\242\346\245|\214\021P?5\023-DF\rH\2074\3622*\244\243\004\212\251(\346\252\311P\236j\244\313\203U\315tE\363J\265*\212\231jU\034\324\312*eZ\231T\324\310\246\247U5:!\251UjP)\342\236>\224\341N\306h\000\216\224\340\304u\251\025\324\366\305J\030v4\241\251w\212p#\265-&)(\244\'4\224\021\232m\024\231\245\315&x\244\007\0249/\315DEDW&\220\214TRUIj\243\203T\344\035j\263\212\205\2051\2050\255*\212\235\005X\214\342\236_\327\221Q1\007\245\'Z\214\216x\247\001\265M&x\252\322\034\346\251\310j\273\212\200\214T2.\341U$\\V\332\324\253S(\251\224T\313S\245N\265:T\350*t\025:\212x\031\247\201N\002\236)qJ)\334\322\322\201N\024\365\315H\006i\333\005.\334t\244\311\024\231\245\315\024\322)\264QHi3I\232:\322\206\300#\035j3M\353LqU\336\253H3U\334dUI\022\252\310\265\\\212\217\0314\2453@J\231F\005<u\247\3435\013\251C\354h\246\363\221J[\203M\317\006\253Jj\253\014\324.*\"\264\306Z\253*f\264\326\245Z\231jU5:\324\351V\022\247AS\240\253\010*e\247\212x\247\016\224\372p\024\340)\300R\205\247\205\245\013O\013O\002\235\212Z\nR\025\244\331\355M \212NM!\024\332)\247\336\232i@&\244T\241\222\233\345\226\031\364\2462\355\250\330T\016\271\250\031*\273\307U\244N*\244\211U\236:\214G\315?e\033iqKO\007\212d\274\212\214PG\024\303L\317\006\240~j&\025\003\212\205\2054\212\201\305\\SR\251\251\222\246J\235*t\253\tV\022\247J\260\225*\324\213N\024\360)\340S\300\247\201N\305(\024\340)\340S\261O\002\234\026\245H\367\032\227\311\002\232\321q\3050\307\216\325\033% \212\220\307\352*6OJn\332c\n`\0252-HN\0054\363H\006\r1\223\'\232\215\223\025\031AQ2\n\255\"\325YW\345\252\016*\026\031\244)\307\024\230\246\342\232x\240\034\322\366\246\236\234\322\001JG\025\021\025\031\351L+Q:b\253\270\252\357L\353Q\270\251\301\251\226\246SS-N\225a*\302U\204\253\tV\026\244Z\221i\340S\305H*@)\352)\370\243\024\340)\300S\300\247\252\324\251\036MXU\300\247\342\232E4\212\214\2504m\024\233i\245\001\246\030\207j\201\3415\037\226\300\323\324\021\326\220\344\232r\255;\024\240T.\274\324\0141P\260\252\356\265^Q\305Q\2219\250\n\321\212iZ\215\306*#J\253KH\302\231F\354\212k\n\214\n1\326\242q\315V\225j\233\216i\240b\243z\225MJ\246\247J\231*\302T\351VR\254%YJ\231jU\251\001\251\026\236\242\236:T\252*@;\323\326\227\024\340)\301jEBzU\210\340<f\247\010\007\002\234\022\224\2454\212a\024\314R\n\r7\255%\004\003M()\245)\246:M\270\246\232BqL$w\353Q8\025\013\255V\222\2538\315U\221sP4t\2051L+Q8\3153e.\321MaM9\"\230E ZR\271\024\2018\244+\324T2.*\254\235\rTu\346\242#\025\023sOJ\231jt5:T\350*\312\n\260\234U\2048\251\325\205J\257R\253T\200\324\212jE5*\232\221MJ\265 \366\024\242\236\242\245U\315[\202<\014\232\225\260\243\212\022\246\002\224\364\250\030\363Q\223LcM\3154\232L\323wS\201\342\214\321\232ajN\325\031\036\224\326\250\315F\325\013\232\257%VcQ5Bi\244f\230\313Q\224\246\221L\"\230E0\214\322\021H\005=W=\2516\342\232G4\307\213p\342\251I\0363\232\253 \252\354)\205i\026\245SS\245X@j\302\n\262\203\0252\324\352jU5*\232\225MJ\246\245SR\255J\265\"\324\252}j@\376\224\345\253\021\256j\312 \0258\245a\220)@\305.\354P[\212\211\215Fi\246\232i\246\232M0\365\247\003JM74\003\203\315)!\272P1QH3\322\240\"\230\302\241aP8\250\031j\007J\205\201\024\332\017Ja\031\250\330TdTl9\240-!\024\3209\251T`R\021L+\221M?v\252\3123\232\246\351\315B\321\346\242d\250\026\246A\232\261\032\325\244\025:T\313S-J\246\245SR\251\251\226\245Z\231jU\251TT\252)\364\340*E\025n\016\225d\032z\232\225H&\236j2)\246\2434\001M4\302)\270\342\230E4\212i\310\240\236)\205\216i\300\322\346\214\323\032\242jcT-\315B\3035\023\n\211\226\241d\250\312R\025\246\025\2462\346\243\"\231\262\224\255F\303\024*\323\310\307Zn3F*\'\\\016*\273\256E@\361\324-\035D\351Y\351VPU\210\352\302\324\252jUj\221Z\246SR\251\251\224\324\313S\240\251\324T\312\2652\212x\251\000\247\205\251\000\253p\217\226\246\013O\002\234\016\r.\352\\\323MFi\r\'ji\246PE0\323M0\323\010\244\316)CS\263\3055\215Fj&\250\332\243j\215\206j\"*2)\245)\245)\214\225\023%0\245\0331Me\250\331h\013M#\'\232\\\036\324\214\204\014\323\010\312\234\325r1Q\260\317Z\211\226\240u\315e\240\251\320U\204\310\251\224\324\213R\255L\246\245SS\245N\225:\n\235\005XAS\250\251Ui\341jEZ\220\n\225S5f5\300\346\246\006\2349\245a\3050\032\\\322\023L4\207\245(\2468\364\250\311\246\026\246\226\2439\246\236)\206\232E\002\234\r\006\230\334T/Q\036\264\323Q\232k\014\324eh\tF\332c\257\245B\313L+M+\212a\025\031\034\323H\246\323\227\255\0142)\254>^\225Y\205D\302\241qU\333\203Y\310\2652\216\225*\324\253R-L\265*\324\310*\302\n\260\202\254 \253\010\265a\026\254\"\325\210\342-\322\245\3733c4\2331OU\251Pb\254\257LR\201\315H\242\211\016\005C\272\227u4\232J)E-0\250\250\331*2\224\241i\214)\230\240\212LR\205\240\212\215\252&\025\031\024\302)\204SqM+J\005!\024\3223P\262\323\n\342\232EF\313Q\221L\"\232V\2000i\3148\342\230\303+U\330Tl*\t\005Wu\254\325\034T\253R-L\265*\212\231EL\202\247AV\021j\302-YE\253(\265a\026\254*\325\210\320\366\253pr\n\277J\212E\033\270\024\005\247\201R\3069\251v\363OQQK\311\250H9\245\353IFh\315\000\323\201\243\024\230\315!Za\025\033-7m&(\305=S\"\221\226\242e\250\331j&Z\211\205F\302\233K\214\212\000\241\205FE4\212\215\226\243\"\232ED\313L+M\"\220\214P\t9\305\005p\rVa\315F\303\025\013\014\324N\225\222\005=EJ\265*\212\231EL\202\254\"\325\204Z\263\032\325\224J\262\211V\021jtZ\2361\310\315\\\014\252\240/Z\025\210<Q\214\320F)sO\214\363V\200\340\032x\030\025\013/4\322\242\230V\232E!\244\242\200i\342\236\006h\333\315!L\324n\225\036\312iZLsR\240\244aP\260\250\210\250\310\250\312S\031*\"\224\230\305\000\320Ni\244SJ\323\010\250\330S\010\250\310\2462\323\010\244\333\232ENi_\345\004Ub*\026\246c\232\032<\212\300\006\244Q\232\225EL\202\247AS\240\253(*\302\n\263\032\325\250\326\254\242\324\352*U\0252T\242\244\024\374\322\023\232J\2325\357V\024\361O\3154\256i\245i\204S\010\246R\021\2121\305 \034\324\2128\247S\363\221F)\031A\250\331*2\224\302\264\345\024\244TL*\026\024\322(\331\232C\021#\245E$[G5Y\206)\270\315\030\244#4b\243aQ\232a\024\322)\2148\246\025\246\221GN\225\013\222MD\302\243+M\013\3159\206\005sj*U\0252\212\231EL\202\247AVPU\230\305Z\214U\250\326\254\242\324\312\264\361R\251\247\203O\006\235\272\2234\344<\325\264\344\014T\341x\245\333N\333Me\250\210\2460\250\332\243&\223u\000\346\236\r<\032}8R\342\223m#%DR\200\230\246\260\250\331j&\024\300\265,i\223S\004\030\306*\033\230\376Z\314t\346\243\306(\245\333I\214Tl*6Zf)\244S\010\246\221M)\221Q\343\002\243aQ\260\250\312\321\216i\262g\025\316\250\251Uje\0252\212\231V\247AVPU\230\305[\214U\250\305YQR\n3O\006\236\r<S\351qOU\253p\203\212\264\2434\340\224\3420*6\025\021\025\023\n\214\212c%0\255\030\305(4\365\251\001\247\346\234\r8\n\220 \"\243h\361L)Q2\324n1P\232@*P=)\335\2529\016\340A\2522\2475]\3053\024\365\031\244e\250\210\250\310\246\221I\267\"\230\313L\305\0140*\006\246\021Q\260\246\204\'\240\245\331\216\265\004\247\002\260UjUZ\225V\246U\251\325jdZ\262\213VcZ\265\030\253IS)\247R\212z\212\221EH\005<\n\221V\246T\251\223\345\251C`\325\205pE\014)\245i\214\265\013-D\302\233\212B\264\322\264\3221FiC\322\371\225*6je5 jBsHF*\'\"\240a\232\211\226\243\350jT4\346\2467J\255(\252\222\n\212\244Z\030TL\265\031\024\303I\315!\024\322\264\306\034T\004S\010\246\225\245\215y\311\244\223\212\245)\306k\031V\246U\251UjeZ\231\026\247E\253\010\265e\026\254\245N\265*\323\300\247\205\247\201R-H\242\244QS\242\324\341qMn\r9[5*f\247\316E(\244a\221Q2f\241d\3057m\033i\245j\027Z\214\323y\240\023R+\021R\254\206\245\022R\031h2f\230Ni\206\230\302\230V\234\274S\310\342\242j\201\352\273\214\324%piV\202i\206\230\302\243\"\223\024b\232\303\212\215\272T$R\005\243\313\356hn\007\025VJ\25175\230\253R\252\324\312\2652-N\213S\"\324\350*\302\n\235EL\202\247QR\205\251\002\323\302S\202\323\300\251TT\351\305L9\244d&\205\201\210\310\025*#\016\2650\024\243\203A\2445\023.i6Q\345\323\031*\026CP\262Rm\246\221\3158-/AFx\244\0074\271\245\245\353\326\202\264\322\264\233i\370\342\241a\315B\300TEj\007^j3M&\233\232\t\246\342\232E&)\2568\250H\246\021@\024\343\322\243qU\234UIEg\252\324\252\2652\255N\253S*\324\352\2252%N\253S\242\324\312\265:-L\253R\252\323\302\322\343\332\200*U\025*\212\261\027$U\305\010\247\347\0314\327\230.Dc\203O\211\303\2140\0242m84\321\212\221c\315+[\236\324\337 \322\371\036\324\206,Tm\035@\311P\264u\013Fi\233M\024\322i\t\244\315(jx4\341J)i\244\ni<TOQ0\250\332\243e\252\3561Q\323H\240f\214\342\221\2104\001L\177AQc4\322\224m\305\014*&\025\003\214\325i\022\263\324f\246U\251\225*tJ\235\022\247T\251\325*eJ\231\022\254\"T\312\225(JxZp\024\355\264\241jEZ\224-H\274T\200\223R\004\006\244H\361Sc\214\036}\352&C\236*xP\236\247\025q\023\324\212\220\306;\n\215\242\250Z:\211\343\250Z:\211\243\250\031=\252&\2175\023\307\212\205\226\230E&)@\247\250\251\002\323\200\3055\251\271\244\250\332\243aQ\221\3155\207\025\004\211\232\204\256)\207\2321M\"\224-+\014\n\204\212n\332wjk\nc\n\211\206j&\025^E\254\364J\235\026\247D\251\321*tZ\235\022\247D\251\321*uJ\235\022\247T\251\002g\245)\214\257ZP\264\270\245\002\245QR*\324\201i\301i\342\245V\305<\267\245\033\260*H\334\032\2345H\256A\342\247\014\030r)\214\202\241h\352&J\205\343\252\354\224\302\225\023\307U\335*\022\264\335\264\340\265\"\2558\322c4\322\270\250\317\024\335\324\204\346\232V\242e\3050\323\030f\242t\364\250J\320E7\024\360\000\246\260\334*=\264\355\235\351\254)\245j&\025\023\212\205\215B\346\251*T\350\265a\022\247T\251\321*\302%N\211V\021*tJ\231R\245\tR \301\342\234\371f\344Rb\223m(Z\225EL\213\232\235c\315N\220\257\361\032kE\203\305FW\024\240\361Mf8\250\304\205\032\255G85:\316\007z\221nTT\242ezL\212i\025\033.j&J\215\243\025\013GU\244J\252\353\212\217\024\341\232\224\003F(4\306\250\332\243\"\201\326\244\n\rG$uY\306*2\336\264\322\300\212\205\210\246f\216\364\356\270\024\366A\267\212lj6\234\323\037\212\214\323I\246\032\205\305V\220Uv8\250\343Z\260\211V\021*\302%N\211V\021*\302GS\242T\312\2252\245J\022\236\251\203\234T\205\001\034\365\246,T\246:M\264\340\2652\014T\352jU\004\364\247b\232S4y&\230\321b\243h\201\246\204\333FqNQ\223\315Y\215\200\351S\357\243}\005\263M&\230\303=\252\026\025\004\213\236\325\t\267\334y\240Z\001A\267\307Ji\217\024\335\224\205*2\264\306J\205\2050\361M/\216E\006\347\214\032\247<\343\265Q{\203\353Q\371\344\3654\276a=\352T9\251vq@\0304\374\346\225z\232c\255B\303\025\031\250\311\250\335\252\274\225U\372\323\343Z\262\213\232\260\211V\021*\312%XD\253\010\2252%N\251R\252T\312\224\361\035/\227JW\035)\273sF\312P\230\247\001\212\220T\310\330\251G\315\322\230\310A\245\010\330\344\322\265\273c \346\243\020\261\353R\01029\024\306\266\3054B\001\346\244X\300\350jQ\037\024\306LS1\351F\342(\363=\251\013\2554\225\365\244\033OJB\0054\201Q\262f\231\262\232\313Q\354\315C7\313\300\252\304\346\243~*\244\262b\251\311>*\263;9\342\230\310\330\252\316\305\r\t6MZ\216J\271\034\231\024\245\250\006\244\003\220hqP0\250\230T\rQ1\250\034\325g\025b1V\343Z\262\211VQ*\312%XD\253\010\225:%L\251S*T\213\036j@\224\375\224\236^iD4\030\351\276]\'\227J#5\"\241\025*pjl\202*\t\t\3155e`q\232\23599\315XQK\260\032cB\rG\345c\245(CC\naQ\351Q\262S\n\232aZaZiZL\021J3\336\227\024\230\246\262g\2655W\373\302\251\\\217\234\342\252\234\324o\310\254\371\301\346\251\030\231\317\025n\336\323\035EZ6c\035+.\356\307\004\220+5\241dj\221\033\025f)*\312\266i\300\346\254 \312\212V\217#5\013&*)\026\252H*\273T\rP\275[\214U\270\305[\215j\324kV\221*\302%XD\251\321*eJ\231R\244\tR\004\247\204\247\010\363N1\2208\024\303\037\255\001\005#(\355H\006(\357J)\313\326\207L\324b0O&\254\306\212\243\203\223R\201N4\337\251\240\342\231\232Ni\254*\")3\232FL\364\250\331qL\"\233\212\220 \3051\227\024\231\2434\202\241\232\000\340\234sY\322&\302EU\220\342\241\333\270T\220\300\255\332\256\307\000\003\201S\030A\035*\254\366\201\201\342\261/l\366\202@\252?g;rE5\020\206\255(-L\213\220y\241\340h\31755\271\347\006\247t#\351P2\324\022-T\221j\254\202\253?Z\201\315_\214U\270\326\255\306*\344B\255F=*\324kV\021j\302%N\251R\252T\201)\352\231\251U)\341y\241\200\355Le\246\021\212n)6f\202\270\246\322n\"\233\274\232o9\251\341<\363V\326\224\212L\n\215\200=)\002\232\017\024\322\302\230j2)\271\"\232Nz\323)\010\346\246P1Mq\223Q\342\223m.\312\n\341O\025\233p\233\311\300\252o\006:\324\"\"3V \214\203W\221x\247\221Q\262\346\252Ml$\353U\215\222\200F*\224\226![\345\025b\010\031\007\024\351>a\206\034\325`\233_\"\254\261$sP=@\342\253H\265NQU$\030\315U\220\326\254kV\343\025j1V\343\025j1V\243\253Q\212\262\202\254\"\346\246U\251U*UJ\220 \240\2554\2554\2550\2550\256\r7\034\321\203M\"\230V\223e(\2175,i\203V@\247m\3155\206*<sA\351\311\250\330\366\024\334f\215\206\215\206\230\313P\225\246\221I\212\221\006N)XsM\333N\tK\267\035i\030\006\025VH\324sT\245\000\232j\302\032\245\020\340\323\300\305.(\3050\256j3\0350\302\t\311\024\024\000t\252\362\301\225$U=\234\363O\"\242a\305WqP8\252\262\255P\230U9\005l\306*\324b\255F*\334b\255F*\312\n\264\225e*\302\n\260\202\247QR\005\247\001A\024\204Sv\323v\323YsL)I\262\220\245FiUsR\025\013J\204T\200\212]\300R3\203Q4\212:TM \365\246\207\006\234$\002\227\315\364\244\336M!\031\246\225\024\323\036j2\244R\253m4\343\311\315(\245\310\025\013K\223@\220c\025^c\232\250\312I\253\020\307\305LS\212a\\\016)\204RSM\024\322)\244TdsPK\010\373\302\241)\305@\353P2\324.\265ZE\310\252\023%R\221+^1V\243\025n1V\243\025f1V\220U\204\253\021\212\264\202\254\240\251\324T\240R\342\214R\021I\266\220\2554\214Q\2674\322\224\306\035\251\236^i\215\307J\211\331\2155d\"\236\327\001G\0315\013]\023\320SE\303w\244iKv\241\010\357R\0021F\352PsOZ\220\014\323YiB\346\225\320\001\223U\037\031\371i\013\343\251\246\231\207\255Ff\31579\247\n\n\347\265\036Ni\352\233zS\261H@\250\230\n\215\2522Nx\244\335FsHi\206\232j7@j\264\221\343\245Wt\250\035}j\264\211T\345\216\251\274y\257\377\331"
+byte_png: "\211PNG\r\n\032\n\000\000\000\rIHDR\000\000\002\000\000\000\002\000\010\000\000\000\000\321\023\213&\000\000\004\255IDATx^\355\335\321\226\2338\014\000\320\036\372\377\237\2749{\232m\272\035H\006BlcI\367>2\t\030ld\311$\231\037?xfYo\000\000\000`\210\217\353\261\345\343=\300\264n\353\rl\010\001\000\300\347E\005\000\000\000\031<\312Ck\353\000\000\000\323\363\204\017\020\t\3302&h\303H\342\210\233g\t\000\000\300d\324\263W\370\2738\324\003\000\000\000@0\341\2273\302\237\000\000\000\000\260\021\355\307\342\2075\3277\030\000\000\200\234\326eU\344\352g}.\ru\334\365\305\362\236\031\000\000\000\007(\013y)\362\"\021p\206)\201\277\231\005\240\004\241\237\223\226\237\353-\320U\343h\325xw\034\"\273\004\000\000`\237\352\221.\014,\200\227\254\227\003\000@1\212\000\000\000\n\363\330\020\000\200\272,\016\003p\221\321\0132O\247\274\345K;n\267\321\215\002.\36642\000\000P\213J\020\000\306\2122\367Fi\'=X6\204h\304l\310\313\375\235\314\3414\353\360\013\001`\2079\005\340MY\002\347\222\345D \367\037\000\000@2\236\333G3\2602\367\273\014\274\305x\201\310\334\301\000\014a\302Ii`\225JSz\016\000\200\246$\230\305\031\000\000\025\025X\360Mv\212M\347\353\246;\003\372s\323\206\240\233\000\n\032Tt\014:\014\2632\000\000\000\000\240\252\343\217\036\254\037\000\000@\025\235\263\377\316\273\347s\307+\305b:]\230N\273\2459=\005\000@8\007+p\271nZ;\377;\360\373\277\002\344\'\016\262og2\005 \253I\342\377\301\262~>a\033\036\322m\226\361\n\220B\2429\314\364\000@\017\346\027\222J\224\005\002\000\345\311l\000\302\020\262y\305\032\034\000\000\364%\347&\224\027\013\010}\307\361\355\353Q_\264\001\250\304\267l+\373\325\371\006\000\000\344\243\324\203\000$\342\000\360\006k\230\000\033\277\213\177\0012\000\235\004\225\211\000\000\000\344\341\203\010\331\254z\364Q\276\334\326\3376\340\177\313\372\252Q\315\310\001`I\001\000\270\222\\\204\211\030\216\343\214,y\016\233\262Q\326N\200\234\3046\000\210\310\014\376\233\013\221\307\234KAs\266\n\262\021\313\001\000\230\205\334t\270 \227<H3\001\000\000\346\263}\340\252\304\002\250\314\017\214\027\267\374\\o\001\000\312\220\010\002\000\000q\251h\342+\327\207\345Nx\207\353\001@j&:\200Wr\177b9\367\331\3255\240_\007\034\3423\235s\233\315\3567\033\022\230\276\223G\312\330\301\234g<\000\000$%\321#\261k\213\374k\217~R\310FS\324}\3762\211\001\300\365\244\220\314l|\2768\376\210@\024\342\003\000\000\314\302z\026\000\000\000@\024Vr\000\000\000\000\000\000\000\340n\211\366]\345h\355\005\000>g\376\207\366\334WD\341s\337\245\351\376\016\"\305\177\003\240\270\267\007@\244\321\3153\341\026\252\001\242y{n\005\030\357v}\254\272\276\005\000\000\000\000\000@X>\376\002P\234\'\316\347\334\\:\000h\313\314\n\000\205\230\370\001\000\200\231\004\371\350\214R\252\270\t\276\316\016\000\000\000\300\033,\347\0009\210f$\026\344!!\000\320\310\005\037\274\270\340\220\034!\017\344\356\327\r\372\317z#\005\374\t\315\213E\017(h\221\007@I\2339\177\263!\006\021\254\021\027\222\347\202F\006\336%\004\024g\000\000\000\360\214<\021\000\200\324<\002I*Q\307\216/\312\336:\342[/&\212D7\020\224&DCY\207\277\010\262\010\024I\035\034\000\000u\325\230\001\217\235\345\261W]\250m\276b\216\004&\3264\336\001\334\265M\245\276!\313\232\323\260\001\300\\\222\335\220\311N\347\205\0067\353%\027\252\347\017\0076\270&\000\325\010\235\000\274\3201q\'\262O\006\306\355\243w\037\361\310ld8g=z\250wO}\357K\377\035\353\314c\257b\353\332\256\006h\313l\000@\006r\364}\3679\337\205:)\370\205\013\336|`\000q\002\000\370\236\222\362S\255\027\342?\351\214\263\357=\373>\272h:\244\364mu{\303\311\010\231\220N\001\372\370oJ\330\233\030\200\224\"\244\027O\303S\204\206\267\223\366l\307\235\330\270#\r\326\363\267\007\000\000\000\370c\306\352\353\374\217^.\347\337\032\315\214\035\007\227+\023\001\000\000\330\220\013\326\266\314\264 0OKz\362<\037\000\340b\023\344c\0234\001\362\333/\276j\024\241;\016\\\204\307K\376\005s\343\23463\025f\327\000\000\000\000IEND\256B`\202"
diff --git a/core/res/geoid_height_map_assets/tile-3.textpb b/core/res/geoid_height_map_assets/tile-3.textpb
new file mode 100644
index 0000000..486adf4
--- /dev/null
+++ b/core/res/geoid_height_map_assets/tile-3.textpb
@@ -0,0 +1,3 @@
+tile_key: "3"
+byte_jpeg: "\377\330\377\340\000\020JFIF\000\001\002\000\000\001\000\001\000\000\377\333\000C\000\004\003\003\004\003\003\004\004\003\004\005\004\004\005\006\n\007\006\006\006\006\r\t\n\010\n\017\r\020\020\017\r\017\016\021\023\030\024\021\022\027\022\016\017\025\034\025\027\031\031\033\033\033\020\024\035\037\035\032\037\030\032\033\032\377\300\000\013\010\002\000\002\000\001\001\021\000\377\304\000\037\000\000\001\005\001\001\001\001\001\001\000\000\000\000\000\000\000\000\001\002\003\004\005\006\007\010\t\n\013\377\304\000\265\020\000\002\001\003\003\002\004\003\005\005\004\004\000\000\001}\001\002\003\000\004\021\005\022!1A\006\023Qa\007\"q\0242\201\221\241\010#B\261\301\025R\321\360$3br\202\t\n\026\027\030\031\032%&\'()*456789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz\203\204\205\206\207\210\211\212\222\223\224\225\226\227\230\231\232\242\243\244\245\246\247\250\251\252\262\263\264\265\266\267\270\271\272\302\303\304\305\306\307\310\311\312\322\323\324\325\326\327\330\331\332\341\342\343\344\345\346\347\350\351\352\361\362\363\364\365\366\367\370\371\372\377\332\000\010\001\001\000\000?\000\364/\245.\352pz\225d\247\356\3151\316j65\0314\302i\245\251\244\323sFh\315&h\315\004\323I\246\223M\335HZ\232M4\232ija4\302i\204\324d\324d\323\t\250\311\246\023Q\223Q\263TL\325\0235D\315Q3T,\325\0135D\315Q\263T,\325\0235D\315Q\263Te\251\205\251\244\323I\244\315&h\315\031\242\2274\240\323\251\300\323\201\247\003R\242\223Vb\212\264!\213\025\353E\251\245\250\017O\017R\007\247\026\030\037Zk\032\214\232\215\215FM74\233\2513K\232L\321\2323M&\232Z\230Z\233\272\220\2654\232ijijaj\215\215FZ\230Z\243&\243-L-Q\263TL\325\0235D\315Q3T,\325\0235B\315Q3TL\325\0235D\315Q\263TE\251\205\251\204\323I\244\315&i3Fh\315.isJ\r;4\340i\302\245E\315[\215*\364H\000\253I\355^\231\276\223}\033\351\341\351\341\351\301\363O\334)\215Q1\250\211\244\315&h\315\031\2434f\220\232ijajaji4\335\324\026\246\026\246\026\246\026\246\026\250\331\2522\324\302\325\0335F\315Q\263TL\325\0235D\315P\263TL\325\0135D\315P\263TL\325\0335D\315Q\263Td\323\t\246\223M&\2234\204\322f\214\322\356\2434\240\322\203O\024\361R\242\022j\334q\364\253\221\307\264d\325\204\253q-z\000z7\322\356\245\017N\017OW\251\003\346\235\2735\033TML\315\031\2434f\214\323sHZ\230M0\2654\2650\265!j7S\031\2522\325\031jaj\214\265F\315L/Q\227\250\331\252&z\215\236\242g\250Y\352&j\211\232\241f\250Y\252&j\211\232\243f\250\213S\013S\t\246\223M&\232M7u!jM\324\233\251A4\360\rH\020\323\304u\"\305S\254F\254\305\016:\325\250\323\025co\002\245\215*\344K\212\355\003\346\215\324o\305(\222\234\036\234\036\244Y*P\371\240\267\025\0214\302i7R\346\214\322\023M&\232Z\230Z\230Z\230Z\232Z\223u\000\323\034\324L\325\0335FZ\243/Lf\250\231\2526z\215\236\242g\250\331\252&z\211\236\242g\250Y\252&j\205\232\242f\250\231\2526j\214\2654\2650\2650\2754\2653u\031\247\005&\234#8\351J#5\"\307\355R\244C\275N\261\217J\231b\036\225*\305R\004\002\244QSF9\253\210\231\253\021\307V\022:\350\326Jxz\013R\007\247\007\247o\251\025\352U\222\244\017\221\3150\236)\204\323wR\206\245\335HZ\232Z\230M0\2650\232ajijM\324\233\261HZ\240s\212\211\232\243-Q\263Te\2526j\211\232\243g\250\231\3526z\211\236\242g\250\231\352\026z\211\232\242f\250\331\252&j\214\265F_\024\302\365\031~i\013f\232h\000\232\235\"\365\253\t\037\240\251<\272M\230\355O\013OU\315J\253\212\235G\025(\034RT\252*\304K\315^\215j\312%YE\2550\324\360\324\355\324\205\2517\323\303\324\212\365 zx\222\234\030\021McL-\315\033\250\335HZ\220\2650\2654\2650\2650\2654\2654\265!jaj\215\232\240f\305F\315Q\226\250\331\252&z\215\232\242f\250\331\252&j\215\232\242f\250Y\252&j\211\232\243f\250\231\2526lT,\377\000\205F\\w\246\347=)*EJR\242\204Q\232\262\007\313V\020p)\370\243m(Jz\256\rJ\027=*@\265 \024\273y\251\221j\314C\025v<U\250\326\254*\325\260\330\245\337K\276\221\244\246\357\247\253\324\241\351\341\351\302Jz\311N\337\236\264\306<\323wQ\272\215\324\322\324\322\324\322\324\302\324\322\324\302\324\322\324\322\324\302\365\0335B\315Q3\324l\325\031j\211\232\243f\250\231\2526z\211\236\242g\250\231\252&j\211\232\243-Q\263TL\330\250X\236\246\242f\246S\200\245\035j`@\024\335\324/Z\234\036\225j#\225\251@\247\201O\013N\333O\214`\324\333)\300S\202\232\231\026\254\"\036\325b4j\271\0225[D\251G4\326\342\243/Az@\364\340\365\"\311O\017K\346S\326J\225d\343\024\026\244&\233\272\215\324\205\251\013S\013SKS\013S\013SKS\013Tl\365\033=D\317Q3\324e\3522\324\306j\205\232\242f\250\231\3526z\211\232\242f\250\231\2526j\214\2651\232\241rs\234\324li\206\223\034R\321N\240T\2129\253\n\234f\255B\277(\251\302\323\302\323\202\324\252\225(\216\236\026\244T\251\026*\263\025\271n\325r8\002\216je\n;T\311\364\253Q\256j0})\254j\00684\335\324n\245\335N\rR\007\247n\245\337OY)\341\363\212v\352Bi7Rn\246\226\246\226\246\026\246\226\246\026\246\226\250\331\352&z\211\236\243g\250\331\352&z\214\276)\245\352\'j\205\232\242f\250\231\2526j\215\232\230A=x\250\233m0\342\230Xv\024\302F*&\344\323H\243\030\242\212QR(\251Q7\036*\340N\000\253\010\230\002\246U\251\002\324\212\225*\245J\022\236#\251\243\2135j8=j\342\"\250\342\2022i\311\035Y\215*\324Q\346\250\253qC6*\t\rE\272\200\324\340iA\247\206\247\006\243u(|S\325\352@\371\247o\365\246\356\315!8\246\226\246\026\246\226\246\026\246\026\246\027\250\331\352&z\205\236\243g\250\231\3526z\215\236\233\277\024\326z\205\232\241f\367\250\213{\323I\035\3114\302\303\265F\315Q\026\246\223Q\261\246\3474\206\220\2121I\2121N\002\245E\315[\202*\274\261`\n\225R\244\013\212z\256juJ\220-J\251R\252U\204\001G\275N\274\324\240b\225FMX\2162{U\350\255\275j\354p\001\332\271\250\344\342\234NEA#qP\226\240=<58\032pjvh\335F\352]\330\247\253\323\374\312R\340\323Kb\232Z\232Z\230Z\230Z\230^\242g\250\231\352&z\211\236\2432TL\365\031zc=3}1\244\250Z^j6\222\241g\246\357\365\246\226\246\026\246\026\240\364\250\311\244\315-\006\233E\030\247\250\253Q%^\211qV\224t\251\200\245\013\223\305N\211\212\231S5 \216\244T\253\010\234T\211\036MXX\352d\2075a QV\243\214/AV\343Bj\322%p\361\276j\300<T,x5Y\233\006\2205<=H\032\227u;u\033\251CR\356\245\335K\272\215\324y\224\205\275)\205\251\245\3526z\215\236\241g\250\231\352&z\210\275F\317Q\227\250\331\3522\364\322\331\250\331\261P\227\250\231\371\246\026\246\227\246\027\246\227\244\335Jd\342\233\232)\331\242\214R\221@\031\251\243J\273\nU\245\\T\350*`8\247\242\022j\334QU\205JxJ\221c\251\3250*x\343\346\254\254u:ES\244&\255G\020\035j\302(\035*\312-y\324G\275YS\221Ls\214\325)\033\006\232\032\234\032\236\257R\256[\245\004\225\353F\374\322\206\245\rK\272\227u\033\251\013\323K\323K\323\031\352&z\215\244\250\231\352\026z\211\244\250\331\3522\365\031z\215\236\243/F\372\211\3335\013=F\315\3150\2650\2654\2654\265&\352]\324\341\203E8\032x\245\245\0035 LT\211\326\257CVUsV#J\262\261\324\311\035N\243\0252.j\312G\232\220%H\251V#\216\255\"U\204CV\0251R*\223V#\216\254\252W\230F\370\253\010\364\216sT\346\353\232\213u(jpz\265o2\251\371\205\027\022\2536W\212\204?\255H\036\227w\245\001\350\337AzizB\364\306z\215\244\250\232J\215\244\250ZJ\211\244\250\232J\214\311Q\263\324e\3526\222\230^\200\364\307z\201\236\230Z\230Z\232Z\232Z\2234\231\2405<58\032x\247\216i\336\302\246ARb\205\034\325\270Z\257C\315\\\215j\312\255<\014T\321\256j\334q\364\253J\234S\325*t\216\254$uj8\352\312GR\210\352DJ\260\211V\021+\307\204\236\2254sv\251w\346\240\224\344Um\324n\247\006\245\337H\\\232\262>h2G#\275F\262T\233\3517\373\322\356\244\337H^\230^\243g\250\232J\211\236\242i*&\222\242i*#%F\322TfJa\222\243/M/F\372c\265D\317Q\226\246\226\244\335F\352L\323I\240\032x5 5\"\342\247T\317\265.0jt\034S\261OU\251\343\\U\330[\025~#\320\325\201\315J\213\232\271\014|U\330\243\342\247\tR\244y\253\t\035X\216*\260\221\340\325\224J\224%H\211S\204\251\343Z\360\245\233wCS$\2305i\037\"\232\346\2539\301\246\346\227u;u&\354\032\235g%v\346\231\300\357K\276\215\343\265;x\355I\276\232^\2432Tm%B\322TM%B\322TM%D\322Tm%F\322Tm%Fd\2442SK\320\036\232\357\305D^\233\232ijM\324f\2279\244&\220\032z\232\225ML\206\254\253qJ\0075*\n\231V\245T\253\010\265e\022\255\302z\003WQ3\322\255D\225r$\253h*tL\325\204\216\254\244uj8\352c\037\024\344\030\251\325jdZ\231R\247H\353\347tl\032\260\222f\254\306\346\245c\300\250d\342\242\335\2127S\203PZ\233\272\245^A\244\335I\276\223\314\305\036e4\311Q\264\225\023IP\264\225\013KQ4\225\023IQ\231*&\222\243i*6\222\230d\246\231i<\332x|\323Y\252-\324o\2434\204\321\232\\\321\326\201\326\236*U\251\226\254%J\242\246QS\240\253\n\2654iV\321x\251\222>j\334y\025v\023\232\277\032f\255G\035\\\216.\225e#\305Y\216<\325\225\217\024\375\264\252\225:\245N\211V\022:\235c\257\232\310*y\247\253`\325\330OCS\261\252\362\265@Z\215\324\340\364n\246\227\251c\220r3H\347\322\2432SL\224\236e4\311Q\264\225\013KP\264\225\013KP\264\264\303%F\322TfJ\215\244\250\332J\214\311I\346f\232_\024\364\222\236\355\306j\035\364n\245\017N\316i3\212p4\361N\003\232\221EH\242\246AS\240\251\320T\350\265a\022\254\306\265a\022\255\242\000\265:\307\306sR\242\342\255@\270<\326\244\003\201Z\021%ZD\253\010\225f5\253\n\264\375\224\365\216\254,ub8\252\312GR\004\257\230\244}\346\233\320\212\271\031\332\005M\346qU\344\223&\242-I\276\224=.\372ij\217~\rX\337\362\324\014\364\303%!\222\232d\250\232J\201\344\250^Z\201\245\250\232Oza\226\232d\367\250\332J\215\244\250\332J\214\311M2R\371\231\247#\324\305\362\265\016\356iCR\251\247\251\301\247\236FE*sR\250\346\244QR\252\361OU\315N\213S\242\324\350\265f4\253H\225:%Z\215*\312\002F\000\251\322?Z\260\221{U\250\343\253\360\'J\321\204c\025q\026\254\"\324\352\2652\212\231W&\246\t\322\254G\035YD\251\325i\341y\257\224\267R\253d\365\251\326\340\214\017J\223\317\310\250\231\362i\273\251\013R\207\243}4\2751\236\244Ir\234\324L\365\031\222\232d\246\264\225\013IP\274\225\003\311U\332J\214\311L2SL\264\303%4\311L/Q\226\246\356\247\003\232\221x\247\227\342\242\r\363S\263\203OV\251\001\315J\207<\032z\256\rN\026\244QS*\324\310\225:\307S\"U\250\343\315Z\216:\266\221q\232\231#\253QGV\322!\216\225:GV\022*\263\034uz\010\375\252\342GW#\\\212\265\032T\252\2652GV\021*UNj\332\'\0252-L\027\212r\212\371\'u\001\261N\rO\017\212\013qF\352M\336\264n\244-M/Q\263\346\204~\324\326|TfJa\222\230\322T-%B\362Uw\222\240i*&\226\243ii\276m\'\231M2S|\312\013Te\371\247\254\270\251<\352x|\212f~j\223w\024\34452\032\225\0175eFj\302\n\221V\246E\253q&j\312\307\355R\244Uj8\261\212\267\034Uv8\270\305J\260\373U\230\341\366\253+\037\250\253\021\307VV:\2364\253\221\014U\224>\225n,U\225\"\247J\262\213S\252T\250\234\325\224\025:\212\230\nP\265\361\376\374u\245-\273\221\320Q\346d\323\267r*U`x\244\'\232i4\233\251\013S\031\2522\324\253\300\3156F\342\2533\342\2432\323\032J\201\344\250ZJ\205\344\252\355%D\322TM%0\311I\346\322\371\224\236e(zij\025\351\333\261S\304\324\245\271\251\024\344S\324\342\246CV\"\003<\325\330\370\351S\252\324\312\275*dNj\364)\322\255\"U\244\2078\342\247X\261V\243N\225n5\2531\240\253J\243\002\235\220\rH\216;U\204b{T\350O\245Y\2103\032\273\032b\244\335\264\342\244B\331\310\346\255G&:\365\253qL*\332J*eqS\243\n\260\246\246S\221R(\257\215\213z\232\2267\302\034TA\260\334\324\241\263\315(~})\305\275\r7y?J\013SKTl\324\300\334\323\214\230\353PI(#\212\254\362T-%1\244\315B\362T\r%D\362Ui$\250ZZ\215\244\246\031)<\312Q%\033\351D\224\375\331\024\014\346\236\017\025,MR1\311\342\244\211\275je\301\251\220b\255F*\334~\365m{T\350\275*\334q\216*\324k\212\271\032\325\350\0235d\303\200\r*\340T\310\330\251\321\316zU\224\311\251\304\031\031<\324\251\t\354*\322[\271\253p\3321#5\261ib\253\202ji\342\000|\242\250\264M\273\245^\266Q\2140\253&\327w+Q\230Y)\213#\206\305\\\216V\342\255\307!\305[\212L\342\256\306r*\302-|`i\276a^\224\007,j`\330\024\273\263C\034\n\013`b\233\272\220\267\025\03357uG#\324\016\365]\336\241i*&\222\241y*\026\222\242g\252\362IU\332J\214\311L2Ry\224\242Jv\372pzz\275J\0374\360jD\251~\265,uaz\325\204\031\2531\n\270\203\000U\270\327\"\255\"t\253\221-\\H\363V\342LU\310\260\265)\223#\002\234\210I\346\255\305\026x\002\257Ed\314>\355\\\207On7V\214V\034c\025n\033\000:\212\273\035\242\000\t\351S\013d\316EXED\357R3G\214\001\232A\0227U\251R$\037\303R\205\031\371F\005+\303\221\3275\017\331\207a\315*\246\323\310\247\026\njX\344\346\264\255\344\r\212\320N\225\361a4\323@\342\236\0374\340\3243d\nB\334\234\323Kb\233\273\212a99\246;\342\253\273\324,\365\003\275@\317Q3\324N\374Uf\223\232a|\212\255#\325v\222\2432S\013\323|\312z\311O\022S\203\324\213%J\257R\253T\361\265MR\306j\312u\2531\212\267\020\315[J\273\n\360*\364i\234U\270\322\256D\265eH\025&\342G\025=\274e\316\024d\326\315\256\230\317\202\303\025\255\005\212G\216+A!T\034\212\231B\2020*\30279\002\246POJ\225b=\352e\2074\357\263\234\325\224\266P2y\251\004c\037(\247*T\241)|\2726b\221\220\021\315f]\023\033c\265Ij\306LV\244A\243\301\355Z\2606\345\025\361ni\271\2434\241\251CR\223J\314\n\202z\324d\344\322c\202)\214\330\252\356\365\003\265@\317P;\324\014\365\013=F\317\305U\221\351\202L\212\206V\305Tg\250\214\224\205\351\273\351\301\352Epj@\342\236\257S\253T\310j\314|\325\220*D\0305f>\265r1\305Z\214U\310\3278\253\361/\025\241\n\360*\352\'\002\246^*tN\346\255End<V\355\205\232FA\3075\265\033\0000\005J\2715aA=jdJ\260\211VcJ\260\251S\242T\253\036\343Rm\344\212\220F})\351\027s\326\237\260R\204\240\306)\214\225\233{\016\3420)\326p\354\353Z\341r\200b\255Z\251\003\006\276+\315&h\316h\315(4\273\251r\017\007\2458\000\0055\333\002\252\310\371\252\356\325\003\265@\355U\335\252\006j\205\236\242g\252\322=B$\301\250\345\220\232\246\317\311\250\313\322o\246\027\245\022T\210\365.\342*Dj\265\033U\2045f#V\321\252d\253q\n\271\030\351\212\265\030\253\360\247\002\257\302\274U\370S\201V\301\300\251\341M\307&\256,E\210\013[6V\273PdV\254H\007J\264\213VQj\312-XE\253\010\225j4\251\325*A\307\025n\025\030\247\010\306\356*`\224\316\344\nz\212~\007\343HV\223h\006\240xC\222H\2428\300l\n\323\212\037\224f\247\216,\034\327\304\006\2234\231\245\315.h&\223u(\227\002\242\222M\325\0035@\355P;Ugj\201\332\241f\250\035\252\026j\255#Ugza\223\212\255#|\306\242-H^\232^\227p\251cz\234\020E=MY\211\252\322U\250\252\352\016*t\025n!\322\257B=j\354KW\241Z\320\201j\364`\001R\250\334\303\025\243\004Y\300\025\265kl\240\002\302\264c\003\265ZAVc\025f1VPU\230\326\255F\265j5\315YU\240\307\316j\334\021\222\265<Q\374\307=\251\345qP\221\226\342\244\002\245*\252\276\264\305 \212B\200\362M5\306@\002\237\005\271$\034V\200\033p*@k\341\243L&\2234f\224\032Bi7SKTL\325\0235B\355U\235\252\273\265@\355P1\250Y\252\027j\253#Ugj\210\265A+r*\"\324\302\364\205\351C\324\310j\322\221\266\234\244\346\254\304\325r3V\342\355Wc5j1V\342\034\212\277\020\253\321\014\325\370V\257D0EX\317LU\313u\307&\266,\"\334A5\260\270\030\002\254\305V\322\254\245Z\214U\230\326\255\306*\314b\254\3060EZAO8\034U\373|2b\235\267nsQHK\034\nhV\317J\231P\234qJ\352FEV(\301\370\351V\022\022\335j\302\333\201\326\254$x\245e4\252\276\265\360\333\n\204\365\246\223FisHM&i\244\324Lj\0265\013\232\254\346\253\271\252\356\325\013\265B\306\240\221\252\244\215U\235\252=\325\024\255\305BZ\243&\232Z\205j\261\033\325\205z\231\0335j#V\3428\253\321\034\342\256F*\324uv*\320\206\257\302:U\370G\"\255\216*\335\272n\344\325\330\027{\214t\255\353e\021\240\035\352\342\032\267\035[\216\254\307W\"\253q\212\267\032\325\230\326\254\306\274\325\2001A^sWm\270\305Y\312\263`\323\035\0009\035*\274\227+\031\250N\247\351\201B^\031\\\016\265\251\034a\324\020*\302\305\216\225*\246jU\2175(\204w\250\335\0247\025\360\223\032\205\2523Gj3A4\334\323I\250\232\241cP\271\252\356j\263\232\201\315@\346\241cU\244j\252\355U\2445\036j)\017\025\016x\246\023M&\224\032\225\rYZ\2363V\343\253\221\325\350j\364F\255\307\315\\\213\265_\207\265h\302:U\370\370\305N\2373\001Zp!\332\005hZ\307\206\025\250\215V\3429\253\221U\270\315Z\216\256EWb\355V\3435n>j\302\2561S\016i\315\320T\320\266*u9<\365\245\221\266\216\265\227:\231\033\212\317\2226F\255]&\r\356\t\256\215#\n8\247\242\234\373T\333x\244I6\2674\351&\335\302\324Y\311\346\276\024\"\241j\211\250\006\202qHM!4\302i\214x\250\036\253\271\250\036\240z\256\365]\352\007<UY\rU\220\325w4\300y\346\242\220\365\250\r0\232J*D5j6\342\246F\346\256D\325z#\232\277\r]\205sWc\\U\310\205_\204c\025\241\025]V\033j\335\252\344\346\265`\255\030\260\243\212\267\031\315\\\214\325\310\232\256Fj\334f\256Dj\344f\256EWb\346\255\240\315J\027\024\377\000+&\225>Y\000\253\312\200\214\212I-\367P\266H{U;\273\020\275\005K\246\257\226\370\255\325PE;\201F\340i\n\212B*&\310\351_\016:b\2538\305@\324\212y\247\036\264\323M\3150\232\215\215D\365]\352\026\252\357U\336\253\275@\346\252Jj\244\206\253\261\246u\250\330\324,j2iz\nAOZ\231\032\255E\315^\210U\330x\255\010{V\204#\030\253\2503V\343\030\025r#\216\265r)*\354J\315\327\245_\205\266\340V\255\250\316\rh!\305Z\210\325\310\215]\214\325\250\315\\\215\252\344MW#j\271\023U\330\232\257Ds\212\2621\201Rn\003\245B\315\363\325\250\346!1K\346\2615<R\034\202j[\225\014\231\252\021\270\216Q[1\311\205\006\223y,EH\264\356\364\206\230y\257\210\235A\025RT\252\216\274\3231\203JO4\323M\2465D\306\243sP=B\365]\352\007\025]\352\254\206\252Jj\253\234\325w\246\257z\211\272\324\rL\240\232L\324\212jx\352\324uv#W\242\347\025\241\017j\320\207\265hF8\2531\324\350{\n\275\016\027\031\353Z\020\313\221\201W\355\343.sZ\220\260A\216\365r3\232\271\021\253\221\232\265\033U\270\332\256F\325n6\253q\265\\\215\372U\370[5z\'\253(sO$\366\246\204\346\246E5:%Y\215*r\273\220\212\307\234\025\227\217Z\320\266\237\200\255VP\202\306\247\024\242\202j2k\342f\025\013\256ER\225pj\022\265\023\036\271\246\253v4\036\264\323Q\260\250\232\241aP\270\250\034Uy*\244\206\252\310j\244\206\252\271\305@\335i\005B\335MB\324\312c\036h\024\365\2531\325\310\205\\\217\222*\3645\241\rhC\332\264\"\346\254\216\005K\t\3475ad\313`V\275\234D\201\232\327\215\202.\005O\023sW\242j\273\023qV\321\252tj\265\033\325\350\236\255\306\325r&\253\221\265]\205\361W\241|\325\304l\324\3523R\005\305J\242\254 \251\327\212\232.s\364\254\353\210\201r})\321\240\0035j\023V\001\247)\244\315FO5\361ST&\253\310*\253u\250$\353P\323\263\221\357HzS\032\243aP\260\250XT\022\n\247%T\220\325Y\rT\220\325g\250Z\232;\324-\336\230i\204TX\311\251\222\002\325!\210\255=\005Z\216\256\304:U\370E_\207\265h\300j\364b\245-\212zK\205\253\226\274\260&\266\241\220\200\000\253\220\271\'\232\275\021\253\261\265\\\215\360*\312IV\021\352\334MWaj\271\033U\310\232\256D\371\253qI\316*\374M\214\032\277\t\357V\343~qVP\206\251\002\355\251\026\245\335\201K\024\303\232\245qq\202\331\357L\212\1770\205\025\243\027\002\246\315*\236iI\353Q\232\370\271\226\240aP\270\315T\221H\317\245U\177Z\214\212fpi\364\204TdTL*\027\025VJ\247-S\222\252IU^\253\275B\302\233\332\2414\334Pc$Tay\253\260\201\306jFPA\305F\253V#\025r!W\241\253\320\326\2045v6\300\247;f\233\031%\200\255{a\2001Z\220\267\025v\023W\242j\267\033U\224z\263\033\325\250\332\256\304\365n\'\253\261\275[\211\352\344MV\324\364\305^\267\2238\006\264\340|qVP\374\325a%\305^\211\204\213N+\264\373S\325\014\247\013J\366\376R\234\034\361X\272\211*F*]92\0015\256\235)\333\250\r\3158\2650\265|j\353P\262\324,\265\004\211\220j\204\313\216\225\\\323\030R\253v4\374qL\"\243aU\344\252rU9j\234\225VAU\\Uw\025\t\024\323\322\242\"\205\\\323\312\361L\331\315L\203\024\346n\324%X\214U\250\352\364=\005^\203\255^\216\254\253b\236\032\226&\303\214\326\265\273qZ\020?j\277\023U\310\332\255\243\342\247G\2531\275\\\215\352\334oW#j\273\023qW\"j\275\023t\253\2617J\264\234\034\212\275o!$V\202\265L\206\254E!C\221Z(D\210\rIn\n9\367\251\'\031J\306\324\340$\006\247\331\r\250+@\032\t\244\006\234O\025\021s\234\036\265\362\003-FR\243h\352\007\217\212\316\236>MTe\301\246\025\246\355\3475&8\246\225\342\242qUd\252rUI\005T\220UY\005V\220Uw\025\013-4\255D\313\3159\006\005;nW4\230\246\223\212h952\n\263\030\2531\365\253\261U\350{U\350\315M\232x<RFI\220V\275\273`\n\275\023\362+B\027\253\221\275XG\253(\365j7\253Q\275\\\215\252\344OW\242|\325\270\236\257\302\365v\'\253\261\266j\334M\202+F3\275x5<G<\036\265>p*{{\222\204/j\322I\200 \372\324\3228#\216\365\235\250\260\362\300\250mxQW\003Rn\245\315.\352%Q\267x\355_#\262TE1Q\225\250]*\224\361g5\237\"`\363P\225\246\225\247\017J\030qU\344\252\222UI\005U\220Ui\005U\221j\263\255Wu\250\212S\031j=\2314\360\231\340R\274ei\241)\222\257\034Th\265e\026\254 \251\343\034\325\330\252\3545r:\237\265*\032\222 \004\2315\247\t\343\212\267\033t\253\3617\025n6\253(\325b6\253q5ZF\253q5^\210\346\256D\330\253q\275]\211\352\364Rr+B\'\253\221\232\275n\3705qFy\035j\300\345pjkU\035\372\325\254g\247j\236\',pz\n\316\324\244\314\241i\320\034(\251\367Q\272\200\364\355\324\273\262\214\t\342\276Rd\250\214t\302\225\004\213UeL\326t\361\363U\331)\205=)v\2201Q\222T\340\364\250%\252\222\n\255 \252\262-U\221j\263\255Wu\252\356\265\036\332\215\222\221S\034\324\210\203\322\234c\315F\321\342\242e\317Zb\245N\213S\250\002\246\214d\325\270\305\\\212\255\307V\0074\252>j\220\374\244\032\277n\331\025n3W\342<U\230\332\255+T\361\265[\215\261Vcz\271\023U\370\037\025y_ T\350\374\325\310^\257B\375*\374RV\204\022\014\001V\243\223\232\320\212L\212\264\255V\"85e[\232\231[\0035\215;\371\227\'\332\255\306x\2517PZ\223u8=9[\203_-\236j29\2460\025\021Pz\325yS#\212\243,95]\355\375*/+\035i\254\265\004\251\306j\254\202\252\270\315Wu\252\316\265Y\326\253\272UgZ\201\326\243+Q\224\311\241\223\260\247\252b\234x\250\\\346\243)H\027\025*\n\223mX\211qV\220U\250\352\334uaE?\035\351G\315\305]\267\\\001Wc\253q\232\265\031\251\321\252\324f\254#{\325\230\332\256\304\365v\026\253\250\374T\350\374\325\330Z\257D\365z\'\253\2615]\215\352\344S\205<\236*\364R\206\344\032\262\222b\254$\231\247M8X\2175\231\021\313\226>\265y\033\212v\352M\324n\2405H\255_1\025\250\312\324l\265\023\n\211\327\212\256\311Q\025\333\332\240t\311\250Y*\t\027\212\245\"\365\025U\306\r@\353U\244Z\256\353U\335j\264\213P2Te)\004tyy4\273*6\\\323<\274\363OX\367R\233zO/m=V\246AVPU\224\025f1VR\244\305 \030j\277\017J\264\206\254\304j\334f\246CV\024\342\245G\2531\275\\\211\353B\007\343\236\225ie\315X\215\363W\242z\273\023\325\370_\245^\211\352\344oR\003\271\261\232\323\200\225P\005[V\251\267\0208\252SL\354v\223SC\300\025d5.\3727Q\272\22459Z\276me\250\212\324l*&Z\211\205B\313\326\242d\250Yj\027Z\253(\252R\202\017\025M\371j\215\226\253\272\325wJ\257\"UvL\232\211\322\242\331\223Jc\300\244\tA\216\233\345d\322\230\251V,\032\231\"\315#Z\363Q\030\n\366\247F\234\325\225Z\235\026\254GV\024T\242\223\034\325\2503\212\271\0375f>*\312t\251\220\363V\024\324\250jt`*\334-WRLU\230\3375j\'\301\253\360\276j\364.\001\031\253\310\300`\203Waz\275\033\361Vm\206\351FkMxc\355J\267J\247\031\251\305\300\333\234\325r\333\3375e\016\005I\272\227u.\352]\324\241\251\352k\347\246\207\212\201\243\250\314u\013\2475\013\'\265B\353P\260\250XT\022\n\251 \353U\312d\034\326s\256\030\212iZ\211\322\240d\252\322&N*#\026\005B\351\236(\020\200=\351\217\035\'\227\212<\2726zR\371t,y52E\203S\371{\227\336\230\326\371\355Q\233r\247\212x\216\244T\251\025pjt\025 \004\324\210\207\251\2531qVc\253h8\0252\364\251\022\247S\300\251W&\247\214z\325\244|t\251\343z\271\023zU\3049\372\325\270d\305]\211\352\364RU\350^\257\305%^\265\227ksZ\261\220Ww\255S\272\217k\006\007\203R\303\222\006MXQ\212\2206)\341\251CS\203R\346\234\032\236\246\274\r\333\002\253\261\250\331\261P3TMP\262\346\240u\367\250\035N*\006\004\216\225ZD89\252\344qT%O\336\032n\312\215\226\241t\315Bb\357PH\230\246,<d\3222\034\323\032>:Q\345\361Hb\243\312\366\243\3134\242<\032\220-9x\251@\004PR\233\260f\245H\201\024\361\020\251\025\000\251UG\245J\0234\021\264\200*\314c\246j\332\216)\352jU\251\324\360*t5:5J\246\245F\253\260I\212\273\033\346\255\306E[\214\325\310\236\257C%^\211\352\3442a\205lE\'\356\361Q\334I\362\250\245\205\361V\203\346\2245?u(l\323\203S\203S\303S\201\257\004qP\271\250\035\352\273IQ4\265\023\313\305Wi*&\222\231\2734\307\\\212\252\311\212\253:\014g\275C\214\212\215\226\240n)\215\310\250\032=\306\244\021|\265\031\206\230c\244\362\275\250\362\250\021\212_&\223\312\244)\212P\225\"\255?o\024\322\234\323\324b\236)\300T\213\301\251\327\326\243\335\271\352\304g\221V\225\252E5*\232\225ML\206\246S\306jEl\325\2045b3W\242n\225r6\253\221=\\\214\325\330\217J\271\023\325\244~\365~\332\344\343i54\222n\024\350\332\254+\324\241\251\341\251\301\251\301\251\301\251\301\252Ej\360\211\rU\221\252\244\215U]\352\026z\211\236\242f\246\023\232r\212y\\\212\202H\370\252\222G\236\265\003\304T\373S\032<\212\254\361\020i\2062h\021\340R\355\342\243d\250\331(\tHc\244\362\361I\202)z\323M\"\216j`\005;\024c4\230\346\235\212x\024\341R\216\225]\270\223\332\255\305\332\255)\004{\324\253R\255J\2652T\240\372\324\253S#U\210\315[\215\252\344MW#j\273\023\325\350\232\255\306\325e_\212\232\027\303\n\272\357R#T\352\365*\265J\032\234\032\234\032\234\032\236\032\236\255\315x\\\246\251\311U$5Y\315@\306\242&\232E&)V\246Q\305+G\362\232\241\"\340\325yNF)\241\306\323\270T$\206\351HPb\232R\233\345\346\220\307M0\346\232\321\342\243+\212i\250\230\3233M\335\212i\220\n<\352p\236\244YjU9\247\250\251DD\364\247\213f\245\362\266\3655\033\305\236\2254Gh\346\246C\310\253IR\250\251TT\312)\343\212z\265J\246\254Fj\334g\245[\215\252\344mW\"j\275\023U\330\216j\322\216*x\370 \324\373\363R\306\334U\204j\231Z\244\rO\rO\rN\006\236\r<5xt\225RZ\247%Vz\201\2522(\305.\332\002\363VR?Z\216\346@\243j\325\006\250\035\t\250\231\030\016\224\301\031\317\"\237\266\232\302\232\0074\2458\246\221M`\030Ui\001\006\242\'\025\003\232\205\237\025\033IQ\027&\22015*\006c\300\253Q\333\310q\301\253q\333H\007\3355e-\237\031\332i\342&\035\2158\254\200t5ZF#9\250\222|\036j\310;\271\025*d\n\271\031\340T\340T\242\244\006\236)V\246Z\231*\324f\255\306j\334f\256D\335*\364M\322\256\302\325v6\315\\\214\003\031>\225\t\233i\251b\270\025n93V\025\252e4\340i\340\323\301\247\003R\003^+\"\3259\227\255Q\220u\252\316*\002)\204Q\212Z\232\030\363\311\245w?\303U\232&s\223J-\375E\006\334SL\000\366\250\244\200\016\325Y\223\031\250Xg\2450\214\032p4\326\3053\245G\"\203U\\b\252\310*\273)4%\273Hx\025m4\247n\306\255E\242\271?t\342\265\255tE\030$\001W\377\000\263\242U\343\255=mbP8\247\030\223\267\024\326\265N\242\201j\230\346\250]X\214\235\2039\254\271,\212\234\343\024\350\240}\330\305h-\266V\244HJ\365\253\n\274T\241x\244<P$\002\244\017R\253f\254Fj\312\036*\304mV\342j\271\023U\330[\245]\210\325\310\332\256B\340\002\017z\204\304\314\307\236*E\201\207\275X\2100\351V\243\223\326\255#T\231\247\003O\006\236\r<\032\361\347J\2472U\tV\252:\324\014)\204R\021M5n\022\014|S\322\035\334\323\314@v\246\230\352&\\TG\255\014\241\205U\232.\rS\333\202j\0318\250w\363K\234\322\212cT\016\231\250\014\005\317\002\245\217N,\303ul\331\351\n0H\255d\260\215\000\342\236#U\340\nF8\246\022MF\331\246\362)7\237Z\004\236\364\355\343\275C2#\203QG\030\317J\262\024\001\236\364\207\031\245Z\220t\250\035\300\004US!\315M\034\276\265n6\310\253Q\265ZC\232\235\rY\211\252\344mW\"j\275\023U\330\333\245Z\215\252t\346\254%K\264u\035i\244\367\035EK\024\231\253hr*AN\024\361O\006\274\226E\252\223/\025\237*\3259\026\253\260\346\243\"\232E4\2415,D\250\305_\200eO\024\346\004\236)\276Y\357P\312\270\252\376]!\\SfO\220\326pB[\030\246\334A\204\310\025\232F\032\244QN#\002\230FjH\355Z^\202\264\355\264\262{V\202YG\037Q\315M\200\275)\215%1\237\322\243$\236\264\224\322i\204\323MFN)7R\026\240>:S\274\312PjE5 <Uk\210\333\250\351U9\035i\312\325j\027\307Z\275\035\\\214\234sS+U\230\332\256Fj\344G\245]\211\272U\330\333\212\265\033U\250\332\255F\3258\366\244x\311\345z\324`\0259\253p\311\221V\223\232v1N\006\234\255^W%S\230U\031\207\025FQU\210\246m\315K\0349\353S}\234c\245:;M\307\245\\Kr\203\030\2462\200x\245+\201U\234\014\363\315D\330\003\201P2\346\227fF\rD`U\344\n\255r\000\214\326)L\261\251\025qC\220)\221\251v\300\025\277\247\331\034\002\303\025\247\263`\300\250Y\252&&\243\3154\232i4\322i\214i\231\244-M&\230i\271\244-NSR\003R\003R\006\300\247\3440\301\2523\306\003eEB\005O\0305~\002E^C\305=j\314f\256Fj\344F\257Dj\334mV\320\325\250\332\255F\325j3\232\231i$L\214\325p\333\032\256\305!8\305M\311\247\000GZp\257.~ES\226\250\313T\345\025\\\216i\361C\270\364\253\211\016;U\210\355\363\324U\330\241U\035\0056\343\n\274U\0227\034\323%<qU\230f\242aM\305(\024\307\\\212\317\272S\264\326[\020\246\2432zS\222&\224\360+Z\306\304\002\t\034\326\332\3425\300\250\244\2235]\230\323\r4\221L&\243-L-L-L-I\273\024\322\364\335\324f\2239\247-H\r<5;4\345jl\204\032\210(\31752\001Vc|U\244\222\245\r\232\263\033U\270\216j\354g\025r#W#5n3Vc5i\rZ\215\252\302\232y9\025VU\301\247\301!\007\025u\0375(j\007^+\313\244<U9MT\222\252\310*!\036Mh[\301\201\234U\225\213=\252dL\nq<{UY\316\343\201\332\242\333\305C \250\030Svf\230S\024c\212\215\306*\205\3361Y\023\250\'\345\346\231\024\005\210\310\255kx6\340\001\212\325\210\010\320b\202sQ\265FsMcP\263Tl\325\031j\214\2654\2650\265!ji4\204\321\272\2245(jpjw\231\212p|\323\303R\026\024\200\363R\253\001\324T\361\310\t\025e*\302\n\260\234U\250\232\256\304sWb\253q\232\267\031\253iVP\325\2045e\rH\r5\306G5]xz\275\031\310\340\324\240\323\324\327\226\311U$\252\262T\004f\247\267\203q\316+M\"\332\240S\266zP\303h\250\334\361\216\365Y\2074\323\300\252\322\236qQ\3434`\ncS0*\0311\315P\226#)#<T_b\000\363V\342\265D\000\342\234\355\260\361\214\323\321\313\032\224\360*\031%\n\t5J[\325\\\363U\216\242\t\347\245\037o\214\322}\241\033\241\243x=)\206ALi*3(\246\371\264\307\237m@\327\200t\250\215\343\036\224\365\272jx\270cS\244\247\034\323\213\023OV u\2517\232P\371\251\343\367\247\367\251U*\314d\216\265m=j\302U\210\352\354]\252\354Un3V\3435j3VP\325\2045f3S\nq\031\025VE(\331\02542U\260r*E\025\345\222\032\251!\252\315\315\010\233\230V\234\020\355\025c\024\207\201\357Q1\342\240s\324\324-Q\310\330\252\262\034sQ\027\003\2554\315\237jkJ=j\007\271\003\245R\271\272\003\241\346\243\206\350\036\365ed\337R\006=*\'\0074,\273?\n\251q\251\225$\003Y\263_\273\367\252\215#\267SM.GZa\227\322\201pGzQz\313\336\246[\364?z\246[\204q\301\246\271\003\221P<\307\265U\222V5\017&\246\215\t5m#\030\251V1R\252\323\302\212\177\035\351\014\253\234R\207\031\251\221\352\302\266MXJ\260\230$U\310\207j\260\023\214\212\2321\203W\"\355W\"\253\221\325\250\352\324f\254\245XJ\260\206\247S\232\220sL\221r*$\0305n3S\003^U)\252\222\034\324[rj\345\254\005\233\245i\210\266\212B\270\344\324NE@\354*\273\032\211\217<TL3\326\253L@\2522IP<\270\357U\236s\330\325y&n\325RB\317\234\322\300\010`+^.\000\251\2529\016;\325\033\213\225@@\353Y2\271s\232`J\016\005W\222AU\332J\211\244\250\332Jn\372U\235\220\360jU\275q\324\346\236/3\326\227\317S\332\236\222\'z\225$L\361VU\301\357R\006\003\275)\235W\2750\334g\356\320$8\347\232\003sS!\253\t\323\212\261\031\253Q\232\265\027QW\243\343\007\265[A\221NN\265n*\275\025ZJ\265\031\2531\232\265\031\253)S\255N\225*\322\260\310\252\303\206\305N\207\025aMyT\2035\027\226X\361SEhI\344V\224\020\204\035*f\340sU\235\262MV\223$\324,\246\242~*&5\014\207\000\346\263.%\3118\252N\365]\3335\021\353N\n\034`\323M\267>\324\242\r\274\212\235\034\255;\317\342\232\356XqY\362\332\263\234\365\250~\312GQQ\310\205\007\002\263\347v\006\252\263\032\211\230\324Li1\232pBi\336Q\243\3114\322\230\245\tN\013\216\364\360\017cR\2430\357Roj\006M<\034T\212\325:T\3501S\243m\342\254\241\315Z\214U\310\305^\217\221VS\245=z\325\230\352\344F\256Dj\334ua*\314f\254\2475a*e\251\224\324\202\241h\362\331\025 N=\351\352ppk\314\266n5f;lu\034\325\250\341\305M\267\002\241\223?\205C\345g\223\322\241p\240\361U\337\212\254\3475\013\266\001\315f\334\317\270\220\247\212\240\355P75\033\na\024\200\342\232\327\005j#r\344qP5\313\203O\216g|U\264}\243\236\264\3573\'\245)\301\034\325y\242\3348\252\022\330\263d\325\tm\031{Usl\304\364\246\233F\364\2446\305\006H\244\001\207j\031\210\246y\236\246\227p4\322@\243u=ML\206\244\342\214zR\212\221jt8\251\225\252\302|\334T\311\225<\325\350Nj\344g\326\257BF*\324~\2250J\2321\212\263\035[\214\325\310\315YCVR\254!\305XCV\026\244\007\025\"\232\\s\232\224\014\212G_J\363\310b\311\253\313\026{T\2420)\257\201P\2200Y\272U\013\233\241\321zV{\316{\032\256\323\222y4\323.{\325;\211\267p\275*\213\363P\260\250\210\250\332\231\365\250\335\2608\252\315\315>%\365\251\232\335[\266)R\035\2751N8\007\223L/\375\334\032\215\213\372\322y\244pM5\3468\342\253I.z\212\256d^\324(\311\245p\270\347\025VIQx\025Q\2432\234\203M\373&:\265\006\334\216\206\220@\304\324\211o\357R\224T\036\364\300rx\251\000\247\201NU\317J\225P\324\252\2652-L\203\025j2\017Z\267\020\002\256\304GCV\343\307j\266\225a\rL\242\254F*\312\n\267\021\253IVP\325\204\251\320\325\2045.x\245W\251\224\346\245^:S\310\334+\202\206,b\256\252`R0\252\362\270S\317J\314\274\273\317\312\275+-\344\311\252\362IPn\315#\023\212\256\365\003TMQ0\250\332\242j\256\346\241=jH\363\326\244iO\255F\323\036\334S\001$\362i\031\310\350j=\355\330\322\035\346\242e\220\372\324N\204\014\271\250|\345Rr*\031/\030gh\305T\222\351\333\275W23\032\2269\034p\rN\245\217Zx\311\247g\024\231n\302\201\0339\346\245\020m\353N\tO\tOT\"\244\037J\225@\251\227\035\252U\251S\212\265\031\253Q\232\271\023U\310\332\255Fj\312U\250\371\253\010*x\3705r3V\022\254!\253\tS\245H[\212@jTj\262\255\305H=\253\377\331"
+byte_png: "\211PNG\r\n\032\n\000\000\000\rIHDR\000\000\002\000\000\000\002\000\010\000\000\000\000\321\023\213&\000\000\010\026IDATx^\355\335\333\266\243(\020\000\320^\231\377\377\344\311\232\311\255\317\211A\243\010J\301\336O\335&\022\254*\020r\351\376\363\207q]\247\007\000\000\000\350\323ez\000\000`\216w\216\001\000\000\000\000\240y\276\007\320;\037\330\000\000\000\014\3046\037\000\000\000\002\261\221\357\231\354\002\000\000\000\000\000\000\020\224\237\247\237O\016\000\2003\370\352#\000C\370\262\351\256}?\254\335>\000\000\000la\237\272\326\2277\024\2003\031\240T\241\260\000\200\026\014\277&\271\016\037\001\250\301\300\002\000\200q\244\277\022p\271\244\217\263\235\035\026\000\000\020\200\255K\232\270\220\2440\350\225\332\006\000\000\000\000\000\000\200\306\370y\017\000\300D\344\005\222oj\026\220]\000\201\242\237}\215\335)\023\211@\251\007\240Weni\014E\321@P%\266\037\327\177\247G\3063\356$x\235)\242q#RV2\270-\222pT\001\037\224\004\014\305\220\007\310Sl\323\227?\021\347\237\311\016\037a/V\nG\t\327a\000\240\214\217e\014\000\000\0004\315Nv\007\237\005@\017\214d\000 \213\275\024@\030\246l8\235a8\250\265\211_\373<\000\000\200\265|\017\000*1\270\000 &o\304\303VF\r\0000 K \000*q\2139\316\330\261\366I\346\237\177\246\007\016u\271\304*@\005\223ic\340bU\305(nY\t\233\231\260\035o\207\020\016@\222a\025Ce\233\237E\340\306\345`\253\024\300\031\352\025\317\346\226\317.\200g\2077\367\033\316r/V\025\013\035+6\300\2135\304\261v\254\215\2469\23745}\030\350\336\216\t\205\346\310\346\300\336\223\177\335vCO=;u\254\022\205;c)0\357\371\271?\363\357\323\227\316\333\250`S\354\224\316E\372\350 \016\234\2456(\220\222\307{z\253.\257\300\253\235*z\377w\233\317\362\374#w\303G\256o\337\322\373\355\361\275\276T\037a\234\236\311\332\245\332\261\323s\027Q\007AK\r\231\313\333\205\335\277\352\237zZ@M\\F\023\235xIv\246\334\257;\236\205\264k\240\354:y<\305r\307\010\272\036]?\027WnJ\353\315\257\002\350\272\026\000xi\366\226X\362>\324\354E\236huLJ&\342L\253/x \367\334\326\nLf\273\275\324\333\350.\271\005\020\226\312\205\035F\2330\000\206\222\275Jrw\250Fh\271d\017\3142\376\326\340\311\375\240\0029\005\240\tV\274p\234\357\343\355\3733b\n\264\364m\246\253\315t\204|\257\001\375\1772o?/~\3765\177\230\'\213\"\2779\340T\311\021\r\000\014\315\372`@_\222\036{\303\367\345\342z\365\223\264\330\351\353\234\344\214\315\017\3449\314\255\326\006\275\035\002\360#\275\364x\334 \334&\016\220N@}9\257\233s\016\007\311\034\255\263\247\315>\300(\214\367C\031q\000\000\235K\254\257\217Z\003\336^:\361\362ws\307\227\034\325\357~\345D\035\000\212r;?E;ao\247\'\024$\255\300K\353\363\201MqE\333\223/\035\345Mc\272=+@H\323\301\177\214\274)&\357,\032\2646\225k\237G\001\347\314\005\234\312\010\033\314$\341\341\006\275\202\355LN\005*\202\200$\215\202r\346\r\326\020Y\240M\226\021T\244\274\356\002\205\241\314r\245L+\r+\221\321\022m\324\327}*\363\304H\036\000\000\300\273\373^\346\366\357V\333\353m\323\326&pO\366\236W\262\247\t\222\202\204\364\270R\016\022\020\000\030\213\033t1\307\255\252\226Ii\023Z)\207\251\352\375R\177\215\223\240\261]\025\300\030\344\231e\325\027\003\023\275Wd\357\327GS\224\333fG\317xU\225\310\177W\001\001\322\322\003\3752\367\000\235\221f\036T\302\340v\177\003\354\262\343\\\032\223\223\312\234s\310\'\336T\245\300:\363\270\307_[^\353\265\334\267\320\226\003[r\250\337~I\002\214hy\236!\214\334D\346\236G\'\024\000@\024\211\031\273\3517\t\250\344\261s\177O\275B\030\234\002(\372\366\0304\250B\211Wh\222\352L\367\274k\254\"L+\301\224I\330O+e\332[\322X\305\237\353\025\214\374\240\024M\235w&\026\255\213N\201<\254t\334+\361\260\256\002\310\"\270023\000c\353\177Ig\214\337\t\303\340f\n\340\232\271\007\337=q\354n`^\305\246\343\232I\263X\3018f\246\001F\241\000\000\370\253\231]@3\035\031\324\001?\370\336\370\n\326+\017\033\303\026@\177W\004\001\031\210\320\260R\003t\266\235\331\007\0060\304\002s\335E\256{\026\364c\300\251\3170\347I)\3600\340<\310/\362\017\034\312\244\003P\313\3103\254\315\355J\267\"y\025J\253A\363\177R\325\326j\346\001\000\0006zno\326\356r\326>\017\000\200#X\235\215\300\'>\207z\206\273\335\261\365\3363\325Q2S%\333\002 $\267\002\000\240+\3365H\213\037\227\330\313\326\370\361\247/\261\307\023@\014\346Z\232\024bY\372\326\311\020=\216\342o0MP+\tT\033^\225k:\330+^\004\357\377\213E\274n\003\005\271\027\227 \212P\2261\005{X\3363u\275\225E\370\302\010\177\001\'\022;\240W\366\r#X\270\213)\000\200w\013S\346>U\032N\314\342\211C\213\266>\277\204\353\031/\332\233IA\025\ti\221FV\2512\034\006\261\030\273\305\007\233\361\253\320bt\270\005\223\321\371\036\270\2310~\214\350\327\201\217\007hV\361\\\335\277\330\004\204\264yB\230\216\367{\003\323\2034\346\232\221\351:\224\312I\236\201\377)\203\234L\334\316y\235\367j)\247\035\372qidf\341`\006>\360\303\2140\n\367\374\215\014\r\030Q##\377\327\367\320\357\223\267\217oZ\223*\224\337\331I=\276\305[\332\211D\326F\267\267\002\366N\036@4\257Qo\364\277\023\217\256\364\234\316\2757\376hF\273\336\257\366\006\344q\376\3077\345\247\177/-{L\326\356\330\313\257\016\036\365\222\247h\372\342\026\252\244\351~\227\262p\375\364m\210\372\246\032\365\003\301\275\177&\367\366\207\rr\316iC\334\236\307b\235\231\253v\205^sr\263\241S\031\2553\006\245\001\000\000p\246*\273\262*\215\022\301\206\367\212\032sV\321\306\215X\302\245\257\313\201\263\2345\037m\027\247\247\241\010+\320,\377\034khU\263W\265\361z\202v\273\025\327\217\355\354\256o\232U\2605\301[\237\017\254\031\354FV\347$x\237\250\361{\374\3703j\357\331l\315l\017\324\267b\3325\\w[\021\345\272\016\313\341\351W\312@\016+kj*\232F3\320\340\024\300\331d`\352m\206\353?<>\351\345\215z\340TE\227\230\305\233\343\253\3523\310\363\277\240\370\370\247(\327\252\336A\362\345&\265\260 \353\242\030\275<Y#%\005\361\230a\000\330\343\324E\330\334V\261\263\233\333\221\2273FDYo\246\"\200.|\033\341\337\036\007\000j\330\260\375r\263\036\333\206R)h\356\235\030 \251\346@\255\3316p\276\364\030O\037=\312\271\257>\246I\314\367\245 \347\354\337\337\251\3139\177L3\221Z\273\216^\216y\342\320\321\326^\010o.{\343\266\367\374%\305\276=[\252\235\n\372\330\311\026\313\324/\345\002\363\210q\271\366\272\262=sa\003\031\266\343Um/\200C|&\253\321\216\306St\t\377\'\225\252]\n7G4\351\002\330]\246p\223./\226\030|\203Z\231\370Rc*\361r\245\232^\345\320\027\013\344\371c\371\300\266\365\277\306\033\032\215\032\347J\001\236\252M|\026\021P\201\201\005g:{\004~\274\376\307\201\031\033o\366\033\237\036\337\177\034\3615X\n\320m\016\000\000\000\000IEND\256B`\202"
diff --git a/core/res/geoid_height_map_assets/tile-5.textpb b/core/res/geoid_height_map_assets/tile-5.textpb
new file mode 100644
index 0000000..0cb5489
--- /dev/null
+++ b/core/res/geoid_height_map_assets/tile-5.textpb
@@ -0,0 +1,3 @@
+tile_key: "5"
+byte_jpeg: "\377\330\377\340\000\020JFIF\000\001\002\000\000\001\000\001\000\000\377\333\000C\000\004\003\003\004\003\003\004\004\003\004\005\004\004\005\006\n\007\006\006\006\006\r\t\n\010\n\017\r\020\020\017\r\017\016\021\023\030\024\021\022\027\022\016\017\025\034\025\027\031\031\033\033\033\020\024\035\037\035\032\037\030\032\033\032\377\300\000\013\010\002\000\002\000\001\001\021\000\377\304\000\037\000\000\001\005\001\001\001\001\001\001\000\000\000\000\000\000\000\000\001\002\003\004\005\006\007\010\t\n\013\377\304\000\265\020\000\002\001\003\003\002\004\003\005\005\004\004\000\000\001}\001\002\003\000\004\021\005\022!1A\006\023Qa\007\"q\0242\201\221\241\010#B\261\301\025R\321\360$3br\202\t\n\026\027\030\031\032%&\'()*456789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz\203\204\205\206\207\210\211\212\222\223\224\225\226\227\230\231\232\242\243\244\245\246\247\250\251\252\262\263\264\265\266\267\270\271\272\302\303\304\305\306\307\310\311\312\322\323\324\325\326\327\330\331\332\341\342\343\344\345\346\347\350\351\352\361\362\363\364\365\366\367\370\371\372\377\332\000\010\001\001\000\000?\000\325\210r*\326\332c\257\245S\272\203zq\326\271\353\244*Nj\203>*&;\272S\243OZq\217\212\253\"Ug\025\021\025\033.*\"\265\023\212\254\313M\021\344\323\230m\340TMQ\363HN\005D\304\323w\221NI\260y\251~\320\244t\250\245UpNj\250\201[5\024\226\313\216*\224\220`\324~MH\251\212\260\210jeLv\247\010\363\332\236\"\245(\007|R\205\007\2758*\216\324\274P>\224\360=\251\340T\210\2652\n\235\026\247E\253q\255Z\215j\344kW\"J\267\032U\224\\U\204\025:-[\215j\312-Y\215*p\000\247n\354)\342\244\002\245U\251A\342\245^+\224H\360j\310N)\205EB\351\3075\215\250\332\203\222+\233\271\214\2515\004g\346\346\256*\361JEV\225j\233\255DF*6\025\003T.*&Zh\030\2465D\334\232i\024\302)\214\265\023-3i\245\333A\004\212@\215\332\227\313\343\232i\266R3P\274\000S\004C5:E\236\3258\207\003\232]\241z\322\023L\306\343NU\003\245<&z\322\371b\236\020R\355\245\013R\252\324\252\265:\n\260\213V\243Z\271\022U\310\222\256\304\265i\005N\202\254\"\325\230\326\256D\265e\026\254\242`PF)V\244Z\221y\251\207\025\"\325\210\327&\271\245^j\302\307\305F\361\324.\244\203\212\315\271\214\220w\n\347\357m\216N\005d\264e\036\255\306r\005+\n\255\'5]\222\240u\250\030TL*&Z\211\205FE7mFS\232F\\TdSJ\323\nR\010\251|\272M\270\355Ml\343\212\210\356\244\332\306\224D\307\255H\260\016\365(@:\n\010\250H$\346\223\031\245\013O\tO\000S\250>\324S\224T\2503S\252\324\350\265b5\253q-\\\211j\344b\255\304*\312\212\235\005Y\215j\324kW#\025e\026\254\252\361\315F\364-J\24352\214\np\346\254D\274f\254E\367\253\237)\212\221\007\024\025\315@\311\203Un\242\014\206\262\036\000\344\202+\026\356\334+\236*\262\246\332q\034T\016\265\023/\025]\326\253\270\250XS\n\324L\265\031ZB\224\302\224\306J\217m!ZM\224\340\224\326\300\250Z\233\326\215\236\224\345^\330\251\004g\256)\3338\351M)L\332O\322\232\313\212hQ\353K\201J\0058\n\\R\201O\013J\026\246E\253\010\265:-X\215j\324kV\343\025n!W\"Z\262\213V\021j\314kVcZ\266\202\254\247\025)l-E\324\323\305M\030\251\324qNQS\257\240\251\342Z\306)\236\324\340\230\024\205j\027L\325k\205\371Mf\204\371\3533P\207\014H\025\224\313\212cTn8\250\034Ug\025\003\255FR\230R\243d\250\212\321\266\223m1\222\230c\246\371t\322\230\246\232\211\206j\"(\013R*\342\237\200)~\224\264b\243lTE3\336\223\313\367\247\005\024\360=\005;fz\321\263\024\273i\333qJ\00752-XAV\021j\302-Y\214U\250\305\\\211j\354kVQju\025b1V\243\025j5\342\247QO\3054\256\r9EJ\234T\353\315H\242\254 \253\n0++m.\332aZ\211\205W\225A\0305O\311\352k#P\307#\275bH\274\232\211\2050\256j\'Z\205\324T\014\242\233\260\036\264\326\213\025\023GP4t\315\264\025\244+L+M\"\243e\250\231i\205i\205i1\212N\224\233\21581\247\002i\371\310\246\220\r0\250\244\331\232p@)\352\276\325 L\212]\224\334{Rn\\\342\236\005L\202\254\242\324\350*\302\n\263\032\325\270\326\256\302\265u\027\212\235\026\247E\253\010*\312\n\262\203\212\231jA\3158\256G4\320*U\251V\247\214U\250\327\035idp\200\346\251\021M\"\241\225\266\203T^\340\346\2432\231\010\024L\010\210\342\260n\306\342k6H\352\006\216\243d\305@\365]\306j\026\024\312z\034\360i\255\035@\321\324L\224\320\264\205i\245i\205j6J\211\226\241aL\"\223\024m\244+F\332p\247\204\310\250\330\020x\244\301\247\001OT\024\360\230\245\342\223u79\2441\344{\323\343\007\241\253(*\304b\254\"\325\210\326\255\306\265n5\253\221-\\AV\021jtZ\235\026\254*\324\353R\255J\264\343\300\246\255J\005L\213\221V\243\000\n{\316\261\257^k*\352\3739\301\253\246\230\307\025\237r\3475E\315\020\374\315\305Y\237\0011X\2271\362k>D\250\031*\274\242\251\3109\250H\246m\3155\242\250\310 \324\313\363\255F\361\324\r\035G\267\024\025\2462\324dTl\265\013\n\211\226\230V\223e.\312M\224\205iU1Noja\244\300\245\340R\206\024\027\250\313\023N^jUZ~\336iv\340\203S\3063V\221j\302-Y\215j\324kW#Z\271\032\342\255\"\325\224Z\235\005N\202\245\025*\232\221j\314c\212k\036iTT\261\256ML\\ \252\322\337\204\350j\224\267\245\272\232\253\270\312}\253\245<TN+:\340u\252R\016\rE\014\233d\253\023J\010\254\371\271\252R/5ZU\252r\257\006\2522\234\324e)\230\307Zi\344Te\t\247\306\2705#G\232\205\343\250\032:\214\246*2\264\302\265\033-D\313Q\025\246\225\246\355\242\220\212JF8\351Q\223\2323HO\245!\367\244\335\216\224\231&\224\n\225EL\2434\374`S\212\344\212\261\032\020\325m\026\247AV#Z\267\022\325\270\305\\\214U\230\305X^\005J\225:T\303\255H\2652\216jq\302\324}MJ\253S)\300\250g\014\340\366\025\223pB\344f\252\002]\272\325\350\227\000WHG\025\024\230\000\326e\311\347\212\244\307\255V\301\335H\354sP\2775\013-W\2213T\344CU\335@\250\231sQ2R\010\271\247\210}i\255\026\332P)\032<\212\201\243\250Y*\026LTl*&\025\013\naZc-0\256)\244SH\244\305!Zn\332n(\013\232k!\246\343\024\n\225EH\005<S\327\222*\310\2178#\265N\203\326\254F\265a\026\254\242\325\230\305Z\217\265Z\216\254\245N\265:\n\262\202\237\212\221j\302\017ZWn\302\221j`jU\246J\245\201\305a\336.\3065\005\270\346\257)\342\272\031$\n*\224\323\0228\2522\261&\252\266i\270\3438\250[$\323J\323\031*\007J\257$UY\340\315@\320\221M\020\372\323\274\254R\371u\033\246j\002\204\032Q\356)\0320j\254\221\324\014\276\265\013-D\313Q\025\250\330S\010\246\021L+M+I\262\215\264\302(\t\232x\216\202\242\243(\r7e=V\245\013\305\033i\350\2705r>EL\253V#OJ\267\032T\352\270\251\343\025j5\253(\270\253\010*\302\n\262\203\212\235E>\236\202\246\007\002\230[\232z\232\2206)\301\352@r+\013Q\220\0075\005\261\310\315\\\007\212\336\237\322\2522\325y\022\230\2109\334)%A\216*\261\213\322\233\345S\035p*\253\014\232c&j\026J\211\343\250\374\272B\224\335\264\326Z\201\326\230\026\215\265\014\261\367\252\256\225\013%B\311Q\262\324,\264\302\264\302\264\302\264\233h\333F\312iNiBb\224\214t\250\230\032m.)B\324\200R\201O\013\212\2263\203W\023\006\254 \003\025e\0179\253\t\311\253Q\245YE\253\n*t\031\353VQj\302\216*@qN\0075\"\323\213qH\274\365\251\001\002\223\314\317JUl\232\260[j\023\355\\\276\2416\351\210\007\275X\265\030QVI\256\226X\362*\233\251\007\245!\213\"\242h\366\324.3Q\354\246\262\325y\026\2520\301\246\221Q2\323\n\361L\333M+M)Q\262\324.\225\036\314Q\266\221\223\"\251\310\2305\003\255B\313P\262\346\242e\246\025\310\250\312`\322\024\246\354\240%.\332M\200R\025\244+M+\232aJM\264\340\264\354R\201O\307\024\016\r[\205\262*\302\236j\324F\256\306*\334hx\315[\215*\302GV\021*T\03056q@\346\245Z~\354R\016z\322\226\240\232E5*rE\027\223yp\266=+\224i\014\267\034\372\326\274<(\251\031\253\257a\232\205\343\250\031j\027\031\250\nS\n\324l*\254\203\255Ve\346\230V\243e\246\021L#\024\230\244+M)Q\262Te)\214\265\031\340\3242\307\273\232\252\311\216\265\013GP\230\352&J\214\2450\255&\312B\224\233x\246\355\305&\332]\224\326\025\031\\\323H\244\305(\024\270\247*\324\230\243fj\304K\267\255Y\215sV\342N\225v1W\"<\n\270\235\252\302\324\302\235N\025\"\324\231\240ry\247\026\002\214\323I\315*\361R\306~a\212\253\253I\266\023\315s\226\243t\244\326\322\034\n\031\253\271+\201P\275Ts\203Q\036i\254\264\302\271\025]\324\203P:\346\2532b\242e\305F\302\243\333C-G\266\223h&\224\255FV\230\313Q2\324\0169\246{\032\202T\364\252\344Tl\242\243d\250\231*\026Z6\323J\321\266\232V\233\212R8\246l\3157m\006<\212h\212\235\345P\"\346\236#\245\331\212U\0375H\300\361\212\265\000\343\232\271\030\253iVc\253q\0360j\322\232\225i\364\340i\341\251\331\244-\212\001\247\nu(\251c\342\262\365w\314x\254\333(\361\315h\203\201Lf\257Aq\305Wq\305A\345n&\230\320\343\221Q\225\246\025\250\035*\006^j\007LT\014\242\242d\315&\312aZ\215\226\233\266\220\212a\024\3223Q8\305@W&\230\313Le\014*\007\212\240d\305D\313Q:\324%h\333HV\200\264\2333L1\322l\243e\'\227HR\224%8&i\342:_.\232\313I\262\236\007\255X\217\002\254F\325j3\232\267\035Z\217\203VS\232\235)\304\320*A\357J[\322\223\2558\nz\323\361\305\000T\313\302\223X\232\223y\222\005\024\220\307\261EHMB\355^\222\303 \324\016\264\213\036\325\317sQ\225\354j\273\256\323Q\232\211\305Us\203P\310\300\212\254\335i\204Rb\230\304S\010\244\333M S\010\024\302\265\033\246EWd\305F\302\243#m5\230b\242!Z\242x\275*\273\307\212\204\2574\233h\333M\305.)\n\342\232\027=\251\3333I\262\223\313\346\202\224\233qO\002\237\266\230R\220\2554\014\032\231*e\340\325\310NqWc\251\322\255F*\302\234\n\\\346\234\242\237\264\321\212p\024\340)\302\236:S\221w\032m\324\302\030\311?\205c\'\357d.js\300\250\231\252\006j\364\362)\247\2574\322sQ8\035\252\274\303\"\253g\326\230\334\325i\227\212\250T\346\230V\230V\232EF\313I\212c\036\324\303HE4\212i\034T,\274\324\016\274\324L\271\025\023-DS\024\231\301\250\244\346\253\224\346\223m!Zi\024\230\247\000\017ZpP)\017\024\302=)\240\034\323\266f\217.\224/\265=V\227m1\226\232#\334x\253P\301\201\310\251\032\014\364\247F\214\235E[\211\263Z\021/\002\254(\305<t\247\n\225EH)qF)@\247\201O\305=\230D\204\232\302\274\2717\022\355\007\212\2225\n\242\2075]\315B\306\275T\216*2)\206\243j\211\207\025M\324\202qQ1\250\233\232\201\222\243e\250\230TD\021M\357JG\025\004\213H\007\255!\244#\212\211\2526\250\335r3P\225\305D\302\242a\305D\303\025\023S\010\244\305\005j2\271\246\225\244\305\030\315\000{\323\200\035\350\300\317\025\"\256i\305@\246\021@\024\255\300\250\031\262j\325\274|d\325\221\307J\221\005X\010\033\265H\220\000sV\221p*e\247\201N\002\244SR\212Z\\S\200\251\000\247\242\3675\233\251\334\355R\252k2\3352w\032\267\234\n\215\332\253\273T,\325\353#\2450\212i\024\302\264\307N*\253\2575ZT\364\250\010\2460\250\234T,*&\025\t\353G4\204f\233\214Rb\2028\250\312\372\324r(=*,qP\270\250\030TL*\031\005E\212M\264\233qA\034SqMaM\305\0053\322\233\267i\245#\212j\360j`h\316iqN\013Mu\250\322\034\265]Q\265p)\300T\350*d\030\251\324\324\252ju5 \247\001N\3058\034S\201\247\2575*\255H\005\023H\"\214\375+\234\271\223\316\227\216\225$ch\247\026\250]\252\006j\205\215z\332\236)M6\232N)\255\315W\221j\254\225Y\226\243aP\275Dx\353Q\265@\303\006\233\232v)\204sJ\005\014\274S\nqQ0\305D\303\212\256\342\241aL)P\262sQ\262S6Rm\244\"\233\212B\264\335\224m\244+\232n)\n\322\212u:\236\2640\317JX\306*QR*\324\350\206\254(\002\244\003=*E\025*\324\313R\001K\216)1\353R \251\2213S\204\307Z\216i\204JI\254K\313\343&@<UhT\236OZ\261\322\243f\250\035\252\026j\211\232\275q\rJ\005.\332\215\226\231\266\241\220u\252R\214\032\200\232\211\352\023P\311Q\324L)\201y\2511\3054\255 \247\036E4\257\034T2-@\313P\262\324,\264\312C\036\352\211\243\250\312\323\n\324dSh\244\315\035i\n\321\262\224GA\217\035\250\331F\332P(\247(\251TT\3121Rg\0254{Oz\235F:sR\nz\232\225\016jQ\322\2274\001\223S \253\000\205\0243\340Vm\365\332\200T\236k\025A\221\363\3335mF\000\241\215B\306\240cQ1\250\311\257\\\214\325\210\316MH\303\002\243\316i\255PH*\234\313UY9\250\331j\006\025\003\365\250\311\301\244\"\223\024\206\222\216\264\230\245\003\212\212E\250\035j\026\025\023\n\211\222\223\030\240`\365\2441\203Q4 \364\250\0319\3057\312\246\262\342\233\2126\322\355\247\010\351\341qHE7m&\332M\264\005\247\205\247\201R\n\220sO\013\351R)+R+\023S)\251R\246\335\201M\315H\206\247CN.;\232\257qp\241\t\006\271\371\244i\245\340\325\230c\332*S\305F\306\240sP\261\250\231\2522\325\353\360\363\305]\2120\0074\222\361\322\242Z\030qP8\252\322\214\325r\265\033-@\351\212\256\351U\331i\240Rb\233E(\024m4\355\230\025\033\255B\313\232\211\222\242d\250\312S\nSq\212i\024\303P\260\346\220-!\2174\323\035\001)\302:v\312B\270\246\342\215\264m\244+@ZP\264\340)\300T\212*QO\247\255H\225:\320O4\242\246Q\305H[\002\253\317\'\312H5\213$\256\354FN*h#\000s\326\254\343\003\212\215\315@\355U\335\352\027\222\241/\232aj\366H\276Y1ZJ\006*)y\250\361KP\270\252\356\271\250\031j2*\'L\324\016\225\003G\232\214\307Ld\244X\211\243\312\346\234#\247yt\245*\'N\265\\\2550\250\2462T,\225\031ZaJiJc%D\311H\026\224\241\246\355\366\245\tK\262\223\030\2460\3153m\006\234\007\024\005\311\245+F(\305(\024\361N\025 \247\255J\275jaJ94\354\201N\rMy1\221U\244\311\3435\003\332\356\031\217\357TA\214M\265\3705:\311\221H\304T.F9\250X\002\274TE\021\201\301\250\377\000v\271\365\246*&I\'5\354\261\304K\364\253d\020\265\016\t4\270\244\"\243~\225]\373\324,)\204sLe\250\035j\"\224yY\246\230E\'\226\000\243\313\024\276]\036]F\313\351Q2u\315@\311L\331M)Q2TL\224\302\224\323\0354\2450\305\232o\225K\345\323Lt\335\224\036\005BFi1\212i\031\246\260\245\035)\300`PM&(\240u\247S\305H\265\"\324\240T\203\245&H4\240\344\323\267b\240\335\311\240\234\322+\355<Uk\245\017\363\n\254\222\02585`>i\216\001\006\240u5\001R3Q\0255\031\316+\337\002*\364\246Hs\322\242\034Rri1Q\275Uzn3Me\246\021\232\214\246i\246*\004t\326J\217fivb\233\232v3\322\243(ED\310MD\361b\230c\366\2464u\023%FR\231\345\321\345\346\232c\246\024\305\n\240\236i\216\203\265DEFE0\256i\245qM+\353Q\2650\323\227\2458\n1HE6\212p\247\212\221jE\251\226\226\202h\034\323&|\016*\020\334R\202i\245\251\205\263PN\230]\303\2556)2*\\\344TmP\265B\302\230\302\275\325\230\223M\'\327\2553u&i\t\357P\271\315Wzh\353JFi\204S\225)JRy^\324\306\212\231\345b\230c4\301nX\346\245\021\000\005\006,\324f\034v\250Z,\236\225\033G\212\211\222\242x\352\023\035/\226)\245)\214\270\250\034c\245F\006)\255Q\225\3154\256)\207\330S\010\250\332\242aM\"\225y\247\n\\R\021Q\232QJ:\323\305J\274\324\202\236\016)wQ\234\323\272\n\212R\270\371\215D\030R\027\250\313f\231\234\032\224\'\232\244\032\240\352`\223\025:\234\214\3225D\303\232\211\252&\036\225\356\254\270\250\3150\232@i\030\346\2425\023\nh\034\323\261I\212\231Tb\236\020\032R\243\025\013\255Bh\t\232]\270\024\320\271\346\235\266\215\240\324m\030\025\033C\270dUW\214\203\322\242)\236\325\031\216\233\266\243a\315E%@\313Q\225\246\354\246\025\364\2462\201Q\265D\334\324dS\010\246\021M\007\006\244\352(\315\004\323\0174\200S\200\245\251\024\342\245\rN\315&\352p4\342\340\n\241p\344\26501\247\346\223v)\240\345\252h\337k\324\027\250X\356\250\341l\255Hj&\250\230TF\275\325\352\006\250\330\322f\2239\246\236\365\031\244\305;\024b\236)\340\322\265F\336\365\036\334\322\343\003\212M\271\247\005\243m\001)\031j\"\010\250]7v\2506\020\324\311#\347\212\205\223h5Y\205FV\243+M)L`1\357P\267\035*\027\250\2150\212i\\Tl2j2\264\303\301\245V\305;9\351Hi(\006\2274\264\345\247\212v\352\\\322\346\243w\305T\221\262\324\253N\3155\215,JpX\323\315%\313~\344\343\232\243l\374\220j\331\351Q\265D\325\021\025\356%\270\250\332\2424\224\235(\3051\205 \024\360(\333K\266\234\0058\014\324n\231\245\013\305\0333K\263\002\215\264m\315;g\024\306\002\242+Q\262\324,*6\030\252\262\014\324\016\270\250\312S\010\002\243`*\027\305@\325\013\323\n\036\246\223\201Q\261\346\2439\246\032\214\212f9\247)\245\315!\244\024\242\224\361NZ\226\212L\320\0335\023\344\344\325|d\323\205:\232\3252\377\000\253\000Si]wDEe\257\311.*\3709ZcTmQ5{fsHi\270\246\221\212JP)\n\322m\247\001N\002\214S\202\323\266\342\223ni\3018\244\333\212B(\3058-\0140*\007\025\031\366\250\230\361Q1\250\217\275B\342\253\260\346\243aQ7\025\003\232\205\215Dj&\034\324m\222i\245x\246b\230E1\205FE4\255&1A\244\315\002\234\264\032U\342\245\007\212\t\246\232\024\363N\342\253\310\230<S@\247\001H\302\244\204\344m4\2450i[\345^k\036f\377\000H8\365\253\310~QCTL*&\025\355C\255:\214SXS@\247\205\245\331K\262\223m.\3326\323\302\320iB\323\261HE4\255\001i\341x\246\270\252\356*\006\030\250\232\242aQ\270\342\240l\324D\376\025\023\232\201\315@\365\023\n\214\217Jcq\365\250\210\3151\251\270\342\232\325\013\032a4\334\321M<Sz\236(\247\n\\\361H\r<\032\\\321J:\322\323Xn\250\312\021H)q\3054\214\034\212x\224\367\243%\301\315d\314\000\270\343\326\264#\037(\241\205D\325\033W\264\201N\305(\024\025\315\001)\352\224\375\264\273i6sMd\247\204\244aH\251K\214R\342\220\255&(\003\232\220\014\212\215\305WaQ0\315W\224\343\201U\036LTfBi\205\261\326\243b\010\340\324\'\223P\311\317J\200\323\010\3151\205D\302\230ED\302\232x\250\336\241j\214\322\037j94\323B\014\320\303\024\320i\335\251)\300\323\201\247\nu)\034Rc\024\244pj\035\264\264\021M#\024\341\367X\326C\374\323\3765\245\030\371E\rQ5D\302\275\245\016E<\014\322\201N\0034\360\264\365\030\024\354f\200\264b\215\242\215\264\2052i\312\264\215\035.\312M\224\233(\330\007Zq\034qP\270\250\030T\022p*\204\362c\201T]\2114)\315+.F* \204\032G\025]\315@y8\244n\005Bi\246\243aQ\265D\306\241sP\261\250\311\246\203\223Rf\232}\350\r\203C\220E0S\373Sh\025\"\212x\247\001N\002\227\031\243\030\250X\037JA\326\235\212i\024\262|\2201\365\254t\033\246\255$\351CTL*&\257dS\201S!\315J\0059E<\nv3N\002\227\024\241h\333H\306\221Fj@\264\355\264yt\205qH\026\215\264\204Uw\025\021\\u\252s\347\234Vl\334\036j\253\002\335(D \324\342>3Lu\364\252\262\034\032\204\340\217z\201\206\323Mc\221P\2650\232c\034T\016\325\0135B\315Q1\250\3157q\355N\317\255\004\323sN&\201KE\003\255J)\302\244\002\224S\300\245\307\265\0057\014Ur\273X\212\\R\005\311\250\357\316\310p+2\325r\371\255 8\246\260\250\232\241j\366E\351O^\rZ^E<\n~)@\247\001N\240\361I\270\na\303\037JT\0252\212\220\npZ\ng\265FS\024\230\2467\025\013\016\365\003\325g@A&\251=\276\366\366\250\336\334/j\213\313\013A \n\205\330b\263\356\034\202qU\274\323Am\335j6\250X\323\013TL\365\0035D\306\242cQ1\246\026\246\347\232]\324\205\361M\337OBZ\244\351M\007\232x\024\240sR\250\315;\030\247\212p\024\360)\330\245\002\253\310\270z1J\253\315S\324\217\312\005U\264\\\n\275\216)\214*&\025\013\n\366E\034S\300\253\021\2361S\250\251\002\322\355\245\305\007\000f\243-H9\316i1R\240\251\007\024\340j\302-+\n\214\212\215\252&\300\250\034\325g\3115\003\324y\002\241\221\252\254\215P\263T\016\325J~k>F\330jDp@\2476*\007aP\267\265Ws\212\205\232\242g\250\232J\210\276i\205\251\241\251\333\2513M&\237\033T\244\346\205\034\325\2208\243mH\243\212v)TT\200S\300\245\305(\246H\2319\246\355\247\204\302\223X\367\255\276B\007J\222\336<(\253\030\250\336\241j\205\253\331\224qN^\265\"\037\232\256 \310\251\000\245\305(\025\034\265\0363O\2152y\247\024\000\346\234\007\034R\323\324U\244\340R\265B\325\003\236i\2147\003UO\007\223Q;dUv\340UWl\032\205\3375Y\316MD\306\240sUf=k.v\353Kn\341\207\275H\357\212\201\244\006\242g\250Y\263Le\315@\353\212\254\324\302p*2\324\335\324\340\324\271\246\026\247\304jqR\240\346\247\307Jx\024\360\264\355\271\247\205\245\003\024\361N\305.)6\344s@L\221Q]\270\2110:\326:\251\222L\232\272\213\201Jj&\250Z\241j\366\304L\212f9\251PU\250\316\005;94\345\346\236\005G\'&\221W\234T\244`qM\344\322\343\002\224\n\224\n\231\017\024\023Q9\250\017\255\034\021UYpOz\202CU\344\351Tf<\361P1\250\034\324D\343\255F\304\032\2550\3105\225t\207\223T\321\312\036*W\230\260\346\241\337\223C\270\307Z\256\317\317\024\3174\212C.j\0269\246\021\232\211\226\242<P\032\227u%I\030\346\254\245Y\215sVB\212P1N\024\341N\247\001O\002\235\2121J\0274\343\210\324\223X\367sy\256@\242\030\260*|b\230\325\013T-P\265{x_\226\205\2175 \\S\306E(52\260\002\227w\2457\2559\0079\247\023\305 \024u4\341\326\245\002\236:R\023P\261\315D\306\243g\307\322\242f\316qUdj\253#\325Y\rWcP9\305W\221\352\271\222\243w8\252S8#\004U1\215\324\222\201\216*\253\270CP4\271\351Q\2310i\215&i\236`\246\2313H_\024\201\367\036h\221x\315@z\323\205(\025i\024\005\367\253\021DO5r8\3609\025&1J\0058\014\323\200\245\002\244\024\352SH9\251\020`\363U/\246\302\355\006\263\242\217sd\325\300\270\024\326\250\332\241j\205\252&\257s\000m\241F\r<\n\\R\342\212QR\216\224\341\305\035M\006\222\236\242\244\024\244\340TL\325\016\356i\214j23PH\330\315T\221\352\254\215P9\250d8\252r\311T\244z\256\322s\305!\220\221\315V\227-U\330\005>\365\013\270\317&\252\315\317CU\217\006\242rsQ\226\3050\265\001\270\246;\322)=ju\223\214\032M\231\351J\027\024\340\274\325\210\306p+B \025j]\330\245\0074\372x\036\224\360\236\264\273pi@\245\244\357\305H\243\271\246\314\373W5\225!2\275O\034{E<\361Q\265D\306\241j\205\252&\257vQ\223O\013\315(\024\340\264\273h\333F\332p\024u\247 \240\365\240S\324S\307\002\230\315P;\324\005\271\241\337\025\031\224b\252\310\371\252\222>*\263\275WiqP\311(#\025FBI8\252\333Y\316)\031\002\216j=\303\322\242\221\205P\231\262p*\264\2101\235\334\325f8\353P\273T,\325\003\311Q\031i<\332\024\356<\324\243\212w=\251\351&\323\203S\207SN,;T\22075y\rH\005J\202\245U\311\346\247\003h\340Uw\224\253\363OY\225\273\323\203\002i\304\201M\363\000\245\r\232\212y21U\343\217\234\232\233\2651\215D\306\242cQ5B\325\023\032\367\244\\S\300\247\005\247\001K\212\\{Q\212gCG&\2348\315%8S\305)<T\016\325]\332\242\335\315A4\265\030|\212kt\252S63T&\227\025L\313\270\343\232^\325\023\020*\274\222\355\351\305Uy3\336\243/\357U\246\233\035\rg\315+)\252\257p\335\352\006\231\215F\\\232\211\336\240\221\352\003%&\363\232\225\036\246\017N\363\005*\266ML\246\245U\315X\210m5q*\302\363S%M\030\313T\300\214\32471\003\326\241\020\205\357FpqR/4\2059\241\334(\250F\\\344\323\372R\023Q\261\250\230\324Lj65\013\032\211\215{\341\247\255I\326\224\nu\024S\010\364\243\030\244\242\234)\340\342\243v\252\3625Wf\250\367`\325y~f\300\245\013\264TR=Q\235\305f\316s\234Ud\030j{6\005V\226N\0175FY@\357U^\340\n\256\367G\267\025ZI\262\0175I\3459\344\346\240\222L\324-!\002\231\347v\246<\234\324\022=W\337\315.\372\2266\315I\273\024\34595f4\316*`1S%L\255V\342l\212\267\030\251\2623R\306y\025ch\340\367\250\2475\016Gz\000\035i\305\325\005@\323\0268Z6\347\2559@\024\214j2\324\3065\023\032\211\215F\315Q1\250\230\327\320\000d\323\302\323\261N\024\242\227\024\224\323I\236)(\245\024\244\340T\016\365Y\336\241f\2463\0002i\261\214\363D\216\000\252\023\313\214\325\007\223uU\221\211\310\2507\001\326\241\232a\214\n\245$\243\234\232\314\232}\316pj\007\220\001\315U\226\340\016\365N[\237z\254\3679\357P\264\376\365\023OL\363rhy\006*\006\2235\021|\032O2\245\212Nj\312\275M\031\031\311\253j\343\034S\303T\252\365*\022\306\264aP\024f\254\007\300\342\234\246\254Fy\025h\0163\351U&\223\3465Y\244\'\245 v\247\034\221\315:5\301\251\030\342\231\272\232\315Q\026\250\331\252&j\215\232\243f\250\330\324lk\350UZ\220\014S\261N\013F\3321HE4\212\214\360h4\235)sLv\252\316\365]\332\242\3150\234\237j\014\201G\025Zy\2532yI\357U\267\032\206I\007j\252\357\305R\232oJ\245+\2229\252\022\270^\365Fk\214w\2522\334\023\336\252<\331\357Q\0313\336\230\362c\275E\277\336\2173\025\033L}i\206Zi\2234\201\351\351&\rYI\211\253Q\271\"\247V5\"\311S\243\325\313c\226\346\264Q\270\247\006\346\245SV\021\352g\230\210\316*\233\266T\223L\006\236)I\245V\346\225\332\230[\212\215\232\243-Q\263Tl\325\031j\215\232\243&\243&\276\214Q\305<{\323\300\247Rb\220\323I\246\226\250\330\212nsE5\233\025\004\217U\235\352\006z\215\237\002\253\313)\003\212\254\327\030\030&\253\3119#\255Ty\200\252\317?\275Wyrj\255\304\373W\255Pi\300\0075F\342\357\260\254\331\256I\357T%\236\252I-Wi)\003\323\035\351\201\263J[\212\201\232\230Z\233\270\323\303f\244S\315Z\216\255FqSn\251\020\346\254\306j\334\'\006\257\243qO\rR\253\324\310\365&\340\303\031\252\316\330R)\241\251\341\250\337J\036\202\364\322\374Tl\325\031jc5F\315Q\226\246\026\250\313S\t\257\243\326\236)\340\323\251\245\2050\2650\275F^\232Z\231\277\024\031*&\222\240w\252\356\365\003\275A$\234UYd\343\255Uy=\352\264\217U\244z\253$\225\001\223\255P\272\233<\n\241#\026\035j\224\307\025Bf\252R5Vv\250\031\351\276e4\276h\337\212i\222\230\315Q\026\2405=ML\255\212\261\033\325\204z\224=L\215Vcj\265\024\234\325\324\223\212\224=H\257\232\225_\024\342\374\361QJ\334\373SCS\203\321\276\224=\033\351\245\3522\364\302\324\302\325\031jajajaji5\364\222\323\3058PZ\243-Q\263Tl\365\031zizc5Fd\250\332J\205\344\250\035\352\274\222Ug\222\252\311-Uy*\254\222\325i%\367\252\317&j\031$\n+:\342A\234\3257\237\216*\234\262\016rk>iG5M\345\252\362IU\313\022h\372\232ajM\324\233\251\214\365\031z@\365*=N\255\221S#b\246V\251C\324\321\275[\216LU\204oJ\265\034\270\340\325\205|\364\251\221\252`\324\245\275)\254r\rB\037\236i\333\3517\322\357\243}4\2750\2750\2750\2750\2654\2650\2654\232Bk\351Q\322\234\r\005\261L-Q\263\324L\365\023=0\2754\275F\317Q3\324FJ\205\244\250\036J\255$\225VI*\254\222UY%\252\222IU\236Z\201\3445VY}\3536\346\343\255gIpMWyI\252\322d\365\252\316\r@\355P\263Te\375i\246Ja\222\220\313M2S\013P\rJ\206\254#T\352\325\"\265H\032\246\215\215[\210\346\255#b\247V\315X\205\271\253j\325 z\013\322o\252\356\330\177\255.\372B\364o\243}!zizazizajijijM\324n\257\245\305\005\261L/Q\263\324L\365\033=F\317Q\263\324fJ\211\244\250ZJ\205\244\347\255D\362T\017-V\222Z\251,\265U\345\252\262KUd\222\253\274\200\014\223Ud\234\023\305S\232oz\316\232\\\346\2523\n\211\210\250\235\352\254\222Uv|\324\016\325\0135FZ\230\315Q\226\305&\352P\325*\232\225jT\253\010jU\251\026\247\216\256F8\251U\252d&\254\306pj\312\275?}\033\351\013\324r\034\256Gj`~)\013\321\276\215\364\322\364\322\364\205\351\273\251\013SKSwQ\272\215\325\364\301jc=F\317Q\263\324L\365\033=B\322Tl\365\031z\211\236\241i*\273\311P\264\265]\345\252\322IU]\362j\274\257\201\326\251\264\234\362j\264\327\n\265\237,\345\317Z\200\275T\232J\243,\225X\311\232cIP\274\225RW\252\306L\032k>j&j\2179\240\324mM\247\255L\242\245Z\225\005N\242\245Z\225jd\253(ML\265e\rJ\255S+\361N\337N\r\232B\324\335\334\021P\206\244-I\276\202\364\322\364\205\351\273\350\335I\272\232Z\223u\031\245\335_K\026\250\231\3522\364\306z\211\236\241g\250\231\352&\222\243/\232\215\232\253\311&*\264\222\372T,\304\324.\330\352j\264\222UY$\252S\314{U\t\'#\275S\222]\307\255B[\025\023\311T\245\222\251H\3715\0136*\026z\201\344\252\362>j\271=i\205\251\245\251\231\2434\207\232LS\324\032\231EN\253S\"\342\246Q\232\225V\245U\251PU\2055*\361R\251\251CT\240\361K\272\234\255C5F[\025\036\356i\013SKSKSKRn\244\335K\272\215\324\233\2517Q\272\224\032\372I\244\250Y\351\205\3526z\215\236\241g\250Y\3522\371\244\006\230\355\201T\245~\265X\266M#\266\005S\226J\254\362U9&\252\222\310\rg\316\370\'\025I\236\230d\250\235\370\252R\234\232\256\347\025]\332\253\273T\016\325\0136j75\0214\204\322P\005.)\300T\252\265*\255L\213S\250\251UjU\024\354T\211S\251\251\224\324\253R(\346\245\307JN\224\3654\255P\267z\2046sHZ\232Z\230Z\232Z\220\265&\352]\324\273\2517Q\272\215\324\273\253\350\306\222\243/L/Q\264\225\013IQ\0311Q4\231\250\313\322\254\237-C$\231=j\274\244\036I\250I\013\316j\264\323g\201T\235\352\254\322\340U\007\233\223U\244\2275VV\3105U\372T\r\221P\263T\016\325ZCU\334\325w5Y\330\212\2179\2460\3154\203M\305\030\240\n\220-=TT\252\242\245U\251\225jUZ\224\nx\025 \025\"\214T\200T\212je9\251\343\\\323\363\203M&\234\247\326\206jc\237\2275S8&\202i\244\323\030\323\t\246\356\243u.\3527Q\272\215\324n\240\267j\372)\236\242g\250\332J\211\244\250ZJ\215\244\250\332J\214\2754\311\201PI)\035*\264\223\037Z\257$\307\326\253\264\231\315@\317U&~\rf\313\'5Y\346\250\232L\324L\325\023\236*\253\266*\027j\256\346\2539\252\356j\026\250\310\240\323M7\024\240S\202\324\212\265 ZxZ\221EL\202\245\013R\001R\240\251\002\324\212\264\360\264\340*D\253qc\024\215L\3174f\232\317H\355\362UV<\320M4\232a4\302i\231\2434f\227u\033\250\335F\352@\325\364+IQ\264\225\023IP\264\225\013IQ\264\225\031\222\232e\250\314\231\250\244z\251$\225Y\244\311\346\241y*\273\313U.%\342\263e\222\2533Te\361M\3633M/U\344\3475Y\332\241v\252\362\032\256\325\013u\246\363IF)6\323\302\323\302\324\201i\341j@\265 Zz\214T\312je\\\364\251Uj@)\340b\236)\330\245Z\261\031\342\203M4\225\024\234S\031\262\225\001jM\324\231\246\223Q\261\246\223M\315\031\243u\033\250\3154\265(j\372\t\336\241i*\026\222\241i*\026\222\2432S\014\231\246\263SK\340T\022IU\036Nj\273\311\212\255$\265U\346\367\252\223M\232\252\3075\013SJdu\250_\345\342\230Z\243v\342\252\310j\026\250Z\240j\214\2126\322m\245\013F\312P\265\"\255H\253R\005\251\024S\361J\005H\253S\3061S\201R\001K\212z\2558-(\034\324\212\010\247S\t\246\026\244?0\250\266\225\353\322\241\221q\310\250\263\212\t\244&\243c\232i4\322i3Fh\335Mg\244\006\234\r{\353\311U\236J\205\244\250ZJ\211\244\250\232Za\232\217<t\250\344\233\336\252I8\035\352\253\334\201P4\331\025ZIj\2735B\347\212\200\232\214\232ij\206S\232\203&\241\221\252\271l\324lj&5\031\024l\365\245\331M+@\024\354R\205\251\002\323\302\201O\013R\005\247\205\247\005\251\225*U\025*\212\220\npZp\030\247\212r\217Zp\024\204`\323\030TMQ\346\235\234\216i\204\216\207\245W\2210x\250\263HM4\232a\246\223HN)\245\251\013SsK\234S\201\257vy*\273\311U\336J\205\244\250\332J\205\244\250\232\\T^v\016MC-\317\275Sy\211\250\231\352?7\007\025\024\217P4\224\302\331\250\211\2461\246\026\250\\\324]MG\"\212\200\212\214\255D\302\230E\030\245\240\212LR\342\224\nz\323\305=EH\005H\005<\n\225je\247\250\251qJ)\300S\200\251\002\344\322\355\305\014*6\024\302\265\023G\3157n)\244S\010\310\301\252\262\251\006\230\016i\r0\323I\246\023L-M\315.isFk\334\032J\256\362Uw\222\241i*\026\222\242ij\027\226\253<\276\365\003I\357Q3\324O.\005W2\363N\336\n\324\016y\246\027\3057~i\214\325\0314\306\351Q7\265D\315Q\261\250\311\24674\314R\355\243\030\240\322\036\264b\236\0058.*e\214\021\301\247l\3059EH8\247T\213R-J\246\245^i\340R\212x\251T\323\210\310\244\333M+L\"\243ja\250\315F\302\242q\221UXm4\302i\244\324l\330\250\213f\233K\2323FiA\257gy*\007\222\240y*\006z\211\244\250\036J\201\344\250\032J\201\344\250^Z\201\344\250\014\234\322\211\251\305\363Q\26574\023Q\223L\3150\324,9\246\021\212\214\323\0174\336\224\271\244\315\035iqF\332z\247\275L\027\326\2342:T\241r9\243m8\nZx\251\026\244\025*\032\230\036)qN\035j@02ju\301^(8\250\310\024\306\025\003\212c\n\214\323\032\2435Ve\346\253\223\212\215\237\322\2429&\222\2234\231\245\315.h\025\353\355%@\357P<\225\013IP\264\225\013\311U\336J\201\344\252\357%B\317Q3\324e\251\233\271\247\2074\355\371\024\231\346\220\2650\232a<\322\036\224\316\325\013\232\214\232ni\t\246\021F)\312)I Rn\346\236\257R+\212\220\032z\276)\371\356)\300\323\251E<qO\006\244V\251\320\346\245\307\024\243\255K!\004\014T\221\034\nV\250\233\212\215\215Fy\2465D\324\303Q\266{Uv;\270=j\264\203\025\001\246\023\212i4\334\322f\212\\\323\201\257\377\331"
+byte_png: "\211PNG\r\n\032\n\000\000\000\rIHDR\000\000\002\000\000\000\002\000\010\000\000\000\000\321\023\213&\000\000\006\nIDATx^\355\334\331\222\343*\014\000\320\251\374\377/_\327\255^&\023\307\361\0166H\347<ug\263A\002\204\235\356?\177\026=\336\037\000\310\3439\005\016\257\217\002?\014\214\340z\t\260j\265\222^\022\200Z\376\016\255.\206\330\\\272NO~\372\010|\231\313!\200l\254\224@\010&\263\334\304\037v\351u?|\346\274M\023\000\000O)J\2433\265#\001H\200W\237\306\374\260\322E\237\336C\034\263\361\235}\242\tki\013\034\021w`\265=\2435#n\002$#\337\001\350\325\236b\344\343k-\202\313\336\373\347c\'\002\000\000\320\250\367]\035\000deM\204-\334\003\200\027\351\006D\272\006\357\224\246\226(\222\010\355\366\326\243\335S\273\330\277\216\360\325z\010\312\330f\003i\002!\030\312\225\005\336B\310\035r\3330\0026\274\204\261X]v[kn;0\000\000\000\255^\r\264U\00463a@h\215\226*\244e\321\201>\030\253\237<\327\324x\335s\331\337\005\014\001;\017\000\310\300\325\025\000\2567\267\203\236{\374\220\242\037\226\200\222\000\340\020\313Mr\022\000\240\030[\022\216YX\214\027\236\242\031C\307\203\277\3373\377\344\266\377Bz\327q\271\314\362\\,\001\000\000\202[.\007\211gs\304\177_\270\371\365\304\"\360\331u\232\001\235\2366\274\221\311\300-\032\276\027\320\360\251\305\262m\001\332\366*\256P\374\336q\351\317\003\000\000\240_\007.\000\034x\013\037T\337\236\377=@\365\003\001\000\274R-\002\375\010\271]*9\r\207\354 \000\000\000\366\262=\034i\240;~O\341\3305\200c\357\2421g\322\360\314{i\310\321\261,\001\202\220\000\000\300\026u\326\376\311_;\037-M\306\312|JD\337=3\354\350\237\367\000\365\244\347s\207jv\214\177\000 \026e@^\027\307\376\342\303\261b\020\221\344\\\034\001\312+=\263X\251j\321\263\000\364\241tm\261\327E+\346E\207\341V\367E\371\276#\323\004\t\200$\310N\002\000\020\304\335\027\010\370L\251\321$\303\005\000\000 \205\347\366\317>\020\000\000\346\204\272\231\025\2521\020\302\337QYit>\367\373\225>\237Rf\003t\356\222\315\373\273g\017CtB\017\274/\t\2045\236\362-\000@\313n\232\243n:\354uj-\372\265>\227NH\200N\010\024@\026f|\000\322\010\177%\347\234\331\356Q,\360\211\274\210hv\032\000 \021\253A^\207b?*\n\037J\304\276\274\306k\030~~_K\203b1^;PGv5e\327\213+(\026@\000 55\005p\237\331\031h\366\211\266\335v\332+\007^yz\352\356\355n5a\033\006\020\307\356E\013\000\350\2225\377\220\323\273\332\323\037\320\233(\211V*pM\365\307\025\'s\3051\340v\022=9\t\000\220\315\317\314\177f\376/\265\275\340\026gBO \3061@\034\026w\000:\365\334\226\254\355O\326\326\272\265\347I\351\353\337#\214eK\224I\007\000\220E\266%o\302\032\010o\014\212\344$@.\3432 }QpP\244Q\023\251-i\t\"\000\000k\324\214\'\331=\323\244\303#\373\360\033\001\240\013V:\000R\033\206\351\327\343\263K\333!i\033\236\335k\340%\001\000\000\267R\220f%\362\367\320\357\000\000@L\253\273\035_\364\017\332\003\2555k5\023/\326\332\371p1\t\000\000@o\036\207vy\t+\337\355M>\324\243tO\334\001j3\323\002\r\331\276= $\t@\033\232\250\216\374\337\204x\232H\254\375d\"\000\000\334\257\323\355\004u\004\332\246\311l\370\307x \227@\213\031@%j\203\276X\3316x\351\244x\371]\240E\005>\242q\001[\370lR\300\266\0010\343\247\242Q\374%7\310\201\254*\307]Q\331\262\237\340\307\215Q\334\226\225Ub\0228\362\247\001}\306\247\317\263\256\356H\002\320\200\207\214\246\016\211\005\345Xbc\020\307\344ZK\200\342\347\023|\345\037\312\367X\020o\201\017\330M\337\033\335\200\355\002\200*\202\327\204\300\275\224\345e\350\307\234\252.\321U?\2746#\"\210b\201,\366A\271\350\266\250D\026\000xuam\320\3656\023\000\000B\272pC\000e5\277\305l}t5}~\315G\267\007Y:\261\351L\006\000`\037\305\335\001Y*\177\026\254\3747\260\346s\344\367\364\233?\317\336|\367\353\320x\307\216On\364\333$\255\233n\0104\313\310IN\002\320\271I9@O\204\357rmM\372\022\340IW\314\3201k\226.\366,<\365k\375\025\177.\2306>\035\340\323c_\346\036\347\3338\240\233\302\013\264i\347\000\336\371r\000\200\306\274U3\212\233#\036\017;f\000\216\262\366\002@\233\336\327\350\367\337\273\366\322\230\274\373\331\363-\017\225\023@\n\347g>\"\350~\375\352\276\0017\323\177\000O\243)Q\241\224\211h\247\246\030b\201\364\000\200\313(\312\001\340(\233W\200:\314\257\000\204aQ\333a\347u\312\235/\007\000\332c9\007\2022\275M\244\332\034\213\377~\361\372,^\213\000\000\000\226\330\005\001\000\344\223\352\346\017\000@V\212>\026I\020\000\210\305\332\016\231Mf\200\305/\202,>\t\000\000\000\320\024W2\000\200\350&\367y\200\210\346\206\372\353\343\377\275\374Lvs\031C\022\022\000\000\000 \231\177\267\305\335 \007\000\000\200\256\270\273[N\207}\331\341)\003\000\000\264\302\226*\013\221\006\0023\305\001\300Yi\2777=\214\352\210\264\335\220\272\351\000\000\000t\306=\201\354\356\276\212!\003\201UwOT\000\000+\354k\000\016\262\337\343\213<\000\000\272\246\230\001\000\200\350T\375\200/\005\364\345w\336n1j\377\003\020\243\377\206\213\235b\021\000\000\000\000IEND\256B`\202"
diff --git a/core/res/geoid_height_map_assets/tile-7.textpb b/core/res/geoid_height_map_assets/tile-7.textpb
new file mode 100644
index 0000000..83f1fcb
--- /dev/null
+++ b/core/res/geoid_height_map_assets/tile-7.textpb
@@ -0,0 +1,3 @@
+tile_key: "7"
+byte_jpeg: "\377\330\377\340\000\020JFIF\000\001\002\000\000\001\000\001\000\000\377\333\000C\000\004\003\003\004\003\003\004\004\003\004\005\004\004\005\006\n\007\006\006\006\006\r\t\n\010\n\017\r\020\020\017\r\017\016\021\023\030\024\021\022\027\022\016\017\025\034\025\027\031\031\033\033\033\020\024\035\037\035\032\037\030\032\033\032\377\300\000\013\010\002\000\002\000\001\001\021\000\377\304\000\037\000\000\001\005\001\001\001\001\001\001\000\000\000\000\000\000\000\000\001\002\003\004\005\006\007\010\t\n\013\377\304\000\265\020\000\002\001\003\003\002\004\003\005\005\004\004\000\000\001}\001\002\003\000\004\021\005\022!1A\006\023Qa\007\"q\0242\201\221\241\010#B\261\301\025R\321\360$3br\202\t\n\026\027\030\031\032%&\'()*456789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz\203\204\205\206\207\210\211\212\222\223\224\225\226\227\230\231\232\242\243\244\245\246\247\250\251\252\262\263\264\265\266\267\270\271\272\302\303\304\305\306\307\310\311\312\322\323\324\325\326\327\330\331\332\341\342\343\344\345\346\347\350\351\352\361\362\363\364\365\366\367\370\371\372\377\332\000\010\001\001\000\000?\000\364\241\311\247\272\014UR\270j\221E;\024\240R\342\227\024\355\271\244\3074\341\326\234)\340S\302\324\2121O\025\"\324\202\236\265*\324\213R\016\265 \247\221\300\247!\035;\324\312)\342\235\232p4\352\rFx\346\254B\341\206\r<\257j\257 \321\022\0265<\361\205N:\325#\2321IM84\204\n\215\224TD\n\211\361U\334Uw\025\003\212\201\305VqU\334T-P\270\252\322\016*\234\253Ud\025Y\305FV\242\"\241a\232\214\245D\351Q\024\250\235k\327\2323\236)\256\016\334\032\213m8\np\024\340)@\245\3058\nP\264\273i\341i\301i\340S\200\247\201O\024\360*E\251\026\244Z\221jE\251\224\016\365\030 \310qV\024\323\301\247\003N\024\340ii\254)\250v\265[S\221\232c.\346\346\254\300\2358\242\362<(\"\263\366\322m\244+LaL5\033TdT,*\027\250\\Ug\250\030Uy\005We\250XT\016*\264\202\252\312*\234\202\240qP\221Q\262\324L\265\031\024\306\025\013\n\211\205{\010\036\265\024\200T;y\245\013N\333K\266\234\005.)\300S\200\247\005\247\001N\002\224\np\024\360)\300S\305<T\202\236)\336b\257SR\t\227\037/&\201\276C\311\300\251\321v\212\224\032p4\341N\024\341N\024\244f\243#\006\254Dr\274\320\300\203V\255\337\212}\340-\030=\2534\214RQ\212c-F\302\243+Q\270\305@\365]\352\026\250XT\014*\007\025\013-@\353U\344\030\315U\220U9\005VqU\330Tei\2053Q\262\032\210\245D\313\216\265\013\212\205\305z\363q\322\240y0phV\rR\252\322\342\212)\300R\343\322\224\nx\247\001N\305-(\247\np\247\np\247f\202r1\232b\214I\216\306\257\244`\n\225i\340\323\201\247\003O\006\234\r8\032x\247\001HW4\014\257J\221X\265O\037\006\255I\363\304@\254\366\217\232aZLSH\250\312\324o\305Wz\201\352\273\212\210\212c\n\205\326\241+Q2Uy\027\025VAU$\030\315R\222\253=@\302\230E5\216:Tg\232\215\270\252\356j\007\250\034W\255<\200\n\243<\302\2515\367\224\371\315\\\203RW\003\232\224\335\016\306\234\267 \367\245\373H\315J\223\206\251D\202\234\034z\324\313\315;\024\264\003N\247R\212p\245\315;\265F\354W\232\204\334\342A\3175\247\004\333\30758\251\001\245\024\361O\035i\302\236;S\3058S\351\030qO\203\275J\006\rJ\200\223\365\250\245]\246\2414\322)\244Tn*\026\250\034T,*\007\025\t\025\033T-Q3TLj\0075RSTe9\252\216*\273\255DV\232\313Q2Tl0*\273\232\200\324l\265\023\255z4\267\003\007\232\313\270\270\311<\325\tK9\342\230\211*\037\224\232\260\262\314\007SR\255\304\242\247K\306\376!Z6wq\271\001\216+ac\211\324\020\334\3242\304\253\321\251\022M\275\352q>;\346\246W\rN\306zR\342\224\np\247\nQN\002\231*dV;\356[\234\023[V\317\200*\3720\"\244\007\024\361O\024\361\3058\032p4\360i\342\236)q\272\236\213\264\324\246\245\214\322N\205\206@\252\244\021I\214\323J\323\031j\026J\201\326\241e\250\035j\026Z\205\226\241u\252\3561P\265W\220\3259Nj\254\225Y\226\242e\250\310\250\330TNqU\2449\250\030TdS\010\250\334q]K\\\356\025\\\202\346\246H\272T\313\030\251\004c\322\235\345\n_!Oj>\317\267\225\342\246I$A\200\306\247Id\'\3469\0251\215\230d\032\201\244\222>\271\305Y\266\273$\326\224r\206\02584\340i\302\236\0058\nv*)\030(\254\231\030I?\313ZP\214\001V\343r\rZV\315H\0168\247\003N\006\234\r8\032z\232\224\032vjd\0250ZF\351\212lNw\n\270pW\212\253\"\342\241\351IM8\250\236\241l\032\211\222\241d\250\035qP\262\324n\234U9V\2538\252\262UI\005Wu\250Yj\026Z\215\226\253\310j\273\344\324%j6\025\031\024\322*\027\256\221b\247\252\000jA\307Jp85 l\323\301\247n\247\251\315<b\246A\307\322\247\004\355\310\374i\031C\212\245*\030\216S\212\236\326\364\202\003\032\325\216`\303\203S\t*EqR\253\n\220\032R@\352qY\367\327\001\020\205\357Tl\327s\3565\265\020\030\251\024s\305YQ\3005 j~iA\247\003N\006\244SO\006\236\234\232\270\215R\001L~\225[y\215\262zU\350\246WN\265\034\247\232\200\3223\201Q\026\315B\3715\001\342\234\016W\232cTL\265\021Z\205\305T\225j\243\255U\221j\254\213U\331j\026Z\205\226\241qU\235j\026Z\205\226\243aQ\221Q\267\002\253\270\256\246\202h\335F\352p|S\303\323\267\323\325\352Uz\231_<T\253&:R\207\301\241\300qT\336\022\255\225\251\340\270)\303U\305\272\351\315J\267$\036\265f;\241\336\246\373j\201\305V\236\367\336\263\036G\270\177j\321\266\214\240\025\240\215\212\231\033\326\254#v5&y\247\003J\r<\032p4\360i\340\324\261\034\346\255G\324U\2208\246\260\252w*q\305$\014Ur;U\304o1zTrD\303\'\025M\362\247\232\001\310\250\334\324M\355L$\212\214\271\357HZ\243cP\265W\220f\253\272\325Y\022\252\310\225Y\322\253\270\250\030TL\265\003\256*\006Z\205\205D\313L\331P\313\201U\034\327M\272\220\265&\3527\321\276\234\037\232xcN\017R+\324\311%J$\251\267d\0029\247+z\323\260\032\232a\006\221\240*F\r&]i\004\222zT\352]\307Jp\267.~cVa\267\013V\200\333\214S\325\252\302\002}\252\324})\344\323\201\247\212p4\340\324\360\302\235RBy\253\221\236EZV\315;\031\250\244\2175Q\334[\003\221\326\241\267\324B\313\216\325\260\223,\313\322\253\\@95A\270\250\330\324D\340\322\036j6\025\013\002)3Q\2675\013\324\017P8\315U\224b\251\311U\330T,*\026\025\023-B\313P\272f\2421\324R\035\242\250\310rj\006\025\321f\232Z\230^\215\364\273\250\335OY=i\373\307jz\275J\257\212\231d\310\305J\222\021R\254\204\234\324\350\371\025&jD\3062FE&\325\'\216\224\341\020\364\251Q@\343\025&=iA\247\356\243uH\2221 f\257\305#*\362*A \'$\322\203\371R\371\200w\247\007\317\265<\020\335\351\341G\367\215/\314:05$nC|\325z3\337\265ZF\251\224\322\221Y\372\224d\247J\310H\210`En[\202\261\256MYl2\237Z\314\230`\232\254MF\306\224\036)\254*\027\034\032\216\230\325\023{\324\016*\273\325Yj\243\216\265\003\212\205\224\324ej2\265\023\255@\303\025\014\214*\224\315Y\357\'\315Q\264\265\320\026\246\226\250\313P\032\215\364\340\364\340\324\360\324\365z\225Z\245G\305L\255S#\35550q\236*P\334T\261\313\216\243\212\220\225\352*h\361\267=\350i\001n)w\322\356\24058\032r6\r]\023\226@\270\351J\244\324\241\370\245\r\315;u85H\256j@\364\354\325\313yr\274\232\275\033\202*ej\225Z\251j\023\2026\016MS\215G\031\255 \000E\307J3\317\025R\344s\232\244z\324mB\232SP\310j#\322\243j\205\315D\306\242|Ui\024UWJ\256\313Le\250\\b\253\261\305W\222LU)\256\000\351T\244\270&\252\311)j\254\325\023WB\315L-L-I\272\215\324\007\247\207\247\206\247\206\251U\352D|\032\235\037\336\247V\315J\215\371\324\252\365 j\230>\341\322\236\257\371\324\204\250\307\257zqe*1\326\223u8\032pj\221\r^\205\224!\365\251RA\214b\220\236y\2434\240\323\305H\264\360i\340\324\3219C\317J\277\034\230\344T\253p\007=\252\031\365 \200\205\252P\271\231\2131\315]\333\307\025f\026\334\230=\250\'\006\240\270?-S&\243jh84\273\252\'94\302x\250\232\241~*\0065\023\032\211\252\273\340\346\240b\001\250]\200\252\322\275Q\232`\277Z\317\226Fj\254\365\003\212\205\205B\302\241j\333-L-L-M\335F\352]\324\340\325\"\265<5H\034c\336\236\216*Tz\260\262qS\307&\322\rK\346\2069\003\025 \223 T\310\343\036\364\360\370\346\237\36374\241\251\301\251\301\251\352j@jdsVT\232\221NMHq\332\200*@)\343\232P)\375*Tn0E=da\307QJ]\210;\016\rUh\345rwQ\004\255\013\355qZ\221I\270U\210\376\\\232Rj)\233(j\236)\255Q\221\315!\351Q=B\315\212\214\277\255F\346\252\274\233MFe\035\352\026\225}j\027\220\016\365VI@\351U$\230\325Ief\252\257\317Z\205\305@\342\240qP\260\250\036\241j\325-L-L-M-@jp4\006\251U\251\341\251\301\251\352\3252\265J\257S\253\344T\350\330\025*H\000 \363OW\251\225\263O\363\010\030\007\212\221Yvd\236i\003\023\323\245=^\244V\251T\324\311\326\256G\265\207&\226\236\rJ\244S\201\251\001\247\216\264\341\315=jE\247m\311\310\353OV*p\303\"\244\222\3329Wp\024\220\257\226p{U\235\324\205\252\263\271v\300\351JS\002\242aQ54\216*7\351U\332\241`s\315B\344\363\203U_\336\240r\rB@\'\236*\tT\212\254\365]\352\007\252\357P\260\250\034T\017P=B\325\003\n\320-L-M-L-@4\340\324\340i\352j@iwS\203T\252\365\"\265X\215\252\300$u\251\021\206EX\334:c\232Uz\2205.MM\023\200\016ic \223\272\245FS\327\212\224\020:\034\324\310julT\310sR\212z\340S\367\np>\224\360\364\360\324\365j\2205L\2074\346 \nXe\031\332MJ\024~4\355\330\246\034\277\002\200\241hcU\330\344\323\010\250\311\250\230\324-\315D\375*\273Uy\006j\263\214T\017\232\201\311\250O\0078\252\362rs\212\205\233\003\201U\236\241z\201\333\214Uw\250\032\241j\205\252\321jijaji4\252i\304\322\253T\252\324\355\324n\247\007\251\025\252Uj\231\032\254,\234T\210\334\324\301\360\335jM\340\236*Ej\2206i\301\260jEl\234\232x8\251\221\252ej\235\033\246j\326\365\000c\255;u(l\323\201\251\003S\203\323\203\324\201\351\352\3652\2759\233\"\230\215\207\253Fb0iL\231\031\024\365\223\tM\017\232k\034\323\010\342\243cQ6OJ\214\241=\351\214\204Uy3U\3335\003\325w\250^\240aP=@\340\212\254\365]\352\0075\003\232\201\315@\365\013T\017S\223L-M&\232Z\200\324\355\364\241\271\247\207\247\007\247\206\245\006\244V\305J\255R\243\325\200\371\002\246\215\307B)\340\374\330\0257\335\305J\215\305H\247&\244\347\322\234\032\244\014*U\315H\255\216\265:=L\036\244W\355R\003N\337N\337J\032\236\036\234$\251U\352tz\227vEF\371\007\"\246\211\303\'5*\256\345 \036\224\210\340\214\032yB9SI\311\353Mj\211\215D\336\324\336E1\213\n\205\333\332\253\276*&\013\216z\325f\343<Uw\250[\245B\302\241\224n\252\262%Uu\305WqU\334T/P5B\325\013\323\213S\t\246\223I\2323I\232pj]\364\340\364\360\365\"\265I\232z\265N\246\246\rS\241\356:\324\321\311\216\243\232\220\023\336\244V\251\003T\313\'@zT\203\004R\240\311\353S\253\355\340S\367\344\212\2205J\257\212\2208\251\026J\223viwR\207\247\007\247)\251\225\252Uj\234=;9\246\357h\217\003\"\246I\207U\342\225\324\246\037\261\251\221\362:\322\223\336\242f5\033\034\324g\"\232}\350b\010\252\354*&Z\201\205@\342\253\270\250XT\rP=W\220Ui\005V\220Ug\025\003\212\205\205@\342\241aL&\232M4\232L\321\272\2234\233\250\335NV\251CT\212\325(jz\265N\255S+\342\245V\251\327\234{\324\341\370\332{T\212}jQ\202x8\247\364\034\034\323\324\364\317J\221\037i\310\251\003\356l\232~\374\267\024\360\334\324\201\252da\216M<5<585;u\000\324\212\325*5J\257R\253\324\252\365/\017\326\223\312\301\371N)e\220\354\t\327\232\2263\201R\026\342\230G\031\246\026\035\206h\013\270g\275B\352W\255G\273\025\033\021Q7\025\023T\017U\336\240z\201\352\026\250\034Ui\005VqU\234T\014\265\003\212\205\205B\302\253\023M&\232M4\265&\3523HM\000\323\201\251\024\324\252\325\"\265H\032\244V\305L\255S\243T\350\370\251\223,x\251\225\261\326\244\rR\003\351R\253\345pi\350\t\351O\r\212pjr\265J\032\236\036\244W\251\003S\303S\303Q\272\236\032\236\032\246V\251\025\371\251\203T\361\276\0075(j\205\2372\014\366\251\321\352V8\034SKn\030\246\001\212_\247\024\016xj\2574E\016GJ\256y\250\330\324,j\007j\201\315WcP=D\331\250Z\241u\252\356\265^E\252\356\270\025]\305@\302\240qT\311\246\223L&\233\232J\\\322\023E(4\360\325\"\265J\246\244SR\003R\251\251\321\252tl\324\350\307\265L\030\2202jEj\225H\357Rdg\212\221\033\007\"\244$\261\317Jr\343<\363K\236x\247\003R+T\212\325 j\2205;u.\352pjxj\221Z\245V\251\321\252Uo\230\n\262\274\324\027\n\335S\255@\223\310\207\346SV\222\3600\301\340\324\261\271bI\351R\003\232:Q\327\353J\300H\244\036\265I\320\2515\013\212\201\205W\221j\006Z\205\326\240e5\013q\326\241qQ\267J\256\342\253\275Vz\256\302\241u\252\356+<\232i5\0314\334\321\232Pi\017Zu\024\340i\352jE5 5*\232\225ML\206\247F\305Y\211\271\346\246SOB;\323\267b\254\"\202\231\3174\261\261\006\254;p1K\021\014piH\305\001\251\342\244V\247\206\251CS\203S\303R\203O\006\244\rR+T\310\325:\034\221V\221\252P\271\243\310\004\321\366A\235\330\253\n\243n\010\305A(1\237Z\204\315\203\2021NI\206jRy\014\275)\222\000\334\325WZ\201\220T\016\225\013F{T\016\225\t\030\355P\270\007\255V\221qP0\250\331sP:qU\335*\273\255VqU%\006\262w\323K\323KSwQ\272\234\032\235\232\001\247f\224S\305<\034S\301\251\024\324\312jd5:\032\231Z\247V\251\024\323\301\311\251s\267\200sR!\365\342\237\270\376\025&\010\000\323\321\371\245lg\345\247\002@\346\236\017\24585H\036\236\036\234\032\236\255R\003O\006\244V\251\025\252\3025Z\211\363V\220\324\353R\nx\024\025\r\324T\022[+d\342\263\246\267x\330\225\351K\024\344|\257\305M\272\242j\201\205B\342\242\'\025]\352\273\324\016*\027\\\212\256\302\243\"\242qU\244\025ZN\365Y\305D#\311\311\2546U\r\301\310\246\230\201\357BC\273\275G2\252q\236j\024\345\261R\310\2331\212h4\274\216\264\3455 4\341N\3158\032\221ML\246\246SS\241\251\224\324\313\310\310\251\024\324\200\323\324\324\310A\353S#\214`\324\252\343\'#\212L\363\300\342\227w9\2517\226\306iA\247\251\247S\207\024\345lS\303T\201\352Ezxj\221Z\247\215\352\3227q\326\256D\371\025eMJ\246\244\006\226\203\315D\350\017Z\245s\007\004\257Z\253\034\204pz\212y9\250\332\241qQ0\250\034T\016*\006\034T\r\301\250Xu\250\033\203P\271\252\317U\334f\241)\232F\\q\\\256\r4\222)\003\262\3645\033e\2174\344m\224\346\224\2767S\323n\340j\304\221\243.\340qQ\004\\q\326\215\244S\226\235J\rH\246\246CR\255N\206\246\\\232\263\033\014R\347\232\2205=MJ\246\246SN\r\203R\371\271\024\356\n\217Z\010\333@jxjpz~\372pjx4\340jEj\221Z\244V\251\221\252\324OV\342c\273\025q\rL\246\244\006\236\r\031\246\232\212^\225\224\343\023\034S\373S\032\242j\211\205B\342\241qQ\025\315W\2250j\263\212\256\375*\007\346\240aQ\025\246\021\212\255!\346\271\242)\244S\010\244\333M+I\266\224\np\334x\315=r\rNHd\344\363LZ~(\035i\353R\245L\2650\251TT\311\301\342\245\335\223O\006\236\rJ\206\246SO\006\234\r<7\024\273\210\024\375\300\217zvW\034u\247.\336\364\270\317\335\244\r\203R+\324\200\323\201\251T\324\212je5b6\301\253\221\276q\216\265r6\316*u5\"\232x4\354\322\023PJx5\234>g&\236j3Q5F\302\242lTOP\236\rG\"\344UI\027\232\252\342\253\313\305V2`\323K\203U\335\362MW\222\271\242qHZ\232M&h4\001KN\002\237\212Jr\324\202\223\2758T\253S\251\251\243\035\315H*d4\360zT\200\323\301\251T\324\212\325 4\340\324\355\324\271\247\003N\006\237\237JP\304\016)\353\2029\353I\312\232z\275H\247\232\224\032\231\rL\246\245V\253\220\2660j\344g\025aZ\244V\251\003R\356\244-PN~CU\020b\221\2150\232\215\252&5\023TMQ\232i\344Uy\026\252H\265B\343\200k8\276Z\230\362\021ML\221\232d\225\3160\315DA\024\334\321E:\227\212QO\035)*@0\0058t\245\305-=*d\367\253\010r8\251\000\251\024\323\305J*E\305;#\265=MJ\r85874\270=\216i\300\221\326\234\0334\340y\247n\315(<\324\231\365\353H\016\rJ\017\245J\247\212\221\032\247SS+U\270\030\021W\021\252uj\225Z\236\032\227u\005\252\t\233<T$\342\230MFM0\232\215\252&\250\310\250\310\250\311\305B\346\253Hk>\353\2258\254\365\213\004\223PL2\334S\320aj\031k\234ja\250\310\301\246\232ZQN\353J\005<t\244\247\216i\353N\245\3059je5b/Z\2274\240\324\212jE5 4\340i\340\324\200\323\272\032:\322\362:S\203\032xl\323\201\247\003N\006\235\270\223N\316i\352qR\241\'\245H\207&\247SR\253U\250[\006\257#T\352\325*\265<r(-Lg\307\326\242-\334\323\t\246\023L&\230i\206\243ja\250\333\245@\365VW\305Vy*\214\244\263{T\017\307J\252\303-O\306\005V\224\327:\324\303L4\303IN\006\236\r8S\207JJr\324\203\212x\245\245\025*\325\210\317\002\236)\342\236\016\rH\r85<\032\220\032x8\247f\224\032v\352Pi\340\322\346\236\r8\036)A\247\203O\006\236\255R\247&\247\004v\251\024\325\3301\216z\325\244j\260\255R)\251C`\363Li\007\'\265G\273\'&\232[&\232M4\232a\246\323M0\323\010\250\330UyN\005e\\\312rqU\203\356\024\326\305V~j2\270\244n\225Rc\326\271\363Q\232a\246\232i\245\024\341O\006\237\221\212JU\342\245\316qNZu8\nz\365\251\326\244\006\236)\342\234\r8qRn\315<\032xjp4\354\322\346\234\r<\032\\\323\324\323\263J\r<\032vi\353R\243b\246F\343\025<c$\003V\224c\245N\222c\212\264\215\305J\207\232s?5\031$\375(\315%\024\204SH\246\232n3HV\2435\004\215\212\316\270\230\034\200k6f\004\324C\332\220\212aZ\211\352&\351T\346\357XMQ\032\214\232CGZAN\035i\302\226\224\032vx\251\242\0314\3420i\302\236\0059x5*\324\253O\024\341N\025 \247\np4\340i\340\323\301\245\245\025 \245\247\016)\331\245\31585(jxjz\232\231\032\254\304\325r6\350z\325\205\000\266EXSR+S\272\321\212P)qF)\n\323J\323N\0050\232\215\232\240\222P\240\344\326]\335\340\350\reKpOJ\215r\375jP\270\244j\215\215WsQ=T\232\260\232\243j\210\322Rt4\235)\342\234)M(\247\212\222#\206\253.\001\031\024\300*E\024\270\251\005H\225 \245\035i\342\244\024\354R\323\205<S\326\235@\251\001\247\nPih\245\006\224\032x5\"\232\231MXF\253P\276:\325\224|c5eZ\244\006\244\rN\006\234)\331\244\2445\031\2463\001U\345\235W\275g\334_\355\007\006\262\246\277f\316\rSfy\r\002/Z\225W\024\343\322\242cQ3T\016j&5Z^\225\204\325\033Tt\332i\344QJ*U\240\214\032QO\024\364\034\212\262y\300\245e\332i\313N\002\234\005H\240\212\224\014\323\261\306)G\025 \247\203E(4\340jU4\354\321\212u<\032v)(\024\352\0058\032z\232\235\032\247CS\251\351VCg\034\346\255\306\334\n\2305H\2475*\323\300\247b\230N*&\220Uy.@\3175Rk\265\037\305Y\267\027\231\350k9\313\312}\251V\337\035jA\020\024\245E0\323\030\324\016\325\0135D\306\231\236*\274\335+\021\205D\325\031\246\032m(\245\024\354\372R\203\332\235\214S\305H\275j\310q\263\035\350\352i\340T\202\224\n\221jT\031\2470\244\357O\024\340i\324\231\305(<\324\212\324\360j@iisK\232\013\021N\355\223A\342\200iA\251\024\324\252jt5:\266*\302\2779\035j\304.s\212\266\255R\251\251\224\324\242\231$\252\200\344\342\262\256\265UN\024\325\003~\362\0363P\310\362\037Z\200\243\267SJ \035\352A\030\035\250*\0054\212\215\215@\315\212\211\232\240f\250\031\271\2461\250\313qPL\334\032\310j\205\205Fi\246\232E%\024\242\234\016jE\367\251\027\322\245\013\307Zp\253p\306\031\t\364\244\3075 \031\247\005\247\201S(\305!\240S\251\302\235Hi)\342\236)\352i\340\322\346\224\032\0174\354\2221J)wQ\332\234\2652\032\260\225\"\236jp\340~U,r\200\3035z7\315XV\247\371\252\275N*95\024@y\254{\273\366\231\260\235*\232\304\\\345\3715ec\000T\235\260E!@G\035j\"0i\013b\230Z\242f\250\235\252\007j\201\332\240v\250\213S\031\252&n*\274\255Y\306\243aQ\232\214\322\036\224\332(\035i\342\236\265*\212\231E?mX\200\237\272;\323\366\025<\324\200S\200\251\024S\263IE8S\305-6\227\024\242\236:\323\205<\032\\\322\206\245\006\235\232Z^\264\202\236\t\251\227\246jdj\2234\370\316[\025$\352c\034u\244\217P\300\303pEL50\007\025^[\311%\345sP\215\357\367\215J\251\212\225x\247\206\240\2657v)\215%F\315\273\247Z\205\233\025\031z\215\232\241sP9\250\030\324,\324\315\325\023\265W\220\3253L5\023S\r!\246\232J)\342\236\2652\324\350*]\265f\331rH\003\232V\316\356i\352)\370\247\201\232\\`\321@\024\016)\353K\364\245\307\024\224\242\234)\302\226\224\032Zu.i\374\020(\317\006\200i\342\236\246\245\rR\006\247+\340\361VZA*\017QU\236\020\306\225b\003\265I\214\014P8\245\335F\372<\312<\312B\324\306j\210\276*3\'\2551\210\307\025\0215\023\034\324Lj\0075\003\032a5\023\232\255#T\rQ\032\214\323M4\323M7\275-<S\326\247J\260\202\245\002\254\333\022\255\305:C\227\310\247\250\247\342\227\265\004~\224\240\0222(#\201K\214\nQNQ\326\227\024\230\243\024\264\340i\324S\251A\245\245\351K\316y\242\234\032\236\r<\032xjxj\261\033|\206\231\346sN\337\232pl\365\246\027\246\357\244/I\276\215\364o\246\027\246\026\246\023Q\261\246\357\3051\271\372T.\010\252\356j\0265\0315\013\032\256\346\243j\211\2523M4\323M4\332Zx\251\026\246J\263\035N\242\254\333\256\343\216\346\245\222\023\030\311\246-IJ(\31794\240q\301\243\2674\270\247\001J(\245\024\237J\\R\201\232u(\240zR\347\024\341\322\227\255-\024S\201\247\006\247n\245\rOY\010\342\215\374\322\211)\376g\035i\205\350\337H^\223}&\3727\322n\246\226\246\026\246\226\246\023LcL-\236*\031\024\212\200\367\250\\\324,j\007\250\330\324f\2434\303\326\232M#SE8S\205H\265:\n\263\035N\2654Rl>\225l\376\372\"T\222E@\243\006\245\024\224R\212p\346\224S\251\324b\2121E(\245\242\234(\306iz\032ZQK\212(\245\006\215\324n\243u.\357J7R\357\240\271\305\001\350/M\335F\372]\324\233\251\013S\013SKSKSKTmM2z\363PI\355P1\250^\240z\215\215FM0\323\r4\323Z\212QO\025\"\324\311V#\253)O#5j\331\202\251\365\246\377\000\025?<SM&i\300\323\251\300\376\024\341O\300\365\245*G=i:\322\322\342\223\030\245\0034\001\353KI\322\226\234\264\352BqJ\r!\244\315\031\030\246\226\"\215\324n\243u.\3527Rn\243u\033\250\335I\272\227u4\2654\2657u\031\246\223LcQ1\246\026\343\006\241\221p2:T\rP\275D\306\243&\243&\232i)\244\346\212QR-H\005J\265a*\302\032\222\254[\270V\344f\234\347,H\242\220\322QN\006\234\r8\034T\213\3158t\243\003\034R\212\\Q\266\212)GJCE(4\374\322\023M\315\004\346\214R\036)\244\322d\322f\223u;<P\r\004\322n\243u&\3527Q\272\215\324\322i\244\323sHM4\232a5\023S7v=*\031\007\247J\201\252\273\032\215\2150\323I\244&\233\232\005<T\212jAR\255N\225a*A\322\246\204e\205J\343\r\315&i\t\246\320\r;4\340i\300\324\200\342\237\232\\\343\2458\014\232p\030\245\002\220\212L\322\322\036\264\224R\346\202sM&\214\322\203E!\024\302i\204\322\203\232\\\361\212L\321\2323M4\322h\335F\3527Rn\244&\232M!4\322i\204\323\t\250\332\243\'\326\242qU\030\324f\230M4\232i4\231\245\245\025*\324\200\324\250j\302\032\235jQRF\373H\305Lr\347\216i;\322\036\224\323I\232\\\323\201\247\251\365\247\203R\016\224\361\357R\014R\321\320\321\326\223\034\321Hi\010\315\035)3Fi\244\321\273\336\233\236iwzP^\230\315L\245\315\031\2434\240\320M74\322i\244\321\272\2234n\243u4\232nh\3154\323\032\243j\215\2522}j\2214\302j3M4\322h\2434\361OSR\003R\251\253\010j\302\032\224S\207Z\274\233V\023\317&\253\347\232Ri\246\231K\232p4\360i\352x\367\251\007J\220\032\220S\261K\214\321\212)1Hi;SM%!4\322i\245\2517Rf\2234\322h\315\031\367\244\335N\316G\024\252q\326\220\232ajBi\244\322\026\244\315\033\250\315\004\323I\244\315\031\246\236j6\250\215F\325@\232i4\302i\204\322f\214\322\323\201\247\003R)\251T\324\350j\312\032\235iI\305[\265\304\200\2065\034\213\261\310\240\021McL4\200\323\201\251\024\323\324\324\240\361O\006\244SO\006\235J\r.8\246\237jCHi\264\323L=)\244\323OZ\030\00085\036h\315!4\231\2434f\2246)X\372SwqL&\2234\322\324\233\2513Fh\315\031\244&\233\2327PM0\324mQ\232\316&\230M4\232a4\231\245\240S\307Zp\251\005H\246\247CV\020\325\2054\254x\251l\345\304\240\032\275=\271\220\3461\237\245Tu1\360\334T{\263M&\233\232p5 5\"\232\221MH\246\244\006\236\r8\032\\\323\267qIHi\246\222\232i\206\232i\215L&\2234\231\244\2434Rf\220\2327SI\244&\220\232i4\334\321\272\214\322f\2274\231\244&\2234f\220\323\rF\325\226M0\232i4\322i3KN\024\361N\024\361R-J\246\247F\253(\324\342j5b\216\010\255\233=McC\270U\033\253\257>B\325\016\352B\324\231\247\203R\003O\006\244SR\003R\003R\003K\232\\\322\346\2274f\220\323M4\323I\346\230M0\323\r4\323sFh\242\220\360i\271\2434\231\246\223I\232i4\334\321\2323KFi3M&\2234f\214\346\232MF\325\222Z\230Z\230Z\223u\031\245\006\236\rH)\300\323\205H*E5*\265L\257O\337HM cN\006\235\272\220\265\001\252E5 5\"\232\220\032\221MH\r<\032p4\264\240\322\321Fi\246\233Mja\246\232a\246\023L&\2234\240\322\346\220\236)\204\323sM-Fi\t\244\315!8\244\315\031\245\315\024\204\323I\244&\2234f\202i\215X\244\323\013SsIFi\300\323\324\324\200\323\301\247)\247\203O\006\244\rO\rO\017N\rK\232]\324n\2434\240\324\212jE5*\232\220\032x5\"\232x4\340i\300\323\250\315\024f\214\323MFi\244\323s\351L&\230\306\230i\271\2434\240\323\331~\\\212\210\234S\017\037Ji4\231=\251\271\2434QE&h\315\004\323I\2444\231\244\315\033\275\351\t\254&ja4\334\321\232\\\322\203R)\251\001\247\203N\006\236\r8\032x4\340i\301\251\341\251wS\203R\346\226\234\r<S\301\251T\324\212j@i\300\323\301\247\203N\006\236\r.i\t\244\315\031\244&\230M0\232a84\322\336\264\322i\215Q\232\006M(4\340\347\030\244q\305DM0\232M\330\246\226\240\032v}(\315\035h\351Hi3IHM0\322f\2234\026\254\026ni\244\322f\214\321\232p4\360jE4\361\322\236\r8\032p4\360i\300\323\201\247\003N\006\224\032p4\340i\340\323\201\247\203R)\251\024\324\200\323\301\247\203N\3158\032p4\354\321I\2323HM0\232\214\232a4\322i\244\323I\250\311\244\007\035i\336\342\223uI\346\002\270\"\241c\315DM4\232@\330\315(\351\305(4f\2274\271\244&\222\220\323I\246\223M4\334\322f\260Kd\322\023I\272\214\322\346\224\032z\232\221O5(4\340i\300\322\203N\006\234\032\234\032\234\r8\032vi\300\323\201\251\001\247\003O\006\244\006\236\r<\032\22058\032x4\360i\300\323\263Fi\t\244&\214\323\030\324li\204\323\t\246\223M&\230M4\236jEn9\2466G=\251\003qL-M&\230M&i\331\030\343\255.sFOz(\315\033\250\316h\246\032i8\244\246\236\264\206\271\342\334SKR\203FiA\245\006\236\rJ\206\244\006\234\r;4\340iwR\206\247\006\247\006\247\206\247\003N\006\234\247\232\220\032p5 4\360i\340\323\301\247\203O\006\236\246\234\r8\032v\352\\\321\232ni3HNj6\246\023L&\230M4\232i4\302iA\342\244\335\270TG\214\342\230M4\232ni\t\240\032x<\361O\335\232C\3056\2234f\214\322\023M\'4\204\322f\232k\233-I\272\215\324\240\322\346\234\r9NML\r<\032x4\240\322\346\215\324\240\323\203S\303S\203S\203S\301\251\024\323\301\247\203O\006\236\r<\032x4\360\324\360i\340\323\201\247f\234\r\031\2434\204\322f\220\232i4\303Q\232a4\322i\204\322f\214\322\206\301\346\2069\025\0214\334\342\233HzQ\236)CS\325\251\375E34\323II\2323Fi)\264\231\256_4f\224\032\\\323\201\247f\236\265 4\360i\300\323\201\245\315&i\300\322\203O\006\234\032\234\032\236\016jPi\340\323\301\247\203N\006\236\032\236\r<5=MH\032\22458\032vh\315.\352Bi3\357I\232L\323I\246\032\214\323\r4\323s\326\2234\023K\234\214S\032\230M%%&h\3158\032z\2659\271\346\230i\206\220\2323Fi\r!4\225\312f\2274\240\323\201\247\003N\006\244\006\234\r8\032x4\240\322\346\214\373\322\203N\rJ\032\234\032\244SR!\251\001\247\206\247\206\247\003N\rO\rOV\247\206\251\001\247\203J\032\234\032\235\2323J\032\202i3Fi3HM0\363L&\230i\206\233\330\323s\315-\004\323I\374i\207\332\232M&h\2434\003N\rO\rIM4\323\326\212(\244=i\246\271,\322\203N\006\234\r8\034\323\301\247\003N\006\234\r<\032]\324n\2434\271\245\006\234\032\234\rH\rL\r<\032p4\360\324\340\324\340\324\360\324\360\325 4\360i\341\251\331\245\rN\rK\232\001\245\315&h\315&i\t\247.0sM+\270dT\0140y\246\023M\355M\'\2323A4\323M&\233I\301\240\373RQ@4\340\324\340h\246\236i\017ZJPh4\206\270\372p\247\003N\006\234\r8\032Pi\331\245\006\235\232\\\322\356\240585.iA\247\203R\241\251\001\247\006\247\206\247\006\247\206\247\003O\rO\rR+S\301\247\206\247\006\245\31585.\352\\\321\2323Fh\315\006\223v(\017\214\212B\233\206sP\260\3052\230zSM\031\240\234\323M74\032LdqI\232J)sN\rK\234\321M4QE\025\306\212p4\340i\300\323\201\245\315(4\271\247\003K\272\227u.\352\003S\267R\203N\006\236\rJ\247\024\360i\301\251\301\251\340\323\203S\301\247\206\247\203O\006\244\rO\rN\rK\232pjPisF\352\\\373\322f\227>\364f\212m\001\261Ms\232\214\323\r0\232nisHM6\212L\322Q\232L\321\232\\\322\203KE\024\235\351h\2560\032\\\323\201\247f\224\032viA\245\315(4\003K\232]\324\003N\rN\rN\006\234\246\245\rO\rN\335N\rN\rR\003O\006\234\r<\032xj\220585<5(4\340iCR\356\245\315.i3K\2323Fi\t\244&\232M4\232\214\323M4\232L\321\232)\t\240s\305%4\234\321E\031\2434\340h\315.i(\242\270\300iA\247\003N\006\2274\240\322\346\214\322\356\245\315\033\250\315(jv\352Pi\341\251\340\324\201\251\301\251\300\323\303S\203S\303T\201\251\301\251\340\323\301\251\003S\201\247\003N\006\235\232\\\323\201\245\315\031\2434f\227>\364g\336\2234\231\244\3154\232a\246\323M!\244\315.i\r\024\037j@3IHi)s@4\354\322\321E\025\377\331"
+byte_png: "\211PNG\r\n\032\n\000\000\000\rIHDR\000\000\002\000\000\000\002\000\010\000\000\000\000\321\023\213&\000\000\010\244IDATx^\355\335\211r\343\250\026\000\320\224\373\377?9\256\031\333\261\343\310Z@\002\304rN\275z5-K\010.\227EJ\267\363\365\325\266\353\364@\264\313\364\000\000\334\214\263@\034_L\001\240I{\227\300q6\tc\333\233\037\000\000\264\304\356\276\021:\352\240\016\236o.\222\000\000\000\006\322\301C\014\201<\354\301AM\017\242\313y\023\376Y\367\345\227>\030\234\004\340N\"\000E5\275u\206\303\256\327\nV\336\3470<\241&\'\334\362t\225\266\271T\265\336\356S\352\226T\306\302\017\000\3000\376<\365D>\377\337O\216\272\002\242\310\256l\016=\366\036\272\230:\351Tj\362\234\374\345\345\240\254\376\000\371\305\316\265?\347/^\266\370\301\016\347n\000R\266\244s\347v\024\345\\7G\205\\\200\256\254\217\371\365O\2419\326\260\235z\t\\/\355\330i\360\346\177\215\033\201\200w\357K\217\000+\227\354\267p\257_\233\'\354\223\251\330.=bu\375\372\367\236\001\007\003\370H\262\333\377gI\2537\007+\372!w}a0\251\207\350_\006lv\331B\23473\340E\252\001,Z\233\"#\267\0001\247Oo\033s-\211=\202\177\371\372\236\034\237\232v\322\264\023\243\035. \334\322\033\260\321M\373t\206\310\301\235\241\000\260\311TIE\244\343\246\303!\nx\226\370\343\360\r#\224\274W\007\256\361\275\031 C\221\201.2 Z\300\357\364l%\246\263-\271\037\014h$\351\265\2228\225\0216\240\214\214Kc\306\242\367\252\260J\220G\360Fbrb\372A\022\\\223dnw\374mG\371\3737\345\021\250G\220\362\206\352\377\322\237?\244\377\351\235\340d\313[\261:\004\007\2431s}7w\214\332\365\232\241\0041h\233\224\270\333\022\027G\313\316X\021\316\270\'U\221\002\305]oQ\177\306\375\300\032p+\342~\371L\031\227\231c\207L\0373S\227\237Yc\325\335V\313\317b#\177\267\306I*\t\026t\302\210:*\305\274\231\242\014\330 \3152\022\\Ng9/\357}\340\357\216\177\231\331c\366m\305\373\037\312T\243c\367\010\177<\324O\377\274\350-\376i\272\"M)l\272D\364\362\231jI\210&\202u\323LE\003\325\222\000\024\360\231\274\237G\252\325PU[!\244e\375|?\364\364\360\026\335D\016\323o\004\223g\235\233}\340g\034\021#<\342\324t\256\263wm\345a\272\025^\361\035\324S6N\022\240\227|\310\334E\231\213\317\245\321j\327b\272a>\333\376\356\334\177%pH\225\203\257\262\251\255\177\227\023b\036z\303\327y\211S5\364\376\275I\034\306Wy\251\313}\312U\356K\366\033\264`\324\321\000\037\014\006\306r0\343\017^\276\310\322\014\220T\360\264\032|bn\327\327\032SM\225\032\262\024\263\245\343i|\374[\2034\362\224\032\346v\357{\036\236Y\211\302\006j\352\036\231\262\234\212\215\326\345\271\036\3578\323%\274cC\317\243k\227\361\246>\350\313\357\243\364\333\221m5\014\374\245:\334\366\340A\215\240\037:\374\220\370\360-\215\276\342\342\253\016\300\274\364S{\372\022\311\350\325]\326V\302\370\301G_\214\374\341\214\333\345\346.\346\311\214<\262\3155:\214td\023c\371;1\313\377%\003E\246\305\246^\037\265\316\266\315\030\312F\002\314~|\213\374\354\007\231=\357\371\375\347\350\303\341l8\243Ag\t\014VHH\002\213\312b.\0178EH\252P\205T]u\346\310\'\241\310\216L\225?T\241\325\356\214\314Z\226\034H\200\003\227&TG-h\200Ic\215\350\214mi\"\275\347\305\334\267\020\317\035\203\362n\251\273\224\276\344UM\3347+b\276\n\323o\230\372m\031\214\252\245Q]w]\227\227\320\345O\316WwL\233Rs7/\370\255\262\177\250RLOC\256\247\266\000\000df\303\315\017\273h\316\"\367r\272O\362!!\356\352\347k=\265\005\010`\320\307\020\255A,\375`E\002\014h)\031*\325Vm\213\022\0326t\230\"\317&u\330\264\014\032\233\355C\255\356\\\356M\356\263\335\303\3534\237\353\266:\332h\307\336\216\334{\035\365\212\232H%\000\014-j\276(\305\274\224\334RH\257\313\0371\222*\277\267\257\253\277\273\000\000\241r\274\030]+\362\343\263\217\003\347\261\027H$GR\001\355iy*\270\327\335\252\020\353\362w\001h5\200\337\267\212\377\274 \270\264\333\214\363\265\033\271[\036\267[\373\032\264<\375\1775_}\356\222\366b\322\302\022\253\264n}M\241\247\006y3\224\247\326\256{uDw3\t\250\333\301\016\\\272|.;\347\216\245\260T\207x\351JjG\3126\317<k/\024\237+\027\206\260\020\323\363}V\354\363H]\374| \277\231\020\317\034\242\177\277\263A\235\tP\373l\005\344V\347\334D1\022\240\2544\361n\357/P?*\334h\265\017\230\351\357\303e>\335\313NV\0327\257p\212+\000\220X\242\355\305\314\366\222`\211:\201rtYaf\230\206u\327y\377\372k\0221.\022\340T\242\017\230\010`xf\001\350\310\216\367k\346\200\236D\'@\364\005\324\315x\236\032,\"\2035\027\210`~\200\\\306\332O\233K\022\020\304\303\204\020R0\222\000\000\310\256\350\246\263\350\315\200\342\306z\013\013\345\031c\000D\032\355\021l\264\366\206\312\025\227\327\326$\327\r\240\022R\034>\030\026\000\000\205\005\377x \370\304E\037\277O\341{\362g\212y\337w?\376\273\334N|\232\006P\251r\203\002\000\200\363\331\375u\306\223\347\311&\035\240?\006\327P\002X\014``\r\315U\000\225\351b\013\265s\031\350\242\355\275\320\031\033v&\371\207{93\205=;@G0\034IO;d+\274\271\030\021\207<\267\2033\333B\250A@j\0161\t<\246\272\350\246\006\204\217&\374\364d\262\016\215N%\316\225\252\347u|\363R\245B_\306J\354\261Z\233\226\3301\022\353EeL@{\255\246\362xa]\r\307\233\311y\327\351\001\232\264\222\360\037_\361\360i\373\014*\267\222\000\237\364\367P\002\272;\340\024\240I\177Fw\324JA\027\226\246w\271\360\264\024\241\023\235\3319\025\206\343\200\276Z3k%Yv\265~\327E\355\352\247\271+\211\020\255\237\250\214c\265\317V?\234J\231I-k>\016\3157\340d\342\007\264+j\335\357\226y\234\036\254\216\346\225\017\223\344\377\277\351\201v$i\177Y\025V\371Y\245\n\253\306\'\3354\234\253N\007\312ha\262Y\331\025\267\255\205\340\367C\264\233\367\234\t\032~\216\335\257\333i0?\241\013\362\372\327\006\325O\225\325W\020\000\000v\261\323]t\211y\254\21397\271\2147\317V\364\275\340l\245\0374\251\327V5\347>7\260\2327\327\255S!\347\320\255\327(7\334G\'\003\000\200B<\200\320\213\2507nP\234\207\274A\351x\300L02}?\274\200\257L\337\"\213\032\027\233\002:\034:b@\303\270b7\000\r1\265\225\2623\211v^\006\320\201z\226\250\357\235\223\361l\013f\017R\267\235\t\300nUE\274\252\3120(+\307\340$\000@\303\354%\001\000\000F\343u\036\264\307\270\005\362\363\256x0\226\226f\350*r(0\347\027\270\005\014\342{z\240vV\256\204\022|Q\004\0041p\273w\321\3074A\242\002\0000\252\337w@v\3050,/\203i\225\245+\0053\300\227T\202\202\016\016\267\304SV\342\342\032v\260_Z4`\223a\312\034\0100\260?\213\200\025a<\372|`\257\316\257<\013<\261\345u\275\377O\224a8\225\317\375\024\"\017\250\217MI1u\206\272\316ZAG\254\375\300@.\266\026\000\000\000\003\272.<\013z1\006\000\320\232\205\215\035\314\221.0,\303\037I\000\000\000}\332\263\325\337s\r\320\000\203\033\000\000\330\340wb\017L\337\003\000\000@\223<\322\017N\002\000\000\000\000\000\020\311w\n\003\214\3516\377\373\361b\333\364\037\000\000\300P\274\312\005\000\000\000\000\000\200\221\370\213\242\000\000\000\000\000\000\000\000\000\000\000\000\320\013\337\'\010\000\000\243\363T\000$\364\037\213Ab\036\317\"\"\006\000\000\000\000IEND\256B`\202"
diff --git a/core/res/geoid_height_map_assets/tile-9.textpb b/core/res/geoid_height_map_assets/tile-9.textpb
new file mode 100644
index 0000000..5397cb37
--- /dev/null
+++ b/core/res/geoid_height_map_assets/tile-9.textpb
@@ -0,0 +1,3 @@
+tile_key: "9"
+byte_jpeg: "\377\330\377\340\000\020JFIF\000\001\002\000\000\001\000\001\000\000\377\333\000C\000\004\003\003\004\003\003\004\004\003\004\005\004\004\005\006\n\007\006\006\006\006\r\t\n\010\n\017\r\020\020\017\r\017\016\021\023\030\024\021\022\027\022\016\017\025\034\025\027\031\031\033\033\033\020\024\035\037\035\032\037\030\032\033\032\377\300\000\013\010\002\000\002\000\001\001\021\000\377\304\000\037\000\000\001\005\001\001\001\001\001\001\000\000\000\000\000\000\000\000\001\002\003\004\005\006\007\010\t\n\013\377\304\000\265\020\000\002\001\003\003\002\004\003\005\005\004\004\000\000\001}\001\002\003\000\004\021\005\022!1A\006\023Qa\007\"q\0242\201\221\241\010#B\261\301\025R\321\360$3br\202\t\n\026\027\030\031\032%&\'()*456789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz\203\204\205\206\207\210\211\212\222\223\224\225\226\227\230\231\232\242\243\244\245\246\247\250\251\252\262\263\264\265\266\267\270\271\272\302\303\304\305\306\307\310\311\312\322\323\324\325\326\327\330\331\332\341\342\343\344\345\346\347\350\351\352\361\362\363\364\365\366\367\370\371\372\377\332\000\010\001\001\000\000?\000\363<\322\206\247f\234\r(4\271\2434\271\367\243u.h\rK\272\214\322\356\247\006\247\006\346\244\rO\rN\rO\rN\rO\rO\rO\rO\rO\rR\006\247\206\247\203N\rN\006\234\r8\032\\\321\232]\324\271\2434f\214\322f\214\322f\232M4\323I\244\2444\224QE\024\200\3434\235i\r%\024S\250\242\212up\364f\234\r(4\354\320\r.\3527Q\272\2274\003K\2327R\346\224585<5<5<5<585<5<5<5H\032\236\246\244\006\236\r<\032x4\340i\300\323\201\245\006\2274f\2274f\214\342\214\373\322f\214\320M%6\220\365\244\244=i(\244&\200sKM\246\232(\242\212QKE(\024\265\303\323sJ\r;4f\2274n\245\315\033\250\315(j\\\321\2323F\352pjxn)\301\251\301\252@\324\360\324\360\324\360\324\360\325\"\265J\255O\006\236\r<\032x4\360i\300\323\201\245\315;4f\214\322\347\336\212L\321\232\\\321HM%!\024\224\206\222\212i\245\024\264\332i\242\212(\245\024\264\270\245\351Hk\2074\224\231\346\234\r\031\2434f\214\321\2327R\356\243u.\3523F\352]\324\340\324\360\324\340\324\360\325\"\265<5<5H\rH\246\245V\251\003S\201\247\203R\003N\006\236\r8\032]\324\354\322\346\214\322\321I\232\\\321Fh\242\214\323M!\244\242\223\275-\024\230\346\222\233E\024S\251@\245\242\232k\2104\224\206\214\322\203HM&i3Fh\315(4\271\2434f\215\324\340iA\247\006\247\006\247\206\251\003S\325\252@\325 j\221Z\245SR\003O\006\236\r<\032x4\340i\300\323\263N\006\2274f\235E\031\242\224\032Z(\242\220\322\032LqII\336\226\212)\264\204RQJ\005-:\212CI\\I\024\332CIE\024\206\220\322f\223>\364\271\2434\271\2434n\245\006\224\032p4\340i\341\251\341\251\341\252@\325\"\265J\246\245V\251\024\323\301\247\203O\006\236\r<\032p4\340\324\340i\300\320\016)\300\321E(9\245\247QE\024Sh\2444\224b\227\336\222\220\322R\001\212Z)@\245\242\232h\256,\212a\024\323IE\024\332i\246\346\212L\322\346\214\322\346\214\322\203J\r(4\340i\341\251\301\252Ej\221Z\244\rR\251\251T\324\200\324\201\251\340\323\303S\301\247\003N\006\234\r<\032p4\354\320\r.h\3158S\251\324QE\024\204Rb\212LR\320zSh\246\236(\245\305.(\242\212CI\\{\n\214\212a\024\322))0i)\246\233Hz\322Q\2323Fi3N\006\2274\240\323\201\247\006\247\203OSR\003R+T\252\325*\232\221MH\r<\032x4\360i\340\323\201\247\203N\006\234\r<\032\\\322\346\226\234;S\251\302\212)qF)(\"\223\024b\223\024R\032J)\000\305-\024QHM%\025\313\2749\252\355\031\025\031\030\353M\"\230E%4\323H\246\323i\244\346\214\322f\214\321\232\\\322\346\234\r(4\240\323\301\247\203R)\251T\324\212jU5(5 4\360i\340\323\301\247\203N\006\236\r<\032x4\340ii\302\224S\351\324\3521N\242\227\024\224Rb\223\024PFi\264\204RQE\024\202\226\232z\n(\256}\201\035EB\342\241x\3628\250\031\010\250\310\246\221M\"\232i\246\230i\264\207\255%\024QJ\r(4\352Pi\340\323\301\251\024\324\212jE52\232\220\032\220\032x4\360i\340\323\301\247\203O\006\236\r<S\305.iE<S\305<R\323\251qKE\024b\223\024\224SH\2444\224\206\222\212CHh\315\024\231\2435\222\312\r@\360\372UwB\265\031\301\250Y*\026LS\010\3054\212a\024\322)\206\232Fi(\2444\264R\212\\\323\251\342\234*AR)\251V\245SR\n\220\032x4\360i\340\323\301\247\203O\006\244Zx4\360ii\324\341\326\244Zx\247\np\024\264\354QF(\244\"\222\220\212B)\246\233Hi(\2444\224R\032J+0\212a4\326P\303\232\255$\004Ur\204\036j9W\035*\"3Q\025\246\021M\"\230E4\212i\244\242\212)E(\024\352p\247\212x\355R-J\265*\324\202\236)\342\244\006\236)\340\323\305<S\305<S\305>\234;S\207Z\221i\342\236)\330\247\001K\212Z(\244\305%!\024\323M=i\207\255\024\332)\r%\024\332(\252\014\265\021Zk\014Te\210\250\\\346\240e\3109\250\031\010\250\310\250\312\324dSH\246\021M\"\223\024\230\243\024\264\240R\201K\212p\247\nx\025\"\324\253R-H)\342\244\024\361O\035\251\342\236\264\361R\nx\355O\024\361N\024\361OZ\220\nx\024\360)\300R\342\226\2121M#\024\204R\032a\246\236\264\323IHi)\017ZJ):\320\005-Se\250\312\324N8\250XT,\246\242<S\030\217Ja@\325\023\305\351P2z\324M\221\332\223\031\244\3054\212LQ\266\227\024b\235\212P)@\247\001O\002\236\005J\242\244Zx\251\005<S\305<S\305<v\247\212\220S\326\236)\342\236)\353R(\251\000\247\201O\002\235K\212Z)\010\244\246\322\032a\246\032CM\2444\224\206\222\212(\243\025]\205D\302\243e\250Yj\026\250XTL*\"1I\274\212C\206\250\312\343\202*\027\217o#\2450\212M\264\233h\333K\266\215\264\273i\301iB\322\201O\002\236\005H\005<\nx\024\360*AR\001N\002\236\005<S\305<T\202\236\005<S\300\251\024T\200T\200S\305<R\201K\212\\RQM\244=i\246\230i\246\233M4Sh\240\212LP\005\030\245\252\3548\250XS\010\250\331sQ\262rsQ2T\016\230\250J\324l\264\322(\353\301\246\225\364\346\2431\203\315FT\2126\321\266\227m\033iv\323\202\322\205\245\333N\013O\002\234\005H\0058\n\220\nx\024\360)\340S\300\247\201O\002\236)\340S\305<T\212*E\025 \024\361O\024\361J)\324R\021IHi\247\2554\323M0\323i\r%!\024\224QE\024\240T.*\"\274\323\nSJ\212\201\316\r0\340\365\250\23528\252\345i\245i\205j2\264\336E\033A\351HW\324S\nc\245&\332]\264\273iv\322\355\245\333J\026\234\026\234\026\236\026\234\026\234\005<\nx\024\360)\340S\300\247\201N\002\236\005<S\300\251\000\251\000\251\000\247\212x\247\212p\351N\024\264PzSi\246\220\322\032a\351M4\323IM=h\242\212LQ\212\\QQ\311\307QQ\2023\315D\347\346\"\241c\316\r0\25794\205A\250\210\347\035\252\'J\214\2554\255FV\230R\230W\024\017zw\226\010\342\231\266\227m.\332]\264m\245\333K\266\234\026\236\026\224-8-<-8\np\024\360)\300S\300\247\001O\002\236\005<S\300\247\212\220T\213O\035\251\342\234:S\307JQKE!\244\246\322\032Jm0\364\244\246\322b\222\212(\242\212*2\373\206\rDN)\215\202sLe\3151\224\324y\307\024\322\271\2462\344TEqM+\232C\0354\305Lh\361L)M\332iv\322\355\243m.\332P\264\241iv\323\202\323\202\340\323\266\322\205\247\001J\0058\np\024\360)\300S\200\247\001O\002\236\264\361O\025 \247\212x\355O\024\341N\035)GZu\031\246\223\232)\264\206\222\232i\246\233M\242\212m\024QE\030\250\260\001\246\355\004\320c\301\246\225\246\221\305Wd\301\246\036)\206\243jn)@\247b\220\307\232\215\242\305Fc\246\354\366\243\241\301\245\0034\340\224\241)BS\266R\354\245\333N\333F\332\\S\261J\0058\np\024\340)\300S\200\247\nx\247\212x\247\212x\353N\024\360i\342\234)h\242\212)\264R\021M4\303\326\220\322R\021IHE\030\244\305.(\305-T\rRdu\240\270\357L\334\017CLn{\323\0175\023\000)\207\024\306Q\330\3231F(\247\250\247c=E#F\010\250\214x\355Lhw\366\346\232ad\367\251\025r\005<%.\312]\224\273)v\320\026\227m\033iv\322\201K\212v)qJ\0058\np\247\n}<S\205<S\201\247\203N\024\340i\331\245\242\212)1A\024\206\230i\246\220\323h\244\305%\024QE\025L\214\032c=0\266i\003\342\232_\232\003f\230\355Qg4\034\201MV\311\346\236V\224\n\\S\305<-)L\320\023\006\234\321\002:Ub\2066\351\305<.E8-.\3126R\354\243m\033h\333F\3326\322\342\227\024\270\245\002\226\235N\024\352p\247\003N\006\234\r<\032x4\340i\300\361K\232Z(\242\212CL4\323M\242\223\024\230\244#4b\214PE\000R\342\263\013\346\232M0\234S\013S\013Q\277\024\302\364\201\251\373\251\274f\234\r<\nx\024\340\265*\212xZpJv\323\216)\222\306\032>\225]\007j\227m.\332]\264m\244\333F\3326\322m\243\024b\214Q\212ZP)i\324\240\322\203N\006\234\r8S\305<\032x\247\nZPih\243\024\270\246\232i\246\232i\024\224Q\212n1E\024QF+\017\1774\360\334SI\250\330\323sL&\231\234\232\t\305;w\024\3459\247\014\323\303S\325\215L\207&\246\002\244\002\236\005</\024\320\241\216\017z\202X\274\267\247\005\315;m\033h\333I\266\215\264\233h\333I\212M\264b\214QE(\024\264S\251E8S\205H:\323\205<S\251A\245\035i\324S\250\246\221L4\323\322\222\212)\010\346\222\212)1@\353K\\\343\032r\266E)5\0314\332k\032fy\245oZi5\"\032\234t\246\223J\257\212\235d\350EYI\001\353R\214v\251S\223S\252\323Ja\270\244\236=\313\357P \342\244\333F\332B)1F(\305&)\010\246\342\214Rb\212(\242\212u\024\341O\024\361\332\236)\342\224S\207ZQ\326\235J\0058RR\032a\246\232CIE\030\244\"\223\024Q\336\200=)Es&\2054\342i\204\323sMcH)\343\221\212\215\226\225\016*p\374S\013P\032\244Rj\312?\0305:\260\307Z\2327\301\253\321\020\302\234\313\363S\2312\275*\243.\327\305<\n1I\212LQ\212\010\246\342\220\212LSH\244\305\024\230\244\242\212u\002\234)\342\236;S\305<t\240u\247S\307ZQN\247R\021M\"\232i\246\232i1K\212\\sI\212LQ\214\322\025\346\224\016\324\240W.i\005\004\323O\025\0337\245&iA\247\251\024\244dR\005\243\245!\247-N\270\247n\002\234\262U\210\336\257[\311\203VD\300\232\262\010e\025Z\3456\2604\320(\"\223\024b\214R\021M\"\223\024\334R\021HE%6\220\322QJ:R\212x\353N\024\361O\024\361@\353O\035i\302\234)\302\224QM4\323M\"\233E\024\242\214Q\2121HG4Q\326\271L\323Kb\233\273&\232\317\315FM\000\323\363H_\025*6Fi\331\246\223@\245c\201M\363qHe\315H\222U\210\234\325\270\345\"\255\246H\334*\314R\236\206\211\244/\317\245*\034\212~(\305\030\244\"\223\024\322)\010\244\3054\212f)\010\244\246\322\021IJ\0058S\205<S\307Zp\247\322\212p\247\201N\024\340)\330\244#\232CL4\322)\244Q\212Z1K\2121F)\244P\005-q\346\233\324\320\307h\367\250I\246\323\226\245\003\212c\016jU\030\024\356\324\334\323\226\221\307\025\003)\246\200sS\306*\324b\255\306\271\305[\214\225\\T\360\214\346\247\n\010 \365\250\323\344}\265`\014\322\342\214R\021M\"\232E4\2121M\"\230E4\212i\024\322)(\242\224S\3058\nx\247\np\247\nx\247\212p\247\nZ)\010\246\221M\"\233\212JP)h\242\227\024\322)1K\212\343\013f\232[oJc1=i\271\244\247%L(\306M<\nZi\247-\0148\250\217\"\220/52\361V#Ry\253\221\n\266\243\002\254\300\234dT\354\234dT2/\000\216\325,|\201Rm\243\036\324\230\246\221M\"\233\2121M\"\230E0\212i\024\322)\244Rb\224\nZp\247\212p\351O\245\024\360)\342\236)\302\235N\244\305%!\024\322)\244RQE/jZ)\010\244\242\270|\342\202A\2461\246\347\232Zz\361R\nx\247QM<\323\322\207\351Q\nv)\311\311\2558\020\005\025aTT\3123Va;j\354 8\'\265C,xb;S\"\033X\255O\2121HE4\212i\024\334PE4\212aZi\214\216j2)\244SH\244\242\224\np\247\n\220R\342\235O\024\341O\024\361N\024\242\212)1M\"\232i\270\244\242\235J\005\006\222\212+\200-\315\001\251i;\323\205H\007\024\341O\035){P)\017Z\221(z\217\024\247\245I\002\026`kN1\200*\302\324\312*e\025j\014\203O\230g\004Uc\303\203V\227\221F)1M\"\232E7\024\021M\"\232T\0321\306*\006\\\032i\024\302)1E.)@\247\212p\247\322\201O\024\360)\302\236)\324\270\353F3@\024\224\323M4\332\r \024\264\341E\024\230\243\025\347D\363NZx\351KJ*U\247R\203JM\000\322\023\315J\235(\316M&)\247\322\257Z\307\205\031\253\212*d\025aEL\203&\254\'\007\212\224\214\212\254\313\301\366\251\3429QO\305!\024\322)\244Rb\233\212i\024\322)*6\03750\212a\024\230\243\024b\234\0058\np\024\341J\005<\nx\247\212p\247\nQE\024\206\232i\204R\021IE(\024\264\270\342\200(\305\030\2577\034\323\201\247\203N\245\035i\340\323\301\247\nF\342\2054w\247\226\302\323Q\262jZD\\\2775\243\021\342\254\245N\202\245\310\002\244G\253P\363\315X\306EVu\301>\364\260\235\247\006\254\342\202\264\322\264\322\264\334SH\246\021M\"\223\025\031\034\323H\246\021I\266\223\024b\235\212p\024\340)i\300S\200\247\212x\024\340)\324\275\250\305\030\246\232i\024\206\233I\2121KN\002\212P(\305\030\2575\002\236\0058Q\232\\\323\225\252@i\371\305!9\245QK\212\033\221L\037+U\205\344S\200\301\253q\036\225q\010\035iL\276\224\345rjt\255\033q\2003V\2052H\362*\020\207>\365:\036\306\244\3054\255&\332i\\Tl)\204SH\246\221M+M+M+M\333I\2121J\005.)\300P\0058\nx\024\340)\342\234)\300R\322\342\222\232z\322\032i\024\204Rb\214P\005-(\034S\200\245\244\305y\270Z\\b\212B9\2434\341\326\245Zu\024\364\024\204\363N\034\212FL\362)\312\330\353Rn\315O\023\0003\336\245\016O\322\246\217\236\265aMX\213\226\025\245\017J\262\265&2)\0259\351Oh\3062)\000\310\240\2554\214Tl)\204SJ\323\010\244\333M\333HV\230V\220\2554\255&\332\\R\342\214R\342\234\0058\nx\024\340)\340S\200\245\305\024\021M4\322)\270\244\305\030\240\n1K@\024\360)h\305y\2304\354\346\232N)A\315\004S\220z\324\271\300\245\003\024\243\232\223\240\246\232QS\001\305\030\024\030\373\212|C\'\025d\214b\245F\253\0108\311\253p\0163W\2438\002\254#T\312\325\"\363S\371{\223\"\242\t\212R\265\033\naZaZiZaZ6PR\230V\230V\232V\223m&\3326\321\2121@\024\340)\340S\200\247\201N\002\235\212\\z\320G4\204SqHE&)1F\3321F(\305(\024\340)qF+\313\305:\233\324\323\207\024\340sN\024\341\326\234[<S\201\30585\004\322\216\265(<S\224S\311\342\235o\313\223V$\351Dun3\221V\340\340U\3055:\032\225M<?5j\t{\036\206\245e\364\250\272\366\246\225\246\225\246\2244\302\264\233)|\272k-F\303\025\021\300\246\344QF(\333F\332M\264\273iB\323\302\323\200\247\001O\013F=)q\221JG4\322)1I\212L{Rb\227m&(\305\030\243\024\360)qF+\312\305-8R\232\000\251\000\300\240\236\324\243\2123J\032\236\r(5\"5H\r9\217\024\350\016\01754\215O\214\325\250\210\365\253\221\034U\244j\231Z\245V\245\316\rK\024\234\214V\222r\242\230\340\006\351H\303\212M\224\322\264\322\264\334\000ipOALh\237\322\253:\234\340\320\266\254\3434\326\262n\306\241h\335\017\"\220>:\361O\0074\360)vQ\266\235\262\224-8-8-;\003\246h\372R\201\216\224b\220\212LRm\244\305\033h\305\030\244\305\030\245\3058\n1K\212\362\214\322\212~8\245\002\236\006(&\221FNM8\2650\265&\352xzQ\'\255=^\245\022\nz\276jd \032{\216\364\364n*x\233\201W\242|u\251\325\252tz\225[4\254\330\2536\213\274\346\264\320\342\221\316\347\030\243\024\264\306 S0Z\244[|\363R\254[y\247\272\251N:\325#lY\262EZHF\334T2\302\312x\025]\243\335\324Tmh\030t\252\222@\321\036(G\365\253\n3N\tN\331@J]\264\270\243m\024\270\245\000\032B\264\233i\010\244\305*\216\264\230\240\212LQ\212]\264\340(\305\030\257%=jTZy\342\223v(\r\232w\035\351\245\361\322\243/L\336(\337\336\223\314\315H\032\246\215\207J\221\030d\324\350A\251\224`\324\200\3664\231\332\246\247\203$f\256+p*tj\2305L\217O-\232\275b\3406=kQ\024\036\225\033|\214sK\221Mg\364\241\020\267&\254EnX\360*\372[\205\030\"\234b@9\305FD=\360*\264\262 8Z\256\323m<S\326F\223\036\225(\205Xt\346\206\266\300\371j\264\266\233\263\221Td\262 \344\n\215T\241\346\247A\232\227e.\312M\264\322\264\230\243\024\270\245\333AZM\264\205i6\322\250\000\234\322b\215\264\230\243\024b\235\2121F+\3111\315H\274R\232h\031\247\022\0050\275F\315L\'4\302i\246OJE\220w\247\254\303\326\244YT09\247\254\336c\341x\253h\330\034\032\262\262|\2714\261\311\274\373\324\2238\001GsVm[\367d\032\235\0375*\271\035*d\223&\254+S\303\324\360\273\0021[6\363\344\014\365\253C\347\3523A\210t\002\221m\035\317\3355v\033=\277z\254\210\302\014\360\005C-\317e\252\31730\250\031\232\252\276\342i\2715<R\221S\213\254\034T\3510r0qW\002\006Q\300\346\230\326\201\272U9\3541\310\025I\255\3323\2208\247\'5!^)\205i\245i\245h\013K\266\224-\005i1I\266\215\264\230\306i\002\322\355\244\305&)qKE\030\257&\331\212pZk\373Sz\n\214\234\323\t4\302Oz\211\346\003\201P\371\244\236i\371\310\250\217ZQOZz\261C\221W\"\23389\347\275X3\340\343\265Ii \363y5=\311\314\253\212\263\013`}ju|T\252\364\365|\032\230MV#9\346\256F\330\034\325\353yFEn[H\205GL\325\245e\'\370sR\371\321\240\371\216\343\350)>\322\010\302.\rC$\245\270&\253\021\232M\264\322\225\023\307P\024\240)\024\354q\232\221\t\035+F\336^0\335*\364d\036\225ab\0140\302\240\237O\005IQX\3676\215\021\334\005B\215\232v\332B\264\322\264m\243m.\334Rm\244\333AZM\264\322\274\322\343\024\323\3054\232M\324n\245\315\'>\224\273I\257+lRg\216)\204z\324nj\026`*#%D\362Uwj\214\266)\313%?viA\247\006\305(z\221\037oJ\262\254\n\362y\244Y\2126Gj\320\211\314\307y\364\253L\333UO\255>9sS\253\322\371\230\245\216B^\267\355\255\013@\035O\024\004!\261V\243!:\346\256\3013\266\002\212\324\2066#.j\177\335G\313\234\232O47B\007\2654\234\367\243\024b\227\024\322\200\323\014T\337*\236\261\002)D\035\305H\250E_\266\343\025}\010\317<T\340c\336\253\\[\t\201\342\271\313\270\r\274\247\035(\214\206\034S\212\322\025\244\333N\333HG4\025\246\342\220\2121M\300\250\335\200\250\211,x\247\010\031\251\337g#\255 \217\232\221a8\247yT\206<W\224\025\036\224\323\201P\271\250\035\260*\263\022MF\306\241cQ1\250\215 5\"\276)w\322\356\247\006\247\207\251\222BF\321Ly\n\235\244V\345\236\014JE>v\373\253N\210\360*\33251\337\232\265n\200\200Ml\333]\210\343\nNG\245)\273\313p1Wm\246\014>`\010\255{f\207\031R\006:\324\263_\240\033R\253\t\313\036Nj\302>ju\346\237J\r\007\353J\r\024\340\231\247*S\300\301\251\000\366\251\020\340\325\373e\336G5\243\345qQ:\n\310\324\255<\300N+\017i\205\275\252\302:\260\340\322\221I\200)\t\024\231\024\322\342\2432\016\325\031v=\005&\347\364\246\222\347\265\013\003\261\311\253Q[\205\353S\355\013Ma\236\324\301\037=)\341\016)\014f\217.\274\204\212\211\205B\374UW\3115\013qQ\265D\325\013\na\024\323I\232\\\322\356\245\rO\rOI6\266jI\037p\007\336\266l\234yC\236\224\347\270\005\261\217\306\244\216L\324\302LSD\233\237\025~96\000\005Z\215\213\n\265o\t\221\376n\007z\272\322*\r\211NIXp\rL\256OSVcj\265\033\342\254\244\2250z]\364\273\251U\252@i\353RS\200\006\236\242\237\264\324\360\314b\"\265\355\356\026T\301\373\325#GU\346\2040<V%\345\230\311\300\254\326\264pr\271\024\236T\302\233\344\316{R\213Y\217\\\323\276\303.y\3158i\316z\223S\307`\243\250\315Y\032x#!x\2456 \034\025\250\332\311Gjg\223\216\202\220\300~\224\242\020:\322\024\002\220\250\024\224\323M\257\0365\023\036*\274\207\203U\232\241j\214\364\250\332\243\"\243\"\230E&(\305%\024\340iI\251\003.\337z\277gq\225\353\322\234\322\345\252\344-\305L[\212\215$\303\326\234\r\270\014\326\214X\002\255\tx\302\360)\312jej\231\036\254F\365e\036\247G\251\203\232xzxzP\365\"\266jd5.}\351\312\325(5\"\234\365\245#\232\275g(\215\206k\\:\272|\247\232\211\205A%\272\277QP\265\232\366\000\325w\266\307E\240D\000\345)\2050\331\002\246\t\274r)D\002\203\010ZM\341F\007J\202I\262x\246\371\343\0375D\323F\017\024\217.zTe\252\026j\214\275(9\2434\332\362\006\025\013\212\251)\346\253\275DE0\212a\024\302\264\322\264\302\264\322\264\205i\244SH\305(\245\305C<\276Z\222\016)\332d\345\263\223\326\264\201%\271\253\321>\005J_\212\213v\0335\255k\'\356\324\326\2042f\255!\251\224\323\267\372T\250MXF\305YI\005L\263\001R\254\242\236$\247\t(\022T\311%N\222T\242Z\221^\246G\251\224\346\245Q\2322T\3475b+\355\204\014\326\2147K(\031\353S\343\214\212\214\322\034S\n\003Q\030\2114\360\240S\361\212\2574\300dt\252NI\031\rU\244W\316s\232\202\\\2163LRE;y\0244\231\025\02350\265(zv\354\320My\023Uy\033\322\252?Z\205\2050\2554\2550\2554\2550\2554\212a\024\323L\"\232i\264\352\315\324$\302\220*\326\212\204\214\236\225\253\374uj3S\003\305D\347\006\264\355\030\030\226\264\242\224\001\201V#z\260\255R\241\031\353S\254\201{f\227\314$\344S\326CS\243\023S+\021\326\244\022S\303\323\203T\210\325:\275H\262T\213%X\216J\265\033U\225aMs\236\225]\301\007\"\254\332\335la\232\337\212ex\301\0074\034\032a\024\200\322\346\220\220*3(9\025J\347\'\221T\335\231V\242\016O\336\244`\r7g\025\033\014Td\323\r4\232@h\017\212R\365\344\2621\252\316j\026\031\246\225\246\025\246\221M+NX\201\004\261\305Wu\347\212\210\212a\250\3150\323\t\244\243\265f]\246\351\000\255{\030\374\250F:\325\241\367\252t5:\322\262f\255@\031\000\364\253)!\0075r)\263\336\256F\371\251\343 \036j~\033\356\324\253\220=\351y\0075<s`sN\363A\247\007\247\211)\302Z\221f\307z\224M\232\221d\251U\352x\344\305]\212^*e\222\246S\221Lz\205\362\274\325\375:\364\203\265\217\025\246\263\202z\323\314\312\007Z\210\334(\353Lk\324\025]\356\331\363\216\225\037\236Oz\212IK\036\264\306pG5\0162j@\243\034\322\021\201P\265B\324\323\315Fi\271\244&\232My[\324\014)\273x4\302*2)\244Rb\232\335*&\025\023\n\205\252\0264\302i\244\321JzV|\347\367\2035\253f\013F\t\253\241EH\270\251\227\024\375\352*\304rdd\364\246Ip\027\201V-\244\316+J90\005YG\3175b9MXY)\333\363F\352xjxz]\364y\224\tjT\224\325\210\336\255\306\331\251\324\212\263\033U\205z\231d\305!|\232q\033\326\230\243\3139\025g\355g\030\357A\271\'\2754\316i\245\311\245\016i\333\251\214i\264n\300\342\223\314\315!zal\324Li\264\323\315Fx\246\223M&\274\265\2522\264\230\3050\2550\2550\2554\212c\n\205\252\027\250\036\241ja\244\245\002\234G\025F\346>sW4\351\2066\261\300\025\246\010<\002)y\024\354\232P\244\375*O7h\300\252\345\213\311Zv\274\001Z\010\365z\006\335\305Y\300\035)\003\324\210\331\251\001\247\006\247n\244\337@|\323\3075\"\266*\302I\212\263\034\265a$\253\t-X\216J\225_4\375\325\"59\2153\275<c\024\022(\016)\300\323\267SI\246\223M&\230M4\265!4\231\2444\303Lja\024\323^`E \000R0\310\342\243+\212a\025\033\na\250\232\241j\205\352\006\250\310\246\221M\3058\np\\\324\0271\344b\250\020\361\234\212\261m~\361\261\3632kF\rAX\363W\005\302\205\310\002\241{\242N)V\\\212\236\021\223Z1\014\n\262\215\212\265\004\2705o\315\342\220?5\"\311R\211)\302J]\364\273\350\rS#f\235\272\244F\2531\275XW\251VLT\361KV\222Q\353R\t\005J\262S\367\344Rf\205\223\024\374\203M\350i\333\351w\346\215\324\322i\244\323\t\246\346\2234\271\244\242\242q\212`>\264W\231\225\246\221M\"\230\302\243\"\243e\250\332\242aP\260\250\\T,)\204SH\244\333J\026\244\013\212\216E\315Wh\001\250M\267=(0\024<U\310\203\005\000\232q\214\346\247\211=j\324\177)\253\321\276EN\246\246F\305N$\247\207\315<=J\257\305/\231N\363)C\323\203\324\321\2658\2775,oVQ\252Q%<IR\307.OZ\264\254}jt~9\247\2111R\ti\342L\364\245\316i\341\360(\363)\013\322\206\247n\244\335M&\220\232J(\244\006\202qMnj\"0h\2578+M+L+Q\260\250\330sLaP0\250\330TL*\026\025\021ZaZn\3326\323\325i\032\243\306z\323X`PW\214\323H\311\025\"\234S\267\323\225\352\302\034\325\250\016*\342\232\231N)\341\205<0\247\253T\201\2517S\267\323\325\362je\347\255I\274(\244W\311\251\325\361R\254\2250~)C\346\244W*\302\256\307>@\251\204\331\2453zS\322\\\324\352\365 z]\364\240\320[\024\t1N\017\232]\324\205\251\273\251A\247f\226\233Fr)\244\342\230Ni3^zV\232V\243+Q\262\324L*6\025\023-D\302\243aP\262\324ei\214\264\335\264m\243\024\322\271\244\333\212\215\305+\034\001L\003\234\324\200R\021J\2654mW\"j\266\207\"\234d\305\002\\\324\252\365a\017\024\354\321\232P\325\"6*p\331\034SY\215*\275J$\251VJ\235d\342\236\262`\322\274\26543\347\203S\211H\247\2113S#\342\246\022T\202Zw\235@\233=\351|\312O2\234\262{\323\304\224o\245\017OSO\006\2279\244&\243\'\232B\324\302i7W\020R\243+Le\250\231j&Z\211\226\243e\250\231j&Z\211\226\230V\230R\233\262\223m&\3326\323J\324,\274\323\030f\200\265 \024\021J\0059\001\'\212\267\020\351V\323\245)\024\321\301\251\220\324\352\324\360\324\354\321\232p\353\305N\206\225\31538\247\253T\252\325*\275J\0374\355\331\251\"85d7\024\252\325:\311\305<IN\022\342\227\315\245\363h\023R\211sR\007\247\t)\341\251\352jEj\2245.\3523Q\263S\t\315F\317\212a\222\271r\225\033%D\313Q2\324L\265\023-D\313Q2\324l\265\031Jc-FV\232V\233\266\232E&)\010\250XS6\322\355\247SM\024\36485n6\351V\220\346\224\232eH\271\251T\324\200\323\301\247\212p5 <SK\346\220\032x5*\324\200\324\210jPjDlT\241\251\301\252P\334S\303S\267SK\342\215\364\241\251\352\3252\275<75\"\236j`i\331\251\025\251wQ\277\212\214\266i\204\342\242w\250KV;-DW\332\243t\301\250\231*\026Z\211\226\242d\250\331*&JaJ\215\222\230R\232\313L)L+M+L\"\230V\233\266\223\024\230\246\221F)EH\214E[\211\315N\334\212h\251T\324\213\315H\005H\005;\024S\351\273y\247\001O\002\245Zx\251\024\342\244\006\2245J\255N\335\315L\215\232x4\271\246\226\244\335N\rR+T\212j\302sS(\342\234\033\265\005\251U\351\333\3517R\347\271\250\335\352\026j\210\265Qd\250\331j6N9\353P\262TL\225\023%F\311Q2Tl\225\031J\215\222\230R\243e\246\262\324ei\205i\245i\205i\214\2704\322)\204b\214Rb\235\212U\025:\034T\313\'cO\034\324\212*U\251T\324\200\322\356\245\3158\0323NZx\247\347\024\241\251\301\351|\312<\312\225$\251\224\346\246V\305H\032\224\2654\323i\300\323\301\251\243oZ\266\203\214\324\201\261N\006\224\234\324d\340\323\203f\234\010\035i\035\270\342\240g\250\331\251\205\251\205j\"\231\2462\324,\225\033%D\311Q\262TL\225\033%F\311Q2sL)Q\262S\nTei\205i\245i\205*7^j2\264\322)1F(\305(\025\"\324\212\271\2531\245K\267\024\n\220\032p4\341N\006\227u\004\320\254jA%.\362i\273\311\247\253\023R\014\232xZ\225\026\246\034S\267b\234\257\315)j\004\225 `i\331\245\006\244C\212\266\217\305K\270S|\316iZN8\244\031\357J[h\250\314\246\2173\212c5FZ\2235;&j6J\215\222\242d\250\312Tl\225\023%F\311Q\224\250\331*\"\225\031J\215\222\230\313Q\262S\nS\nSJ\324L\265\033%1\226\223m&\332\n\320\026\236\005J\202\254\245I\232\000\3158\nv)\302\235M4\240\323\205\004\322\203N\006\236\rH\246\245SN\335\212Q%;vi\312i\371\315\000T\200T\213\315?\024\341R\253\340T\201\263N\343\326\234\010\245/\212k8\"\240\335\315.\352Bi\231\2435\242R\243)Q\262TL\224\306J\210\245F\311Q\262Tl\225\033%BR\243e\250\331*2\224\302\225\031ZiZ\214\2550\245F\311Q\272\323v\321\266\215\264\233)\301jE\030\251\024\342\237\232@\3305:\020\325&(\002\226\212a8\243u(4\340i\324\3655\"\324\253N\306h\332i\301H\247\250\247\347\024\241\252e\251\007\255H\2434\360\224\275\005*\265\005\350\3631A\223\"\233\272\233\236i\331\244\3154\320\rm\024\250\312Tl\225\031J\215\222\243d\250\331*6J\211\226\242aQ2Tl\265\033-D\313Q0\246\021L\"\233\266\232R\243)Q\262S6Rm\245\333F\332\002\323\202\323\302\323\266\322\025\247\247\006\254\216E8\n\030T\014H4\204\346\201\326\234*E\024\354S\324T\310*P8\243<\324\213Rb\214SM*\324\240\342\244\00752\n\224P\335\351\203\212k5F\033&\235\232M\324f\234\0334\271\240\322W\377\331"
+byte_png: "\211PNG\r\n\032\n\000\000\000\rIHDR\000\000\002\000\000\000\002\000\010\000\000\000\000\321\023\213&\000\000\006\"IDATx^\355\335\333\266\342(\020\000\320\263\234\377\377\344q\315x;G1\367\020\002\324\336Om4\tP\005\001\264\273\177~h\307%=\000\000\000\000\004`G\000\000\000\310\315:\003\000\000\000\000\000\000\000\000\000\000\000\000\000\000\032\345/\013\003\000\000\000\000\324\315>.\000\000\000DdG\000\000\000\000\000\000\000\000\240]~\373\001\000\000\254b\021\001\000\000\000q\\\323\003\204\364\177\036\\\345\002\313\330=\214N\006\000\000t\3114\017\000\242\2623\314\274\213\311bl\022\000b2C\210I\334\001\000\000bJ\326\203v\004[\267r\201\177\013\370\312S\250\232h\002\000\000\000@\\\266\370\001\000X\301\3641\256{\354%\000\000\000\324+\347\277\362\221\363Z\344\347\267\337\214\320s\203\223\000\020\227\376\017\000\221\331%\000\350\226!\036\200\233\322\317\203\322\367#\221\006 }\235\231\315e\000\000\000\200\340\016\336\177\002\000\000\340 \326s\237\242\374\000\"S\3343]\006\000\000\000h\202\235\000\000\000\200p\236\337\241G\371*\2357\266\001\240\t\272*\000\333\230\341C([\273\374\326\363h\335\353?\233\277Zo\004\'\001\242\223\001\301I\200,\314\246\000h\215g\027\000\000\000@\313\354\356\000\000\000S\374(,\2364\346\351k\000\000B0\r\004\010\353\342+\344\310D?8\t\000\000\020\203\255\277\350~3\300\022\000\000\272c\246\307:2\006 \212\333\026\200Q\237\317\034\2601\004\000\315\362\030\007V\353vE\330m\305 \247\367\216\242\323\304!\326\335\261\n\000\000`-s\310\270\304\036\"3\002\000\000DgF\030\214\357\205\203\223\000\020\217\'=\000\360\306\324\240m\342\007\000\360db\024\234\004\240i\333\276\255\333v\326\235\036\323\236#b\266#\205\340|Gt\n\000\200\354rNZ\314\340\033\2645\001\266\2367\355\230\253RJ\215C\200\234\002hC\215\317\020\312\362\314\216N\006\004\367L\000y\020L\032\360\3645\264\3004\026\200\332\205\231c}U\364\353\000\347\021\014\010n|\335\264kx\030\277,\237\216j\251\r\341\033:\345\250\342\0015\032\032\005&\031\"j\366\027\316j\343\264:\343\350\313\241\tp\350\305Y\244\332\241\007z\226\253\343\355\271\316eh\004\23696\3646m\022\313S\355\351\2720+C\202e\270D.F\253\315\036Q\254(\226\034+\355+[B\177;g\313yT\350-\220b\332\274\264{\007P.k\017j\334W\005\376*\222\365F\213/\366\371\301\301-\200\345\336\316.\027\240\266\034\325.k\003\267\366\363\017\327G\361/\267?\354\314\025\010\355\264\356\263q\010z\225\367\275\334\317\361\000\210\313(P7\361\251J\301\'\377\\\3443\025e\3566a\2156L\246\206_\351\25589\013\220\363Zg\030\215\322N\023\355r9\352\236s&\312DnU5\366\2430\037\313\267\267?\347UU\305\3330\030\214\301\203\313\315\237\2763P;O\347\333|\314\026\273G\347~\275#{\375d\016\330!Z-\177\213\215E(\377\235\330\357\370\250<\362\341\370\373\260\322XG\205\355^Yu\371\353\363\217I\301?\333\307\200\3475G\022\366\357pz\203\221\023\342\032i\220\221\303\204\223\366\240\212\205N\332\232\3434X\266\301\203\031\035}\375\316u\333|\335V\354t#-;r\370\347\347\237\364@)\243%\"\261\252\245^_*\255:\351h\203\205\031<\310\200\365-\025z\026\326\201\201\345\372*kN\272\337+G\302\244\327x\177\235\276\307\264{\000s5Z\235?\323/V\250\341\316P\354\366\333\014\027z\205\327\267n\225\327\223#}\007\377\324/c/i\201\316,\014P\232\036\177\256\357\'B\020\022o\204\206i\317\242\230]\236+\277\267\017?W\202\'\016\002\023\267\236x\213E\021\037uo\332\312\332w\2668\373\252\314\237\340-9\233i\255Z\\\261SW\375;,,\366X;,<=\214z\333cc\311\346N\273\275?\367\231V\215%}O\216\036\267\016\276|niq\353\374b\'\217\216\253\266]\232\000[i\3346=\306\303g\364\326\006\361\231<\327\333E\326\236{\214\301ei\372z\314\327\227I\237n\225\374\374\300\324\253Pj\257\372\\\371\026\217\202\237\027\372=m\361\371\375\231k\332>\254\255\345\332\317\323\210\300=}Xo\r\322[}8\214T\201\272\035:\021\233\272\370\324{\034cn@\236{\177\316i\273\243\005\357[\360V\345$\201\337\233\007\257m\263\304\3201j$Rls\037\036\273\034#\013i\275\353\365\036\373\326\343\303N\317\004\350)\017~\353\322S\245\250\305\340b \210\337\252\367\376\\<\303\231y\2253\251\037\227\272\234[\241\r\032+.p4\203B\007\0041<)\000\254d\235\373-c\233\344\\v\316\333R\360\353\343\311\361*g\321\362\226\266\265r[\317k\307\226\314\351[\3771gJ\323\361o\272\360\203\014P\253\364\227\000\265Jfx\265\344iw\t\320]\205*QK\302Nk\243\2244\301P\302\"i\242\224\032\2052\335\'\323ez\244i\356\322\004\207\030\226\016\000\317\317\351(\301I\000\000ZP\366\207\007wK\247T\243N+\362X\311G\217\217\275\001\264\245\374\250CU$\000\363<\362\001\272t\375\367\371C\370e\032~\032\254\250e0\227[\323\274\375\017\022\327\206\243\014l\363\335\355\215\231\000\224\366\3754\002\330\302L\026\240\010\303-\000\205y\364\000\000\204\347\357-\001\000,c\332\004\020\234\357T\000\000\370f\273\200A\022\003\000\200\305\354=\003\000\000\000\000\361\330\031\355\204@\002\000\000\300\371\254\317\001\000\200I\345\376\322\263\345IY\325\265wu\005\nF\373\003\000PX\271\325f\353\252\233\254WW \000\000\000\000\000\000:\344\013E\000\000\000\000\000\250\312\177]O\370\366;\244\233\330\000\000\000\000IEND\256B`\202"
diff --git a/core/res/geoid_height_map_assets/tile-b.textpb b/core/res/geoid_height_map_assets/tile-b.textpb
new file mode 100644
index 0000000..b04a194
--- /dev/null
+++ b/core/res/geoid_height_map_assets/tile-b.textpb
@@ -0,0 +1,3 @@
+tile_key: "b"
+byte_jpeg: "\377\330\377\340\000\020JFIF\000\001\002\000\000\001\000\001\000\000\377\333\000C\000\004\003\003\004\003\003\004\004\003\004\005\004\004\005\006\n\007\006\006\006\006\r\t\n\010\n\017\r\020\020\017\r\017\016\021\023\030\024\021\022\027\022\016\017\025\034\025\027\031\031\033\033\033\020\024\035\037\035\032\037\030\032\033\032\377\300\000\013\010\002\000\002\000\001\001\021\000\377\304\000\037\000\000\001\005\001\001\001\001\001\001\000\000\000\000\000\000\000\000\001\002\003\004\005\006\007\010\t\n\013\377\304\000\265\020\000\002\001\003\003\002\004\003\005\005\004\004\000\000\001}\001\002\003\000\004\021\005\022!1A\006\023Qa\007\"q\0242\201\221\241\010#B\261\301\025R\321\360$3br\202\t\n\026\027\030\031\032%&\'()*456789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz\203\204\205\206\207\210\211\212\222\223\224\225\226\227\230\231\232\242\243\244\245\246\247\250\251\252\262\263\264\265\266\267\270\271\272\302\303\304\305\306\307\310\311\312\322\323\324\325\326\327\330\331\332\341\342\343\344\345\346\347\350\351\352\361\362\363\364\365\366\367\370\371\372\377\332\000\010\001\001\000\000?\000\310\315-\004\346\212(\245\315-\024\003O\006\234\r;4\273\251sFh\335F\352v\374\014S7SKRn\243}(jP\364\340\364\341%<I\357N\022R\371\264\276m;\315\367\245\022S\204\224\3572\227\314\243\314\243\314\251c\2235)l\212Bx\254\373\221\345\266}j5|\323\213SKS\013Rn\240\265\033\2517Q\272\233\272\224\034\325\210\370\024\354\322n\244-\201Q4\300TfqQ\231\3523=!\270\250\332\350Uv\271$\325\243I\232\\\321\2323Fih\315\024S\201\247\003K\232PisK\232Bizu\246\226\246\226\244-I\272\223u.\3527\322\357\245\017K\346R\371\264\242JQ%H\254M<\023N\334iw\032]\364n\243}H\222b\247\022\344u\240\311\216\365\005\313\007_\245T\007\322\235\272\230Z\232Z\233\272\227u\033\2517Rn\240\232\222>\265eM\014@\250ZP;\325y.3\300\250\014\204\323K\323\013\323\013\324l\365\0235D\317\212\326\242\212(\242\2123K\232L\322\203N\315\031\245\315.isK\232\032L\214S7SKRf\223u!jM\324n\243}\033\350\337F\372pzp~j\302?\025&\372M\364o\245\337K\272\223u(z\221d\247\027\310\252\357)\246+f\224\265F[\024\205\2517Q\272\214\322n\245\rAl\232\2323\201Ry\230\250%\237\035\rViI\357Q\226\315&i\t\2463Tl\325\031j\215\232\242c[tRf\214\321\232J(\315.h\315-\031\245\315\031\245\317\275(4n\3054\2654\2654\265&\3527SKRn\244\335I\272\215\324\233\350\337OS\232v\354S\326lT\202l\322\371\236\364\242J\220IK\276\215\324\233\251\301\351\346L\n\202Bv\223H\207\212R\325\03357u\033\250\335F\352M\324\273\250\335R\tp)\217)\250K\023IHM4\232i4\302\325\033\032\214\232\214\232a5\273A\246\321E&h\315%\024\271\2434\264f\2274f\214\321\232ijajn\352M\324\233\251\013R\026\246\226\243u!jizM\364\365\233\024\246\\\322y\224\242Zp\232\244YI\357R\254\224\361%8=.\352pj\035\270\244V\334\244Ss\216)\245\251\205\251\273\250\335J\032\215\324u\245\346\212Bi\271\244\315!4\204\323I\246\023Q\223Q\261\250\311\246\023L&\272\n\r6\212L\322QHh\315\031\244\315(4\271\2434\243\212L\346\202i\205\251\205\251\245\2517f\220\2650\265&\352izM\364\205\351\205\351\273\350\337K\346R\371\224\273\351C\323\226J\235e\315J\036\236\036\234\036\234$\244y:sJ\222\000iX\367\024\302\324\302i\271\245\315.iG4\361J[\212\215\232\2234\204\322f\222\220\232a5\0314\302j64\302j2i\244\327EHi(\246\321E!4\334\321\232L\322\346\214\322\346\202\324\200\322\026\2463S\t\246\026\243<SKS\013SKSKSK\323\013\323w\322\027\244\337F\372<\312]\364\340\364\242J\231%\305L\262\324\213%?\314\364\240=#>M9^\244W\310\347\2654\2674\322h\315-.}i\333\370\342\223u\033\250\244\315!4R\023L&\230MFM0\232a5\0314\323L&\272J\017Jm\006\233E!\246\232JL\321\2323Fh\315\004\320N)\244\324d\323I\3053=\350-Q\226\246\026\246\026\246\226\246\026\246\226\246\026\246\227\244\337I\276\200\364\355\364\273\351|\312Q-H\263\221\336\247I\263R\254\224\340\364o\346\244W\247\253\340\020)7R\356\245\315.h\006\2274f\224Q\232L\321\326\202i\244\323\t\250\311\246\023L&\230M0\232a\246\232\351h\2444\224\323E!\244\246\322\032J)3@4w\244&\232M0\232c\036\324\204\323\t\246\023Q\263Te\251\205\251\205\251\205\351\245\351\205\351\273\350\337I\276\227}.\372<\312]\364\241\352T\223\336\254$\324\363/\245*\311\236jUz\2326\371I\245\315(4\340is\315.is\3158R\346\220\232\000\240\232i4\302i\204\323\t\246\023L&\230i\246\230M0\327OE\024\204b\222\220\212i\244\244\"\230h\244\244<P\017\024\236\364\204\323I\246\023L\246\223L&\243cQ\226\250\331\2526j\214\2650\2650\2750\265&\372iz7\322o\245\337F\372]\364\340\365\"\275H\257R\253f\246CS\003R\253\343\212x4\360isN\006\2279\247\201KFi:\322\346\2234\322i\204\323I\246\032a4\303M4\323Q\232i\353]0\245\242\212LRR\021\212i\246\232a\246\232)\t\243\266(4\323LcQ\223\232Bj64\302j65\023\032\214\232\211\232\243-L-Q\223M&\220\232ijM\324\233\250\335F\372]\324\360\325\"\265J\246\246SS\306\325:\234\014\232\"$\2615`\032p4\360iA\315H\264\354\322\023Fh\315\031\244&\232M4\323\t\246\232a\246\232i\246\032a\246\036\365\324QK\2121IHFi\r4\212i\024\303L4\206\232i\302\203L&\243<\323I\250\330\323\t\250\330\324Lj&5\0335D\306\243&\243&\232M0\232B\324\322\324\334\322f\214\322\346\224\032\221MH\246\245Z\231jelu5 \334\374t\025:|\243\025(4\360qN\0074\360i\333\251wQ\272\2234\271\2434f\232M4\232i\246\232i\246\236\264\303\326\232i\206\232k\247\245\002\226\212)1IM\"\230E0\212a\246\232h\031j\220\323\032\243cQ\261\246\026\250\313Tl\325\033\032\211\215F\306\242f\250\311\250\311\3150\232c\032a4\322i\244\322\023I\232)sN\006\244PML\213\212\235EJ\242\246LT\240\323\303T\212qO\rN\rN\rJ\032\215\364\273\251sJ\r.h\315%6\233M4\206\232i\246\230i\244S\ru\000R\321E\024Rb\220\212a\024\302)\204S\010\305\"\214sJM0\232\211\215BO4\306j\214\265F\315Q3Tl\325\0335DZ\243-L-L-L-L-L&\220\232Bh\315\002\236\005H\270\025 j\221Z\245V\251U\252Uj\2205H\255\212v\372P\364\340\364\340\324\355\364\241\251\300\323\201\245\006\235\2323HM%%!\246\232CM4\303M4\303]E\024\nv)\010\243\024\224\206\232i\204S\010\250\330Rt\246\223Q\261\250]\252\"j2\325\0235F\315Q3Tl\325\0235F\317Q3\324e\351\205\351\245\351\245\251\273\2517Q\270R\027\002\200\364\242J\220=H\255R\253T\252\325*\275<IR\007\305;\314\247\007\247\207\247\007\247\207\247\006\247\203O\006\234\r8\032vh\315\031\243\024b\222\232E!\024\322)\204S\r4\212\3521F)h\242\212)\246\232i\244S\010\246\021\315Fj65\023\032\205\2175\023\032\205\232\242f\250\231\2526j\211\232\242g\250Y\3526z\214\2750\2754\2754\2757}4\312\0057\315\317J\003\323\203S\203T\201\252Uj\221Z\244\017R\007\247\207\247\211)\301\351\341\351\341\351\341\252@\324\365j\221MH\r<\032p4\340ii@\245\243\024b\232E!\024\302)\244S\010\246\221]=\024R\342\222\212(\246\221L4\302*6\3435\0215\033T.x\250\030\324.j\0265\023\032\211\232\242f\250Y\252\026j\211\232\243f\250\313S\013Te\351\205\351\215%G\270\232z\265(l\323\303S\324\324\201\275\352Ea\232x\223\322\236\257R+\032P\3478\024\375\304S\225\352A%H\032\234\034\346\247V\251\024\324\252j@jAO\024\361N\002\234\005(\024\270\244\"\220\212i\024\322)\204SH\246\021]-\024\242\226\212CA4\224\207\2554\323\010\250$<\324MQ1\250\\\324.j\0075\003\032\205\215D\306\242cP\261\250\035\252&lTL\325\031jaz\215\232\243g\246\023M\3159I\251\001\247\255H\r.\352p$\324\203#\255J\204S\214\224\345\222\245\022\003\326\234\007\245<T\252i\370\364\251\025\252u5*\232\225jQR\001O\002\236\0058\n\\R\342\214R\021M\"\232V\232E0\2554\255t4\240R\321E!4\224\231\244\246\223McU\334\345\252&\250X\324\016y\250\234\325w5\003\032\205\215D\306\242f\250]\252\273\265B\306\242cQ\026\246\223Q\226\246\023M\335\353H95*\2169\247\n\220t\251\025sO\333\266\233\346`\361O\014M.\354Q\27352\014\324\350\265a\022\244\333\353J\023\322\237\202)\313\326\247Z\235*u\251\024T\240S\300\247\201N\002\234\005.(\333F\332iZiZiZa\024\322\265\277E\024\334\321Hh&\222\220\232i\2461\252\354y\250\330\324,j\274\206\241sP5B\365\003\324Lj\0275\003\232\201\215D\306\241cQ\023M&\230MFM&iG\006\246S\232pZ\225H\305L\224\366\\\216*\002\2304\340\330\244\335\232\221Fj\304B\256\304\231\251\200\305<.iB\363R\252\344sM)\203R\240\342\246Z\235*e\025*\212\220\nx\024\340)\300S\202\322\355\244\333HV\232V\232V\230V\230V\266\350\240\323h\246\232)3Hi\244\324NqP1\344\324lj&\252\362T-P\265D\302\241aP\270\250\034Uw\025\003T\017Q1\250\311\246\032a\024\302(\301\245\305J\275*E\346\236\006\rJ\247\025(a\212\211\332\241\316M=x\251\343\346\254\307\305\\\215\300\025 l\232\260\203\212SOC\232\221\223\214\322(\305L\242\246AV\024T\252*@\264\360\264\360\264\360\264\340\264\273i\n\322m\246\225\246\225\246\025\246\025\255|RQHz\322\032m!\244\2444\302j\'<T\016pj65\023\032\205\352&\250Z\243aQ0\250\\Uw\025\003\212\256\342\253\270\250\231j\"\264\335\264\205i\002d\323\304x\245\362\351v\323\325qO\333K\332\230X\212o&\234\006)GZ\265\020\251~\225b%&\255\"\342\246\007\002\234\243uXH\352m\234Sv\324\212\265*\n\260\202\246U\251B\323\302\323\302\323\302\323\266\321\266\215\264\233i\245i\245i\205i\205kO\024\204RR\032Jm!\244\246\232a\250^\241z\214\324MQ\265B\325\033TMQ\232\205\205@\342\240qU\335*&J\215\2435\023Di\236Y\357Hc4\2011N\000\367\245\002\224\nz\2558\256ivf\223\312\2441\342\230i\312\271\2531-YT\315Z\215@\251@\251V2j\314Q\342\255\004\000P\027\232\177\223\306E*\307O\013\212\231\026\254*\324\241i\341i\341i\341iv\322\355\243m&\332B\264\322\265\031ZiZ\277M\2444\230\246\221Hi\246\222\230i\215Q=B\342\242j\214\324mQ5F\325\023\n\215\205D\374\032\205\306j\022\264\206,\216j3\020\035\005D\321f\243x\261P\025\305&\332B\224\233iB\322\204\247\205\247\205\245\002\206\342\241v\250\361\223S\306\265:\220*d5e\rX\214d\325\330\323\212\231V\245U\342\227o5<c\326\234W\024\201jdZ\260\253R\205\247\205\247\205\247\205\247m\243m\033i6\322m\246\225\246\025\246\025\253dRSM6\220\322SH\246\323Z\243j\215\273\324M\322\242aQ\021Q\260\250\330TL)\214*&\025\013\324{\t\240\246)\205i\276]4\245E\"qUY2i\2451M\305&\332]\264\241i\301i\333i\002\320W\212\201\322\232\253S\242\324\351\0215f8qRc\025<\0035\241\030\300\251\325sS\250\247\204\315H\251N\307\024\212\2715aW\0252\255J\253O\013O\013O\013N\013F\3326\322m\244\333HV\230V\243e\251\373\320E0\212B)\010\246\221M\"\232E4\212c\n\215\205B\325\021\250\330TdS\010\250\330TMQ0\250\266n5(@\242\241q\223L\331K\266\242q\212\201\305W\"\243aL\331K\266\227m(Z~\332\002\323\212TL1Q\260\315\013\0375f8\352\322\000*a\216\324\205sV-\327\006\264\020T\350*U\025:\255<\n\220G\271iV<S\302\346\246E\251@\247\252\323\302\323\302\323\266\321\266\215\264\233i6\322\025\246\025\250\312\324\204z\322\032i\244\246\342\220\212n)\244SM1\252&\025\013\n\215\205FEFE0\212\215\252&\025\031\024*c\232k\nn\314\320S\024\302*\027\025\003\255B\313Q\225\244\333I\266\234\022\227e<&i\342*y\217\212\201\342\246\010\252E\212\246T\247\2044\365CR\355\251#\030\253h\3252\275Z\213\232\265\032\346\246\020\232\231#\300\240\2474\241qR\001R*\346\245\013R\005\247\005\247b\227m&\3326\322m\246\225\246\025\2462\320E4\212a\024\224Si\244b\220\361Q\232cTl*6\025\023\n\214\212\214\212c\n\210\212\215\205\"\246\346\251\0311\232\204\2574b\230\302\243e\250Yj&Z\211\226\243)I\345\322\354\245\tR\2549\251\026\034S\366`S\010\244\362\363M1\342\224%H\221\022j\312C\221R\255\267\265I\366ojQm\216\324\361\021\251\022#\232\275\024\\\n\273\004\\\325\321\030\305/\227Ha$dTe1NU\251\225jP\264\365ZxZv\337Z]\264\233h\333HV\232V\232V\243+Qb\220\323H\244\246\323i\r4\323M0\212\215\205F\302\242aQ\260\246\021Q\260\250\310\246\025\247F\270\247\025\250\231)\230\2468\250\215F\302\243+\232\215\222\233\266\223m(\214\232zE\315ZH\270\240\250\024\322*\"\274\323\325i\nd\324\211\025N\221b\254\307\035YH\200\251\2260jO \021\322\233\366nzT\211m\203\322\255$X\025*\361S\246MJ\005H\203\"\242t\346\220%J\253R\252\324\201i\341iB\321\266\215\264m\244+M+L+L+U\351\010\246\221M\"\232i\246\232i\206\233F)\214*&\025\031\024\306\025\031Z\215\226\243\"\2435\"/\002\244\333Q\262\324%i\214\265\036\312\215\222\231\262\220\2453\313\245\021T\213\020\251\004x\247\355\2462\324dSv\344\324\251\036jA\016jd\207\0252\303S\244X\251B\032\22649\253h\234S\304U:CNh\261Dp\222j\332\333\340t\250\331pjh\327\212\215\226\220/5\"\255L\253R\005\245\013K\266\227m\033h\333M+M+M+L+T\361F)\244SH\246\021M\"\223\024\326Zn)\010\2460\250\310\2460\250\312\323Yx\315D\302\243e\250\314D\236\265(B)\300R2\361P\025\250\331j<Rl\315!JB\224\337.\234#\245\013\212~))\215Q\021\232UJ\263\032U\224\2175*\305Vc\213\332\246\021S\2045*E\212\235R\245H\352\312G\201N\362\362jx\240\002\244u\300\252\254\274\323\324`R\025\246\355\346\244U\251\225j@\264\241iv\321\266\227m&\332B\264\322\264\302\264\302\265G\024b\220\212i\024\302)\244SqHE0\322c\212a\024\302*2)\245i\2733Q\025\244\t\223La\264\342\244\333\3057o42\361P\225\246\024\315\'\223I\263\024\306Z\214\212P)M%!\246Q\214\322\210\352E\212\254G\035ZH\352\314qf\255$5*\303O\021R\210\361R\254u2&*`\265\"\'5aW\002\230\353\232\204\245.\332M\264l\247\252\324\212\265 Zv\3326\322\355\243m!ZB\264\322\264\302)\205k?\024b\220\323H\246\021M\"\233\212\010\250\310\244\3054\212\214\2557\024\322)\204SJ\323v\323\014yjy\030\030\246\201JW\212\211\226\230\026\203\214TLj6\250\361F1H\0015 N)\254\265\021\030\245Q\232\260\221\346\254$5:\307\212\235#\315[\212:\270\221\324\242:]\224\361\030\247*T\241)\352\2252GRc\024\326Zf\312M\224l\243m8-=V\236\005;\024\273h\305\030\244\333HV\232V\230E0\255f\342\222\220\212i\031\244\"\230E7\024\204S\010\243\024\302)\245j2)1HV\233\266\233\266\225S\223H\313\3157m\005j6Zc-@\347\025\021\250\230\322\016i\304S\220\n\221\230\001P3f\230FjX\243\315\\\216<U\224Z\231S5f8\252\334q\212\262\211S\254|S\035v\322)\251\000\251TT\250\234\325\205N)\n\342\220\255&\332n\332\002\363K\262\227e(Z\220\n]\264\270\243m.\332B\264\322\264\322\264\302)\214\265\225\2121I\212LSH\246\221M\"\223\024\322\264\335\264\204S\010\246\025\244\333I\266\223m7o5 L\niJn\312\nT.\2705\033t\252\3169\250XTei1K\214\323\225M)\214\232A\001\251\004\0252E\212\262\211R\205\305K\020\346\256 \342\254GVR\246\007\002\243\220f\242PA\253(3S\252\324\310\265:\216(\"\231\266\214S@\245T\251\004y\247\210\263M1b\232\026\234\0058-.\3326\322\025\244+L+L\"\243aYX\346\223\024\230\244\"\223\024\322)\245i6\322b\232V\232V\232V\233\266\215\264l\246\224\244\331\315H\023\"\232\311HR\233\266\232\321f\252J\230\252\314*\026Zi\025\0369\251\2213R\252\n\235c\024\024\024\004\024\270\305I\035ZT\310\247*`\325\210\305[\215ju\247\n~\320EF\311\203O\214\342\254\241\251T\324\300\321\232\\f\232\374p)\025x\247\252\340\324\312\265(Z\0313P\262`\322\005\311\245\000\347\024\360\264m\244\333M+L+L+Q\262\326F(\305&)1HE!\024\233i\245i\245i\010\246\225\246\225\243m\033)Lt\335\224\276VjA\036\0055\243\246\224\250\366sJW\002\251N:\3257Z\201\205&\334\322yx4\365\030\251\024f\246Piv\223K\214R\355\315*\2575z\001\220*\307\227OH\361V\024b\236)\342\2363K\264\232@\270\251V\244S\315YS\221F)\340`SB\344\344\324\212\264\355\265*\212\224\n]\264\307L\212\213f)\3120y\247\355\346\215\264\205i\205i\205i\204Tl+\037\024b\223\024\230\366\244\305\033i6\322\025\246\225\246\225\244+I\262\200\224\355\224\273)\273*E\217\212pJC\036j6\212\231\345\342\243\227\201Y\362\214\232\254\353P\224\247*P\311L\3075*T\312E8\221\212i\346\225EJ\211\223W\240\217\025mR\244\013\212x\024\241jEL\324\311\0259\223\024\300\2315\"\305S,5*\304E=c\251V>\016j=\2704\365\024\375\264\345Z\231V\237\267\212M\264\315\234\323\n\342\244\333\362\203AZaZiZaZ\215\205F\302\261\261F)\270\243\024\230\245\305!\024\230\244\"\232V\223m\033iBS\202\322\354\245X\362jA\035;\313\243\313\250\335@\252\322\034UII5U\320\232\201\2434\317.\232W\024\021\221L\331\315<Fi\353\031\247\2244\251\021&\254,<T\311\026\rZE\000T\302\237R*\323\302T\350\2252\256)\031sO\216,\324\342*\262\221\002)\306,Rm\305!<`S6f\234\027\024\340)\300T\250*R\0061M\333J\0274yt\246<-0\212iZaZ\215\205F\302\243aX\300Q\212B)\247\212i4\224s\351Hr(\006\227\031\245\331J\026\234\022\227e8%H\261\324\201)\257\305B\317P;f\240u\315B\321\324m\025B\321\324f:\212H\361L\013\232<\263\232z\256*E\025(\214R\205\000\324\212EJ\274\324\312*E\025*\214\325\204Z\224-J\242\236\0058.jdZ\235W5b4\342\225\226\230\313P\343$\342\236\005.)B\322\343\024\365\342\2369\247\342\225EH\026\224\247\025\013/&\230E1\205F\302\242aQ\262\3268Z1Q\265\'\226Z\244X\t\352)\342\000)Lb\2436\340\3645\004\260\2249\024\211\351S*\346\237\262\224%8%8GS,t\245p*\274\2435]\226\241\220\005\025_\314\346\224\020z\320\312*\026Ni\236^j)b\342\253\252`\324\342,\212C\r\013\021\025(\030\024\335\244\323\225\rX\215\rN\026\244QS\"\325\205Z\225V\245T\251\002S\325*eJ\231R\254\"\361C\n\215\207\025\010\0304\354Q\212p\024\340\264\354S\220sR\020\0059EH\203&\236W\025\023\256~\265\t\025\033\n\211\205F\302\243aX\324c4\242.y\251\200P0\005\033I\351A\030\035j2i\t\305!\001\3075\031\267\364\247F9\301\353S\005\247\205\245\331\212@~lU\205Zd\202\2532\346\242\223\n*\224\274\325r\244\032L\322\026\"\243g4$\274\363S\262\007Z\254a\301\247\205\300\247*\344\324\242.)\246,R\010\375\252d\216\246\010\005;mH\261\346\247H\352\302\307\305=W\025:-H\022\235\214T\211S\245Y@1Lu\246\021\305BW\232\\R\355\245\003\024\360)H\342\235\030\317J~9\245\305H\215\203R1\365\2460\250\030sQ\260\250\330TL*6\025\214\027\361\251Q=i\342<\363I\267\007\245\014\307\030\250Y\252\"rh\353R\'\002\244\002\232@\317J\225E<\n\\S#\\\271\253J\274Tn*\007\030\025J^M@V\242t\250Yi\204TL\264\314\02552\271\3059>c\203S\371y\024\320\2305f5\004S\214Y\240AO\021b\236#\245\021\324\250\230\251\221j\302\247\024\204`\324\321\221R\323s\223S\306\001\024\376\2254oRu\246?J\213\024\001K\212\\P)\325,k\201Hz\323\2513\203S!\336>\224\036\265\033\016j&\025\023\n\215\205B\302\263\025*M\240\n:\212a5\033\232\205\2174\312z\322\223\216i\301\370\342\234\274\324\252\265(ZR\274S!\341\315Y\003\212\216A\200MRv\'5\013.j\"\265\023\255DV\243d\250\212\323\nf\200\270\247\257\006\256Dw\n$\\sI\023`\342\257F\273\205K\260\n<\274\323\204F\234\"5\"\305O\n\027\255J\035@\250$q\232X\344\251\374\314\212n\376j\304r\020)\373\362jd\247\356\244\334qL\357O\024\242\234\005.)@\251\001\3055\251i\010\247+\025\351S*\356\346\221\222\242d\342\240qP\260\250\330U\000\270\240\340Td\322\032\205\315B{\323i\340P\307\212E<U\204\025:\212~8\245\347\024\221\240\337\232\230\374\242\243s\221U\0359\246\024\250\331*&Z\210\255F\313P\262\3236\322\354\243mM\017\006\255\024\334\265\017\225\264\325\230\344\332*_754L;\325\215\353@\221hi@\351P<\271\351M\014\306\234\020\236\265*\307\212\220%<GR\252\201\326\234\000\365\251\024\343\245J\017\024\204\322\nx\247S\200\247b\227\024\264\032@9\305-8\n\236#\306)\314*\'\025Y\305B\302\242aT\rF\306\243cLg\342\241&\232i@\247\342\232\343\"\210\200<U\225\\qR\255<\032ZU\340\346\234[ \324f\230\302\242e\250\231j&Z\210\212c-D\311L+I\217j6\323\227\203\305Z\211\370\301\251\366\006\246\371T\341\035H\024\216\224\034\322\000\324\340\204\323\304t\274\npjQ!\025\"K\353R\356\310\310\245\334M9sS(\342\236)\373sJ\006)@\247\201N\002\234\005;\024b\227\024\322)@\247/\314i\377\000t\361O\316E1\371\025Y\352\026\250\232\263\230\324Lj\026j\214\234\323M&9\247\216\00586i\257\351P\243lz\274\247#5 jpjZp4\200\201\301\247\210\375i\256\230\250H\250\330Tl*&\025\021\024\302)\204Sv\321\266\227\024\345\342\246Y\rI\346R\211i\302Zp\2234\360\364\340\374Q\346\032n\354\321\232PsOZ\221IS\355S\251\317J\225EL\265 \024\340)\330\245\002\234\0058\nx\024\340(\305\024\204Q\212T8jRrh\017\212Fj\201\315@\324\306\254\206j\211\232\242c\232BqM\242\236)qM5\024\243\034\323\240\270\354j\322I\232\2247\255;9\247\nF\007\250\247\307q\3742\017\306\236\314\276\265\001\031\246\260\250\231j&Z\215\226\243+M+L\"\214Q\2121N\024\360)v\323\200\247\201R\001O\035(\002\214S\266\322\205\247\201R\001OPGJ\235:T\353R\250\247\201N\002\234\026\234\026\234\026\234\026\234\005.\3326SJ\321\212n(5\031\342\230Z\230y\250\3150\326\031j\215\2156\220\232J3J\r8\032By\250\2449\025\014C\347\253\253\362\324\352jA\355N\006\235\232f\321\232v\320p{\322\201\305#.FEF\313Q\262\324L\265\031Za\024\302\264\334R\342\214R\201O\002\234\005=P\323\302\323\200\247\250\247\205\030\240\256)@\247\001N\002\244\002\236\005H\243\0252\324\313R-<\nx\024\340\264\340\264\354T\2423\212i\\Q\212n)\n\323J\323H\250\315F\302\243\246\032i\256|\232a4\332BqH[\024\302\364\241\351\333\250\r\232k\232\205N\032\255+\344T\361\265J\017\024\360iA\247u\245\035)\312)\330\3151\226\243e\364\250\231j&Z\214\2550\212n)1K\212P)\300S\224T\352)\373h\333J\005<\n~(\331J\026\236\0058\n\221EH\005H\242\245Z\221jP*@)\340T\201iv\340\212pbh#\024\230\244\"\232E4\212k\n\211\205D\302\243\"\230i\206\271\322i\244\322TO%D_4\205\351\003\342\235\346P$\346\234[5\033t\342\235n\371m\254j\370\030\251\025\251\340\323\3058S\324S\205<R5V\226P\247\024\335\331\355HT\032\215\226\242+M\333M\333K\266\227\024\001N\025\"\234T\201\251\302\234\005H\253O\013N\333K\266\224\nx\025\"\212x\024\345\251\205H\242\246QR(\251Uj@\270\245\003\232VQ\214\212i\311\034\322Rb\223\024\204S\010\250\331j\026Z\211\205FEFk\234\244<T.\335\205Ws\3150\2750\275&\372\013R\253\363\315J\257RpED\312U\262\265b+\223\300z\270\2370\342\245\034S\325\252AO\034\323\305>\232\325\237u\301\247\301\312\214\324\333\007jk!\250\231)\205)6\322m\244\333F\3321N\247\212\221MH*E\251\005-8R\201N\003\322\234*AR(\251\005H\2652\324\350*eZ~\332\220\240\t\232\217\024\302)1F)\r!\024\322)\214*\027\025\013-D\302\242a\\\331\353Lv\300\252\254\325\0136j3M&\233\2327S\263R+T\310sO\333L+\203\232\225.\031:U\313k\201\'\r\326\257\252\251\035\205\007`\007\232\024\203\322\244\006\224\265C$\241G\275R\226O9\300\035\005Y\215p\005J)OJaZaZaZiZM\264m\244\305.)@\247\212x5\"\232\22058\032vi\302\236:S\2058T\253R\255J\242\245Z\235\rN\206\245\003=($\3644\303L4\224Q\214\323\274\2763Q\021Lj\211\205B\302\242aP\260\256d\232\2573UGja4\302i\204\323I\244\31585H\246\246\215\271\253jF)\255\3157fjH\243 \344U\261+\001\201Mb\344pi\251<\221q\326\245K\341\2347\024\367\277\215W\206\346\251\231\344\270l(\300\253pC\264s\326\254\201\212Z\0014\204\2323\352(\3004\322\264\233(\331I\345\321\262\215\224\270\245\002\226\236\016)\352i\342\245QR\001N\013\351O\002\236\0075\"\324\212je5*\232\235MH\255\212R\324\302i\231\346\226\212T\344\325\223\235\234UV\0305\031\025\023\n\205\205D\325\013\n\345\\\340U)\033&\240&\230M4\234\323M4\322P*E\251\220\324\353%.\374\232\225Njej\225\006j\\\niAPI\000jbY\202y\253\321@\2508\025>1K\364\240\nZ1\232]\264\340\202\227`\240\306)\273\005&\3126\322l\3154\255\030\305\030\315.)\352*U\025*\212\225V\244QO\013\232v\302)\3123O\003\024\365\251T\324\252\325 j\\\322SM(4\3602)\007\312r*_;#\232\205\216M0\323\030T\016*\026\025\013\n\343fz\250\315\232\214\232a4\224\206\232i)@\247\003OSR\203J\t\006\247F\251\224\214\325\204z\225H\247\022(\306i\350\2650\030\034R\321\214\322\205\247b\214R\201N\002\234\005.)\010\244\305&\3326\322\024\246\354>\224m=\305.\332r\255H\253R\250\251TT\252\265*\257\2558\014PW\373\264\003\353O\003\322\234)\340\323\324\323\203R\203A\244\006\244CO#5\031\244=i\244S\032\242a\232\205\205D\302\270)\0375\t4\302i\264R\023M4\224\023@4\3655\"\265J\246\244Z\231ML\206\246\006\236\274\324\300qO\035)\324\352v)@\247b\224\np\024\270\247\001K\212\000\245\332)\245Gj\002\203K\264\nLz\322\021\352(\013N\tO\013R*\324\252\265\"\373S\305;u\031\346\203\203\365\241\01685(\347\2458S\300\245\242\235\326\234\200g\2321\203\305;4\322i3Hi\206\243j\211\205B\302\274\355\215FM4\232m\031\244\315!4\334\321@\247\212z\232\231\rL\265(\036\225\"\261\251\326\246Z\220\032x\247\212p\024\361N\024\340)\300S\261@\024\354R\342\212Bh\244\2434\023FE\002\244\002\236\277J\220})\340\372\323\263F\352\\\322\346\224\032p9\353N\\\212\231H>\306\236\005?\024\252\007~izg\024\001A\024\224\206\222\220\232i\246\032\215\207\245D\302\274\321\2150\232i4\231\367\244\315!ji4\231\245\006\234)\342\236\265*\n\235je\251\224T\252*E\251\224T\200S\200\247\201O\002\236\005.)pi\324\240R\321Hi)3F\352JL\322\322\203N\r\212\221\\T\201\251wR\203N\315(4\271\245\315(jz\265J\0105*\277@\177:\224\032x\366\245\002\220\320y\024\322\010\246\322u\244\244\"\230E4\212k.k\313\t\246\223M&\232M!4\231\244\242\234\0058T\200T\212*e\0252\n\231EL\242\246QR\001R(\251\005<\nx\024\360)\340R\212ZZ3Fi3Fi\t\246\223I\2328\307\275\007\2123\212\001\247\003\232^\247\212\220\034S\303S\203S\201\247Q\232\\\321\232p8\247\253\324\253&jU\223\037J\225^\244\017\232RE&\352BsM4\235\250\372\323M\030\246\342\232Ey94\204\323i\264\332\r\024\240S\300\247\201R(\251\024T\310\265:-L\242\245QR\250\251\024T\252*@)\340S\300\247\001O\002\227\024b\203HM4\232L\322\023I\232L\321\272\214\323\272\320\t\034v\241\207qJ\275)\303\245<\021J\r<S\201\247R\216\264\264R\346\200i\301\252E\222\245W\251\003\323\304\236\264\273\2517Q\272\214\363\305\005\251\013Rn\244$SI\025\344\331\244\244&\232M%\024\240S\300\247\001R\001R(\251\225jeZ\225EL\242\245QR(\251TT\212*@*@)\340S\261K\214t\245\006\203M&\232i\244\322f\233\232\t\244\2434\231\247\003N\004\366\247\014\343\236iW\234\346\227\214P)\300\323\305<S\2058R\321E\024S\263N\r\212\221^\236\036\224=.\3727R\357\245\335I\272\232M!jaj\362\274\322\023M&\222\212P)\300S\300\247\201R(\251\024T\352*U\025*\212\225EJ\242\245QR\250\251\000\247\201O\002\236)sE\031\246\2264\026\3154\232L\323sIE!4\206\200sJ\r=i\331\305\024\2434\341N\247\003O\006\236\r8\032Z)GJR(\244\"\200is\212P\364\355\364\273\350\337J\036\227u\033\350\335A4\302k\313(\'\024\332)E-<\nx\024\360*E\0252\255L\242\245QR\250\251\024T\312*U\025\"\324\202\236\005>\2274\231\245\315&i3IHE4\212L\322f\215\302\223\"\223\255\030\305/\326\224\023N\025 \245\247R\212u8\032p4\340isN\024\242\224\n\\PE2\227&\214PM&qF\352\003R\357\240\275 zx|\322\023_\377\331"
+byte_png: "\211PNG\r\n\032\n\000\000\000\rIHDR\000\000\002\000\000\000\002\000\010\000\000\000\000\321\023\213&\000\000\003,IDATx^\355\335\335r\263 \020\000\320\214\357\377\314\231\357\247^\264:QQ(\260{\316]\2155\215\254\013,\306\276^\324\360\336n\000\000\000\000\242R\006\000\000\200r\306\321P`\331n\210@\026\270+d8\000\000\000\000\300\240\324\343\000\000fb\035\032\000\000\000 \253\307\353zJK\220\316\343\274\001\000\000\000\000@\037\n\274@\t9#\261\326\215\357F\003\000\000\000\"2\337\375\311\371\000\230C\353z0\000\320\231\311\331=\261\007I\261?\035\000\000\000\337\230\002\002\000\000@\036ow\010\000@J\326\002 )\343\377\334\264?\000\000\000\300\324\324\366\001\000\000 0\367u\000\000\000\000\000\000\000\300)\313\353y\371N\00506Y*=!\000\000\211x\252?\000\000\000\307L\034\271\302\322\002\000\000\000\000\000\000\000\000\000\000\000\304\347\373\003\000\000\000\000\000\323Q\332\005\000\200\346\026\343n\000\000\000\000\000\000\000\306gu\033\000\000\000\000\000\000\000\000\350\316\215L\000\000$b\370\233\331{\273\001\000\000\000\000\000\022\361\317|\000V\026\0169\262\215\217\355\317\3718\003\000\000\000\000\000\000\201Y\014\312\300MC\214\357N\224\272\037\016\000\000\000Z0\341\006\000\000\000\202Q\356\000\000\000\000\210F\305\007\000\000(\342\271\032\344 \322\001\000\000B3\355\003\370D\206\004\000\200\021\271\343\233j\004\023\3004\244l\000\000\332\263>\014\000\000\220K\257\325\207\306\357kz\013<\3208C\001\000\300\2573\306\005\000\000\340\216O\363\311\375r\354\317-\333\337\333\357?\276\345\257\355\266i\254\'\374\376\007(j\261\363\2679\337\003\310K\206\000\350\257h\360\307\320\264e \006I1]\276Hk\004@\215cp\317\345\206\006\000\000\000xB\021\002\200\036\364?\311\t\000\340\036\353\327\305$\\\000f\243\273\007\000\010\242h`W\2643\000\014M\257\266\223k\251B\000\000_r\345\276\007\244\315\272v\201\267\333P\231\006\344\221\377\001\324:J\031\334[\004\260\022\n\311\t\000\200tL\'!%\227>\000[\372\006 \275\2654\232\257B\252\007\000\000\000\000\000RQ\024\005HNG\000\337\344[ \377]\303\236\337\257?\314\327I\233\321\325\000\000\00000\005\201\030\224\037\000\370Hg\237\233\366\207C\023_\"\2139\000\'&\016o\036\221\034r8\270\302\017^b<\325\233\253\372\001\211L\2171\213\253\027\266\026\315\355j\234\000\323\223\356os\352\206\246#\003\000\200r\2469\311M\033\000\255g\200N\0140\245i\223\027Uh\177`X\022TX\0361GWM\002\260\3051y\350\270\0279~\025\200n\364\251\000\3008L\035\353i5\312ku\\xI\001\2759\377\364!\362\206\241\217ON\000\000\000P\235\t\037+\217\320\316N\000d\367/\002N\352\016\313\351\036\244 \n !\027>@R:\000\000\200F\024\3449d$\016\000\361U\350\357+\034\002\200N\344p\000\000\000\000\000\000\000\000\000\000\000\006\362\007Wv]\t\272\351Z\n\000\000\000\000IEND\256B`\202"
diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml
index 6aea32f..39012ae 100644
--- a/core/res/res/values/strings.xml
+++ b/core/res/res/values/strings.xml
@@ -6407,4 +6407,22 @@
<!-- Display content to tell the user the sim card name and number-->
<string name="default_card_name">CARD <xliff:g id="cardNumber" example="1">%d</xliff:g></string>
+
+ <!-- Strings for CompanionDeviceManager -->
+ <!-- Title of Watch profile permission, which allows a companion app to manage watches. [CHAR LIMIT=NONE] -->
+ <string name="permlab_companionProfileWatch">Companion Watch profile permission to manage watches</string>
+ <!-- Description of Watch profile permission, which allows a companion app to manage watches. [CHAR LIMIT=NONE] -->
+ <string name="permdesc_companionProfileWatch">Allows a companion app to manage watches.</string>
+ <!-- Title of observing device presence permission, which allows a companion app to observe the presence of the associated devices. [CHAR LIMIT=NONE] -->
+ <string name="permlab_observeCompanionDevicePresence">Observe companion device presence</string>
+ <!-- Description of observing device presence permission, which allows a companion app to observe the presence of the associated devices. [CHAR LIMIT=NONE] -->
+ <string name="permdesc_observeCompanionDevicePresence">Allows a companion app to observe companion device presence when the devices are nearby or far-away.</string>
+ <!-- Title of delivering companion messages permission, which allows a companion app to deliver messages to other devices. [CHAR LIMIT=NONE] -->
+ <string name="permlab_deliverCompanionMessages">Deliver companion messages</string>
+ <!-- Description of delivering companion messages permission, which allows a companion app to deliver messages to other devices. [CHAR LIMIT=NONE] -->
+ <string name="permdesc_deliverCompanionMessages">Allows a companion app to deliver companion messages to other devices.</string>
+ <!-- Title of start foreground services from background permission [CHAR LIMIT=NONE] -->
+ <string name="permlab_startForegroundServicesFromBackground">Start foreground services from background</string>
+ <!-- Description of start foreground services from background permission [CHAR LIMIT=NONE] -->
+ <string name="permdesc_startForegroundServicesFromBackground">Allows a companion app to start foreground services from background.</string>
</resources>
diff --git a/core/tests/fuzzers/FuzzService/FuzzBinder.java b/core/tests/fuzzers/FuzzService/FuzzBinder.java
index 52aafeb..7fd199a 100644
--- a/core/tests/fuzzers/FuzzService/FuzzBinder.java
+++ b/core/tests/fuzzers/FuzzService/FuzzBinder.java
@@ -34,12 +34,12 @@
fuzzServiceInternal(binder, data);
}
- // This API creates random parcel object
- public static void createRandomParcel(Parcel parcel, byte[] data) {
- getRandomParcel(parcel, data);
+ // This API fills parcel object
+ public static void fillRandomParcel(Parcel parcel, byte[] data) {
+ fillParcelInternal(parcel, data);
}
private static native void fuzzServiceInternal(IBinder binder, byte[] data);
- private static native void getRandomParcel(Parcel parcel, byte[] data);
+ private static native void fillParcelInternal(Parcel parcel, byte[] data);
private static native int registerNatives();
}
diff --git a/core/tests/fuzzers/FuzzService/random_parcel_jni.cpp b/core/tests/fuzzers/FuzzService/random_parcel_jni.cpp
index dbeae87..264aa5f 100644
--- a/core/tests/fuzzers/FuzzService/random_parcel_jni.cpp
+++ b/core/tests/fuzzers/FuzzService/random_parcel_jni.cpp
@@ -38,7 +38,7 @@
return registerFrameworkNatives(env);
}
-JNIEXPORT void JNICALL Java_randomparcel_FuzzBinder_getRandomParcel(JNIEnv *env, jobject thiz, jobject jparcel, jbyteArray fuzzData) {
+JNIEXPORT void JNICALL Java_randomparcel_FuzzBinder_fillParcelInternal(JNIEnv *env, jobject thiz, jobject jparcel, jbyteArray fuzzData) {
size_t len = static_cast<size_t>(env->GetArrayLength(fuzzData));
uint8_t data[len];
env->GetByteArrayRegion(fuzzData, 0, len, reinterpret_cast<jbyte*>(data));
diff --git a/core/tests/fuzzers/FuzzService/random_parcel_jni.h b/core/tests/fuzzers/FuzzService/random_parcel_jni.h
index bc18b2f..c96354a 100644
--- a/core/tests/fuzzers/FuzzService/random_parcel_jni.h
+++ b/core/tests/fuzzers/FuzzService/random_parcel_jni.h
@@ -24,5 +24,5 @@
// Function from AndroidRuntime
jint registerFrameworkNatives(JNIEnv* env);
- JNIEXPORT void JNICALL Java_randomparcel_FuzzBinder_getRandomParcel(JNIEnv *env, jobject thiz, jobject parcel, jbyteArray fuzzData);
+ JNIEXPORT void JNICALL Java_randomparcel_FuzzBinder_fillParcelInternal(JNIEnv *env, jobject thiz, jobject parcel, jbyteArray fuzzData);
}
diff --git a/core/tests/fuzzers/ParcelFuzzer/Android.bp b/core/tests/fuzzers/ParcelFuzzer/Android.bp
new file mode 100644
index 0000000..b71a06e
--- /dev/null
+++ b/core/tests/fuzzers/ParcelFuzzer/Android.bp
@@ -0,0 +1,40 @@
+package {
+ default_applicable_licenses: ["frameworks_base_license"],
+}
+
+java_fuzz {
+ name: "java_binder_parcel_fuzzer",
+ srcs: [
+ "ParcelFuzzer.java",
+ "ReadUtils.java",
+ "FuzzUtils.java",
+ "FuzzOperation.java",
+ "ReadOperation.java",
+ ":framework-core-sources-for-fuzzers",
+ ],
+ static_libs: [
+ "jazzer",
+ "random_parcel_lib",
+ "binderReadParcelIface-java",
+ ],
+ jni_libs: [
+ "librandom_parcel_jni",
+ "libc++",
+ "libandroid_runtime",
+ ],
+ libs: [
+ "framework",
+ "unsupportedappusage",
+ "ext",
+ "framework-res",
+ ],
+ native_bridge_supported: true,
+ fuzz_config: {
+ cc: [
+ "smoreland@google.com",
+ "waghpawan@google.com",
+ ],
+ // Adds bugs to hotlist "AIDL fuzzers bugs" on buganizer
+ hotlists: ["4637097"],
+ },
+}
diff --git a/core/tests/fuzzers/ParcelFuzzer/FuzzOperation.java b/core/tests/fuzzers/ParcelFuzzer/FuzzOperation.java
new file mode 100644
index 0000000..033231d
--- /dev/null
+++ b/core/tests/fuzzers/ParcelFuzzer/FuzzOperation.java
@@ -0,0 +1,22 @@
+/*
+ * 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 parcelfuzzer;
+
+import com.code_intelligence.jazzer.api.FuzzedDataProvider;
+
+public interface FuzzOperation {
+ void doFuzz(FuzzedDataProvider data);
+}
diff --git a/core/tests/fuzzers/ParcelFuzzer/FuzzUtils.java b/core/tests/fuzzers/ParcelFuzzer/FuzzUtils.java
new file mode 100644
index 0000000..5e9e5ec
--- /dev/null
+++ b/core/tests/fuzzers/ParcelFuzzer/FuzzUtils.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package parcelfuzzer;
+
+import android.os.Parcel;
+
+import com.code_intelligence.jazzer.api.FuzzedDataProvider;
+
+import randomparcel.FuzzBinder;
+
+public class FuzzUtils {
+ public static FuzzOperation[] FUZZ_OPERATIONS =
+ new FuzzOperation[] {
+ new FuzzOperation() {
+ @java.lang.Override
+ public void doFuzz(FuzzedDataProvider provider) {
+ // Fuzz Append
+ int start = provider.consumeInt();
+ int len = provider.consumeInt();
+ Parcel p1 = null;
+ Parcel p2 = null;
+
+ try {
+ p1 = Parcel.obtain();
+ p2 = Parcel.obtain();
+
+ byte[] data =
+ provider.consumeBytes(
+ provider.consumeInt(0, provider.remainingBytes()));
+ FuzzBinder.fillRandomParcel(p1, data);
+ FuzzBinder.fillRandomParcel(p2, provider.consumeRemainingAsBytes());
+
+ p1.appendFrom(p2, start, len);
+
+ } catch (Exception e) {
+ // Rethrow exception as runtime exceptions are catched
+ // at highest level.
+ throw e;
+ } finally {
+ p1.recycle();
+ p2.recycle();
+ }
+ }
+ },
+ new FuzzOperation() {
+ @java.lang.Override
+ public void doFuzz(FuzzedDataProvider provider) {
+ // Fuzz Read
+ // Use maximum bytes to generate read instructions and remaining for parcel
+ // creation
+ int maxParcelBytes = provider.remainingBytes() / 3;
+ byte[] data = provider.consumeBytes(maxParcelBytes);
+ Parcel randomParcel = null;
+
+ try {
+ randomParcel = Parcel.obtain();
+ FuzzBinder.fillRandomParcel(randomParcel, data);
+
+ while (provider.remainingBytes() > 0) {
+ provider.pickValue(ReadUtils.READ_OPERATIONS)
+ .readParcel(randomParcel, provider);
+ }
+
+ } catch (Exception e) {
+ // Rethrow exception as runtime exceptions are catched
+ // at highest level.
+ throw e;
+ } finally {
+ randomParcel.recycle();
+ }
+ }
+ },
+ };
+}
diff --git a/core/tests/fuzzers/ParcelFuzzer/ParcelFuzzer.java b/core/tests/fuzzers/ParcelFuzzer/ParcelFuzzer.java
new file mode 100644
index 0000000..688c812
--- /dev/null
+++ b/core/tests/fuzzers/ParcelFuzzer/ParcelFuzzer.java
@@ -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 parcelfuzzer;
+
+import android.util.Log;
+
+import com.code_intelligence.jazzer.api.FuzzedDataProvider;
+
+import randomparcel.FuzzBinder;
+
+public class ParcelFuzzer {
+
+ static {
+ // Initialize JNI dependencies
+ FuzzBinder.init();
+ }
+
+ public static void fuzzerTestOneInput(FuzzedDataProvider provider) {
+ // Default behavior for Java APIs is to throw RuntimeException.
+ // We need to fuzz to detect other problems which are not handled explicitly.
+ // TODO(b/150808347): Change known API exceptions to subclass of
+ // RuntimeExceptions and catch those only.
+ try {
+ provider.pickValue(FuzzUtils.FUZZ_OPERATIONS).doFuzz(provider);
+ } catch (RuntimeException e) {
+ Log.e("ParcelFuzzer", "Exception occurred while fuzzing ", e);
+ }
+ }
+}
diff --git a/core/tests/fuzzers/ParcelFuzzer/ReadOperation.java b/core/tests/fuzzers/ParcelFuzzer/ReadOperation.java
new file mode 100644
index 0000000..5c227e3
--- /dev/null
+++ b/core/tests/fuzzers/ParcelFuzzer/ReadOperation.java
@@ -0,0 +1,24 @@
+/*
+ * 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 parcelfuzzer;
+
+import android.os.Parcel;
+
+import com.code_intelligence.jazzer.api.FuzzedDataProvider;
+
+public interface ReadOperation {
+ void readParcel(Parcel parcel, FuzzedDataProvider provider);
+}
diff --git a/core/tests/fuzzers/ParcelFuzzer/ReadUtils.java b/core/tests/fuzzers/ParcelFuzzer/ReadUtils.java
new file mode 100644
index 0000000..0eff5f2
--- /dev/null
+++ b/core/tests/fuzzers/ParcelFuzzer/ReadUtils.java
@@ -0,0 +1,435 @@
+/*
+ * 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 parcelfuzzer;
+
+import android.os.Binder;
+import android.os.IBinder;
+import android.os.IInterface;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+
+import parcelables.EmptyParcelable;
+import parcelables.GenericDataParcelable;
+import parcelables.SingleDataParcelable;
+
+public class ReadUtils {
+ public static int MAX_LEN = 1000000;
+ public static int MIN_LEN = 0;
+
+ private static class SomeParcelable implements Parcelable {
+ private final int mValue;
+
+ private SomeParcelable(Parcel in) {
+ this.mValue = in.readInt();
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ out.writeInt(mValue);
+ }
+
+ public static Parcelable.Creator<SomeParcelable> CREATOR =
+ new Parcelable.Creator<SomeParcelable>() {
+
+ @Override
+ public SomeParcelable createFromParcel(Parcel source) {
+ return new SomeParcelable(source);
+ }
+
+ @Override
+ public SomeParcelable[] newArray(int size) {
+ return new SomeParcelable[size];
+ }
+ };
+ }
+
+ private static class TestClassLoader extends ClassLoader {
+ TestClassLoader() {
+ super();
+ }
+ }
+
+ private static class TestInterface implements IInterface {
+ public Binder binder;
+ private static final String DESCRIPTOR = "TestInterface";
+
+ TestInterface() {
+ binder = new Binder();
+ binder.attachInterface(this, DESCRIPTOR);
+ }
+
+ public IBinder asBinder() {
+ return binder;
+ }
+
+ public static TestInterface asInterface(IBinder binder) {
+ if (binder != null) {
+ IInterface iface = binder.queryLocalInterface(DESCRIPTOR);
+ if (iface != null && iface instanceof TestInterface) {
+ return (TestInterface) iface;
+ }
+ }
+ return null;
+ }
+ }
+
+ public static ReadOperation[] READ_OPERATIONS =
+ new ReadOperation[] {
+ (parcel, provider) -> {
+ parcel.setDataPosition(provider.consumeInt());
+ },
+ (parcel, provider) -> {
+ parcel.setDataCapacity(provider.consumeInt());
+ },
+ (parcel, provider) -> {
+ parcel.setDataSize(provider.consumeInt());
+ },
+ (parcel, provider) -> {
+ parcel.dataSize();
+ },
+ (parcel, provider) -> {
+ parcel.dataPosition();
+ },
+ (parcel, provider) -> {
+ parcel.dataCapacity();
+ },
+
+ // read basic types
+ (parcel, provider) -> {
+ parcel.readByte();
+ },
+ (parcel, provider) -> {
+ parcel.readBoolean();
+ },
+ (parcel, provider) -> {
+ parcel.readInt();
+ },
+ (parcel, provider) -> {
+ parcel.readLong();
+ },
+ (parcel, provider) -> {
+ parcel.readFloat();
+ },
+ (parcel, provider) -> {
+ parcel.readDouble();
+ },
+ (parcel, provider) -> {
+ parcel.readString();
+ },
+ (parcel, provider) -> {
+ parcel.readString8();
+ },
+ (parcel, provider) -> {
+ parcel.readString16();
+ },
+ (parcel, provider) -> {
+ parcel.readBlob();
+ },
+ (parcel, provider) -> {
+ parcel.readStrongBinder();
+ },
+
+ // read arrays of random length
+ (parcel, provider) -> {
+ byte[] array;
+ if (provider.consumeBoolean()) {
+ int pos = parcel.dataPosition();
+ array = new byte[Math.min(MAX_LEN, parcel.readInt())];
+ parcel.setDataPosition(pos);
+ } else {
+ array = new byte[provider.consumeInt(MIN_LEN, MAX_LEN)];
+ }
+ parcel.readByteArray(array);
+ },
+ (parcel, provider) -> {
+ char[] array;
+ if (provider.consumeBoolean()) {
+ int pos = parcel.dataPosition();
+ array = new char[Math.min(MAX_LEN, parcel.readInt())];
+ parcel.setDataPosition(pos);
+ } else {
+ array = new char[provider.consumeInt(MIN_LEN, MAX_LEN)];
+ }
+ parcel.readCharArray(array);
+ },
+ (parcel, provider) -> {
+ int[] array;
+ if (provider.consumeBoolean()) {
+ int pos = parcel.dataPosition();
+ array = new int[Math.min(MAX_LEN, parcel.readInt())];
+ parcel.setDataPosition(pos);
+ } else {
+ array = new int[provider.consumeInt(MIN_LEN, MAX_LEN)];
+ }
+ parcel.readIntArray(array);
+ },
+ (parcel, provider) -> {
+ double[] array;
+ if (provider.consumeBoolean()) {
+ int pos = parcel.dataPosition();
+ array = new double[Math.min(MAX_LEN, parcel.readInt())];
+ parcel.setDataPosition(pos);
+ } else {
+ array = new double[provider.consumeInt(MIN_LEN, MAX_LEN)];
+ }
+ parcel.readDoubleArray(array);
+ },
+ (parcel, provider) -> {
+ float[] array;
+ if (provider.consumeBoolean()) {
+ int pos = parcel.dataPosition();
+ array = new float[Math.min(MAX_LEN, parcel.readInt())];
+ parcel.setDataPosition(pos);
+ } else {
+ array = new float[provider.consumeInt(MIN_LEN, MAX_LEN)];
+ }
+ parcel.readFloatArray(array);
+ },
+ (parcel, provider) -> {
+ boolean[] array;
+ if (provider.consumeBoolean()) {
+ int pos = parcel.dataPosition();
+ array = new boolean[Math.min(MAX_LEN, parcel.readInt())];
+ parcel.setDataPosition(pos);
+ } else {
+ array = new boolean[provider.consumeInt(MIN_LEN, MAX_LEN)];
+ }
+ parcel.readBooleanArray(array);
+ },
+ (parcel, provider) -> {
+ long[] array;
+ if (provider.consumeBoolean()) {
+ int pos = parcel.dataPosition();
+ array = new long[Math.min(MAX_LEN, parcel.readInt())];
+ parcel.setDataPosition(pos);
+ } else {
+ array = new long[provider.consumeInt(MIN_LEN, MAX_LEN)];
+ }
+ parcel.readLongArray(array);
+ },
+ (parcel, provider) -> {
+ IBinder[] array;
+ if (provider.consumeBoolean()) {
+ int pos = parcel.dataPosition();
+ array = new IBinder[Math.min(MAX_LEN, parcel.readInt())];
+ parcel.setDataPosition(pos);
+ } else {
+ array = new IBinder[provider.consumeInt(MIN_LEN, MAX_LEN)];
+ }
+ parcel.readBinderArray(array);
+ },
+ (parcel, provider) -> {
+ ArrayList<IBinder> arrayList = new ArrayList<IBinder>();
+ parcel.readBinderList(arrayList);
+ },
+
+ // unmarshall from random parcel data and random bytes
+ (parcel, provider) -> {
+ byte[] data = parcel.marshall();
+ Parcel p = Parcel.obtain();
+ p.unmarshall(data, provider.consumeInt(), provider.consumeInt());
+ p.recycle();
+ },
+ (parcel, provider) -> {
+ byte[] data = provider.consumeRemainingAsBytes();
+ Parcel p = Parcel.obtain();
+ p.unmarshall(data, provider.consumeInt(), provider.consumeInt());
+ p.recycle();
+ },
+ (parcel, provider) -> {
+ parcel.hasFileDescriptors(provider.consumeInt(), provider.consumeInt());
+ },
+
+ // read AIDL generated parcelables
+ (parcel, provider) -> {
+ TestClassLoader loader = new TestClassLoader();
+ parcel.readParcelable(loader, SingleDataParcelable.class);
+ },
+ (parcel, provider) -> {
+ TestClassLoader loader = new TestClassLoader();
+ parcel.readParcelableArray(loader, SingleDataParcelable.class);
+ },
+ (parcel, provider) -> {
+ SingleDataParcelable[] array;
+ if (provider.consumeBoolean()) {
+ int pos = parcel.dataPosition();
+ array = new SingleDataParcelable[Math.min(MAX_LEN, parcel.readInt())];
+ parcel.setDataPosition(pos);
+ } else {
+ array = new SingleDataParcelable[provider.consumeInt(MIN_LEN, MAX_LEN)];
+ }
+ parcel.readTypedArray(array, SingleDataParcelable.CREATOR);
+ },
+ (parcel, provider) -> {
+ TestClassLoader loader = new TestClassLoader();
+ parcel.readParcelable(loader, EmptyParcelable.class);
+ },
+ (parcel, provider) -> {
+ TestClassLoader loader = new TestClassLoader();
+ parcel.readParcelableArray(loader, EmptyParcelable.class);
+ },
+ (parcel, provider) -> {
+ EmptyParcelable[] array;
+ if (provider.consumeBoolean()) {
+ int pos = parcel.dataPosition();
+ array = new EmptyParcelable[Math.min(MAX_LEN, parcel.readInt())];
+ parcel.setDataPosition(pos);
+ } else {
+ array = new EmptyParcelable[provider.consumeInt(MIN_LEN, MAX_LEN)];
+ }
+ parcel.readTypedArray(array, EmptyParcelable.CREATOR);
+ },
+ (parcel, provider) -> {
+ TestClassLoader loader = new TestClassLoader();
+ parcel.readParcelable(loader, GenericDataParcelable.class);
+ },
+ (parcel, provider) -> {
+ TestClassLoader loader = new TestClassLoader();
+ parcel.readParcelableArray(loader, GenericDataParcelable.class);
+ },
+ (parcel, provider) -> {
+ GenericDataParcelable[] array;
+ if (provider.consumeBoolean()) {
+ int pos = parcel.dataPosition();
+ array = new GenericDataParcelable[Math.min(MAX_LEN, parcel.readInt())];
+ parcel.setDataPosition(pos);
+ } else {
+ int len = provider.consumeInt(MIN_LEN, MAX_LEN);
+ array = new GenericDataParcelable[len];
+ }
+ parcel.readTypedArray(array, GenericDataParcelable.CREATOR);
+ },
+
+ // read parcelables
+ (parcel, provider) -> {
+ TestClassLoader loader = new TestClassLoader();
+ parcel.readParcelable(loader, SomeParcelable.class);
+ },
+ (parcel, provider) -> {
+ TestClassLoader loader = new TestClassLoader();
+ parcel.readParcelableArray(loader, SomeParcelable.class);
+ },
+ (parcel, provider) -> {
+ SomeParcelable[] array;
+ if (provider.consumeBoolean()) {
+ int pos = parcel.dataPosition();
+ array = new SomeParcelable[Math.min(MAX_LEN, parcel.readInt())];
+ parcel.setDataPosition(pos);
+ } else {
+ array = new SomeParcelable[provider.consumeInt(MIN_LEN, MAX_LEN)];
+ }
+ parcel.readTypedArray(array, SomeParcelable.CREATOR);
+ },
+ (parcel, provider) -> {
+ TestClassLoader loader = new TestClassLoader();
+ parcel.readParcelableArray(loader);
+ },
+ (parcel, provider) -> {
+ parcel.hasFileDescriptors(provider.consumeInt(), provider.consumeInt());
+ },
+ (parcel, provider) -> {
+ TestClassLoader loader = new TestClassLoader();
+ parcel.readParcelableArray(loader);
+ },
+
+ // read lists
+ (parcel, provider) -> {
+ TestClassLoader loader = new TestClassLoader();
+ parcel.readArrayList(loader);
+ },
+ (parcel, provider) -> {
+ TestClassLoader loader = new TestClassLoader();
+ parcel.readArrayList(loader, Object.class);
+ },
+ (parcel, provider) -> {
+ TestClassLoader loader = new TestClassLoader();
+ parcel.readArrayList(loader, SomeParcelable.class);
+ },
+
+ // read sparse arrays
+ (parcel, provider) -> {
+ TestClassLoader loader = new TestClassLoader();
+ parcel.readSparseArray(loader);
+ },
+ (parcel, provider) -> {
+ TestClassLoader loader = new TestClassLoader();
+ parcel.readSparseArray(loader, Object.class);
+ },
+ (parcel, provider) -> {
+ TestClassLoader loader = new TestClassLoader();
+ parcel.readSparseArray(loader, SomeParcelable.class);
+ },
+ (parcel, provider) -> {
+ TestClassLoader loader = new TestClassLoader();
+ parcel.readSerializable(loader, Object.class);
+ },
+
+ // read interface
+ (parcel, provider) -> {
+ TestInterface[] array;
+ if (provider.consumeBoolean()) {
+ int pos = parcel.dataPosition();
+ array = new TestInterface[Math.min(MAX_LEN, parcel.readInt())];
+ parcel.setDataPosition(pos);
+ } else {
+ array = new TestInterface[provider.consumeInt(MIN_LEN, MAX_LEN)];
+ }
+ parcel.readInterfaceArray(array, TestInterface::asInterface);
+ },
+ (parcel, provider) -> {
+ int w = provider.consumeInt(MIN_LEN, MAX_LEN);
+ int h = provider.consumeInt(MIN_LEN, MAX_LEN);
+ TestInterface[][] array = new TestInterface[w][h];
+ parcel.readFixedArray(array, TestInterface::asInterface);
+ },
+ (parcel, provider) -> {
+ ArrayList<TestInterface> array = new ArrayList<TestInterface>();
+ parcel.readInterfaceList(array, TestInterface::asInterface);
+ },
+
+ // read bundle
+ (parcel, provider) -> {
+ TestClassLoader loader = new TestClassLoader();
+ parcel.readBundle(loader);
+ },
+ (parcel, provider) -> {
+ parcel.readBundle();
+ },
+
+ // read HashMap
+ (parcel, provider) -> {
+ TestClassLoader loader = new TestClassLoader();
+ parcel.readHashMap(loader);
+ },
+ (parcel, provider) -> {
+ TestClassLoader loader = new TestClassLoader();
+ parcel.readHashMap(loader, String.class, String.class);
+ },
+ (parcel, provider) -> {
+ HashMap<String, String> hashMap = new HashMap<>();
+ TestClassLoader loader = new TestClassLoader();
+ parcel.readMap(hashMap, loader, String.class, String.class);
+ },
+ };
+}
diff --git a/core/tests/fuzzers/java_service_fuzzer/Android.bp b/core/tests/fuzzers/java_service_fuzzer/Android.bp
index 625de14..6acb198 100644
--- a/core/tests/fuzzers/java_service_fuzzer/Android.bp
+++ b/core/tests/fuzzers/java_service_fuzzer/Android.bp
@@ -37,4 +37,12 @@
"framework-res",
],
native_bridge_supported: true,
+ fuzz_config: {
+ cc: [
+ "smoreland@google.com",
+ "waghpawan@google.com",
+ ],
+ // Adds bugs to hotlist "AIDL fuzzers bugs" on buganizer
+ hotlists: ["4637097"],
+ },
}
diff --git a/keystore/java/android/security/KeyStoreException.java b/keystore/java/android/security/KeyStoreException.java
index 1a81dda..6536e43 100644
--- a/keystore/java/android/security/KeyStoreException.java
+++ b/keystore/java/android/security/KeyStoreException.java
@@ -138,6 +138,16 @@
* provisioning server refuses key issuance, this is a permanent error.</p>
*/
public static final int ERROR_ATTESTATION_KEYS_UNAVAILABLE = 16;
+ /**
+ * This device requires a software upgrade to use the key provisioning server. The software
+ * is outdated and this error is returned only on devices that rely solely on
+ * remotely-provisioned keys (see <a href=
+ * "https://android-developers.googleblog.com/2022/03/upgrading-android-attestation-remote.html"
+ * >Remote Key Provisioning</a>).
+ *
+ * @hide
+ */
+ public static final int ERROR_DEVICE_REQUIRES_UPGRADE_FOR_ATTESTATION = 17;
/** @hide */
@Retention(RetentionPolicy.SOURCE)
@@ -157,7 +167,8 @@
ERROR_INCORRECT_USAGE,
ERROR_KEY_NOT_TEMPORALLY_VALID,
ERROR_KEY_OPERATION_EXPIRED,
- ERROR_ATTESTATION_KEYS_UNAVAILABLE
+ ERROR_ATTESTATION_KEYS_UNAVAILABLE,
+ ERROR_DEVICE_REQUIRES_UPGRADE_FOR_ATTESTATION,
})
public @interface PublicErrorCode {
}
@@ -184,6 +195,16 @@
* This value is returned when {@link #isTransientFailure()} is {@code true}.
*/
public static final int RETRY_WHEN_CONNECTIVITY_AVAILABLE = 3;
+ /**
+ * Re-try the operation that led to this error when the device has a software update
+ * downloaded and on the next reboot. The Remote provisioning server recognizes
+ * the device, but refuses issuance of attestation keys because it contains a software
+ * version that could potentially be vulnerable and needs an update. Re-trying after the
+ * device has upgraded and rebooted may alleviate the problem.
+ *
+ * <p>This value is returned when {@link #isTransientFailure()} is {@code true}.
+ */
+ public static final int RETRY_AFTER_NEXT_REBOOT = 4;
/** @hide */
@Retention(RetentionPolicy.SOURCE)
@@ -191,6 +212,7 @@
RETRY_NEVER,
RETRY_WITH_EXPONENTIAL_BACKOFF,
RETRY_WHEN_CONNECTIVITY_AVAILABLE,
+ RETRY_AFTER_NEXT_REBOOT,
})
public @interface RetryPolicy {
}
@@ -217,6 +239,13 @@
* when the device has connectivity again.
* @hide */
public static final int RKP_FETCHING_PENDING_CONNECTIVITY = 3;
+ /**
+ * The RKP server recognizes the device, but the device may be running vulnerable software,
+ * and thus refusing issuance of RKP keys to it.
+ *
+ * @hide
+ */
+ public static final int RKP_FETCHING_PENDING_SOFTWARE_REBOOT = 4;
// Constants for encoding information about the error encountered:
// Whether the error relates to the system state/implementation as a whole, or a specific key.
@@ -236,7 +265,7 @@
private static int initializeRkpStatusForRegularErrors(int errorCode) {
// Check if the system code mistakenly called a constructor of KeyStoreException with
// the OUT_OF_KEYS error code but without RKP status.
- if (errorCode == ResponseCode.OUT_OF_KEYS) {
+ if (isRkpRelatedError(errorCode)) {
Log.e(TAG, "RKP error code without RKP status");
// Set RKP status to RKP_SERVER_REFUSED_ISSUANCE so that the caller never retries.
return RKP_SERVER_REFUSED_ISSUANCE;
@@ -272,7 +301,7 @@
super(message);
mErrorCode = errorCode;
mRkpStatus = rkpStatus;
- if (mErrorCode != ResponseCode.OUT_OF_KEYS) {
+ if (!isRkpRelatedError(mErrorCode)) {
Log.e(TAG, "Providing RKP status for error code " + errorCode + " has no effect.");
}
}
@@ -309,10 +338,11 @@
public boolean isTransientFailure() {
PublicErrorInformation failureInfo = getErrorInformation(mErrorCode);
// Special-case handling for RKP failures:
- if (mRkpStatus != RKP_SUCCESS && mErrorCode == ResponseCode.OUT_OF_KEYS) {
+ if (mRkpStatus != RKP_SUCCESS && isRkpRelatedError(mErrorCode)) {
switch (mRkpStatus) {
case RKP_TEMPORARILY_UNAVAILABLE:
case RKP_FETCHING_PENDING_CONNECTIVITY:
+ case RKP_FETCHING_PENDING_SOFTWARE_REBOOT:
return true;
case RKP_SERVER_REFUSED_ISSUANCE:
default:
@@ -346,6 +376,11 @@
return (failureInfo.indicators & IS_SYSTEM_ERROR) != 0;
}
+ private static boolean isRkpRelatedError(int errorCode) {
+ return errorCode == ResponseCode.OUT_OF_KEYS
+ || errorCode == ResponseCode.OUT_OF_KEYS_REQUIRES_UPGRADE;
+ }
+
/**
* Returns the re-try policy for transient failures. Valid only if
* {@link #isTransientFailure()} returns {@code True}.
@@ -362,6 +397,8 @@
return RETRY_WHEN_CONNECTIVITY_AVAILABLE;
case RKP_SERVER_REFUSED_ISSUANCE:
return RETRY_NEVER;
+ case RKP_FETCHING_PENDING_SOFTWARE_REBOOT:
+ return RETRY_AFTER_NEXT_REBOOT;
default:
return (failureInfo.indicators & IS_TRANSIENT_ERROR) != 0
? RETRY_WITH_EXPONENTIAL_BACKOFF : RETRY_NEVER;
@@ -620,5 +657,8 @@
new PublicErrorInformation(0, ERROR_KEY_DOES_NOT_EXIST));
sErrorCodeToFailureInfo.put(ResponseCode.OUT_OF_KEYS,
new PublicErrorInformation(IS_SYSTEM_ERROR, ERROR_ATTESTATION_KEYS_UNAVAILABLE));
+ sErrorCodeToFailureInfo.put(ResponseCode.OUT_OF_KEYS_REQUIRES_UPGRADE,
+ new PublicErrorInformation(IS_SYSTEM_ERROR | IS_TRANSIENT_ERROR,
+ ERROR_DEVICE_REQUIRES_UPGRADE_FOR_ATTESTATION));
}
}
diff --git a/keystore/java/android/security/keystore2/AndroidKeyStoreKeyPairGeneratorSpi.java b/keystore/java/android/security/keystore2/AndroidKeyStoreKeyPairGeneratorSpi.java
index afec830..2830d7eff 100644
--- a/keystore/java/android/security/keystore2/AndroidKeyStoreKeyPairGeneratorSpi.java
+++ b/keystore/java/android/security/keystore2/AndroidKeyStoreKeyPairGeneratorSpi.java
@@ -648,6 +648,7 @@
// {@link android.security.KeyStoreException#RKP_TEMPORARILY_UNAVAILABLE},
// {@link android.security.KeyStoreException#RKP_SERVER_REFUSED_ISSUANCE},
// {@link android.security.KeyStoreException#RKP_FETCHING_PENDING_CONNECTIVITY}
+ // {@link android.security.KeyStoreException#RKP_FETCHING_PENDING_SOFTWARE_REBOOT}
public final int rkpStatus;
@Nullable
public final KeyPair keyPair;
diff --git a/libs/WindowManager/Shell/res/drawable/size_compat_restart_button.xml b/libs/WindowManager/Shell/res/drawable/size_compat_restart_button.xml
index e6ae282..2994593 100644
--- a/libs/WindowManager/Shell/res/drawable/size_compat_restart_button.xml
+++ b/libs/WindowManager/Shell/res/drawable/size_compat_restart_button.xml
@@ -1,19 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
- Copyright (C) 2019 The Android Open Source Project
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
--->
+ ~ 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"
@@ -25,13 +25,12 @@
android:fillAlpha="0.8"
android:pathData="M0,24 a24,24 0 1,0 48,0 a24,24 0 1,0 -48,0"/>
<group
- android:translateX="12"
- android:translateY="12">
+ android:scaleX="0.8"
+ android:scaleY="0.8"
+ android:translateX="10"
+ android:translateY="10">
<path
- android:fillColor="@color/compat_controls_text"
- android:pathData="M6,13c0,-1.65 0.67,-3.15 1.76,-4.24L6.34,7.34C4.9,8.79 4,10.79 4,13c0,4.08 3.05,7.44 7,7.93v-2.02C8.17,18.43 6,15.97 6,13z"/>
- <path
- android:fillColor="@color/compat_controls_text"
- android:pathData="M20,13c0,-4.42 -3.58,-8 -8,-8c-0.06,0 -0.12,0.01 -0.18,0.01v0l1.09,-1.09L11.5,2.5L8,6l3.5,3.5l1.41,-1.41l-1.08,-1.08C11.89,7.01 11.95,7 12,7c3.31,0 6,2.69 6,6c0,2.97 -2.17,5.43 -5,5.91v2.02C16.95,20.44 20,17.08 20,13z"/>
+ android:pathData="M0,36V24.5H3V30.85L10.4,23.45L12.55,25.6L5.15,33H11.5V36H0ZM24.5,36V33H30.85L23.5,25.65L25.65,23.5L33,30.85V24.5H36V36H24.5ZM10.35,12.5L3,5.15V11.5H0V0H11.5V3H5.15L12.5,10.35L10.35,12.5ZM25.65,12.5L23.5,10.35L30.85,3H24.5V0H36V11.5H33V5.15L25.65,12.5Z"
+ android:fillColor="@color/compat_controls_text"/>
</group>
</vector>
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/dagger/WMShellBaseModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java
index 4ea8a5d..dc1634a 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java
@@ -697,10 +697,13 @@
@WMSingleton
@Provides
- static Optional<DesktopModeController> providesDesktopModeController(
- @DynamicOverride Optional<DesktopModeController> desktopModeController) {
+ static Optional<DesktopModeController> provideDesktopModeController(
+ @DynamicOverride Optional<Lazy<DesktopModeController>> desktopModeController) {
+ // Use optional-of-lazy for the dependency that this provider relies on.
+ // Lazy ensures that this provider will not be the cause the dependency is created
+ // when it will not be returned due to the condition below.
if (DesktopModeStatus.IS_SUPPORTED) {
- return desktopModeController;
+ return desktopModeController.map(Lazy::get);
}
return Optional.empty();
}
@@ -711,10 +714,13 @@
@WMSingleton
@Provides
- static Optional<DesktopModeTaskRepository> providesDesktopTaskRepository(
- @DynamicOverride Optional<DesktopModeTaskRepository> desktopModeTaskRepository) {
+ static Optional<DesktopModeTaskRepository> provideDesktopTaskRepository(
+ @DynamicOverride Optional<Lazy<DesktopModeTaskRepository>> desktopModeTaskRepository) {
+ // Use optional-of-lazy for the dependency that this provider relies on.
+ // Lazy ensures that this provider will not be the cause the dependency is created
+ // when it will not be returned due to the condition below.
if (DesktopModeStatus.IS_SUPPORTED) {
- return desktopModeTaskRepository;
+ return desktopModeTaskRepository.map(Lazy::get);
}
return Optional.empty();
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
index f1670cd..6be8305 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
@@ -189,7 +189,7 @@
ShellTaskOrganizer taskOrganizer,
DisplayController displayController,
SyncTransactionQueue syncQueue,
- @DynamicOverride DesktopModeController desktopModeController) {
+ Optional<DesktopModeController> desktopModeController) {
return new CaptionWindowDecorViewModel(
context,
mainHandler,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeController.java
index abc4024..5824f51 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeController.java
@@ -64,6 +64,7 @@
import java.util.ArrayList;
import java.util.Comparator;
+import java.util.List;
import java.util.concurrent.Executor;
/**
@@ -99,7 +100,9 @@
mDesktopModeTaskRepository = desktopModeTaskRepository;
mMainExecutor = mainExecutor;
mSettingsObserver = new SettingsObserver(mContext, mainHandler);
- shellInit.addInitCallback(this::onInit, this);
+ if (DesktopModeStatus.isSupported()) {
+ shellInit.addInitCallback(this::onInit, this);
+ }
}
private void onInit() {
@@ -258,18 +261,36 @@
@NonNull
private WindowContainerTransaction bringDesktopAppsToFront() {
- ArraySet<Integer> activeTasks = mDesktopModeTaskRepository.getActiveTasks();
+ final WindowContainerTransaction wct = new WindowContainerTransaction();
+ final ArraySet<Integer> activeTasks = mDesktopModeTaskRepository.getActiveTasks();
ProtoLog.d(WM_SHELL_DESKTOP_MODE, "bringDesktopAppsToFront: tasks=%s", activeTasks.size());
- ArrayList<RunningTaskInfo> taskInfos = new ArrayList<>();
+
+ final List<RunningTaskInfo> taskInfos = new ArrayList<>();
for (Integer taskId : activeTasks) {
RunningTaskInfo taskInfo = mShellTaskOrganizer.getRunningTaskInfo(taskId);
if (taskInfo != null) {
taskInfos.add(taskInfo);
}
}
- // Order by lastActiveTime, descending
- taskInfos.sort(Comparator.comparingLong(task -> -task.lastActiveTime));
- WindowContainerTransaction wct = new WindowContainerTransaction();
+
+ if (taskInfos.isEmpty()) {
+ return wct;
+ }
+
+ final boolean allActiveTasksAreVisible = taskInfos.stream()
+ .allMatch(info -> mDesktopModeTaskRepository.isVisibleTask(info.taskId));
+ if (allActiveTasksAreVisible) {
+ ProtoLog.d(WM_SHELL_DESKTOP_MODE,
+ "bringDesktopAppsToFront: active tasks are already in front, skipping.");
+ return wct;
+ }
+ ProtoLog.d(WM_SHELL_DESKTOP_MODE,
+ "bringDesktopAppsToFront: reordering all active tasks to the front");
+ final List<Integer> allTasksInZOrder =
+ mDesktopModeTaskRepository.getFreeformTasksInZOrder();
+ // Sort by z-order, bottom to top, so that the top-most task is reordered to the top last
+ // in the WCT.
+ taskInfos.sort(Comparator.comparingInt(task -> -allTasksInZOrder.indexOf(task.taskId)));
for (RunningTaskInfo task : taskInfos) {
wct.reorder(task.token, true);
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeStatus.java b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeStatus.java
index 2fafe67..e3eb2b7 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeStatus.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeStatus.java
@@ -37,6 +37,13 @@
"persist.wm.debug.desktop_mode", false);
/**
+ * Return {@code true} if desktop mode support is enabled
+ */
+ public static boolean isSupported() {
+ return IS_SUPPORTED;
+ }
+
+ /**
* Check if desktop mode is active
*
* @return {@code true} if active
@@ -54,4 +61,5 @@
return false;
}
}
+
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt
index b7749fc..600ccc1 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt
@@ -33,6 +33,8 @@
*/
private val activeTasks = ArraySet<Int>()
private val visibleTasks = ArraySet<Int>()
+ // Tasks currently in freeform mode, ordered from top to bottom (top is at index 0).
+ private val freeformTasksInZOrder = mutableListOf<Int>()
private val activeTasksListeners = ArraySet<ActiveTasksListener>()
// Track visible tasks separately because a task may be part of the desktop but not visible.
private val visibleTasksListeners = ArrayMap<VisibleTasksListener, Executor>()
@@ -101,6 +103,13 @@
}
/**
+ * Whether a task is visible.
+ */
+ fun isVisibleTask(taskId: Int): Boolean {
+ return visibleTasks.contains(taskId)
+ }
+
+ /**
* Get a set of the active tasks
*/
fun getActiveTasks(): ArraySet<Int> {
@@ -108,6 +117,13 @@
}
/**
+ * Get a list of freeform tasks, ordered from top-bottom (top at index 0).
+ */
+ fun getFreeformTasksInZOrder(): List<Int> {
+ return freeformTasksInZOrder
+ }
+
+ /**
* Updates whether a freeform task with this id is visible or not and notifies listeners.
*/
fun updateVisibleFreeformTasks(taskId: Int, visible: Boolean) {
@@ -127,6 +143,23 @@
}
/**
+ * Add (or move if it already exists) the task to the top of the ordered list.
+ */
+ fun addOrMoveFreeformTaskToTop(taskId: Int) {
+ if (freeformTasksInZOrder.contains(taskId)) {
+ freeformTasksInZOrder.remove(taskId)
+ }
+ freeformTasksInZOrder.add(0, taskId)
+ }
+
+ /**
+ * Remove the task from the ordered list.
+ */
+ fun removeFreeformTask(taskId: Int) {
+ freeformTasksInZOrder.remove(taskId)
+ }
+
+ /**
* Defines interface for classes that can listen to changes for active tasks in desktop mode.
*/
interface ActiveTasksListener {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/OWNERS b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/OWNERS
new file mode 100644
index 0000000..926cfb3
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/OWNERS
@@ -0,0 +1,2 @@
+# WM shell sub-module desktop owners
+madym@google.com
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java
index 44bcdb2..8a9b74f 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java
@@ -38,7 +38,8 @@
* {@link ShellTaskOrganizer.TaskListener} for {@link
* ShellTaskOrganizer#TASK_LISTENER_TYPE_FREEFORM}.
*/
-public class FreeformTaskListener implements ShellTaskOrganizer.TaskListener {
+public class FreeformTaskListener implements ShellTaskOrganizer.TaskListener,
+ ShellTaskOrganizer.FocusListener {
private static final String TAG = "FreeformTaskListener";
private final ShellTaskOrganizer mShellTaskOrganizer;
@@ -67,6 +68,9 @@
private void onInit() {
mShellTaskOrganizer.addListenerForType(this, TASK_LISTENER_TYPE_FREEFORM);
+ if (DesktopModeStatus.IS_SUPPORTED) {
+ mShellTaskOrganizer.addFocusListener(this);
+ }
}
@Override
@@ -86,13 +90,16 @@
t.apply();
}
- if (DesktopModeStatus.IS_SUPPORTED && taskInfo.isVisible) {
+ if (DesktopModeStatus.IS_SUPPORTED) {
mDesktopModeTaskRepository.ifPresent(repository -> {
- if (repository.addActiveTask(taskInfo.taskId)) {
- ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE,
- "Adding active freeform task: #%d", taskInfo.taskId);
+ repository.addOrMoveFreeformTaskToTop(taskInfo.taskId);
+ if (taskInfo.isVisible) {
+ if (repository.addActiveTask(taskInfo.taskId)) {
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE,
+ "Adding active freeform task: #%d", taskInfo.taskId);
+ }
+ repository.updateVisibleFreeformTasks(taskInfo.taskId, true);
}
- repository.updateVisibleFreeformTasks(taskInfo.taskId, true);
});
}
}
@@ -105,6 +112,7 @@
if (DesktopModeStatus.IS_SUPPORTED) {
mDesktopModeTaskRepository.ifPresent(repository -> {
+ repository.removeFreeformTask(taskInfo.taskId);
if (repository.removeActiveTask(taskInfo.taskId)) {
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE,
"Removing active freeform task: #%d", taskInfo.taskId);
@@ -140,6 +148,18 @@
}
@Override
+ public void onFocusTaskChanged(RunningTaskInfo taskInfo) {
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TASK_ORG,
+ "Freeform Task Focus Changed: #%d focused=%b",
+ taskInfo.taskId, taskInfo.isFocused);
+ if (DesktopModeStatus.IS_SUPPORTED && taskInfo.isFocused) {
+ mDesktopModeTaskRepository.ifPresent(repository -> {
+ repository.addOrMoveFreeformTaskToTop(taskInfo.taskId);
+ });
+ }
+ }
+
+ @Override
public void attachChildSurfaceToTask(int taskId, SurfaceControl.Builder b) {
b.setParent(findTaskSurface(taskId));
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/OWNERS b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/OWNERS
new file mode 100644
index 0000000..0c2d5c4
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/OWNERS
@@ -0,0 +1,2 @@
+# WM shell sub-module freeform owners
+madym@google.com
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java
index 7908f35..db0f0bf 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java
@@ -377,6 +377,8 @@
public void dump(@NonNull PrintWriter pw, String prefix) {
final String innerPrefix = prefix + " ";
pw.println(prefix + TAG);
+ pw.println(prefix + " mListener=" + mListener);
+ pw.println(prefix + "Tasks:");
ArrayList<GroupedRecentTaskInfo> recentTasks = getRecentTasks(Integer.MAX_VALUE,
ActivityManager.RECENT_IGNORE_UNAVAILABLE, ActivityManager.getCurrentUser());
for (int i = 0; i < recentTasks.size(); i++) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreen.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreen.java
index d86aadc..2f2bc77 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreen.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreen.java
@@ -73,6 +73,9 @@
/** Called when device waking up finished. */
void onFinishedWakingUp();
+ /** Called when requested to go to fullscreen from the current active split app. */
+ void goToFullscreenFromSplit();
+
/** Get a string representation of a stage type */
static String stageTypeToString(@StageType int stage) {
switch (stage) {
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 b20125d..400039b 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
@@ -123,6 +123,7 @@
public static final int EXIT_REASON_SCREEN_LOCKED = 7;
public static final int EXIT_REASON_SCREEN_LOCKED_SHOW_ON_TOP = 8;
public static final int EXIT_REASON_CHILD_TASK_ENTER_PIP = 9;
+ public static final int EXIT_REASON_FULLSCREEN_SHORTCUT = 10;
@IntDef(value = {
EXIT_REASON_UNKNOWN,
EXIT_REASON_APP_DOES_NOT_SUPPORT_MULTIWINDOW,
@@ -134,6 +135,7 @@
EXIT_REASON_SCREEN_LOCKED,
EXIT_REASON_SCREEN_LOCKED_SHOW_ON_TOP,
EXIT_REASON_CHILD_TASK_ENTER_PIP,
+ EXIT_REASON_FULLSCREEN_SHORTCUT,
})
@Retention(RetentionPolicy.SOURCE)
@interface ExitReason{}
@@ -418,6 +420,10 @@
mStageCoordinator.unregisterSplitScreenListener(listener);
}
+ public void goToFullscreenFromSplit() {
+ mStageCoordinator.goToFullscreenFromSplit();
+ }
+
public void startTask(int taskId, @SplitPosition int position, @Nullable Bundle options) {
final int[] result = new int[1];
IRemoteAnimationRunner wrapper = new IRemoteAnimationRunner.Stub() {
@@ -860,9 +866,12 @@
@Override
public void onFinishedWakingUp() {
- mMainExecutor.execute(() -> {
- SplitScreenController.this.onFinishedWakingUp();
- });
+ mMainExecutor.execute(SplitScreenController.this::onFinishedWakingUp);
+ }
+
+ @Override
+ public void goToFullscreenFromSplit() {
+ mMainExecutor.execute(SplitScreenController.this::goToFullscreenFromSplit);
}
}
@@ -918,33 +927,25 @@
@Override
public void exitSplitScreen(int toTopTaskId) {
executeRemoteCallWithTaskPermission(mController, "exitSplitScreen",
- (controller) -> {
- controller.exitSplitScreen(toTopTaskId, EXIT_REASON_UNKNOWN);
- });
+ (controller) -> controller.exitSplitScreen(toTopTaskId, EXIT_REASON_UNKNOWN));
}
@Override
public void exitSplitScreenOnHide(boolean exitSplitScreenOnHide) {
executeRemoteCallWithTaskPermission(mController, "exitSplitScreenOnHide",
- (controller) -> {
- controller.exitSplitScreenOnHide(exitSplitScreenOnHide);
- });
+ (controller) -> controller.exitSplitScreenOnHide(exitSplitScreenOnHide));
}
@Override
public void removeFromSideStage(int taskId) {
executeRemoteCallWithTaskPermission(mController, "removeFromSideStage",
- (controller) -> {
- controller.removeFromSideStage(taskId);
- });
+ (controller) -> controller.removeFromSideStage(taskId));
}
@Override
public void startTask(int taskId, int position, @Nullable Bundle options) {
executeRemoteCallWithTaskPermission(mController, "startTask",
- (controller) -> {
- controller.startTask(taskId, position, options);
- });
+ (controller) -> controller.startTask(taskId, position, options));
}
@Override
@@ -1036,19 +1037,16 @@
public void startShortcut(String packageName, String shortcutId, int position,
@Nullable Bundle options, UserHandle user, InstanceId instanceId) {
executeRemoteCallWithTaskPermission(mController, "startShortcut",
- (controller) -> {
- controller.startShortcut(packageName, shortcutId, position, options, user,
- instanceId);
- });
+ (controller) -> controller.startShortcut(packageName, shortcutId, position,
+ options, user, instanceId));
}
@Override
public void startIntent(PendingIntent intent, Intent fillInIntent, int position,
@Nullable Bundle options, InstanceId instanceId) {
executeRemoteCallWithTaskPermission(mController, "startIntent",
- (controller) -> {
- controller.startIntent(intent, fillInIntent, position, options, instanceId);
- });
+ (controller) -> controller.startIntent(intent, fillInIntent, position, options,
+ instanceId));
}
@Override
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 eb7b0d7..38fb2df 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
@@ -49,6 +49,7 @@
import static com.android.wm.shell.splitscreen.SplitScreenController.ENTER_REASON_MULTI_INSTANCE;
import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_APP_DOES_NOT_SUPPORT_MULTIWINDOW;
import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_APP_FINISHED;
+import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_FULLSCREEN_SHORTCUT;
import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_CHILD_TASK_ENTER_PIP;
import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_DEVICE_FOLDED;
import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_DRAG_DIVIDER;
@@ -1115,15 +1116,8 @@
* Exits the split screen by finishing one of the tasks.
*/
protected void exitStage(@SplitPosition int stageToClose) {
- if (ENABLE_SHELL_TRANSITIONS) {
- StageTaskListener stageToTop = mSideStagePosition == stageToClose
- ? mMainStage
- : mSideStage;
- exitSplitScreen(stageToTop, EXIT_REASON_APP_FINISHED);
- } else {
- boolean toEnd = stageToClose == SPLIT_POSITION_BOTTOM_OR_RIGHT;
- mSplitLayout.flingDividerToDismiss(toEnd, EXIT_REASON_APP_FINISHED);
- }
+ mSplitLayout.flingDividerToDismiss(stageToClose == SPLIT_POSITION_BOTTOM_OR_RIGHT,
+ EXIT_REASON_APP_FINISHED);
}
/**
@@ -1157,6 +1151,9 @@
case EXIT_REASON_SCREEN_LOCKED_SHOW_ON_TOP:
// User has unlocked the device after folded
case EXIT_REASON_DEVICE_FOLDED:
+ // The device is folded
+ case EXIT_REASON_FULLSCREEN_SHORTCUT:
+ // User has used a keyboard shortcut to go back to fullscreen from split
return true;
default:
return false;
@@ -1670,15 +1667,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());
}
@@ -2032,6 +2043,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;
@@ -2119,6 +2136,16 @@
return true;
}
+ public void goToFullscreenFromSplit() {
+ boolean leftOrTop;
+ if (mSideStage.isFocused()) {
+ leftOrTop = (mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT);
+ } else {
+ leftOrTop = (mSideStagePosition == SPLIT_POSITION_BOTTOM_OR_RIGHT);
+ }
+ mSplitLayout.flingDividerToDismiss(!leftOrTop, EXIT_REASON_FULLSCREEN_SHORTCUT);
+ }
+
/** Synchronize split-screen state with transition and make appropriate preparations. */
public void prepareDismissAnimation(@StageType int toStage, @ExitReason int dismissReason,
@NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction t,
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/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java
index e7036c7..5655402 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java
@@ -55,6 +55,7 @@
import com.android.wm.shell.freeform.FreeformTaskTransitionStarter;
import com.android.wm.shell.transition.Transitions;
+import java.util.Optional;
import java.util.function.Supplier;
/**
@@ -74,7 +75,7 @@
private final DisplayController mDisplayController;
private final SyncTransactionQueue mSyncQueue;
private FreeformTaskTransitionStarter mTransitionStarter;
- private DesktopModeController mDesktopModeController;
+ private Optional<DesktopModeController> mDesktopModeController;
private boolean mTransitionDragActive;
private SparseArray<EventReceiver> mEventReceiversByDisplay = new SparseArray<>();
@@ -90,7 +91,7 @@
ShellTaskOrganizer taskOrganizer,
DisplayController displayController,
SyncTransactionQueue syncQueue,
- DesktopModeController desktopModeController) {
+ Optional<DesktopModeController> desktopModeController) {
this(
context,
mainHandler,
@@ -110,7 +111,7 @@
ShellTaskOrganizer taskOrganizer,
DisplayController displayController,
SyncTransactionQueue syncQueue,
- DesktopModeController desktopModeController,
+ Optional<DesktopModeController> desktopModeController,
CaptionWindowDecoration.Factory captionWindowDecorFactory,
Supplier<InputManager> inputManagerSupplier) {
@@ -246,10 +247,10 @@
} else if (id == R.id.caption_handle) {
decoration.createHandleMenu();
} else if (id == R.id.desktop_button) {
- mDesktopModeController.setDesktopModeActive(true);
+ mDesktopModeController.ifPresent(c -> c.setDesktopModeActive(true));
decoration.closeHandleMenu();
} else if (id == R.id.fullscreen_button) {
- mDesktopModeController.setDesktopModeActive(false);
+ mDesktopModeController.ifPresent(c -> c.setDesktopModeActive(false));
decoration.closeHandleMenu();
decoration.setButtonVisibility();
}
@@ -304,9 +305,9 @@
*/
private void handleEventForMove(MotionEvent e) {
RunningTaskInfo taskInfo = mTaskOrganizer.getRunningTaskInfo(mTaskId);
- int windowingMode = mDesktopModeController
- .getDisplayAreaWindowingMode(taskInfo.displayId);
- if (windowingMode == WINDOWING_MODE_FULLSCREEN) {
+ if (mDesktopModeController.isPresent()
+ && mDesktopModeController.get().getDisplayAreaWindowingMode(taskInfo.displayId)
+ == WINDOWING_MODE_FULLSCREEN) {
return;
}
switch (e.getActionMasked()) {
@@ -331,7 +332,7 @@
e.getRawX(dragPointerIdx), e.getRawY(dragPointerIdx));
if (e.getRawY(dragPointerIdx) <= statusBarHeight
&& DesktopModeStatus.isActive(mContext)) {
- mDesktopModeController.setDesktopModeActive(false);
+ mDesktopModeController.ifPresent(c -> c.setDesktopModeActive(false));
}
break;
}
@@ -471,7 +472,7 @@
int statusBarHeight = mDisplayController
.getDisplayLayout(focusedDecor.mTaskInfo.displayId).stableInsets().top;
if (ev.getY() > statusBarHeight) {
- mDesktopModeController.setDesktopModeActive(true);
+ mDesktopModeController.ifPresent(c -> c.setDesktopModeActive(true));
return;
}
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeControllerTest.java
index 89bafcb..dad9133 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeControllerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeControllerTest.java
@@ -36,7 +36,7 @@
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.clearInvocations;
import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -50,6 +50,7 @@
import android.window.WindowContainerToken;
import android.window.WindowContainerTransaction;
import android.window.WindowContainerTransaction.Change;
+import android.window.WindowContainerTransaction.HierarchyOp;
import androidx.test.filters.SmallTest;
@@ -99,15 +100,14 @@
@Before
public void setUp() {
mMockitoSession = mockitoSession().mockStatic(DesktopModeStatus.class).startMocking();
+ when(DesktopModeStatus.isSupported()).thenReturn(true);
when(DesktopModeStatus.isActive(any())).thenReturn(true);
mShellInit = Mockito.spy(new ShellInit(mTestExecutor));
mDesktopModeTaskRepository = new DesktopModeTaskRepository();
- mController = new DesktopModeController(mContext, mShellInit, mShellController,
- mShellTaskOrganizer, mRootTaskDisplayAreaOrganizer, mTransitions,
- mDesktopModeTaskRepository, mMockHandler, new TestShellExecutor());
+ mController = createController();
when(mShellTaskOrganizer.getRunningTasks(anyInt())).thenReturn(new ArrayList<>());
@@ -124,7 +124,17 @@
@Test
public void instantiate_addInitCallback() {
- verify(mShellInit, times(1)).addInitCallback(any(), any());
+ verify(mShellInit).addInitCallback(any(), any());
+ }
+
+ @Test
+ public void instantiate_flagOff_doNotAddInitCallback() {
+ when(DesktopModeStatus.isSupported()).thenReturn(false);
+ clearInvocations(mShellInit);
+
+ createController();
+
+ verify(mShellInit, never()).addInitCallback(any(), any());
}
@Test
@@ -222,25 +232,29 @@
// Check that there are hierarchy changes for home task and visible task
assertThat(wct.getHierarchyOps()).hasSize(2);
// First show home task
- WindowContainerTransaction.HierarchyOp op1 = wct.getHierarchyOps().get(0);
+ HierarchyOp op1 = wct.getHierarchyOps().get(0);
assertThat(op1.getType()).isEqualTo(HIERARCHY_OP_TYPE_REORDER);
assertThat(op1.getContainer()).isEqualTo(homeTask.token.asBinder());
// Then visible task on top of it
- WindowContainerTransaction.HierarchyOp op2 = wct.getHierarchyOps().get(1);
+ HierarchyOp op2 = wct.getHierarchyOps().get(1);
assertThat(op2.getType()).isEqualTo(HIERARCHY_OP_TYPE_REORDER);
assertThat(op2.getContainer()).isEqualTo(fullscreenTask1.token.asBinder());
}
@Test
- public void testShowDesktopApps() {
- // Set up two active tasks on desktop
+ public void testShowDesktopApps_allAppsInvisible_bringsToFront() {
+ // Set up two active tasks on desktop, task2 is on top of task1.
RunningTaskInfo freeformTask1 = createFreeformTask();
- freeformTask1.lastActiveTime = 100;
- RunningTaskInfo freeformTask2 = createFreeformTask();
- freeformTask2.lastActiveTime = 200;
mDesktopModeTaskRepository.addActiveTask(freeformTask1.taskId);
+ mDesktopModeTaskRepository.addOrMoveFreeformTaskToTop(freeformTask1.taskId);
+ mDesktopModeTaskRepository.updateVisibleFreeformTasks(
+ freeformTask1.taskId, false /* visible */);
+ RunningTaskInfo freeformTask2 = createFreeformTask();
mDesktopModeTaskRepository.addActiveTask(freeformTask2.taskId);
+ mDesktopModeTaskRepository.addOrMoveFreeformTaskToTop(freeformTask2.taskId);
+ mDesktopModeTaskRepository.updateVisibleFreeformTasks(
+ freeformTask2.taskId, false /* visible */);
when(mShellTaskOrganizer.getRunningTaskInfo(freeformTask1.taskId)).thenReturn(
freeformTask1);
when(mShellTaskOrganizer.getRunningTaskInfo(freeformTask2.taskId)).thenReturn(
@@ -248,27 +262,66 @@
// Run show desktop apps logic
mController.showDesktopApps();
- ArgumentCaptor<WindowContainerTransaction> wctCaptor = ArgumentCaptor.forClass(
- WindowContainerTransaction.class);
- if (Transitions.ENABLE_SHELL_TRANSITIONS) {
- verify(mTransitions).startTransition(eq(TRANSIT_TO_FRONT), wctCaptor.capture(), any());
- } else {
- verify(mShellTaskOrganizer).applyTransaction(wctCaptor.capture());
- }
- WindowContainerTransaction wct = wctCaptor.getValue();
+ final WindowContainerTransaction wct = getBringAppsToFrontTransaction();
// Check wct has reorder calls
assertThat(wct.getHierarchyOps()).hasSize(2);
- // Task 2 has activity later, must be first
- WindowContainerTransaction.HierarchyOp op1 = wct.getHierarchyOps().get(0);
+ // Task 1 appeared first, must be first reorder to top.
+ HierarchyOp op1 = wct.getHierarchyOps().get(0);
assertThat(op1.getType()).isEqualTo(HIERARCHY_OP_TYPE_REORDER);
- assertThat(op1.getContainer()).isEqualTo(freeformTask2.token.asBinder());
+ assertThat(op1.getContainer()).isEqualTo(freeformTask1.token.asBinder());
- // Task 1 should be second
- WindowContainerTransaction.HierarchyOp op2 = wct.getHierarchyOps().get(1);
+ // Task 2 appeared last, must be last reorder to top.
+ HierarchyOp op2 = wct.getHierarchyOps().get(1);
assertThat(op2.getType()).isEqualTo(HIERARCHY_OP_TYPE_REORDER);
- assertThat(op2.getContainer()).isEqualTo(freeformTask1.token.asBinder());
+ assertThat(op2.getContainer()).isEqualTo(freeformTask2.token.asBinder());
+ }
+
+ @Test
+ public void testShowDesktopApps_appsAlreadyVisible_doesNothing() {
+ final RunningTaskInfo task1 = createFreeformTask();
+ mDesktopModeTaskRepository.addActiveTask(task1.taskId);
+ mDesktopModeTaskRepository.addOrMoveFreeformTaskToTop(task1.taskId);
+ mDesktopModeTaskRepository.updateVisibleFreeformTasks(task1.taskId, true /* visible */);
+ when(mShellTaskOrganizer.getRunningTaskInfo(task1.taskId)).thenReturn(task1);
+ final RunningTaskInfo task2 = createFreeformTask();
+ mDesktopModeTaskRepository.addActiveTask(task2.taskId);
+ mDesktopModeTaskRepository.addOrMoveFreeformTaskToTop(task2.taskId);
+ mDesktopModeTaskRepository.updateVisibleFreeformTasks(task2.taskId, true /* visible */);
+ when(mShellTaskOrganizer.getRunningTaskInfo(task2.taskId)).thenReturn(task2);
+
+ mController.showDesktopApps();
+
+ final WindowContainerTransaction wct = getBringAppsToFrontTransaction();
+ // No reordering needed.
+ assertThat(wct.getHierarchyOps()).isEmpty();
+ }
+
+ @Test
+ public void testShowDesktopApps_someAppsInvisible_reordersAll() {
+ final RunningTaskInfo task1 = createFreeformTask();
+ mDesktopModeTaskRepository.addActiveTask(task1.taskId);
+ mDesktopModeTaskRepository.addOrMoveFreeformTaskToTop(task1.taskId);
+ mDesktopModeTaskRepository.updateVisibleFreeformTasks(task1.taskId, false /* visible */);
+ when(mShellTaskOrganizer.getRunningTaskInfo(task1.taskId)).thenReturn(task1);
+ final RunningTaskInfo task2 = createFreeformTask();
+ mDesktopModeTaskRepository.addActiveTask(task2.taskId);
+ mDesktopModeTaskRepository.addOrMoveFreeformTaskToTop(task2.taskId);
+ mDesktopModeTaskRepository.updateVisibleFreeformTasks(task2.taskId, true /* visible */);
+ when(mShellTaskOrganizer.getRunningTaskInfo(task2.taskId)).thenReturn(task2);
+
+ mController.showDesktopApps();
+
+ final WindowContainerTransaction wct = getBringAppsToFrontTransaction();
+ // Both tasks should be reordered to top, even if one was already visible.
+ assertThat(wct.getHierarchyOps()).hasSize(2);
+ final HierarchyOp op1 = wct.getHierarchyOps().get(0);
+ assertThat(op1.getType()).isEqualTo(HIERARCHY_OP_TYPE_REORDER);
+ assertThat(op1.getContainer()).isEqualTo(task1.token.asBinder());
+ final HierarchyOp op2 = wct.getHierarchyOps().get(1);
+ assertThat(op2.getType()).isEqualTo(HIERARCHY_OP_TYPE_REORDER);
+ assertThat(op2.getContainer()).isEqualTo(task2.token.asBinder());
}
@Test
@@ -309,6 +362,12 @@
assertThat(wct).isNotNull();
}
+ private DesktopModeController createController() {
+ return new DesktopModeController(mContext, mShellInit, mShellController,
+ mShellTaskOrganizer, mRootTaskDisplayAreaOrganizer, mTransitions,
+ mDesktopModeTaskRepository, mMockHandler, new TestShellExecutor());
+ }
+
private DisplayAreaInfo createMockDisplayArea() {
DisplayAreaInfo displayAreaInfo = new DisplayAreaInfo(new MockToken().mToken,
mContext.getDisplayId(), 0);
@@ -355,6 +414,17 @@
return arg.getValue();
}
+ private WindowContainerTransaction getBringAppsToFrontTransaction() {
+ final ArgumentCaptor<WindowContainerTransaction> arg = ArgumentCaptor.forClass(
+ WindowContainerTransaction.class);
+ if (Transitions.ENABLE_SHELL_TRANSITIONS) {
+ verify(mTransitions).startTransition(eq(TRANSIT_TO_FRONT), arg.capture(), any());
+ } else {
+ verify(mShellTaskOrganizer).applyTransaction(arg.capture());
+ }
+ return arg.getValue();
+ }
+
private void assertThatBoundsCleared(Change change) {
assertThat((change.getWindowSetMask() & WINDOW_CONFIG_BOUNDS) != 0).isTrue();
assertThat(change.getConfiguration().windowConfiguration.getBounds().isEmpty()).isTrue();
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepositoryTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepositoryTest.kt
index aaa5c8a..1e43a59 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepositoryTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepositoryTest.kt
@@ -140,6 +140,32 @@
assertThat(listener.visibleFreeformTaskChangedCalls).isEqualTo(3)
}
+ @Test
+ fun addOrMoveFreeformTaskToTop_didNotExist_addsToTop() {
+ repo.addOrMoveFreeformTaskToTop(5)
+ repo.addOrMoveFreeformTaskToTop(6)
+ repo.addOrMoveFreeformTaskToTop(7)
+
+ val tasks = repo.getFreeformTasksInZOrder()
+ assertThat(tasks.size).isEqualTo(3)
+ assertThat(tasks[0]).isEqualTo(7)
+ assertThat(tasks[1]).isEqualTo(6)
+ assertThat(tasks[2]).isEqualTo(5)
+ }
+
+ @Test
+ fun addOrMoveFreeformTaskToTop_alreadyExists_movesToTop() {
+ repo.addOrMoveFreeformTaskToTop(5)
+ repo.addOrMoveFreeformTaskToTop(6)
+ repo.addOrMoveFreeformTaskToTop(7)
+
+ repo.addOrMoveFreeformTaskToTop(6)
+
+ val tasks = repo.getFreeformTasksInZOrder()
+ assertThat(tasks.size).isEqualTo(3)
+ assertThat(tasks.first()).isEqualTo(6)
+ }
+
class TestListener : DesktopModeTaskRepository.ActiveTasksListener {
var activeTaskChangedCalls = 0
override fun onActiveTasksChanged() {
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModelTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModelTests.java
index 9b37b97..ad6fced 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModelTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModelTests.java
@@ -54,6 +54,7 @@
import java.util.ArrayList;
import java.util.List;
+import java.util.Optional;
import java.util.function.Supplier;
/** Tests of {@link CaptionWindowDecorViewModel} */
@@ -101,7 +102,7 @@
mTaskOrganizer,
mDisplayController,
mSyncQueue,
- mDesktopModeController,
+ Optional.of(mDesktopModeController),
mCaptionWindowDecorFactory,
new MockObjectSupplier<>(mMockInputManagers, () -> mock(InputManager.class)));
mCaptionWindowDecorViewModel.setEventReceiverFactory(mEventReceiverFactory);
diff --git a/location/java/android/location/altitude/AltitudeConverter.java b/location/java/android/location/altitude/AltitudeConverter.java
new file mode 100644
index 0000000..506128e
--- /dev/null
+++ b/location/java/android/location/altitude/AltitudeConverter.java
@@ -0,0 +1,175 @@
+/*
+ * 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.location.altitude;
+
+import android.annotation.NonNull;
+import android.annotation.WorkerThread;
+import android.content.Context;
+import android.location.Location;
+
+import com.android.internal.location.altitude.GeoidHeightMap;
+import com.android.internal.location.altitude.S2CellIdUtils;
+import com.android.internal.location.altitude.nano.MapParamsProto;
+import com.android.internal.util.Preconditions;
+
+import java.io.IOException;
+
+/**
+ * Converts altitudes reported above the World Geodetic System 1984 (WGS84) reference ellipsoid
+ * into ones above Mean Sea Level.
+ */
+public final class AltitudeConverter {
+
+ private static final double MAX_ABS_VALID_LATITUDE = 90;
+ private static final double MAX_ABS_VALID_LONGITUDE = 180;
+
+ /** Manages a mapping of geoid heights associated with S2 cells. */
+ private final GeoidHeightMap mGeoidHeightMap = new GeoidHeightMap();
+
+ /**
+ * Creates an instance that manages an independent cache to optimized conversions of locations
+ * in proximity to one another.
+ */
+ public AltitudeConverter() {
+ }
+
+ /**
+ * Throws an {@link IllegalArgumentException} if the {@code location} has an invalid latitude,
+ * longitude, or altitude above WGS84.
+ */
+ private static void validate(@NonNull Location location) {
+ Preconditions.checkArgument(
+ isFiniteAndAtAbsMost(location.getLatitude(), MAX_ABS_VALID_LATITUDE),
+ "Invalid latitude: %f", location.getLatitude());
+ Preconditions.checkArgument(
+ isFiniteAndAtAbsMost(location.getLongitude(), MAX_ABS_VALID_LONGITUDE),
+ "Invalid longitude: %f", location.getLongitude());
+ Preconditions.checkArgument(location.hasAltitude(), "Missing altitude above WGS84");
+ Preconditions.checkArgument(Double.isFinite(location.getAltitude()),
+ "Invalid altitude above WGS84: %f", location.getAltitude());
+ }
+
+ private static boolean isFiniteAndAtAbsMost(double value, double rhs) {
+ return Double.isFinite(value) && Math.abs(value) <= rhs;
+ }
+
+ /**
+ * Returns the four S2 cell IDs for the map square associated with the {@code location}.
+ *
+ * <p>The first map cell contains the location, while the others are located horizontally,
+ * vertically, and diagonally, in that order, with respect to the S2 (i,j) coordinate system. If
+ * the diagonal map cell does not exist (i.e., the location is near an S2 cube vertex), its
+ * corresponding ID is set to zero.
+ */
+ @NonNull
+ private static long[] findMapSquare(@NonNull MapParamsProto params,
+ @NonNull Location location) {
+ long s2CellId = S2CellIdUtils.fromLatLngDegrees(location.getLatitude(),
+ location.getLongitude());
+
+ // (0,0) cell.
+ long s0 = S2CellIdUtils.getParent(s2CellId, params.mapS2Level);
+ long[] edgeNeighbors = new long[4];
+ S2CellIdUtils.getEdgeNeighbors(s0, edgeNeighbors);
+
+ // (1,0) cell.
+ int i1 = S2CellIdUtils.getI(s2CellId) > S2CellIdUtils.getI(s0) ? -1 : 1;
+ long s1 = edgeNeighbors[i1 + 2];
+
+ // (0,1) cell.
+ int i2 = S2CellIdUtils.getJ(s2CellId) > S2CellIdUtils.getJ(s0) ? 1 : -1;
+ long s2 = edgeNeighbors[i2 + 1];
+
+ // (1,1) cell.
+ S2CellIdUtils.getEdgeNeighbors(s1, edgeNeighbors);
+ long s3 = 0;
+ for (int i = 0; i < edgeNeighbors.length; i++) {
+ if (edgeNeighbors[i] == s0) {
+ int i3 = (i + i1 * i2 + edgeNeighbors.length) % edgeNeighbors.length;
+ s3 = edgeNeighbors[i3] == s2 ? 0 : edgeNeighbors[i3];
+ break;
+ }
+ }
+
+ // Reuse edge neighbors' array to avoid an extra allocation.
+ edgeNeighbors[0] = s0;
+ edgeNeighbors[1] = s1;
+ edgeNeighbors[2] = s2;
+ edgeNeighbors[3] = s3;
+ return edgeNeighbors;
+ }
+
+ /**
+ * Adds to {@code location} the bilinearly interpolated Mean Sea Level altitude. In addition, a
+ * Mean Sea Level altitude accuracy is added if the {@code location} has a valid vertical
+ * accuracy; otherwise, does not add a corresponding accuracy.
+ */
+ private static void addMslAltitude(@NonNull MapParamsProto params, @NonNull long[] s2CellIds,
+ @NonNull double[] geoidHeightsMeters, @NonNull Location location) {
+ long s0 = s2CellIds[0];
+ double h0 = geoidHeightsMeters[0];
+ double h1 = geoidHeightsMeters[1];
+ double h2 = geoidHeightsMeters[2];
+ double h3 = s2CellIds[3] == 0 ? h0 : geoidHeightsMeters[3];
+
+ // Bilinear interpolation on an S2 square of size equal to that of a map cell. wi and wj
+ // are the normalized [0,1] weights in the i and j directions, respectively, allowing us to
+ // employ the simplified unit square formulation.
+ long s2CellId = S2CellIdUtils.fromLatLngDegrees(location.getLatitude(),
+ location.getLongitude());
+ double sizeIj = 1 << (S2CellIdUtils.MAX_LEVEL - params.mapS2Level);
+ double wi = Math.abs(S2CellIdUtils.getI(s2CellId) - S2CellIdUtils.getI(s0)) / sizeIj;
+ double wj = Math.abs(S2CellIdUtils.getJ(s2CellId) - S2CellIdUtils.getJ(s0)) / sizeIj;
+ double offsetMeters = h0 + (h1 - h0) * wi + (h2 - h0) * wj + (h3 - h1 - h2 + h0) * wi * wj;
+
+ location.setMslAltitudeMeters(location.getAltitude() - offsetMeters);
+ if (location.hasVerticalAccuracy()) {
+ double verticalAccuracyMeters = location.getVerticalAccuracyMeters();
+ if (Double.isFinite(verticalAccuracyMeters) && verticalAccuracyMeters >= 0) {
+ location.setMslAltitudeAccuracyMeters(
+ (float) Math.hypot(verticalAccuracyMeters, params.modelRmseMeters));
+ }
+ }
+ }
+
+ /**
+ * Adds a Mean Sea Level altitude to the {@code location}. In addition, adds a Mean Sea Level
+ * 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>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>
+ */
+ @WorkerThread
+ public void addMslAltitude(@NonNull Context context, @NonNull Location location)
+ throws IOException {
+ validate(location);
+ MapParamsProto params = GeoidHeightMap.getParams(context);
+ long[] s2CellIds = findMapSquare(params, location);
+ double[] geoidHeightsMeters = mGeoidHeightMap.readGeoidHeights(params, context, s2CellIds);
+ addMslAltitude(params, s2CellIds, geoidHeightsMeters, location);
+ }
+}
diff --git a/location/java/com/android/internal/location/altitude/GeoidHeightMap.java b/location/java/com/android/internal/location/altitude/GeoidHeightMap.java
new file mode 100644
index 0000000..6430eb4
--- /dev/null
+++ b/location/java/com/android/internal/location/altitude/GeoidHeightMap.java
@@ -0,0 +1,367 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.internal.location.altitude;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.util.LruCache;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.location.altitude.nano.MapParamsProto;
+import com.android.internal.location.altitude.nano.S2TileProto;
+import com.android.internal.util.Preconditions;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.ByteBuffer;
+import java.util.Objects;
+
+/**
+ * Manages a mapping of geoid heights associated with S2 cells, referred to as MAP CELLS.
+ *
+ * <p>Tiles are used extensively to reduce the number of entries needed to be stored in memory and
+ * on disk. A tile associates geoid heights with all map cells of a common parent at a specified S2
+ * level.
+ *
+ * <p>Since bilinear interpolation considers at most four map cells at a time, at most four tiles
+ * are simultaneously stored in memory. These tiles, referred to as CACHE TILES, are each keyed by
+ * its common parent's S2 cell ID, referred to as a CACHE KEY.
+ *
+ * <p>Absent cache tiles needed for interpolation are constructed from larger tiles stored on disk.
+ * The latter tiles, referred to as DISK TILES, are each keyed by its common parent's S2 cell token,
+ * referred to as a DISK TOKEN.
+ */
+public final class GeoidHeightMap {
+
+ private static final Object sLock = new Object();
+
+ @GuardedBy("sLock")
+ @Nullable
+ private static MapParamsProto sParams;
+
+ /** Defines a cache large enough to hold all cache tiles needed for interpolation. */
+ private final LruCache<Long, S2TileProto> mCacheTiles = new LruCache<>(4);
+
+ /**
+ * Returns the singleton parameter instance for a spherically projected geoid height map and its
+ * corresponding tile management.
+ */
+ @NonNull
+ public static MapParamsProto getParams(@NonNull Context context) throws IOException {
+ synchronized (sLock) {
+ if (sParams == null) {
+ try (InputStream is = context.getApplicationContext().getAssets().open(
+ "geoid_height_map/map-params.pb")) {
+ sParams = MapParamsProto.parseFrom(is.readAllBytes());
+ }
+ }
+ return sParams;
+ }
+ }
+
+ private static long getCacheKey(@NonNull MapParamsProto params, long s2CellId) {
+ return S2CellIdUtils.getParent(s2CellId, params.cacheTileS2Level);
+ }
+
+ @NonNull
+ private static String getDiskToken(@NonNull MapParamsProto params, long s2CellId) {
+ return S2CellIdUtils.getToken(
+ S2CellIdUtils.getParent(s2CellId, params.diskTileS2Level));
+ }
+
+ /**
+ * Adds to {@code values} values in the unit interval [0, 1] for the map cells identified by
+ * {@code s2CellIds}. Returns true if values are present for all non-zero IDs; otherwise,
+ * returns false and adds NaNs for absent values.
+ */
+ private static boolean getUnitIntervalValues(@NonNull MapParamsProto params,
+ @NonNull TileFunction tileFunction,
+ @NonNull long[] s2CellIds, @NonNull double[] values) {
+ int len = s2CellIds.length;
+
+ S2TileProto[] tiles = new S2TileProto[len];
+ for (int i = 0; i < len; i++) {
+ if (s2CellIds[i] != 0) {
+ tiles[i] = tileFunction.getTile(s2CellIds[i]);
+ }
+ values[i] = Double.NaN;
+ }
+
+ for (int i = 0; i < len; i++) {
+ if (tiles[i] == null || !Double.isNaN(values[i])) {
+ continue;
+ }
+
+ mergeByteBufferValues(params, s2CellIds, tiles, i, values);
+ mergeByteJpegValues(params, s2CellIds, tiles, i, values);
+ mergeBytePngValues(params, s2CellIds, tiles, i, values);
+ }
+
+ boolean allFound = true;
+ for (int i = 0; i < len; i++) {
+ if (s2CellIds[i] == 0) {
+ continue;
+ }
+ if (Double.isNaN(values[i])) {
+ allFound = false;
+ } else {
+ values[i] = (((int) values[i]) & 0xFF) / 255.0;
+ }
+ }
+ return allFound;
+ }
+
+ @SuppressWarnings("ReferenceEquality")
+ private static void mergeByteBufferValues(@NonNull MapParamsProto params,
+ @NonNull long[] s2CellIds,
+ @NonNull S2TileProto[] tiles,
+ int tileIndex, @NonNull double[] values) {
+ byte[] bytes = tiles[tileIndex].byteBuffer;
+ if (bytes == null || bytes.length == 0) {
+ return;
+ }
+
+ ByteBuffer byteBuffer = ByteBuffer.wrap(bytes).asReadOnlyBuffer();
+ int tileS2Level = params.mapS2Level - Integer.numberOfTrailingZeros(byteBuffer.limit()) / 2;
+ int numBitsLeftOfTile = 2 * tileS2Level + 3;
+
+ for (int i = tileIndex; i < tiles.length; i++) {
+ if (tiles[i] != tiles[tileIndex]) {
+ continue;
+ }
+
+ long maskedS2CellId = s2CellIds[i] & (-1L >>> numBitsLeftOfTile);
+ int numBitsRightOfMap = 2 * (S2CellIdUtils.MAX_LEVEL - params.mapS2Level) + 1;
+ int bufferIndex = (int) (maskedS2CellId >>> numBitsRightOfMap);
+ values[i] = Double.isNaN(values[i]) ? 0 : values[i];
+ values[i] += ((int) byteBuffer.get(bufferIndex)) & 0xFF;
+ }
+ }
+
+ private static void mergeByteJpegValues(@NonNull MapParamsProto params,
+ @NonNull long[] s2CellIds,
+ @NonNull S2TileProto[] tiles,
+ int tileIndex, @NonNull double[] values) {
+ mergeByteImageValues(params, tiles[tileIndex].byteJpeg, s2CellIds, tiles, tileIndex,
+ values);
+ }
+
+ private static void mergeBytePngValues(@NonNull MapParamsProto params,
+ @NonNull long[] s2CellIds,
+ @NonNull S2TileProto[] tiles,
+ int tileIndex, @NonNull double[] values) {
+ mergeByteImageValues(params, tiles[tileIndex].bytePng, s2CellIds, tiles, tileIndex, values);
+ }
+
+ @SuppressWarnings("ReferenceEquality")
+ private static void mergeByteImageValues(@NonNull MapParamsProto params, @NonNull byte[] bytes,
+ @NonNull long[] s2CellIds,
+ @NonNull S2TileProto[] tiles, int tileIndex, @NonNull double[] values) {
+ if (bytes == null || bytes.length == 0) {
+ return;
+ }
+ Bitmap bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.length);
+ if (bitmap == null) {
+ return;
+ }
+
+ for (int i = tileIndex; i < tiles.length; i++) {
+ if (s2CellIds[i] == 0 || tiles[i] != tiles[tileIndex]) {
+ continue;
+ }
+
+ values[i] = Double.isNaN(values[i]) ? 0 : values[i];
+ values[i] += bitmap.getPixel(getIndexX(params, s2CellIds[i], bitmap.getWidth()),
+ getIndexY(params, s2CellIds[i], bitmap.getHeight())) & 0xFF;
+ }
+ }
+
+ /** Returns the X index for an S2 cell within an S2 tile image of specified width. */
+ private static int getIndexX(@NonNull MapParamsProto params, long s2CellId, int width) {
+ return getIndexXOrY(params, S2CellIdUtils.getI(s2CellId), width);
+ }
+
+ /** Returns the Y index for an S2 cell within an S2 tile image of specified height. */
+ private static int getIndexY(@NonNull MapParamsProto params, long s2CellId, int height) {
+ return getIndexXOrY(params, S2CellIdUtils.getJ(s2CellId), height);
+ }
+
+ private static int getIndexXOrY(@NonNull MapParamsProto params, int iOrJ, int widthOrHeight) {
+ return (iOrJ >> (S2CellIdUtils.MAX_LEVEL - params.mapS2Level)) % widthOrHeight;
+ }
+
+ /**
+ * Returns the geoid heights in meters associated with the map cells identified by
+ * {@code s2CellIds}. Throws an {@link IOException} if a geoid height cannot be calculated for a
+ * non-zero ID.
+ */
+ @NonNull
+ public double[] readGeoidHeights(@NonNull MapParamsProto params, @NonNull Context context,
+ @NonNull long[] s2CellIds) throws IOException {
+ Preconditions.checkArgument(s2CellIds.length == 4);
+ for (long s2CellId : s2CellIds) {
+ Preconditions.checkArgument(
+ s2CellId == 0 || S2CellIdUtils.getLevel(s2CellId) == params.mapS2Level);
+ }
+
+ double[] heightsMeters = new double[s2CellIds.length];
+ if (getGeoidHeights(params, mCacheTiles::get, s2CellIds, heightsMeters)) {
+ return heightsMeters;
+ }
+
+ TileFunction loadedTiles = loadFromCacheAndDisk(params, context, s2CellIds);
+ if (getGeoidHeights(params, loadedTiles, s2CellIds, heightsMeters)) {
+ return heightsMeters;
+ }
+ throw new IOException("Unable to calculate geoid heights from raw assets.");
+ }
+
+ /**
+ * Adds to {@code heightsMeters} the geoid heights in meters associated with the map cells
+ * identified by {@code s2CellIds}. Returns true if heights are present for all non-zero IDs;
+ * otherwise, returns false and adds NaNs for absent heights.
+ */
+ private boolean getGeoidHeights(@NonNull MapParamsProto params,
+ @NonNull TileFunction tileFunction, @NonNull long[] s2CellIds,
+ @NonNull double[] heightsMeters) {
+ boolean allFound = getUnitIntervalValues(params, tileFunction, s2CellIds, heightsMeters);
+ for (int i = 0; i < heightsMeters.length; i++) {
+ // NaNs are properly preserved.
+ heightsMeters[i] *= params.modelAMeters;
+ heightsMeters[i] += params.modelBMeters;
+ }
+ return allFound;
+ }
+
+ @NonNull
+ private TileFunction loadFromCacheAndDisk(@NonNull MapParamsProto params,
+ @NonNull Context context, @NonNull long[] s2CellIds) throws IOException {
+ int len = s2CellIds.length;
+
+ // Enable batch loading by finding all cache keys upfront.
+ long[] cacheKeys = new long[len];
+ for (int i = 0; i < len; i++) {
+ if (s2CellIds[i] == 0) {
+ continue;
+ }
+ cacheKeys[i] = getCacheKey(params, s2CellIds[i]);
+ }
+
+ // Attempt to load tiles from cache.
+ S2TileProto[] loadedTiles = new S2TileProto[len];
+ String[] diskTokens = new String[len];
+ for (int i = 0; i < len; i++) {
+ if (s2CellIds[i] == 0 || diskTokens[i] != null) {
+ continue;
+ }
+ loadedTiles[i] = mCacheTiles.get(cacheKeys[i]);
+ diskTokens[i] = getDiskToken(params, cacheKeys[i]);
+
+ // Batch across common cache key.
+ for (int j = i + 1; j < len; j++) {
+ if (cacheKeys[j] == cacheKeys[i]) {
+ loadedTiles[j] = loadedTiles[i];
+ diskTokens[j] = diskTokens[i];
+ }
+ }
+ }
+
+ // Attempt to load tiles from disk.
+ for (int i = 0; i < len; i++) {
+ if (s2CellIds[i] == 0 || loadedTiles[i] != null) {
+ continue;
+ }
+
+ S2TileProto tile;
+ try (InputStream is = context.getApplicationContext().getAssets().open(
+ "geoid_height_map/tile-" + diskTokens[i] + ".pb")) {
+ tile = S2TileProto.parseFrom(is.readAllBytes());
+ }
+ mergeFromDiskTile(params, tile, cacheKeys, diskTokens, i, loadedTiles);
+ }
+
+ return s2CellId -> {
+ if (s2CellId == 0) {
+ return null;
+ }
+ long cacheKey = getCacheKey(params, s2CellId);
+ for (int i = 0; i < cacheKeys.length; i++) {
+ if (cacheKeys[i] == cacheKey) {
+ return loadedTiles[i];
+ }
+ }
+ return null;
+ };
+ }
+
+ private void mergeFromDiskTile(@NonNull MapParamsProto params, @NonNull S2TileProto diskTile,
+ @NonNull long[] cacheKeys, @NonNull String[] diskTokens, int diskTokenIndex,
+ @NonNull S2TileProto[] loadedTiles) throws IOException {
+ int len = cacheKeys.length;
+ int numMapCellsPerCacheTile = 1 << (2 * (params.mapS2Level - params.cacheTileS2Level));
+
+ // Reusable arrays.
+ long[] s2CellIds = new long[numMapCellsPerCacheTile];
+ double[] values = new double[numMapCellsPerCacheTile];
+
+ // Each cache key identifies a different sub-tile of the disk tile.
+ TileFunction diskTileFunction = s2CellId -> diskTile;
+ for (int i = diskTokenIndex; i < len; i++) {
+ if (!Objects.equals(diskTokens[i], diskTokens[diskTokenIndex])
+ || loadedTiles[i] != null) {
+ continue;
+ }
+
+ // Find all map cells within the current cache tile.
+ long s2CellId = S2CellIdUtils.getTraversalStart(cacheKeys[i], params.mapS2Level);
+ for (int j = 0; j < numMapCellsPerCacheTile; j++) {
+ s2CellIds[j] = s2CellId;
+ s2CellId = S2CellIdUtils.getTraversalNext(s2CellId);
+ }
+
+ if (!getUnitIntervalValues(params, diskTileFunction, s2CellIds, values)) {
+ throw new IOException("Corrupted disk tile of disk token: " + diskTokens[i]);
+ }
+
+ loadedTiles[i] = new S2TileProto();
+ loadedTiles[i].byteBuffer = new byte[numMapCellsPerCacheTile];
+ for (int j = 0; j < numMapCellsPerCacheTile; j++) {
+ loadedTiles[i].byteBuffer[j] = (byte) Math.round(values[j] * 0xFF);
+ }
+
+ // Batch across common cache key.
+ for (int j = i + 1; j < len; j++) {
+ if (cacheKeys[j] == cacheKeys[i]) {
+ loadedTiles[j] = loadedTiles[i];
+ }
+ }
+
+ // Side load into tile cache.
+ mCacheTiles.put(cacheKeys[i], loadedTiles[i]);
+ }
+ }
+
+ /** Defines a function-like object to retrieve tiles for map cells. */
+ private interface TileFunction {
+
+ @Nullable
+ S2TileProto getTile(long s2CellId);
+ }
+}
diff --git a/location/java/com/android/internal/location/altitude/S2CellIdUtils.java b/location/java/com/android/internal/location/altitude/S2CellIdUtils.java
new file mode 100644
index 0000000..5f11387
--- /dev/null
+++ b/location/java/com/android/internal/location/altitude/S2CellIdUtils.java
@@ -0,0 +1,653 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.internal.location.altitude;
+
+import android.annotation.NonNull;
+
+import java.util.Arrays;
+import java.util.Locale;
+
+/**
+ * Provides lightweight S2 cell ID utilities without traditional geometry dependencies.
+ *
+ * <p>See <a href="https://s2geometry.io/">the S2 Geometry Library website</a> for more details.
+ */
+public final class S2CellIdUtils {
+
+ /** The level of all leaf S2 cells. */
+ public static final int MAX_LEVEL = 30;
+
+ private static final int MAX_SIZE = 1 << MAX_LEVEL;
+ private static final double ONE_OVER_MAX_SIZE = 1.0 / MAX_SIZE;
+ private static final int NUM_FACES = 6;
+ private static final int POS_BITS = 2 * MAX_LEVEL + 1;
+ private static final int SWAP_MASK = 0x1;
+ private static final int LOOKUP_BITS = 4;
+ private static final int LOOKUP_MASK = (1 << LOOKUP_BITS) - 1;
+ private static final int INVERT_MASK = 0x2;
+ private static final int LEAF_MASK = 0x1;
+ private static final int[] LOOKUP_POS = new int[1 << (2 * LOOKUP_BITS + 2)];
+ private static final int[] LOOKUP_IJ = new int[1 << (2 * LOOKUP_BITS + 2)];
+ private static final int[] POS_TO_ORIENTATION = {SWAP_MASK, 0, 0, INVERT_MASK + SWAP_MASK};
+ private static final int[][] POS_TO_IJ =
+ {{0, 1, 3, 2}, {0, 2, 3, 1}, {3, 2, 0, 1}, {3, 1, 0, 2}};
+ private static final double UV_LIMIT = calculateUvLimit();
+ private static final UvTransform[] UV_TRANSFORMS = createUvTransforms();
+ private static final XyzTransform[] XYZ_TRANSFORMS = createXyzTransforms();
+
+ // Used to encode (i, j, o) coordinates into primitive longs.
+ private static final int I_SHIFT = 33;
+ private static final int J_SHIFT = 2;
+ private static final long J_MASK = (1L << 31) - 1;
+
+ static {
+ initLookupCells();
+ }
+
+ /** Prevents instantiation. */
+ private S2CellIdUtils() {
+ }
+
+ /**
+ * Returns the leaf S2 cell ID for the specified latitude and longitude, both measured in
+ * degrees.
+ */
+ public static long fromLatLngDegrees(double latDegrees, double lngDegrees) {
+ return fromLatLngRadians(Math.toRadians(latDegrees), Math.toRadians(lngDegrees));
+ }
+
+ /**
+ * Returns the ID of the parent of the specified S2 cell at the specified parent level.
+ * Behavior is undefined for invalid S2 cell IDs or parent levels not in
+ * [0, {@code getLevel(s2CellId)}[.
+ */
+ public static long getParent(long s2CellId, int level) {
+ long newLsb = getLowestOnBitForLevel(level);
+ return (s2CellId & -newLsb) | newLsb;
+ }
+
+ /**
+ * Inserts into {@code neighbors} the four S2 cell IDs corresponding to the neighboring
+ * cells adjacent across the specified cell's four edges. This array must be of minimum
+ * length four, and elements at the tail end of the array not corresponding to a neighbor
+ * are set to zero. A reference to this array is returned.
+ *
+ * <p>Inserts in the order of down, right, up, and left directions, in that order. All
+ * neighbors are guaranteed to be distinct.
+ */
+ public static void getEdgeNeighbors(long s2CellId, @NonNull long[] neighbors) {
+ int level = getLevel(s2CellId);
+ int size = levelToSizeIj(level);
+ int face = getFace(s2CellId);
+ long ijo = toIjo(s2CellId);
+ int i = ijoToI(ijo);
+ int j = ijoToJ(ijo);
+
+ int iPlusSize = i + size;
+ int iMinusSize = i - size;
+ int jPlusSize = j + size;
+ int jMinusSize = j - size;
+ boolean iPlusSizeLtMax = iPlusSize < MAX_SIZE;
+ boolean iMinusSizeGteZero = iMinusSize >= 0;
+ boolean jPlusSizeLtMax = jPlusSize < MAX_SIZE;
+ boolean jMinusSizeGteZero = jMinusSize >= 0;
+
+ int index = 0;
+ // Down direction.
+ neighbors[index++] = getParent(fromFijSame(face, i, jMinusSize, jMinusSizeGteZero),
+ level);
+ // Right direction.
+ neighbors[index++] = getParent(fromFijSame(face, iPlusSize, j, iPlusSizeLtMax), level);
+ // Up direction.
+ neighbors[index++] = getParent(fromFijSame(face, i, jPlusSize, jPlusSizeLtMax), level);
+ // Left direction.
+ neighbors[index++] = getParent(fromFijSame(face, iMinusSize, j, iMinusSizeGteZero),
+ level);
+
+ // Pad end of neighbor array with zeros.
+ Arrays.fill(neighbors, index, neighbors.length, 0);
+ }
+
+ /** Returns the "i" coordinate for the specified S2 cell. */
+ public static int getI(long s2CellId) {
+ return ijoToI(toIjo(s2CellId));
+ }
+
+ /** Returns the "j" coordinate for the specified S2 cell. */
+ public static int getJ(long s2CellId) {
+ return ijoToJ(toIjo(s2CellId));
+ }
+
+ /**
+ * Returns the leaf S2 cell ID for the specified latitude and longitude, both measured in
+ * radians.
+ */
+ private static long fromLatLngRadians(double latRadians, double lngRadians) {
+ double cosLat = Math.cos(latRadians);
+ double x = Math.cos(lngRadians) * cosLat;
+ double y = Math.sin(lngRadians) * cosLat;
+ double z = Math.sin(latRadians);
+ return fromXyz(x, y, z);
+ }
+
+ /**
+ * Returns the level of the specified S2 cell. The returned level is in [0, 30] for valid
+ * S2 cell IDs. Behavior is undefined for invalid S2 cell IDs.
+ */
+ static int getLevel(long s2CellId) {
+ if (isLeaf(s2CellId)) {
+ return MAX_LEVEL;
+ }
+ return MAX_LEVEL - (Long.numberOfTrailingZeros(s2CellId) >> 1);
+ }
+
+ /** Returns the lowest-numbered bit that is on for the specified S2 cell. */
+ static long getLowestOnBit(long s2CellId) {
+ return s2CellId & -s2CellId;
+ }
+
+ /** Returns the lowest-numbered bit that is on for any S2 cell on the specified level. */
+ static long getLowestOnBitForLevel(int level) {
+ return 1L << (2 * (MAX_LEVEL - level));
+ }
+
+ /**
+ * Returns the ID of the first S2 cell in a traversal of the children S2 cells at the specified
+ * level, in Hilbert curve order.
+ */
+ static long getTraversalStart(long s2CellId, int level) {
+ return s2CellId - getLowestOnBit(s2CellId) + getLowestOnBitForLevel(level);
+ }
+
+ /** Returns the ID of the next S2 cell at the same level along the Hilbert curve. */
+ static long getTraversalNext(long s2CellId) {
+ return s2CellId + (getLowestOnBit(s2CellId) << 1);
+ }
+
+ /**
+ * Encodes the S2 cell id to compact text strings suitable for display or indexing. Cells at
+ * lower levels (i.e., larger cells) are encoded into fewer characters.
+ */
+ @NonNull
+ static String getToken(long s2CellId) {
+ if (s2CellId == 0) {
+ return "X";
+ }
+
+ // Convert to a hex string with as many digits as necessary.
+ String hex = Long.toHexString(s2CellId).toLowerCase(Locale.US);
+ // Prefix 0s to get a length 16 string.
+ String padded = padStart(hex);
+ // Trim zeroes off the end.
+ return padded.replaceAll("0*$", "");
+ }
+
+ private static String padStart(String string) {
+ if (string.length() >= 16) {
+ return string;
+ }
+ return "0".repeat(16 - string.length()) + string;
+ }
+
+ /** Returns the leaf S2 cell ID of the specified (x, y, z) coordinate. */
+ private static long fromXyz(double x, double y, double z) {
+ int face = xyzToFace(x, y, z);
+ UvTransform uvTransform = UV_TRANSFORMS[face];
+ double u = uvTransform.xyzToU(x, y, z);
+ double v = uvTransform.xyzToV(x, y, z);
+ return fromFuv(face, u, v);
+ }
+
+ /** Returns the leaf S2 cell ID of the specified (face, u, v) coordinate. */
+ private static long fromFuv(int face, double u, double v) {
+ int i = uToI(u);
+ int j = vToJ(v);
+ return fromFij(face, i, j);
+ }
+
+ /** Returns the leaf S2 cell ID of the specified (face, i, j) coordinate. */
+ private static long fromFij(int face, int i, int j) {
+ int bits = (face & SWAP_MASK);
+ // Update most significant bits.
+ long msb = ((long) face) << (POS_BITS - 33);
+ for (int k = 7; k >= 4; --k) {
+ bits = lookupBits(i, j, k, bits);
+ msb = updateBits(msb, k, bits);
+ bits = maskBits(bits);
+ }
+ // Update least significant bits.
+ long lsb = 0;
+ for (int k = 3; k >= 0; --k) {
+ bits = lookupBits(i, j, k, bits);
+ lsb = updateBits(lsb, k, bits);
+ bits = maskBits(bits);
+ }
+ return (((msb << 32) + lsb) << 1) + 1;
+ }
+
+ private static long fromFijWrap(int face, int i, int j) {
+ double u = iToU(i);
+ double v = jToV(j);
+
+ XyzTransform xyzTransform = XYZ_TRANSFORMS[face];
+ double x = xyzTransform.uvToX(u, v);
+ double y = xyzTransform.uvToY(u, v);
+ double z = xyzTransform.uvToZ(u, v);
+
+ int newFace = xyzToFace(x, y, z);
+ UvTransform uvTransform = UV_TRANSFORMS[newFace];
+ double newU = uvTransform.xyzToU(x, y, z);
+ double newV = uvTransform.xyzToV(x, y, z);
+
+ int newI = uShiftIntoI(newU);
+ int newJ = vShiftIntoJ(newV);
+ return fromFij(newFace, newI, newJ);
+ }
+
+ private static long fromFijSame(int face, int i, int j, boolean isSameFace) {
+ if (isSameFace) {
+ return fromFij(face, i, j);
+ }
+ return fromFijWrap(face, i, j);
+ }
+
+ /**
+ * Returns the face associated with the specified (x, y, z) coordinate. For a coordinate
+ * on a face boundary, the returned face is arbitrary but repeatable.
+ */
+ private static int xyzToFace(double x, double y, double z) {
+ double absX = Math.abs(x);
+ double absY = Math.abs(y);
+ double absZ = Math.abs(z);
+ if (absX > absY) {
+ if (absX > absZ) {
+ return (x < 0) ? 3 : 0;
+ }
+ return (z < 0) ? 5 : 2;
+ }
+ if (absY > absZ) {
+ return (y < 0) ? 4 : 1;
+ }
+ return (z < 0) ? 5 : 2;
+ }
+
+ private static int uToI(double u) {
+ double s;
+ if (u >= 0) {
+ s = 0.5 * Math.sqrt(1 + 3 * u);
+ } else {
+ s = 1 - 0.5 * Math.sqrt(1 - 3 * u);
+ }
+ return Math.max(0, Math.min(MAX_SIZE - 1, (int) Math.round(MAX_SIZE * s - 0.5)));
+ }
+
+ private static int vToJ(double v) {
+ // Same calculation as uToI.
+ return uToI(v);
+ }
+
+ private static int lookupBits(int i, int j, int k, int bits) {
+ bits += ((i >> (k * LOOKUP_BITS)) & LOOKUP_MASK) << (LOOKUP_BITS + 2);
+ bits += ((j >> (k * LOOKUP_BITS)) & LOOKUP_MASK) << 2;
+ return LOOKUP_POS[bits];
+ }
+
+ private static long updateBits(long sb, int k, int bits) {
+ return sb | ((((long) bits) >> 2) << ((k & 0x3) * 2 * LOOKUP_BITS));
+ }
+
+ private static int maskBits(int bits) {
+ return bits & (SWAP_MASK | INVERT_MASK);
+ }
+
+ private static int getFace(long s2CellId) {
+ return (int) (s2CellId >>> POS_BITS);
+ }
+
+ private static boolean isLeaf(long s2CellId) {
+ return ((int) s2CellId & LEAF_MASK) != 0;
+ }
+
+ private static double iToU(int i) {
+ int satI = Math.max(-1, Math.min(MAX_SIZE, i));
+ return Math.max(
+ -UV_LIMIT,
+ Math.min(UV_LIMIT, ONE_OVER_MAX_SIZE * ((satI << 1) + 1 - MAX_SIZE)));
+ }
+
+ private static double jToV(int j) {
+ // Same calculation as iToU.
+ return iToU(j);
+ }
+
+ private static long toIjo(long s2CellId) {
+ int face = getFace(s2CellId);
+ int bits = face & SWAP_MASK;
+ int i = 0;
+ int j = 0;
+ for (int k = 7; k >= 0; --k) {
+ int nbits = (k == 7) ? (MAX_LEVEL - 7 * LOOKUP_BITS) : LOOKUP_BITS;
+ bits += ((int) (s2CellId >>> (k * 2 * LOOKUP_BITS + 1)) & ((1 << (2 * nbits))
+ - 1)) << 2;
+ bits = LOOKUP_IJ[bits];
+ i += (bits >> (LOOKUP_BITS + 2)) << (k * LOOKUP_BITS);
+ j += ((bits >> 2) & ((1 << LOOKUP_BITS) - 1)) << (k * LOOKUP_BITS);
+ bits &= (SWAP_MASK | INVERT_MASK);
+ }
+ int orientation =
+ ((getLowestOnBit(s2CellId) & 0x1111111111111110L) != 0) ? (bits ^ SWAP_MASK)
+ : bits;
+ return (((long) i) << I_SHIFT) | (((long) j) << J_SHIFT) | orientation;
+ }
+
+ private static int ijoToI(long ijo) {
+ return (int) (ijo >>> I_SHIFT);
+ }
+
+ private static int ijoToJ(long ijo) {
+ return (int) ((ijo >>> J_SHIFT) & J_MASK);
+ }
+
+ private static int uShiftIntoI(double u) {
+ double s = 0.5 * (u + 1);
+ return Math.max(0, Math.min(MAX_SIZE - 1, (int) Math.round(MAX_SIZE * s - 0.5)));
+ }
+
+ private static int vShiftIntoJ(double v) {
+ // Same calculation as uShiftIntoI.
+ return uShiftIntoI(v);
+ }
+
+ private static int levelToSizeIj(int level) {
+ return 1 << (MAX_LEVEL - level);
+ }
+
+ private static void initLookupCells() {
+ initLookupCell(0, 0, 0, 0, 0, 0);
+ initLookupCell(0, 0, 0, SWAP_MASK, 0, SWAP_MASK);
+ initLookupCell(0, 0, 0, INVERT_MASK, 0, INVERT_MASK);
+ initLookupCell(0, 0, 0, SWAP_MASK | INVERT_MASK, 0, SWAP_MASK | INVERT_MASK);
+ }
+
+ private static void initLookupCell(
+ int level, int i, int j, int origOrientation, int pos, int orientation) {
+ if (level == LOOKUP_BITS) {
+ int ij = (i << LOOKUP_BITS) + j;
+ LOOKUP_POS[(ij << 2) + origOrientation] = (pos << 2) + orientation;
+ LOOKUP_IJ[(pos << 2) + origOrientation] = (ij << 2) + orientation;
+ } else {
+ level++;
+ i <<= 1;
+ j <<= 1;
+ pos <<= 2;
+ for (int subPos = 0; subPos < 4; subPos++) {
+ int ij = POS_TO_IJ[orientation][subPos];
+ int orientationMask = POS_TO_ORIENTATION[subPos];
+ initLookupCell(
+ level,
+ i + (ij >>> 1),
+ j + (ij & 0x1),
+ origOrientation,
+ pos + subPos,
+ orientation ^ orientationMask);
+ }
+ }
+ }
+
+ private static double calculateUvLimit() {
+ double machEps = 1.0;
+ do {
+ machEps /= 2.0f;
+ } while ((1.0 + (machEps / 2.0)) != 1.0);
+ return 1.0 + machEps;
+ }
+
+ @NonNull
+ private static UvTransform[] createUvTransforms() {
+ UvTransform[] uvTransforms = new UvTransform[NUM_FACES];
+ uvTransforms[0] =
+ new UvTransform() {
+
+ @Override
+ public double xyzToU(double x, double y, double z) {
+ return y / x;
+ }
+
+ @Override
+ public double xyzToV(double x, double y, double z) {
+ return z / x;
+ }
+ };
+ uvTransforms[1] =
+ new UvTransform() {
+
+ @Override
+ public double xyzToU(double x, double y, double z) {
+ return -x / y;
+ }
+
+ @Override
+ public double xyzToV(double x, double y, double z) {
+ return z / y;
+ }
+ };
+ uvTransforms[2] =
+ new UvTransform() {
+
+ @Override
+ public double xyzToU(double x, double y, double z) {
+ return -x / z;
+ }
+
+ @Override
+ public double xyzToV(double x, double y, double z) {
+ return -y / z;
+ }
+ };
+ uvTransforms[3] =
+ new UvTransform() {
+
+ @Override
+ public double xyzToU(double x, double y, double z) {
+ return z / x;
+ }
+
+ @Override
+ public double xyzToV(double x, double y, double z) {
+ return y / x;
+ }
+ };
+ uvTransforms[4] =
+ new UvTransform() {
+
+ @Override
+ public double xyzToU(double x, double y, double z) {
+ return z / y;
+ }
+
+ @Override
+ public double xyzToV(double x, double y, double z) {
+ return -x / y;
+ }
+ };
+ uvTransforms[5] =
+ new UvTransform() {
+
+ @Override
+ public double xyzToU(double x, double y, double z) {
+ return -y / z;
+ }
+
+ @Override
+ public double xyzToV(double x, double y, double z) {
+ return -x / z;
+ }
+ };
+ return uvTransforms;
+ }
+
+ @NonNull
+ private static XyzTransform[] createXyzTransforms() {
+ XyzTransform[] xyzTransforms = new XyzTransform[NUM_FACES];
+ xyzTransforms[0] =
+ new XyzTransform() {
+
+ @Override
+ public double uvToX(double u, double v) {
+ return 1;
+ }
+
+ @Override
+ public double uvToY(double u, double v) {
+ return u;
+ }
+
+ @Override
+ public double uvToZ(double u, double v) {
+ return v;
+ }
+ };
+ xyzTransforms[1] =
+ new XyzTransform() {
+
+ @Override
+ public double uvToX(double u, double v) {
+ return -u;
+ }
+
+ @Override
+ public double uvToY(double u, double v) {
+ return 1;
+ }
+
+ @Override
+ public double uvToZ(double u, double v) {
+ return v;
+ }
+ };
+ xyzTransforms[2] =
+ new XyzTransform() {
+
+ @Override
+ public double uvToX(double u, double v) {
+ return -u;
+ }
+
+ @Override
+ public double uvToY(double u, double v) {
+ return -v;
+ }
+
+ @Override
+ public double uvToZ(double u, double v) {
+ return 1;
+ }
+ };
+ xyzTransforms[3] =
+ new XyzTransform() {
+
+ @Override
+ public double uvToX(double u, double v) {
+ return -1;
+ }
+
+ @Override
+ public double uvToY(double u, double v) {
+ return -v;
+ }
+
+ @Override
+ public double uvToZ(double u, double v) {
+ return -u;
+ }
+ };
+ xyzTransforms[4] =
+ new XyzTransform() {
+
+ @Override
+ public double uvToX(double u, double v) {
+ return v;
+ }
+
+ @Override
+ public double uvToY(double u, double v) {
+ return -1;
+ }
+
+ @Override
+ public double uvToZ(double u, double v) {
+ return -u;
+ }
+ };
+ xyzTransforms[5] =
+ new XyzTransform() {
+
+ @Override
+ public double uvToX(double u, double v) {
+ return v;
+ }
+
+ @Override
+ public double uvToY(double u, double v) {
+ return u;
+ }
+
+ @Override
+ public double uvToZ(double u, double v) {
+ return -1;
+ }
+ };
+ return xyzTransforms;
+ }
+
+ /**
+ * Transform from (x, y, z) coordinates to (u, v) coordinates, indexed by face. For a
+ * (x, y, z) coordinate within a face, each element of the resulting (u, v) coordinate
+ * should lie in the inclusive range [-1, 1], with the face center having a (u, v)
+ * coordinate equal to (0, 0).
+ */
+ private interface UvTransform {
+
+ /**
+ * Returns for the specified (x, y, z) coordinate the corresponding u-coordinate
+ * (which may lie outside the range [-1, 1]).
+ */
+ double xyzToU(double x, double y, double z);
+
+ /**
+ * Returns for the specified (x, y, z) coordinate the corresponding v-coordinate
+ * (which may lie outside the range [-1, 1]).
+ */
+ double xyzToV(double x, double y, double z);
+ }
+
+ /**
+ * Transform from (u, v) coordinates to (x, y, z) coordinates, indexed by face. The
+ * resulting vectors are not necessarily of unit length.
+ */
+ private interface XyzTransform {
+
+ /** Returns for the specified (u, v) coordinate the corresponding x-coordinate. */
+ double uvToX(double u, double v);
+
+ /** Returns for the specified (u, v) coordinate the corresponding y-coordinate. */
+ double uvToY(double u, double v);
+
+ /** Returns for the specified (u, v) coordinate the corresponding z-coordinate. */
+ double uvToZ(double u, double v);
+ }
+}
diff --git a/media/java/android/media/AudioHalVersionInfo.aidl b/media/java/android/media/AudioHalVersionInfo.aidl
new file mode 100644
index 0000000..a83f8c8
--- /dev/null
+++ b/media/java/android/media/AudioHalVersionInfo.aidl
@@ -0,0 +1,18 @@
+/* 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.media;
+
+parcelable AudioHalVersionInfo;
diff --git a/media/java/android/media/AudioHalVersionInfo.java b/media/java/android/media/AudioHalVersionInfo.java
new file mode 100644
index 0000000..985a758
--- /dev/null
+++ b/media/java/android/media/AudioHalVersionInfo.java
@@ -0,0 +1,174 @@
+/*
+ * 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.media;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.TestApi;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.util.List;
+
+/**
+ * Defines the audio HAL version.
+ *
+ * @hide
+ */
+@TestApi
+public final class AudioHalVersionInfo implements Parcelable, Comparable<AudioHalVersionInfo> {
+ /**
+ * Indicate the audio HAL is implemented with HIDL (HAL interface definition language).
+ *
+ * @see <a href="https://source.android.com/docs/core/architecture/hidl/">HIDL</a>
+ * <p>The value of AUDIO_HAL_TYPE_HIDL should match the value of {@link
+ * android.media.AudioHalVersion.Type#HIDL}.
+ */
+ public static final int AUDIO_HAL_TYPE_HIDL = 0;
+
+ /**
+ * Indicate the audio HAL is implemented with AIDL (Android Interface Definition Language).
+ *
+ * @see <a href="https://source.android.com/docs/core/architecture/aidl/">AIDL</a>
+ * <p>The value of AUDIO_HAL_TYPE_AIDL should match the value of {@link
+ * android.media.AudioHalVersion.Type#AIDL}.
+ */
+ public static final int AUDIO_HAL_TYPE_AIDL = 1;
+
+ /** @hide */
+ @IntDef(
+ flag = false,
+ prefix = "AUDIO_HAL_TYPE_",
+ value = {AUDIO_HAL_TYPE_HIDL, AUDIO_HAL_TYPE_AIDL})
+ public @interface AudioHalType {}
+
+ /** AudioHalVersionInfo object of all valid Audio HAL versions. */
+ public static final @NonNull AudioHalVersionInfo AIDL_1_0 =
+ new AudioHalVersionInfo(AUDIO_HAL_TYPE_AIDL, 1 /* major */, 0 /* minor */);
+
+ public static final @NonNull AudioHalVersionInfo HIDL_7_1 =
+ new AudioHalVersionInfo(AUDIO_HAL_TYPE_HIDL, 7 /* major */, 1 /* minor */);
+ public static final @NonNull AudioHalVersionInfo HIDL_7_0 =
+ new AudioHalVersionInfo(AUDIO_HAL_TYPE_HIDL, 7 /* major */, 0 /* minor */);
+ public static final @NonNull AudioHalVersionInfo HIDL_6_0 =
+ new AudioHalVersionInfo(AUDIO_HAL_TYPE_HIDL, 6 /* major */, 0 /* minor */);
+ public static final @NonNull AudioHalVersionInfo HIDL_5_0 =
+ new AudioHalVersionInfo(AUDIO_HAL_TYPE_HIDL, 5 /* major */, 0 /* minor */);
+ public static final @NonNull AudioHalVersionInfo HIDL_4_0 =
+ new AudioHalVersionInfo(AUDIO_HAL_TYPE_HIDL, 4 /* major */, 0 /* minor */);
+ public static final @NonNull AudioHalVersionInfo HIDL_2_0 =
+ new AudioHalVersionInfo(AUDIO_HAL_TYPE_HIDL, 2 /* major */, 0 /* minor */);
+
+ /**
+ * List of all valid Audio HAL versions. This list need to be in sync with sAudioHALVersions
+ * defined in frameworks/av/media/libaudiohal/FactoryHalHidl.cpp.
+ */
+ // TODO: add AIDL_1_0 with sAudioHALVersions.
+ public static final @NonNull List<AudioHalVersionInfo> VERSIONS =
+ List.of(HIDL_7_1, HIDL_7_0, HIDL_6_0, HIDL_5_0, HIDL_4_0);
+
+ private static final String TAG = "AudioHalVersionInfo";
+ private AudioHalVersion mHalVersion = new AudioHalVersion();
+
+ public @AudioHalType int getHalType() {
+ return mHalVersion.type;
+ }
+
+ public int getMajorVersion() {
+ return mHalVersion.major;
+ }
+
+ public int getMinorVersion() {
+ return mHalVersion.minor;
+ }
+
+ /** String representative of AudioHalVersion.Type */
+ private static @NonNull String typeToString(@AudioHalType int type) {
+ if (type == AudioHalVersion.Type.HIDL) {
+ return "HIDL";
+ } else if (type == AudioHalVersion.Type.AIDL) {
+ return "AIDL";
+ } else {
+ return "INVALID";
+ }
+ }
+
+ /** String representative of type, major and minor */
+ private static @NonNull String toString(@AudioHalType int type, int major, int minor) {
+ return typeToString(type) + ":" + Integer.toString(major) + "." + Integer.toString(minor);
+ }
+
+ private AudioHalVersionInfo(@AudioHalType int type, int major, int minor) {
+ mHalVersion.type = type;
+ mHalVersion.major = major;
+ mHalVersion.minor = minor;
+ }
+
+ private AudioHalVersionInfo(Parcel in) {
+ mHalVersion = in.readTypedObject(AudioHalVersion.CREATOR);
+ }
+
+ /** String representative of this (AudioHalVersionInfo) object */
+ @Override
+ public String toString() {
+ return toString(mHalVersion.type, mHalVersion.major, mHalVersion.minor);
+ }
+
+ /**
+ * Compare two HAL versions by comparing their index in VERSIONS.
+ *
+ * <p>Normally all AudioHalVersionInfo object to compare should exist in the VERSIONS list. If
+ * both candidates exist in the VERSIONS list, smaller index means newer. Any candidate not
+ * exist in the VERSIONS list will be considered to be oldest version.
+ *
+ * @return 0 if the HAL version is the same as the other HAL version. Positive if the HAL
+ * version is newer than the other HAL version. Negative if the HAL version is older than
+ * the other version.
+ */
+ @Override
+ public int compareTo(@NonNull AudioHalVersionInfo other) {
+ int indexOther = VERSIONS.indexOf(other);
+ int indexThis = VERSIONS.indexOf(this);
+ if (indexThis < 0 || indexOther < 0) {
+ return indexThis - indexOther;
+ }
+ return indexOther - indexThis;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(@NonNull android.os.Parcel out, int flag) {
+ out.writeTypedObject(mHalVersion, flag);
+ }
+
+ public static final @NonNull Parcelable.Creator<AudioHalVersionInfo> CREATOR =
+ new Parcelable.Creator<AudioHalVersionInfo>() {
+ @Override
+ public AudioHalVersionInfo createFromParcel(@NonNull Parcel in) {
+ return new AudioHalVersionInfo(in);
+ }
+
+ @Override
+ public AudioHalVersionInfo[] newArray(int size) {
+ return new AudioHalVersionInfo[size];
+ }
+ };
+}
diff --git a/media/java/android/media/AudioManager.java b/media/java/android/media/AudioManager.java
index 9c5313a..ae0d45f 100644
--- a/media/java/android/media/AudioManager.java
+++ b/media/java/android/media/AudioManager.java
@@ -8502,13 +8502,14 @@
}
/**
- * Returns the audio HAL version in the form MAJOR.MINOR. If there is no audio HAL found, null
- * will be returned.
+ * Returns an {@link AudioHalVersionInfo} indicating the Audio Hal Version. If there is no audio
+ * HAL found, null will be returned.
*
+ * @return @see @link #AudioHalVersionInfo The version of Audio HAL.
* @hide
*/
@TestApi
- public static @Nullable String getHalVersion() {
+ public static @Nullable AudioHalVersionInfo getHalVersion() {
try {
return getService().getHalVersion();
} catch (RemoteException e) {
diff --git a/media/java/android/media/IAudioService.aidl b/media/java/android/media/IAudioService.aidl
old mode 100755
new mode 100644
index 2e766d5..ee453a4
--- a/media/java/android/media/IAudioService.aidl
+++ b/media/java/android/media/IAudioService.aidl
@@ -22,6 +22,7 @@
import android.media.AudioDeviceAttributes;
import android.media.AudioFormat;
import android.media.AudioFocusInfo;
+import android.media.AudioHalVersionInfo;
import android.media.AudioPlaybackConfiguration;
import android.media.AudioRecordingConfiguration;
import android.media.AudioRoutesInfo;
@@ -571,5 +572,5 @@
in AudioDeviceAttributes device, in List<VolumeInfo> volumes,
boolean handlesvolumeAdjustment);
- String getHalVersion();
+ AudioHalVersionInfo getHalVersion();
}
diff --git a/packages/CredentialManager/res/drawable/ic_passkeys_onboarding.png b/packages/CredentialManager/res/drawable/ic_passkeys_onboarding.png
new file mode 100644
index 0000000..388d098
--- /dev/null
+++ b/packages/CredentialManager/res/drawable/ic_passkeys_onboarding.png
Binary files differ
diff --git a/packages/CredentialManager/res/drawable/ic_passkeys_onboarding_device.xml b/packages/CredentialManager/res/drawable/ic_passkeys_onboarding_device.xml
new file mode 100644
index 0000000..9e4f424
--- /dev/null
+++ b/packages/CredentialManager/res/drawable/ic_passkeys_onboarding_device.xml
@@ -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.
+ -->
+
+<vector
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ tools:ignore="VectorPath"
+ android:name="vector"
+ android:width="21dp"
+ android:height="17dp"
+ android:viewportWidth="21"
+ android:viewportHeight="17">
+ <path
+ android:name="path"
+ android:pathData="M 4 2.941 L 20 2.941 L 20 0.941 L 4 0.941 C 2.9 0.941 2 1.841 2 2.941 L 2 13.941 L 0 13.941 L 0 16.941 L 11 16.941 L 11 13.941 L 4 13.941 L 4 2.941 Z M 20 4.941 L 14 4.941 C 13.45 4.941 13 5.391 13 5.941 L 13 15.941 C 13 16.491 13.45 16.941 14 16.941 L 20 16.941 C 20.55 16.941 21 16.491 21 15.941 L 21 5.941 C 21 5.391 20.55 4.941 20 4.941 Z M 15 13.941 L 19 13.941 L 19 6.941 L 15 6.941 L 15 13.941 Z"
+ android:fillColor="#5f6368"
+ android:strokeWidth="1"
+ android:fillType="evenOdd"/>
+</vector>
\ No newline at end of file
diff --git a/packages/CredentialManager/res/drawable/ic_passkeys_onboarding_fingerprint.xml b/packages/CredentialManager/res/drawable/ic_passkeys_onboarding_fingerprint.xml
new file mode 100644
index 0000000..b6ee4f9
--- /dev/null
+++ b/packages/CredentialManager/res/drawable/ic_passkeys_onboarding_fingerprint.xml
@@ -0,0 +1,31 @@
+<!--
+ ~ Copyright (C) 2022 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<vector
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ tools:ignore="VectorPath"
+ android:name="vector"
+ android:width="19dp"
+ android:height="21dp"
+ android:viewportWidth="19"
+ android:viewportHeight="21">
+ <path
+ android:name="path"
+ android:pathData="M 0.25 8.591 C 0.133 8.508 0.058 8.408 0.025 8.291 C 0.008 8.158 0.05 8.025 0.15 7.891 C 1.183 6.475 2.475 5.375 4.025 4.591 C 5.592 3.808 7.258 3.416 9.025 3.416 C 10.792 3.416 12.458 3.8 14.025 4.566 C 15.592 5.316 16.9 6.408 17.95 7.841 C 18.067 7.991 18.1 8.125 18.05 8.241 C 18.017 8.358 17.95 8.458 17.85 8.541 C 17.75 8.625 17.633 8.666 17.5 8.666 C 17.367 8.65 17.25 8.575 17.15 8.441 C 16.233 7.141 15.05 6.15 13.6 5.466 C 12.167 4.766 10.642 4.416 9.025 4.416 C 7.408 4.416 5.892 4.766 4.475 5.466 C 3.058 6.15 1.883 7.141 0.95 8.441 C 0.85 8.591 0.733 8.675 0.6 8.691 C 0.467 8.708 0.35 8.675 0.25 8.591 Z M 11.85 20.916 C 10.117 20.483 8.7 19.625 7.6 18.341 C 6.5 17.041 5.95 15.458 5.95 13.591 C 5.95 12.758 6.25 12.058 6.85 11.491 C 7.45 10.925 8.175 10.641 9.025 10.641 C 9.875 10.641 10.6 10.925 11.2 11.491 C 11.8 12.058 12.1 12.758 12.1 13.591 C 12.1 14.141 12.308 14.608 12.725 14.991 C 13.142 15.358 13.633 15.541 14.2 15.541 C 14.767 15.541 15.25 15.358 15.65 14.991 C 16.05 14.608 16.25 14.141 16.25 13.591 C 16.25 11.658 15.542 10.033 14.125 8.716 C 12.708 7.4 11.017 6.741 9.05 6.741 C 7.083 6.741 5.392 7.4 3.975 8.716 C 2.558 10.033 1.85 11.65 1.85 13.566 C 1.85 13.966 1.883 14.466 1.95 15.066 C 2.033 15.666 2.217 16.366 2.5 17.166 C 2.55 17.316 2.542 17.45 2.475 17.566 C 2.425 17.683 2.333 17.766 2.2 17.816 C 2.067 17.866 1.933 17.866 1.8 17.816 C 1.683 17.75 1.6 17.65 1.55 17.516 C 1.3 16.866 1.117 16.225 1 15.591 C 0.9 14.941 0.85 14.275 0.85 13.591 C 0.85 11.375 1.65 9.516 3.25 8.016 C 4.867 6.516 6.792 5.766 9.025 5.766 C 11.275 5.766 13.208 6.516 14.825 8.016 C 16.442 9.516 17.25 11.375 17.25 13.591 C 17.25 14.425 16.95 15.125 16.35 15.691 C 15.767 16.241 15.05 16.516 14.2 16.516 C 13.35 16.516 12.617 16.241 12 15.691 C 11.4 15.125 11.1 14.425 11.1 13.591 C 11.1 13.041 10.892 12.583 10.475 12.216 C 10.075 11.833 9.592 11.641 9.025 11.641 C 8.458 11.641 7.967 11.833 7.55 12.216 C 7.15 12.583 6.95 13.041 6.95 13.591 C 6.95 15.208 7.425 16.558 8.375 17.641 C 9.342 18.725 10.583 19.483 12.1 19.916 C 12.25 19.966 12.35 20.05 12.4 20.166 C 12.45 20.283 12.458 20.408 12.425 20.541 C 12.392 20.658 12.325 20.758 12.225 20.841 C 12.125 20.925 12 20.95 11.85 20.916 Z M 3.5 3.366 C 3.367 3.45 3.233 3.475 3.1 3.441 C 2.967 3.391 2.867 3.3 2.8 3.166 C 2.733 3.033 2.717 2.916 2.75 2.816 C 2.783 2.7 2.867 2.6 3 2.516 C 3.933 2.016 4.908 1.633 5.925 1.366 C 6.942 1.1 7.975 0.966 9.025 0.966 C 10.092 0.966 11.133 1.1 12.15 1.366 C 13.167 1.616 14.15 1.983 15.1 2.466 C 15.25 2.55 15.333 2.65 15.35 2.766 C 15.383 2.883 15.375 3 15.325 3.116 C 15.275 3.233 15.192 3.325 15.075 3.391 C 14.958 3.458 14.817 3.45 14.65 3.366 C 13.767 2.916 12.85 2.575 11.9 2.341 C 10.967 2.091 10.008 1.966 9.025 1.966 C 8.058 1.966 7.108 2.083 6.175 2.316 C 5.242 2.533 4.35 2.883 3.5 3.366 Z M 6.45 20.566 C 5.467 19.533 4.708 18.483 4.175 17.416 C 3.658 16.333 3.4 15.058 3.4 13.591 C 3.4 12.075 3.95 10.8 5.05 9.766 C 6.15 8.716 7.475 8.191 9.025 8.191 C 10.575 8.191 11.908 8.716 13.025 9.766 C 14.142 10.8 14.7 12.075 14.7 13.591 C 14.7 13.741 14.65 13.866 14.55 13.966 C 14.467 14.05 14.35 14.091 14.2 14.091 C 14.067 14.091 13.95 14.05 13.85 13.966 C 13.75 13.866 13.7 13.741 13.7 13.591 C 13.7 12.341 13.233 11.3 12.3 10.466 C 11.383 9.616 10.292 9.191 9.025 9.191 C 7.758 9.191 6.667 9.616 5.75 10.466 C 4.85 11.3 4.4 12.341 4.4 13.591 C 4.4 14.941 4.633 16.091 5.1 17.041 C 5.567 17.975 6.25 18.916 7.15 19.866 C 7.25 19.966 7.3 20.083 7.3 20.216 C 7.3 20.35 7.25 20.466 7.15 20.566 C 7.05 20.666 6.933 20.716 6.8 20.716 C 6.667 20.716 6.55 20.666 6.45 20.566 Z M 14 18.866 C 12.517 18.866 11.225 18.366 10.125 17.366 C 9.042 16.366 8.5 15.108 8.5 13.591 C 8.5 13.458 8.542 13.341 8.625 13.241 C 8.725 13.141 8.85 13.091 9 13.091 C 9.15 13.091 9.267 13.141 9.35 13.241 C 9.45 13.341 9.5 13.458 9.5 13.591 C 9.5 14.841 9.95 15.866 10.85 16.666 C 11.75 17.466 12.8 17.866 14 17.866 C 14.1 17.866 14.242 17.858 14.425 17.841 C 14.608 17.825 14.8 17.8 15 17.766 C 15.15 17.733 15.275 17.758 15.375 17.841 C 15.492 17.908 15.567 18.016 15.6 18.166 C 15.633 18.3 15.608 18.416 15.525 18.516 C 15.442 18.616 15.333 18.683 15.2 18.716 C 14.9 18.8 14.633 18.85 14.4 18.866 L 14 18.866 Z"
+ android:fillColor="#5e6144"
+ android:strokeWidth="1"/>
+</vector>
\ No newline at end of file
diff --git a/packages/CredentialManager/res/drawable/ic_passkeys_onboarding_password.xml b/packages/CredentialManager/res/drawable/ic_passkeys_onboarding_password.xml
new file mode 100644
index 0000000..61800f1
--- /dev/null
+++ b/packages/CredentialManager/res/drawable/ic_passkeys_onboarding_password.xml
@@ -0,0 +1,31 @@
+<!--
+ ~ Copyright (C) 2022 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<vector
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ tools:ignore="VectorPath"
+ android:name="vector"
+ android:width="20dp"
+ android:height="17dp"
+ android:viewportWidth="20"
+ android:viewportHeight="17">
+ <path
+ android:name="path"
+ android:pathData="M 2 16.941 C 1.45 16.941 0.975 16.75 0.575 16.366 C 0.192 15.966 0 15.491 0 14.941 L 0 2.941 C 0 2.391 0.192 1.925 0.575 1.541 C 0.975 1.141 1.45 0.941 2 0.941 L 18 0.941 C 18.55 0.941 19.017 1.141 19.4 1.541 C 19.8 1.925 20 2.391 20 2.941 L 20 14.941 C 20 15.491 19.8 15.966 19.4 16.366 C 19.017 16.75 18.55 16.941 18 16.941 L 2 16.941 Z M 4.5 11.941 L 5.65 11.941 L 5.65 5.941 L 4.75 5.941 L 3 7.191 L 3.6 8.091 L 4.5 7.441 L 4.5 11.941 Z M 7.6 11.941 L 11.5 11.941 L 11.5 10.941 L 9.15 10.941 L 9.1 10.891 C 9.45 10.558 9.733 10.275 9.95 10.041 C 10.183 9.808 10.367 9.625 10.5 9.491 C 10.8 9.191 11.025 8.891 11.175 8.591 C 11.325 8.291 11.4 7.975 11.4 7.641 C 11.4 7.158 11.217 6.758 10.85 6.441 C 10.483 6.108 10.017 5.941 9.45 5.941 C 9.017 5.941 8.625 6.066 8.275 6.316 C 7.925 6.566 7.683 6.891 7.55 7.291 L 8.55 7.691 C 8.633 7.475 8.75 7.308 8.9 7.191 C 9.067 7.058 9.25 6.991 9.45 6.991 C 9.7 6.991 9.9 7.058 10.05 7.191 C 10.217 7.325 10.3 7.491 10.3 7.691 C 10.3 7.875 10.267 8.05 10.2 8.216 C 10.133 8.366 9.983 8.558 9.75 8.791 L 8.95 9.591 C 8.6 9.941 8.15 10.391 7.6 10.941 L 7.6 11.941 Z M 15 11.941 C 15.6 11.941 16.083 11.775 16.45 11.441 C 16.817 11.108 17 10.675 17 10.141 C 17 9.841 16.917 9.575 16.75 9.341 C 16.583 9.108 16.35 8.925 16.05 8.791 L 16.05 8.741 C 16.283 8.608 16.467 8.441 16.6 8.241 C 16.733 8.025 16.8 7.775 16.8 7.491 C 16.8 7.041 16.625 6.675 16.275 6.391 C 15.925 6.091 15.483 5.941 14.95 5.941 C 14.533 5.941 14.142 6.066 13.775 6.316 C 13.425 6.55 13.2 6.841 13.1 7.191 L 14.1 7.591 C 14.167 7.391 14.275 7.233 14.425 7.116 C 14.575 7 14.75 6.941 14.95 6.941 C 15.167 6.941 15.342 7.008 15.475 7.141 C 15.625 7.258 15.7 7.408 15.7 7.591 C 15.7 7.825 15.617 8.008 15.45 8.141 C 15.283 8.275 15.067 8.341 14.8 8.341 L 14.35 8.341 L 14.35 9.341 L 14.85 9.341 C 15.183 9.341 15.442 9.408 15.625 9.541 C 15.808 9.675 15.9 9.858 15.9 10.091 C 15.9 10.308 15.808 10.5 15.625 10.666 C 15.442 10.816 15.233 10.891 15 10.891 C 14.717 10.891 14.5 10.833 14.35 10.716 C 14.2 10.583 14.067 10.358 13.95 10.041 L 12.95 10.441 C 13.067 10.925 13.3 11.3 13.65 11.566 C 14.017 11.816 14.467 11.941 15 11.941 Z M 2 14.941 L 18 14.941 L 18 2.941 L 2 2.941 L 2 14.941 Z M 2 14.941 L 2 2.941 L 2 14.941 Z"
+ android:fillColor="#5e6144"
+ android:strokeWidth="1"/>
+</vector>
\ No newline at end of file
diff --git a/packages/CredentialManager/res/values/strings.xml b/packages/CredentialManager/res/values/strings.xml
index 8c9023c..870d983 100644
--- a/packages/CredentialManager/res/values/strings.xml
+++ b/packages/CredentialManager/res/values/strings.xml
@@ -18,9 +18,13 @@
<!-- This appears as a text button where users can click to save this credential to another device. [CHAR LIMIT=80] -->
<string name="string_save_to_another_device">Save to another device</string>
<!-- This appears as the title of the modal bottom sheet introducing what is passkey to users. [CHAR LIMIT=200] -->
- <string name="passkey_creation_intro_title">A simple way to sign in safely</string>
- <!-- This appears as the description body of the modal bottom sheet introducing what is passkey to users. [CHAR LIMIT=200] -->
- <string name="passkey_creation_intro_body">Use your fingerprint, face or screen lock to sign in with a unique passkey that can’t be forgotten or stolen. Learn more</string>
+ <string name="passkey_creation_intro_title">Safer with passkeys</string>
+ <!-- This appears as the description body of the modal bottom sheet introducing why passkey beneficial on the passwords side. [CHAR LIMIT=200] -->
+ <string name="passkey_creation_intro_body_password">No need to create or remember complex passwords</string>
+ <!-- This appears as the description body of the modal bottom sheet introducing why passkey beneficial on the safety side. [CHAR LIMIT=200] -->
+ <string name="passkey_creation_intro_body_fingerprint">Use your fingerprint, face, or screen lock to create a unique passkey</string>
+ <!-- This appears as the description body of the modal bottom sheet introducing why passkey beneficial on the using other devices side. [CHAR LIMIT=200] -->
+ <string name="passkey_creation_intro_body_device">Passkeys are saved to a password manager, so you can sign in on other devices</string>
<!-- This appears as the title of the modal bottom sheet which provides all available providers for users to choose. [CHAR LIMIT=200] -->
<string name="choose_provider_title">Choose where to <xliff:g id="createTypes" example="create your passkeys">%1$s</xliff:g></string>
<!-- Create types which are inserted as a placeholder for string choose_provider_title. [CHAR LIMIT=200] -->
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt b/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt
index 1db1d1c..2780c3c 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")
@@ -517,38 +525,4 @@
"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 57e20be..5d22bfd 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialComponents.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialComponents.kt
@@ -140,12 +140,11 @@
) {
ContainerCard() {
Column() {
- Icon(
- painter = painterResource(R.drawable.ic_passkey),
+ Image(
+ painter = painterResource(R.drawable.ic_passkeys_onboarding),
contentDescription = null,
- tint = LocalAndroidColorScheme.current.colorAccentPrimaryVariant,
modifier = Modifier.align(alignment = Alignment.CenterHorizontally)
- .padding(top = 24.dp, bottom = 12.dp)
+ .padding(top = 24.dp, bottom = 12.dp).size(316.dp, 168.dp)
)
TextOnSurface(
text = stringResource(R.string.passkey_creation_intro_title),
@@ -159,11 +158,62 @@
thickness = 16.dp,
color = Color.Transparent
)
- TextSecondary(
- text = stringResource(R.string.passkey_creation_intro_body),
- style = MaterialTheme.typography.bodyLarge,
- modifier = Modifier.padding(horizontal = 28.dp),
+ Row(
+ horizontalArrangement = Arrangement.SpaceBetween,
+ verticalAlignment = Alignment.CenterVertically,
+ modifier = Modifier.fillMaxWidth().padding(horizontal = 24.dp)
+ ) {
+ Image(
+ modifier = Modifier.size(24.dp),
+ painter = painterResource(R.drawable.ic_passkeys_onboarding_password),
+ contentDescription = null
+ )
+ TextSecondary(
+ text = stringResource(R.string.passkey_creation_intro_body_password),
+ style = MaterialTheme.typography.bodyMedium,
+ modifier = Modifier.padding(start = 16.dp),
+ )
+ }
+ Divider(
+ thickness = 16.dp,
+ color = Color.Transparent
)
+ Row(
+ horizontalArrangement = Arrangement.SpaceBetween,
+ verticalAlignment = Alignment.CenterVertically,
+ modifier = Modifier.fillMaxWidth().padding(horizontal = 24.dp)
+ ) {
+ Image(
+ modifier = Modifier.size(24.dp),
+ painter = painterResource(R.drawable.ic_passkeys_onboarding_fingerprint),
+ contentDescription = null
+ )
+ TextSecondary(
+ text = stringResource(R.string.passkey_creation_intro_body_fingerprint),
+ style = MaterialTheme.typography.bodyMedium,
+ modifier = Modifier.padding(start = 16.dp),
+ )
+ }
+ Divider(
+ thickness = 16.dp,
+ color = Color.Transparent
+ )
+ Row(
+ horizontalArrangement = Arrangement.SpaceBetween,
+ verticalAlignment = Alignment.CenterVertically,
+ modifier = Modifier.fillMaxWidth().padding(horizontal = 24.dp)
+ ) {
+ Image(
+ modifier = Modifier.size(24.dp),
+ painter = painterResource(R.drawable.ic_passkeys_onboarding_device),
+ contentDescription = null
+ )
+ TextSecondary(
+ text = stringResource(R.string.passkey_creation_intro_body_device),
+ style = MaterialTheme.typography.bodyMedium,
+ modifier = Modifier.padding(start = 16.dp),
+ )
+ }
Divider(
thickness = 32.dp,
color = Color.Transparent
@@ -698,7 +748,7 @@
},
contentDescription = null,
tint = LocalAndroidColorScheme.current.colorAccentPrimaryVariant,
- modifier = Modifier.padding(start = 18.dp).size(32.dp)
+ modifier = Modifier.padding(horizontal = 18.dp).size(32.dp)
)
},
label = {
@@ -760,7 +810,7 @@
onClick = onOptionSelected,
icon = {
Image(
- modifier = Modifier.size(32.dp).padding(start = 16.dp),
+ modifier = Modifier.padding(horizontal = 16.dp).size(32.dp),
bitmap = providerInfo.icon.toBitmap().asImageBitmap(),
contentDescription = null
)
@@ -770,13 +820,12 @@
TextOnSurfaceVariant(
text = providerInfo.displayName,
style = MaterialTheme.typography.titleLarge,
- modifier = Modifier.padding(top = 16.dp, start = 16.dp),
+ modifier = Modifier.padding(top = 16.dp),
)
if (createOptionInfo.userProviderDisplayName != null) {
TextSecondary(
text = createOptionInfo.userProviderDisplayName,
style = MaterialTheme.typography.bodyMedium,
- modifier = Modifier.padding(start = 16.dp),
)
}
if (createOptionInfo.passwordCount != null &&
@@ -790,7 +839,7 @@
createOptionInfo.passkeyCount
),
style = MaterialTheme.typography.bodyMedium,
- modifier = Modifier.padding(bottom = 16.dp, start = 16.dp),
+ modifier = Modifier.padding(bottom = 16.dp),
)
} else if (createOptionInfo.passwordCount != null) {
TextSecondary(
@@ -800,7 +849,7 @@
createOptionInfo.passwordCount
),
style = MaterialTheme.typography.bodyMedium,
- modifier = Modifier.padding(bottom = 16.dp, start = 16.dp),
+ modifier = Modifier.padding(bottom = 16.dp),
)
} else if (createOptionInfo.passkeyCount != null) {
TextSecondary(
@@ -810,7 +859,7 @@
createOptionInfo.passkeyCount
),
style = MaterialTheme.typography.bodyMedium,
- modifier = Modifier.padding(bottom = 16.dp, start = 16.dp),
+ modifier = Modifier.padding(bottom = 16.dp),
)
} else if (createOptionInfo.totalCredentialCount != null) {
// TODO: Handle the case when there is total count
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialViewModel.kt b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialViewModel.kt
index 6f74998..3d23f5d 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
@@ -60,24 +61,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 {
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/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/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/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/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml
index 7db68b0..a8216e8 100644
--- a/packages/SystemUI/AndroidManifest.xml
+++ b/packages/SystemUI/AndroidManifest.xml
@@ -914,6 +914,29 @@
<service android:name=".controls.controller.AuxiliaryPersistenceWrapper$DeletionJobService"
android:permission="android.permission.BIND_JOB_SERVICE"/>
+ <!-- region Note Task -->
+ <activity
+ android:name=".notetask.shortcut.CreateNoteTaskShortcutActivity"
+ android:enabled="false"
+ android:exported="true"
+ android:excludeFromRecents="true"
+ android:theme="@android:style/Theme.NoDisplay"
+ android:label="@string/note_task_button_label"
+ android:icon="@drawable/ic_note_task_button">
+
+ <intent-filter>
+ <action android:name="android.intent.action.CREATE_SHORTCUT" />
+ <category android:name="android.intent.category.DEFAULT" />
+ </intent-filter>
+ </activity>
+
+ <activity
+ android:name=".notetask.shortcut.LaunchNoteTaskActivity"
+ android:exported="true"
+ android:excludeFromRecents="true"
+ android:theme="@android:style/Theme.NoDisplay" />
+ <!-- endregion -->
+
<!-- started from ControlsRequestReceiver -->
<activity
android:name=".controls.management.ControlsRequestDialog"
diff --git a/packages/SystemUI/res/drawable/ic_note_task_button.xml b/packages/SystemUI/res/drawable/ic_note_task_button.xml
new file mode 100644
index 0000000..bb5e224
--- /dev/null
+++ b/packages/SystemUI/res/drawable/ic_note_task_button.xml
@@ -0,0 +1,28 @@
+<?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.
+ -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportHeight="24"
+ android:viewportWidth="24">
+ <path
+ android:fillColor="#636C6F"
+ android:pathData="M17.6258,4.96L19.0358,6.37L7.4058,18.01L5.9958,16.6L17.6258,4.96ZM16.1358,3.62L4.1258,15.63L3.0158,19.83C2.9058,20.45 3.3858,21 3.9958,21C4.0558,21 4.1058,21 4.1658,20.99L8.3658,19.88L20.3758,7.86C20.7758,7.46 20.9958,6.93 20.9958,6.37C20.9958,5.81 20.7758,5.28 20.3758,4.88L19.1058,3.61C18.7158,3.22 18.1858,3 17.6258,3C17.0658,3 16.5358,3.22 16.1358,3.62Z" />
+ <path
+ android:fillColor="#636C6F"
+ android:fillType="evenOdd"
+ android:pathData="M20.1936,15.3369C20.3748,16.3837 19.9151,17.5414 18.8846,18.7597C19.1546,18.872 19.4576,18.9452 19.7724,18.9867C20.0839,19.0278 20.3683,19.0325 20.5749,19.0266C20.6772,19.0236 20.7578,19.0181 20.8101,19.0138C20.8362,19.0116 20.855,19.0097 20.8657,19.0085L20.8754,19.0074L20.875,19.0075C21.4217,18.9385 21.9214,19.325 21.9918,19.8718C22.0624,20.4195 21.6756,20.9208 21.1279,20.9914L21,19.9996C21.1279,20.9914 21.1265,20.9916 21.1265,20.9916L21.1249,20.9918L21.1211,20.9923L21.1107,20.9935L21.0795,20.997C21.0542,20.9998 21.0199,21.0032 20.9775,21.0067C20.8929,21.0138 20.7753,21.0216 20.6323,21.0257C20.3481,21.0339 19.9533,21.0279 19.5109,20.9695C18.873,20.8854 18.0393,20.6793 17.3106,20.1662C16.9605,20.3559 16.5876,20.4952 16.2299,20.6003C15.5742,20.7927 14.8754,20.8968 14.2534,20.9534C13.6801,21.0055 13.4553,21.0037 13.1015,21.0008C13.0689,21.0005 13.0352,21.0002 13,21H12.8594C12.8214,21.0002 12.785,21.0006 12.7504,21.0009C12.6524,21.0019 12.5683,21.0027 12.5,21H12.0562C12.0277,21.0003 12.0054,21.0006 11.9926,21.001L11.9751,21H9L11,19H11.9795C11.9929,18.9997 12.0064,18.9997 12.0199,19H12.4117C12.4534,18.9996 12.4864,18.9995 12.5,19H12.9675C12.977,18.9999 12.9878,18.9999 13,19C13.0446,19.0003 13.0859,19.0007 13.1249,19.0011C13.4259,19.0038 13.591,19.0054 14.0723,18.9616C14.6201,18.9118 15.1795,18.8242 15.6665,18.6813C15.753,18.6559 15.8346,18.6295 15.9114,18.6022C15.0315,17.2981 14.7125,16.1044 15.015,15.0829C15.4095,13.7511 16.6784,13.2418 17.7026,13.2864C18.7262,13.3309 19.954,13.9529 20.1936,15.3369ZM16.9327,15.6508C16.873,15.8523 16.8651,16.3878 17.4697,17.334C18.2007,16.4284 18.2585,15.8839 18.2229,15.6781C18.1939,15.5108 18.0297,15.3025 17.6157,15.2845C17.2025,15.2665 16.9885,15.4626 16.9327,15.6508Z" />
+</vector>
diff --git a/packages/SystemUI/res/layout/media_session_view.xml b/packages/SystemUI/res/layout/media_session_view.xml
index 530db0d..13c9a5e 100644
--- a/packages/SystemUI/res/layout/media_session_view.xml
+++ b/packages/SystemUI/res/layout/media_session_view.xml
@@ -35,7 +35,6 @@
android:layout_height="@dimen/qs_media_session_height_expanded"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
android:translationZ="0dp"
android:scaleType="centerCrop"
diff --git a/packages/SystemUI/res/values/flags.xml b/packages/SystemUI/res/values/flags.xml
index 49dd574..fd2e324 100644
--- a/packages/SystemUI/res/values/flags.xml
+++ b/packages/SystemUI/res/values/flags.xml
@@ -36,4 +36,8 @@
avatar will no longer show on the lockscreen -->
<bool name="flag_user_switcher_chip">false</bool>
+ <!-- Whether the battery icon is allowed to display a shield when battery life is being
+ protected. -->
+ <bool name="flag_battery_shield_icon">false</bool>
+
</resources>
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index a4e9983..9ee918d 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -2773,6 +2773,10 @@
<xliff:g id="weather_condition" example="Partly cloudy">%1$s</xliff:g>, <xliff:g id="temperature" example="7°C">%2$s</xliff:g>
</string>
+ <!-- TODO(b/259369672): Replace with final resource. -->
+ <!-- [CHAR LIMIT=30] Label used to open Note Task -->
+ <string name="note_task_button_label">Notetaking</string>
+
<!-- [CHAR LIMIT=NONE] Le audio broadcast dialog, media app is broadcasting -->
<string name="broadcasting_description_is_broadcasting">Broadcasting</string>
<!-- [CHAR LIMIT=NONE] Le audio broadcast dialog, title -->
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPasswordView.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPasswordView.java
index 2cc5ccdc..c985fd7 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardPasswordView.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPasswordView.java
@@ -34,6 +34,7 @@
import android.graphics.Rect;
import android.os.Trace;
import android.util.AttributeSet;
+import android.view.WindowInsets;
import android.view.WindowInsetsAnimationControlListener;
import android.view.WindowInsetsAnimationController;
import android.view.animation.AnimationUtils;
@@ -236,4 +237,50 @@
return getResources().getString(
com.android.internal.R.string.keyguard_accessibility_password_unlock);
}
+
+ @Override
+ public WindowInsets onApplyWindowInsets(WindowInsets insets) {
+ if (!mPasswordEntry.isFocused() && isVisibleToUser()) {
+ mPasswordEntry.requestFocus();
+ }
+ return super.onApplyWindowInsets(insets);
+ }
+
+ @Override
+ public void onWindowFocusChanged(boolean hasWindowFocus) {
+ super.onWindowFocusChanged(hasWindowFocus);
+ if (hasWindowFocus) {
+ if (isVisibleToUser()) {
+ showKeyboard();
+ } else {
+ hideKeyboard();
+ }
+ }
+ }
+
+ /**
+ * Sends signal to the focused window to show the keyboard.
+ */
+ public void showKeyboard() {
+ post(() -> {
+ if (mPasswordEntry.isAttachedToWindow()
+ && !mPasswordEntry.getRootWindowInsets().isVisible(WindowInsets.Type.ime())) {
+ mPasswordEntry.requestFocus();
+ mPasswordEntry.getWindowInsetsController().show(WindowInsets.Type.ime());
+ }
+ });
+ }
+
+ /**
+ * Sends signal to the focused window to hide the keyboard.
+ */
+ public void hideKeyboard() {
+ post(() -> {
+ if (mPasswordEntry.isAttachedToWindow()
+ && mPasswordEntry.getRootWindowInsets().isVisible(WindowInsets.Type.ime())) {
+ mPasswordEntry.clearFocus();
+ mPasswordEntry.getWindowInsetsController().hide(WindowInsets.Type.ime());
+ }
+ });
+ }
}
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPasswordViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPasswordViewController.java
index 195e8f9..d221e22 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardPasswordViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPasswordViewController.java
@@ -26,7 +26,6 @@
import android.view.KeyEvent;
import android.view.View;
import android.view.ViewGroup.MarginLayoutParams;
-import android.view.WindowInsets;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputMethodInfo;
import android.view.inputmethod.InputMethodManager;
@@ -200,12 +199,9 @@
return;
}
- mView.post(() -> {
- if (mView.isShown()) {
- mPasswordEntry.requestFocus();
- mPasswordEntry.getWindowInsetsController().show(WindowInsets.Type.ime());
- }
- });
+ if (mView.isShown()) {
+ mView.showKeyboard();
+ }
}
@Override
@@ -227,16 +223,12 @@
super.onPause();
});
}
- if (mPasswordEntry.isAttachedToWindow()) {
- mPasswordEntry.getWindowInsetsController().hide(WindowInsets.Type.ime());
- }
+ mView.hideKeyboard();
}
@Override
public void onStartingToHide() {
- if (mPasswordEntry.isAttachedToWindow()) {
- mPasswordEntry.getWindowInsetsController().hide(WindowInsets.Type.ime());
- }
+ mView.hideKeyboard();
}
private void updateSwitchImeButton() {
diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
index 8019b56..ae2e1d1 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
+++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
@@ -83,8 +83,7 @@
val SEMI_STABLE_SORT = unreleasedFlag(115, "semi_stable_sort", teamfood = true)
@JvmField
- val NOTIFICATION_GROUP_CORNER =
- unreleasedFlag(116, "notification_group_corner", teamfood = true)
+ val USE_ROUNDNESS_SOURCETYPES = unreleasedFlag(116, "use_roundness_sourcetype", teamfood = true)
// TODO(b/259217907)
@JvmField
@@ -227,7 +226,9 @@
val NEW_STATUS_BAR_WIFI_ICON_BACKEND = unreleasedFlag(609, "new_status_bar_wifi_icon_backend")
// TODO(b/256623670): Tracking Bug
- @JvmField val BATTERY_SHIELD_ICON = unreleasedFlag(610, "battery_shield_icon")
+ @JvmField
+ val BATTERY_SHIELD_ICON =
+ resourceBooleanFlag(610, R.bool.flag_battery_shield_icon, "battery_shield_icon")
// TODO(b/260881289): Tracking Bug
val NEW_STATUS_BAR_ICONS_DEBUG_COLORING =
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaControlPanel.java b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaControlPanel.java
index 827ac78..df8fb91 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaControlPanel.java
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaControlPanel.java
@@ -940,19 +940,9 @@
if (mIsSeekBarEnabled) {
return ConstraintSet.VISIBLE;
}
- // If disabled and "neighbours" are visible, set progress bar to INVISIBLE instead of GONE
- // so layout weights still work.
- return areAnyExpandedBottomActionsVisible() ? ConstraintSet.INVISIBLE : ConstraintSet.GONE;
- }
-
- private boolean areAnyExpandedBottomActionsVisible() {
- ConstraintSet expandedSet = mMediaViewController.getExpandedLayout();
- for (int id : MediaViewHolder.Companion.getExpandedBottomActionIds()) {
- if (expandedSet.getVisibility(id) == ConstraintSet.VISIBLE) {
- return true;
- }
- }
- return false;
+ // Set progress bar to INVISIBLE to keep the positions of text and buttons similar to the
+ // original positions when seekbar is enabled.
+ return ConstraintSet.INVISIBLE;
}
private void setGenericButton(
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/MediaTttLogger.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttLogger.kt
index 120f7d6..b55bedd 100644
--- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttLogger.kt
@@ -58,6 +58,27 @@
)
}
+ /**
+ * Logs an invalid sender state transition error in trying to update to [desiredState].
+ *
+ * @param currentState the previous state of the chip.
+ * @param desiredState the new state of the chip.
+ */
+ fun logInvalidStateTransitionError(
+ currentState: String,
+ desiredState: String
+ ) {
+ buffer.log(
+ tag,
+ LogLevel.ERROR,
+ {
+ str1 = currentState
+ str2 = desiredState
+ },
+ { "Cannot display state=$str2 after state=$str1; invalid transition" }
+ )
+ }
+
/** Logs that we couldn't find information for [packageName]. */
fun logPackageNotFound(packageName: String) {
buffer.log(
diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/ChipStateSender.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/ChipStateSender.kt
index af7317c..1f27582 100644
--- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/ChipStateSender.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/ChipStateSender.kt
@@ -56,7 +56,12 @@
R.string.media_move_closer_to_start_cast,
transferStatus = TransferStatus.NOT_STARTED,
endItem = null,
- ),
+ ) {
+ override fun isValidNextState(nextState: ChipStateSender): Boolean {
+ return nextState == FAR_FROM_RECEIVER ||
+ nextState == TRANSFER_TO_RECEIVER_TRIGGERED
+ }
+ },
/**
* A state representing that the two devices are close but not close enough to *end* a cast
@@ -70,7 +75,12 @@
R.string.media_move_closer_to_end_cast,
transferStatus = TransferStatus.NOT_STARTED,
endItem = null,
- ),
+ ) {
+ override fun isValidNextState(nextState: ChipStateSender): Boolean {
+ return nextState == FAR_FROM_RECEIVER ||
+ nextState == TRANSFER_TO_THIS_DEVICE_TRIGGERED
+ }
+ },
/**
* A state representing that a transfer to the receiver device has been initiated (but not
@@ -83,7 +93,13 @@
transferStatus = TransferStatus.IN_PROGRESS,
endItem = SenderEndItem.Loading,
timeout = TRANSFER_TRIGGERED_TIMEOUT_MILLIS
- ),
+ ) {
+ override fun isValidNextState(nextState: ChipStateSender): Boolean {
+ return nextState == FAR_FROM_RECEIVER ||
+ nextState == TRANSFER_TO_RECEIVER_SUCCEEDED ||
+ nextState == TRANSFER_TO_RECEIVER_FAILED
+ }
+ },
/**
* A state representing that a transfer from the receiver device and back to this device (the
@@ -96,7 +112,13 @@
transferStatus = TransferStatus.IN_PROGRESS,
endItem = SenderEndItem.Loading,
timeout = TRANSFER_TRIGGERED_TIMEOUT_MILLIS
- ),
+ ) {
+ override fun isValidNextState(nextState: ChipStateSender): Boolean {
+ return nextState == FAR_FROM_RECEIVER ||
+ nextState == TRANSFER_TO_THIS_DEVICE_SUCCEEDED ||
+ nextState == TRANSFER_TO_THIS_DEVICE_FAILED
+ }
+ },
/**
* A state representing that a transfer to the receiver device has been successfully completed.
@@ -112,7 +134,13 @@
newState =
StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_THIS_DEVICE_TRIGGERED
),
- ),
+ ) {
+ override fun isValidNextState(nextState: ChipStateSender): Boolean {
+ return nextState == FAR_FROM_RECEIVER ||
+ nextState == ALMOST_CLOSE_TO_START_CAST ||
+ nextState == TRANSFER_TO_THIS_DEVICE_TRIGGERED
+ }
+ },
/**
* A state representing that a transfer back to this device has been successfully completed.
@@ -128,7 +156,13 @@
newState =
StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_RECEIVER_TRIGGERED
),
- ),
+ ) {
+ override fun isValidNextState(nextState: ChipStateSender): Boolean {
+ return nextState == FAR_FROM_RECEIVER ||
+ nextState == ALMOST_CLOSE_TO_END_CAST ||
+ nextState == TRANSFER_TO_RECEIVER_TRIGGERED
+ }
+ },
/** A state representing that a transfer to the receiver device has failed. */
TRANSFER_TO_RECEIVER_FAILED(
@@ -137,7 +171,13 @@
R.string.media_transfer_failed,
transferStatus = TransferStatus.FAILED,
endItem = SenderEndItem.Error,
- ),
+ ) {
+ override fun isValidNextState(nextState: ChipStateSender): Boolean {
+ return nextState == FAR_FROM_RECEIVER ||
+ nextState == ALMOST_CLOSE_TO_START_CAST ||
+ nextState == TRANSFER_TO_THIS_DEVICE_TRIGGERED
+ }
+ },
/** A state representing that a transfer back to this device has failed. */
TRANSFER_TO_THIS_DEVICE_FAILED(
@@ -146,7 +186,13 @@
R.string.media_transfer_failed,
transferStatus = TransferStatus.FAILED,
endItem = SenderEndItem.Error,
- ),
+ ) {
+ override fun isValidNextState(nextState: ChipStateSender): Boolean {
+ return nextState == FAR_FROM_RECEIVER ||
+ nextState == ALMOST_CLOSE_TO_END_CAST ||
+ nextState == TRANSFER_TO_RECEIVER_TRIGGERED
+ }
+ },
/** A state representing that this device is far away from any receiver device. */
FAR_FROM_RECEIVER(
@@ -162,6 +208,12 @@
throw IllegalArgumentException("FAR_FROM_RECEIVER should never be displayed, " +
"so its string should never be fetched")
}
+
+ override fun isValidNextState(nextState: ChipStateSender): Boolean {
+ return nextState == FAR_FROM_RECEIVER ||
+ nextState.transferStatus == TransferStatus.NOT_STARTED ||
+ nextState.transferStatus == TransferStatus.IN_PROGRESS
+ }
};
/**
@@ -175,6 +227,8 @@
return Text.Loaded(context.getString(stringResId!!, otherDeviceName))
}
+ abstract fun isValidNextState(nextState: ChipStateSender): Boolean
+
companion object {
/**
* Returns the sender state enum associated with the given [displayState] from
@@ -197,6 +251,31 @@
*/
@StatusBarManager.MediaTransferSenderState
fun getSenderStateIdFromName(name: String): Int = valueOf(name).stateInt
+
+ /**
+ * Validates the transition from a chip state to another.
+ *
+ * @param currentState is the current state of the chip.
+ * @param desiredState is the desired state of the chip.
+ * @return true if the transition from [currentState] to [desiredState] is valid, and false
+ * otherwise.
+ */
+ fun isValidStateTransition(
+ currentState: ChipStateSender?,
+ desiredState: ChipStateSender,
+ ): Boolean {
+ // Far from receiver is the default state.
+ if (currentState == null) {
+ return FAR_FROM_RECEIVER.isValidNextState(desiredState)
+ }
+
+ // No change in state is valid.
+ if (currentState == desiredState) {
+ return true
+ }
+
+ return currentState.isValidNextState(desiredState)
+ }
}
}
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 bb7bc6f..e34b0cb 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
@@ -52,6 +52,8 @@
) : CoreStartable {
private var displayedState: ChipStateSender? = null
+ // A map to store current chip state per id.
+ private var stateMap: MutableMap<String, ChipStateSender> = mutableMapOf()
private val commandQueueCallbacks =
object : CommandQueue.Callbacks {
@@ -87,9 +89,22 @@
logger.logStateChangeError(displayState)
return
}
+
+ val currentState = stateMap[routeInfo.id]
+ if (!ChipStateSender.isValidStateTransition(currentState, chipState)) {
+ // ChipStateSender.FAR_FROM_RECEIVER is the default state when there is no state.
+ logger.logInvalidStateTransitionError(
+ currentState = currentState?.name ?: ChipStateSender.FAR_FROM_RECEIVER.name,
+ chipState.name
+ )
+ return
+ }
uiEventLogger.logSenderStateChange(chipState)
+ stateMap.put(routeInfo.id, chipState)
if (chipState == ChipStateSender.FAR_FROM_RECEIVER) {
+ // No need to store the state since it is the default state
+ stateMap.remove(routeInfo.id)
// Return early if we're not displaying a chip anyway
val currentDisplayedState = displayedState ?: return
@@ -119,7 +134,7 @@
context,
logger,
)
- )
+ ) { stateMap.remove(routeInfo.id) }
}
}
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/notetask/NoteTaskController.kt b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt
index b964b76..6dd60d0 100644
--- a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt
+++ b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt
@@ -17,10 +17,12 @@
package com.android.systemui.notetask
import android.app.KeyguardManager
+import android.content.ComponentName
import android.content.Context
+import android.content.pm.PackageManager
import android.os.UserManager
-import android.view.KeyEvent
import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.notetask.shortcut.CreateNoteTaskShortcutActivity
import com.android.systemui.util.kotlin.getOrNull
import com.android.wm.shell.bubbles.Bubbles
import java.util.Optional
@@ -45,15 +47,22 @@
@NoteTaskEnabledKey private val isEnabled: Boolean,
) {
- fun handleSystemKey(keyCode: Int) {
+ /**
+ * Shows a note task. How the task is shown will depend on when the method is invoked.
+ *
+ * If in multi-window mode, notes will open as a full screen experience. That is particularly
+ * important for Large screen devices. These devices may support a taskbar that let users to
+ * drag and drop a shortcut into multi-window mode, and notes should comply with this behaviour.
+ *
+ * If the keyguard is locked, notes will open as a full screen experience. A locked device has
+ * no contextual information which let us use the whole screen space available.
+ *
+ * If no in multi-window or the keyguard is unlocked, notes will open as a floating experience.
+ * That will let users open other apps in full screen, and take contextual notes.
+ */
+ fun showNoteTask(isInMultiWindowMode: Boolean = false) {
if (!isEnabled) return
- if (keyCode == KeyEvent.KEYCODE_VIDEO_APP_1) {
- showNoteTask()
- }
- }
-
- private fun showNoteTask() {
val bubbles = optionalBubbles.getOrNull() ?: return
val keyguardManager = optionalKeyguardManager.getOrNull() ?: return
val userManager = optionalUserManager.getOrNull() ?: return
@@ -62,11 +71,35 @@
// TODO(b/249954038): We should handle direct boot (isUserUnlocked). For now, we do nothing.
if (!userManager.isUserUnlocked) return
- if (keyguardManager.isKeyguardLocked) {
+ if (isInMultiWindowMode || keyguardManager.isKeyguardLocked) {
context.startActivity(intent)
} else {
// TODO(b/254606432): Should include Intent.EXTRA_FLOATING_WINDOW_MODE parameter.
bubbles.showAppBubble(intent)
}
}
+
+ /**
+ * Set `android:enabled` property in the `AndroidManifest` associated with the Shortcut
+ * component to [value].
+ *
+ * If the shortcut entry `android:enabled` is set to `true`, the shortcut will be visible in the
+ * Widget Picker to all users.
+ */
+ fun setNoteTaskShortcutEnabled(value: Boolean) {
+ val componentName = ComponentName(context, CreateNoteTaskShortcutActivity::class.java)
+
+ val enabledState =
+ if (value) {
+ PackageManager.COMPONENT_ENABLED_STATE_ENABLED
+ } else {
+ PackageManager.COMPONENT_ENABLED_STATE_DISABLED
+ }
+
+ context.packageManager.setComponentEnabledSetting(
+ componentName,
+ enabledState,
+ PackageManager.DONT_KILL_APP,
+ )
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskInitializer.kt b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskInitializer.kt
index 0a5b600..d14b7a7 100644
--- a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskInitializer.kt
+++ b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskInitializer.kt
@@ -16,9 +16,10 @@
package com.android.systemui.notetask
+import android.view.KeyEvent
+import androidx.annotation.VisibleForTesting
import com.android.systemui.statusbar.CommandQueue
import com.android.wm.shell.bubbles.Bubbles
-import dagger.Lazy
import java.util.Optional
import javax.inject.Inject
@@ -27,15 +28,18 @@
@Inject
constructor(
private val optionalBubbles: Optional<Bubbles>,
- private val lazyNoteTaskController: Lazy<NoteTaskController>,
+ private val noteTaskController: NoteTaskController,
private val commandQueue: CommandQueue,
@NoteTaskEnabledKey private val isEnabled: Boolean,
) {
- private val callbacks =
+ @VisibleForTesting
+ val callbacks =
object : CommandQueue.Callbacks {
override fun handleSystemKey(keyCode: Int) {
- lazyNoteTaskController.get().handleSystemKey(keyCode)
+ if (keyCode == KeyEvent.KEYCODE_VIDEO_APP_1) {
+ noteTaskController.showNoteTask()
+ }
}
}
@@ -43,5 +47,6 @@
if (isEnabled && optionalBubbles.isPresent) {
commandQueue.addCallback(callbacks)
}
+ noteTaskController.setNoteTaskShortcutEnabled(isEnabled)
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskModule.kt b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskModule.kt
index 035396a..8bdf319 100644
--- a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskModule.kt
@@ -16,32 +16,47 @@
package com.android.systemui.notetask
+import android.app.Activity
import android.app.KeyguardManager
import android.content.Context
import android.os.UserManager
import androidx.core.content.getSystemService
import com.android.systemui.flags.FeatureFlags
import com.android.systemui.flags.Flags
+import com.android.systemui.notetask.shortcut.CreateNoteTaskShortcutActivity
+import com.android.systemui.notetask.shortcut.LaunchNoteTaskActivity
+import dagger.Binds
import dagger.Module
import dagger.Provides
-import java.util.*
+import dagger.multibindings.ClassKey
+import dagger.multibindings.IntoMap
+import java.util.Optional
/** Compose all dependencies required by Note Task feature. */
@Module
-internal class NoteTaskModule {
+internal interface NoteTaskModule {
- @[Provides NoteTaskEnabledKey]
- fun provideIsNoteTaskEnabled(featureFlags: FeatureFlags): Boolean {
- return featureFlags.isEnabled(Flags.NOTE_TASKS)
- }
+ @[Binds IntoMap ClassKey(LaunchNoteTaskActivity::class)]
+ fun bindNoteTaskLauncherActivity(activity: LaunchNoteTaskActivity): Activity?
- @Provides
- fun provideOptionalKeyguardManager(context: Context): Optional<KeyguardManager> {
- return Optional.ofNullable(context.getSystemService())
- }
+ @[Binds IntoMap ClassKey(CreateNoteTaskShortcutActivity::class)]
+ fun bindNoteTaskShortcutActivity(activity: CreateNoteTaskShortcutActivity): Activity?
- @Provides
- fun provideOptionalUserManager(context: Context): Optional<UserManager> {
- return Optional.ofNullable(context.getSystemService())
+ companion object {
+
+ @[Provides NoteTaskEnabledKey]
+ fun provideIsNoteTaskEnabled(featureFlags: FeatureFlags): Boolean {
+ return featureFlags.isEnabled(Flags.NOTE_TASKS)
+ }
+
+ @Provides
+ fun provideOptionalKeyguardManager(context: Context): Optional<KeyguardManager> {
+ return Optional.ofNullable(context.getSystemService())
+ }
+
+ @Provides
+ fun provideOptionalUserManager(context: Context): Optional<UserManager> {
+ return Optional.ofNullable(context.getSystemService())
+ }
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/notetask/shortcut/CreateNoteTaskShortcutActivity.kt b/packages/SystemUI/src/com/android/systemui/notetask/shortcut/CreateNoteTaskShortcutActivity.kt
new file mode 100644
index 0000000..f6a623e
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/notetask/shortcut/CreateNoteTaskShortcutActivity.kt
@@ -0,0 +1,79 @@
+/*
+ * 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.notetask.shortcut
+
+import android.app.Activity
+import android.content.Intent
+import android.os.Bundle
+import androidx.activity.ComponentActivity
+import androidx.annotation.DrawableRes
+import androidx.core.content.pm.ShortcutInfoCompat
+import androidx.core.content.pm.ShortcutManagerCompat
+import androidx.core.graphics.drawable.IconCompat
+import com.android.systemui.R
+import javax.inject.Inject
+
+/**
+ * Activity responsible for create a shortcut for notes action. If the shortcut is enabled, a new
+ * shortcut will appear in the widget picker. If the shortcut is selected, the Activity here will be
+ * launched, creating a new shortcut for [CreateNoteTaskShortcutActivity], and will finish.
+ *
+ * @see <a
+ * href="https://developer.android.com/develop/ui/views/launch/shortcuts/creating-shortcuts#custom-pinned">Creating
+ * a custom shortcut activity</a>
+ */
+internal class CreateNoteTaskShortcutActivity @Inject constructor() : ComponentActivity() {
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ val intent =
+ createShortcutIntent(
+ id = SHORTCUT_ID,
+ shortLabel = getString(R.string.note_task_button_label),
+ intent = LaunchNoteTaskActivity.newIntent(context = this),
+ iconResource = R.drawable.ic_note_task_button,
+ )
+ setResult(Activity.RESULT_OK, intent)
+
+ finish()
+ }
+
+ private fun createShortcutIntent(
+ id: String,
+ shortLabel: String,
+ intent: Intent,
+ @DrawableRes iconResource: Int,
+ ): Intent {
+ val shortcutInfo =
+ ShortcutInfoCompat.Builder(this, id)
+ .setIntent(intent)
+ .setShortLabel(shortLabel)
+ .setLongLived(true)
+ .setIcon(IconCompat.createWithResource(this, iconResource))
+ .build()
+
+ return ShortcutManagerCompat.createShortcutResultIntent(
+ this,
+ shortcutInfo,
+ )
+ }
+
+ private companion object {
+ private const val SHORTCUT_ID = "note-task-shortcut-id"
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/notetask/shortcut/LaunchNoteTaskActivity.kt b/packages/SystemUI/src/com/android/systemui/notetask/shortcut/LaunchNoteTaskActivity.kt
new file mode 100644
index 0000000..47fe676
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/notetask/shortcut/LaunchNoteTaskActivity.kt
@@ -0,0 +1,53 @@
+/*
+ * 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.notetask.shortcut
+
+import android.content.Context
+import android.content.Intent
+import android.os.Bundle
+import androidx.activity.ComponentActivity
+import com.android.systemui.notetask.NoteTaskController
+import com.android.systemui.notetask.NoteTaskIntentResolver
+import javax.inject.Inject
+
+/** Activity responsible for launching the note experience, and finish. */
+internal class LaunchNoteTaskActivity
+@Inject
+constructor(
+ private val noteTaskController: NoteTaskController,
+) : ComponentActivity() {
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ noteTaskController.showNoteTask(isInMultiWindowMode)
+
+ finish()
+ }
+
+ companion object {
+
+ /** Creates a new [Intent] set to start [LaunchNoteTaskActivity]. */
+ fun newIntent(context: Context): Intent {
+ return Intent(context, LaunchNoteTaskActivity::class.java).apply {
+ // Intent's action must be set in shortcuts, or an exception will be thrown.
+ // TODO(b/254606432): Use Intent.ACTION_NOTES instead.
+ action = NoteTaskIntentResolver.NOTES_ACTION
+ }
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSSecurityFooterUtils.java b/packages/SystemUI/src/com/android/systemui/qs/QSSecurityFooterUtils.java
index 67bc769..5dbf0f8 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSSecurityFooterUtils.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSSecurityFooterUtils.java
@@ -247,7 +247,7 @@
Icon icon;
ContentDescription contentDescription = null;
- if (isParentalControlsEnabled) {
+ if (isParentalControlsEnabled && securityModel.getDeviceAdminIcon() != null) {
icon = new Icon.Loaded(securityModel.getDeviceAdminIcon(), contentDescription);
} else if (vpnName != null || vpnNameWorkProfile != null) {
if (securityModel.isVpnBranded()) {
@@ -476,7 +476,7 @@
@VisibleForTesting
View createDialogView(Context quickSettingsContext) {
if (mSecurityController.isParentalControlsEnabled()) {
- return createParentalControlsDialogView();
+ return createParentalControlsDialogView(quickSettingsContext);
}
return createOrganizationDialogView(quickSettingsContext);
}
@@ -579,8 +579,8 @@
return dialogView;
}
- private View createParentalControlsDialogView() {
- View dialogView = LayoutInflater.from(mContext)
+ private View createParentalControlsDialogView(Context quickSettingsContext) {
+ View dialogView = LayoutInflater.from(quickSettingsContext)
.inflate(R.layout.quick_settings_footer_dialog_parental_controls, null, false);
DeviceAdminInfo info = mSecurityController.getDeviceAdminInfo();
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 350d8b0..9542a02 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/InternetTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/InternetTile.java
@@ -188,8 +188,6 @@
int mWifiSignalIconId;
@Nullable
String mSsid;
- boolean mActivityIn;
- boolean mActivityOut;
@Nullable
String mWifiSignalContentDescription;
boolean mIsTransient;
@@ -207,8 +205,6 @@
.append(",mConnected=").append(mConnected)
.append(",mWifiSignalIconId=").append(mWifiSignalIconId)
.append(",mSsid=").append(mSsid)
- .append(",mActivityIn=").append(mActivityIn)
- .append(",mActivityOut=").append(mActivityOut)
.append(",mWifiSignalContentDescription=").append(mWifiSignalContentDescription)
.append(",mIsTransient=").append(mIsTransient)
.append(",mNoDefaultNetwork=").append(mNoDefaultNetwork)
@@ -226,8 +222,6 @@
CharSequence mDataContentDescription;
int mMobileSignalIconId;
int mQsTypeIcon;
- boolean mActivityIn;
- boolean mActivityOut;
boolean mNoSim;
boolean mRoaming;
boolean mMultipleSubs;
@@ -243,8 +237,6 @@
.append(",mDataContentDescription=").append(mDataContentDescription)
.append(",mMobileSignalIconId=").append(mMobileSignalIconId)
.append(",mQsTypeIcon=").append(mQsTypeIcon)
- .append(",mActivityIn=").append(mActivityIn)
- .append(",mActivityOut=").append(mActivityOut)
.append(",mNoSim=").append(mNoSim)
.append(",mRoaming=").append(mRoaming)
.append(",mMultipleSubs=").append(mMultipleSubs)
@@ -275,8 +267,6 @@
mWifiInfo.mWifiSignalContentDescription = indicators.qsIcon.contentDescription;
mWifiInfo.mEnabled = indicators.enabled;
mWifiInfo.mSsid = indicators.description;
- mWifiInfo.mActivityIn = indicators.activityIn;
- mWifiInfo.mActivityOut = indicators.activityOut;
mWifiInfo.mIsTransient = indicators.isTransient;
mWifiInfo.mStatusLabel = indicators.statusLabel;
refreshState(mWifiInfo);
@@ -297,8 +287,6 @@
? indicators.typeContentDescriptionHtml : null;
mCellularInfo.mMobileSignalIconId = indicators.qsIcon.icon;
mCellularInfo.mQsTypeIcon = indicators.qsType;
- mCellularInfo.mActivityIn = indicators.activityIn;
- mCellularInfo.mActivityOut = indicators.activityOut;
mCellularInfo.mRoaming = indicators.roaming;
mCellularInfo.mMultipleSubs = mController.getNumberSubscriptions() > 1;
refreshState(mCellularInfo);
@@ -428,8 +416,6 @@
state.state = Tile.STATE_ACTIVE;
state.dualTarget = true;
state.value = cb.mEnabled;
- state.activityIn = cb.mEnabled && cb.mActivityIn;
- state.activityOut = cb.mEnabled && cb.mActivityOut;
final StringBuffer minimalContentDescription = new StringBuffer();
final StringBuffer minimalStateDescription = new StringBuffer();
final Resources r = mContext.getResources();
@@ -503,8 +489,6 @@
boolean mobileDataEnabled = mDataController.isMobileDataSupported()
&& mDataController.isMobileDataEnabled();
state.value = mobileDataEnabled;
- state.activityIn = mobileDataEnabled && cb.mActivityIn;
- state.activityOut = mobileDataEnabled && cb.mActivityOut;
state.expandedAccessibilityClassName = Switch.class.getName();
if (cb.mAirplaneModeEnabled && cb.mQsTypeIcon != TelephonyIcons.ICON_CWF) {
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/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
index 3b40aeb..507dec6 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
@@ -465,7 +465,11 @@
private boolean mQsTouchAboveFalsingThreshold;
private int mQsFalsingThreshold;
- /** Indicates drag starting height when swiping down or up on heads-up notifications */
+ /**
+ * Indicates drag starting height when swiping down or up on heads-up notifications.
+ * This usually serves as a threshold from when shade expansion should really start. Otherwise
+ * this value would be height of shade and it will be immediately expanded to some extent.
+ */
private int mHeadsUpStartHeight;
private HeadsUpTouchHelper mHeadsUpTouchHelper;
private boolean mListenForHeadsUp;
@@ -3412,9 +3416,12 @@
&& mQsExpansionAnimator == null && !mQsExpansionFromOverscroll;
boolean goingBetweenClosedShadeAndExpandedQs =
mQsExpandImmediate || collapsingShadeFromExpandedQs;
- // we don't want to update QS expansion when HUN is visible because then the whole shade is
- // initially hidden, even though it has non-zero height
- if (goingBetweenClosedShadeAndExpandedQs && !mHeadsUpManager.isTrackingHeadsUp()) {
+ // in split shade we react when HUN is visible only if shade height is over HUN start
+ // height - which means user is swiping down. Otherwise shade QS will either not show at all
+ // with HUN movement or it will blink when touching HUN initially
+ boolean qsShouldExpandWithHeadsUp = !mSplitShadeEnabled
+ || (!mHeadsUpManager.isTrackingHeadsUp() || expandedHeight > mHeadsUpStartHeight);
+ if (goingBetweenClosedShadeAndExpandedQs && qsShouldExpandWithHeadsUp) {
float qsExpansionFraction;
if (mSplitShadeEnabled) {
qsExpansionFraction = 1;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java b/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java
index 1dd3a96..590a04a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java
@@ -164,6 +164,8 @@
private static final int MSG_UNREGISTER_NEARBY_MEDIA_DEVICE_PROVIDER = 67 << MSG_SHIFT;
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;
@@ -478,6 +480,16 @@
* @see IStatusBar#showRearDisplayDialog
*/
default void showRearDisplayDialog(int currentBaseState) {}
+
+ /**
+ * @see IStatusBar#goToFullscreenFromSplit
+ */
+ default void goToFullscreenFromSplit() {}
+
+ /**
+ * @see IStatusBar#enterStageSplitFromRunningApp
+ */
+ default void enterStageSplitFromRunningApp(boolean leftOrTop) {}
}
public CommandQueue(Context context) {
@@ -1239,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,
@@ -1299,6 +1319,11 @@
.sendToTarget();
}
+ @Override
+ public void goToFullscreenFromSplit() {
+ mHandler.obtainMessage(MSG_GO_TO_FULLSCREEN_FROM_SPLIT).sendToTarget();
+ }
+
private final class H extends Handler {
private H(Looper l) {
super(l);
@@ -1738,6 +1763,17 @@
for (int i = 0; i < mCallbacks.size(); i++) {
mCallbacks.get(i).showRearDisplayDialog((Integer) msg.obj);
}
+ break;
+ case MSG_GO_TO_FULLSCREEN_FROM_SPLIT:
+ for (int i = 0; i < mCallbacks.size(); i++) {
+ 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/NotificationShelf.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java
index d7eddf5..56c34a0 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java
@@ -39,6 +39,7 @@
import com.android.systemui.animation.Interpolators;
import com.android.systemui.animation.ShadeInterpolation;
import com.android.systemui.plugins.statusbar.StatusBarStateController.StateListener;
+import com.android.systemui.statusbar.notification.LegacySourceType;
import com.android.systemui.statusbar.notification.NotificationUtils;
import com.android.systemui.statusbar.notification.SourceType;
import com.android.systemui.statusbar.notification.row.ActivatableNotificationView;
@@ -66,6 +67,8 @@
// the next icon has translated out of the way, to avoid overlapping.
private static final Interpolator ICON_ALPHA_INTERPOLATOR =
new PathInterpolator(0.6f, 0f, 0.6f, 0f);
+ private static final SourceType BASE_VALUE = SourceType.from("BaseValue");
+ private static final SourceType SHELF_SCROLL = SourceType.from("ShelfScroll");
private NotificationIconContainer mShelfIcons;
private int[] mTmp = new int[2];
@@ -112,19 +115,24 @@
setClipChildren(false);
setClipToPadding(false);
mShelfIcons.setIsStaticLayout(false);
- requestBottomRoundness(1.0f, /* animate = */ false, SourceType.DefaultValue);
- requestTopRoundness(1f, false, SourceType.DefaultValue);
+ requestRoundness(/* top = */ 1f, /* bottom = */ 1f, BASE_VALUE, /* animate = */ false);
- // Setting this to first in section to get the clipping to the top roundness correct. This
- // value determines the way we are clipping to the top roundness of the overall shade
- setFirstInSection(true);
+ if (!mUseRoundnessSourceTypes) {
+ // Setting this to first in section to get the clipping to the top roundness correct.
+ // This value determines the way we are clipping to the top roundness of the overall
+ // shade
+ setFirstInSection(true);
+ }
updateResources();
}
public void bind(AmbientState ambientState,
- NotificationStackScrollLayoutController hostLayoutController) {
+ NotificationStackScrollLayoutController hostLayoutController) {
mAmbientState = ambientState;
mHostLayoutController = hostLayoutController;
+ hostLayoutController.setOnNotificationRemovedListener((child, isTransferInProgress) -> {
+ child.requestRoundnessReset(SHELF_SCROLL);
+ });
}
private void updateResources() {
@@ -185,9 +193,11 @@
+ " indexOfFirstViewInShelf=" + mIndexOfFirstViewInShelf + ')';
}
- /** Update the state of the shelf. */
+ /**
+ * Update the state of the shelf.
+ */
public void updateState(StackScrollAlgorithm.StackScrollAlgorithmState algorithmState,
- AmbientState ambientState) {
+ AmbientState ambientState) {
ExpandableView lastView = ambientState.getLastVisibleBackgroundChild();
ShelfState viewState = (ShelfState) getViewState();
if (mShowNotificationShelf && lastView != null) {
@@ -246,7 +256,7 @@
/**
* @param fractionToShade Fraction of lockscreen to shade transition
- * @param shortestWidth Shortest width to use for lockscreen shelf
+ * @param shortestWidth Shortest width to use for lockscreen shelf
*/
@VisibleForTesting
public void updateActualWidth(float fractionToShade, float shortestWidth) {
@@ -281,9 +291,9 @@
/**
* @param localX Click x from left of screen
- * @param slop Margin of error within which we count x for valid click
- * @param left Left of shelf, from left of screen
- * @param right Right of shelf, from left of screen
+ * @param slop Margin of error within which we count x for valid click
+ * @param left Left of shelf, from left of screen
+ * @param right Right of shelf, from left of screen
* @return Whether click x was in view
*/
@VisibleForTesting
@@ -293,8 +303,8 @@
/**
* @param localY Click y from top of shelf
- * @param slop Margin of error within which we count y for valid click
- * @param top Top of shelf
+ * @param slop Margin of error within which we count y for valid click
+ * @param top Top of shelf
* @param bottom Height of shelf
* @return Whether click y was in view
*/
@@ -306,7 +316,7 @@
/**
* @param localX Click x
* @param localY Click y
- * @param slop Margin of error for valid click
+ * @param slop Margin of error for valid click
* @return Whether this click was on the visible (non-clipped) part of the shelf
*/
@Override
@@ -478,13 +488,15 @@
}
}
- private void updateCornerRoundnessOnScroll(ActivatableNotificationView anv, float viewStart,
+ private void updateCornerRoundnessOnScroll(
+ ActivatableNotificationView anv,
+ float viewStart,
float shelfStart) {
final boolean isUnlockedHeadsUp = !mAmbientState.isOnKeyguard()
&& !mAmbientState.isShadeExpanded()
&& anv instanceof ExpandableNotificationRow
- && ((ExpandableNotificationRow) anv).isHeadsUp();
+ && anv.isHeadsUp();
final boolean isHunGoingToShade = mAmbientState.isShadeExpanded()
&& anv == mAmbientState.getTrackedHeadsUpRow();
@@ -506,41 +518,40 @@
* mAmbientState.getExpansionFraction();
final float cornerAnimationTop = shelfStart - cornerAnimationDistance;
- if (viewEnd >= cornerAnimationTop) {
- // Round bottom corners within animation bounds
- final float changeFraction = MathUtils.saturate(
- (viewEnd - cornerAnimationTop) / cornerAnimationDistance);
- anv.requestBottomRoundness(
- /* value = */ anv.isLastInSection() ? 1f : changeFraction,
- /* animate = */ false,
- SourceType.OnScroll);
-
- } else if (viewEnd < cornerAnimationTop) {
- // Fast scroll skips frames and leaves corners with unfinished rounding.
- // Reset top and bottom corners outside of animation bounds.
- anv.requestBottomRoundness(
- /* value = */ anv.isLastInSection() ? 1f : 0f,
- /* animate = */ false,
- SourceType.OnScroll);
+ final SourceType sourceType;
+ if (mUseRoundnessSourceTypes) {
+ sourceType = SHELF_SCROLL;
+ } else {
+ sourceType = LegacySourceType.OnScroll;
}
- if (viewStart >= cornerAnimationTop) {
+ final float topValue;
+ if (!mUseRoundnessSourceTypes && anv.isFirstInSection()) {
+ topValue = 1f;
+ } else if (viewStart >= cornerAnimationTop) {
// Round top corners within animation bounds
- final float changeFraction = MathUtils.saturate(
+ topValue = MathUtils.saturate(
(viewStart - cornerAnimationTop) / cornerAnimationDistance);
- anv.requestTopRoundness(
- /* value = */ anv.isFirstInSection() ? 1f : changeFraction,
- /* animate = */ false,
- SourceType.OnScroll);
-
- } else if (viewStart < cornerAnimationTop) {
+ } else {
// Fast scroll skips frames and leaves corners with unfinished rounding.
// Reset top and bottom corners outside of animation bounds.
- anv.requestTopRoundness(
- /* value = */ anv.isFirstInSection() ? 1f : 0f,
- /* animate = */ false,
- SourceType.OnScroll);
+ topValue = 0f;
}
+ anv.requestTopRoundness(topValue, sourceType, /* animate = */ false);
+
+ final float bottomValue;
+ if (!mUseRoundnessSourceTypes && anv.isLastInSection()) {
+ bottomValue = 1f;
+ } else if (viewEnd >= cornerAnimationTop) {
+ // Round bottom corners within animation bounds
+ bottomValue = MathUtils.saturate(
+ (viewEnd - cornerAnimationTop) / cornerAnimationDistance);
+ } else {
+ // Fast scroll skips frames and leaves corners with unfinished rounding.
+ // Reset top and bottom corners outside of animation bounds.
+ bottomValue = 0f;
+ }
+ anv.requestBottomRoundness(bottomValue, sourceType, /* animate = */ false);
}
/**
@@ -626,10 +637,11 @@
/**
* Update the clipping of this view.
+ *
* @return the amount that our own top should be clipped
*/
private int updateNotificationClipHeight(ExpandableView view,
- float notificationClipEnd, int childIndex) {
+ float notificationClipEnd, int childIndex) {
float viewEnd = view.getTranslationY() + view.getActualHeight();
boolean isPinned = (view.isPinned() || view.isHeadsUpAnimatingAway())
&& !mAmbientState.isDozingAndNotPulsing(view);
@@ -657,7 +669,7 @@
@Override
public void setFakeShadowIntensity(float shadowIntensity, float outlineAlpha, int shadowYEnd,
- int outlineTranslation) {
+ int outlineTranslation) {
if (!mHasItemsInStableShelf) {
shadowIntensity = 0.0f;
}
@@ -665,18 +677,24 @@
}
/**
- * @param i Index of the view in the host layout.
- * @param view The current ExpandableView.
- * @param scrollingFast Whether we are scrolling fast.
+ * @param i Index of the view in the host layout.
+ * @param view The current ExpandableView.
+ * @param scrollingFast Whether we are scrolling fast.
* @param expandingAnimated Whether we are expanding a notification.
- * @param isLastChild Whether this is the last view.
- * @param shelfClipStart The point at which notifications start getting clipped by the shelf.
+ * @param isLastChild Whether this is the last view.
+ * @param shelfClipStart The point at which notifications start getting clipped by the shelf.
* @return The amount how much this notification is in the shelf.
- * 0f is not in shelf. 1f is completely in shelf.
+ * 0f is not in shelf. 1f is completely in shelf.
*/
@VisibleForTesting
- public float getAmountInShelf(int i, ExpandableView view, boolean scrollingFast,
- boolean expandingAnimated, boolean isLastChild, float shelfClipStart) {
+ public float getAmountInShelf(
+ int i,
+ ExpandableView view,
+ boolean scrollingFast,
+ boolean expandingAnimated,
+ boolean isLastChild,
+ float shelfClipStart
+ ) {
// Let's calculate how much the view is in the shelf
float viewStart = view.getTranslationY();
@@ -755,8 +773,13 @@
return start;
}
- private void updateIconPositioning(ExpandableView view, float iconTransitionAmount,
- boolean scrollingFast, boolean expandingAnimated, boolean isLastChild) {
+ private void updateIconPositioning(
+ ExpandableView view,
+ float iconTransitionAmount,
+ boolean scrollingFast,
+ boolean expandingAnimated,
+ boolean isLastChild
+ ) {
StatusBarIconView icon = view.getShelfIcon();
NotificationIconContainer.IconState iconState = getIconState(icon);
if (iconState == null) {
@@ -817,7 +840,7 @@
|| row.showingPulsing()
|| row.getTranslationZ() > mAmbientState.getBaseZHeight();
- iconState.iconAppearAmount = iconState.hidden? 0f : transitionAmount;
+ iconState.iconAppearAmount = iconState.hidden ? 0f : transitionAmount;
// Fade in icons at shelf start
// This is important for conversation icons, which are badged and need x reset
@@ -847,7 +870,7 @@
}
private float getFullyClosedTranslation() {
- return - (getIntrinsicHeight() - mStatusBarHeight) / 2;
+ return -(getIntrinsicHeight() - mStatusBarHeight) / 2;
}
@Override
@@ -904,7 +927,7 @@
/**
* @return whether the shelf has any icons in it when a potential animation has finished, i.e
- * if the current state would be applied right now
+ * if the current state would be applied right now
*/
public boolean hasItemsInStableShelf() {
return mHasItemsInStableShelf;
@@ -962,7 +985,7 @@
@Override
public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft,
- int oldTop, int oldRight, int oldBottom) {
+ int oldTop, int oldRight, int oldBottom) {
updateRelativeOffset();
}
@@ -981,12 +1004,11 @@
/**
* This method resets the OnScroll roundness of a view to 0f
- *
+ * <p>
* Note: This should be the only class that handles roundness {@code SourceType.OnScroll}
*/
- public static void resetOnScrollRoundness(ExpandableView expandableView) {
- expandableView.requestTopRoundness(0f, false, SourceType.OnScroll);
- expandableView.requestBottomRoundness(0f, false, SourceType.OnScroll);
+ public static void resetLegacyOnScrollRoundness(ExpandableView expandableView) {
+ expandableView.requestRoundnessReset(LegacySourceType.OnScroll);
}
public class ShelfState extends ExpandableViewState {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelfController.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelfController.java
index 3b1fa17..bb84c75 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelfController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelfController.java
@@ -18,6 +18,8 @@
import android.view.View;
+import com.android.systemui.flags.FeatureFlags;
+import com.android.systemui.flags.Flags;
import com.android.systemui.statusbar.notification.row.ActivatableNotificationView;
import com.android.systemui.statusbar.notification.row.ActivatableNotificationViewController;
import com.android.systemui.statusbar.notification.row.dagger.NotificationRowScope;
@@ -42,14 +44,17 @@
private AmbientState mAmbientState;
@Inject
- public NotificationShelfController(NotificationShelf notificationShelf,
+ public NotificationShelfController(
+ NotificationShelf notificationShelf,
ActivatableNotificationViewController activatableNotificationViewController,
KeyguardBypassController keyguardBypassController,
- SysuiStatusBarStateController statusBarStateController) {
+ SysuiStatusBarStateController statusBarStateController,
+ FeatureFlags featureFlags) {
mView = notificationShelf;
mActivatableNotificationViewController = activatableNotificationViewController;
mKeyguardBypassController = keyguardBypassController;
mStatusBarStateController = statusBarStateController;
+ mView.useRoundnessSourceTypes(featureFlags.isEnabled(Flags.USE_ROUNDNESS_SOURCETYPES));
mOnAttachStateChangeListener = new View.OnAttachStateChangeListener() {
@Override
public void onViewAttachedToWindow(View v) {
@@ -88,7 +93,7 @@
public @View.Visibility int getVisibility() {
return mView.getVisibility();
- };
+ }
public void setCollapsedIcons(NotificationIconContainer notificationIcons) {
mView.setCollapsedIcons(notificationIcons);
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/notification/Roundable.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/Roundable.kt
index ed7f648..0eb0000 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/Roundable.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/Roundable.kt
@@ -74,8 +74,8 @@
@JvmDefault
fun requestTopRoundness(
@FloatRange(from = 0.0, to = 1.0) value: Float,
- animate: Boolean,
sourceType: SourceType,
+ animate: Boolean,
): Boolean {
val roundnessMap = roundableState.topRoundnessMap
val lastValue = roundnessMap.values.maxOrNull() ?: 0f
@@ -105,6 +105,30 @@
}
/**
+ * Request the top roundness [value] for a specific [sourceType]. Animate the roundness if the
+ * view is shown.
+ *
+ * The top roundness of a [Roundable] can be defined by different [sourceType]. In case more
+ * origins require different roundness, for the same property, the maximum value will always be
+ * chosen.
+ *
+ * @param value a value between 0f and 1f.
+ * @param sourceType the source from which the request for roundness comes.
+ * @return Whether the roundness was changed.
+ */
+ @JvmDefault
+ fun requestTopRoundness(
+ @FloatRange(from = 0.0, to = 1.0) value: Float,
+ sourceType: SourceType,
+ ): Boolean {
+ return requestTopRoundness(
+ value = value,
+ sourceType = sourceType,
+ animate = roundableState.targetView.isShown
+ )
+ }
+
+ /**
* Request the bottom roundness [value] for a specific [sourceType].
*
* The bottom roundness of a [Roundable] can be defined by different [sourceType]. In case more
@@ -119,8 +143,8 @@
@JvmDefault
fun requestBottomRoundness(
@FloatRange(from = 0.0, to = 1.0) value: Float,
- animate: Boolean,
sourceType: SourceType,
+ animate: Boolean,
): Boolean {
val roundnessMap = roundableState.bottomRoundnessMap
val lastValue = roundnessMap.values.maxOrNull() ?: 0f
@@ -149,9 +173,101 @@
return false
}
+ /**
+ * Request the bottom roundness [value] for a specific [sourceType]. Animate the roundness if
+ * the view is shown.
+ *
+ * The bottom roundness of a [Roundable] can be defined by different [sourceType]. In case more
+ * origins require different roundness, for the same property, the maximum value will always be
+ * chosen.
+ *
+ * @param value value between 0f and 1f.
+ * @param sourceType the source from which the request for roundness comes.
+ * @return Whether the roundness was changed.
+ */
+ @JvmDefault
+ fun requestBottomRoundness(
+ @FloatRange(from = 0.0, to = 1.0) value: Float,
+ sourceType: SourceType,
+ ): Boolean {
+ return requestBottomRoundness(
+ value = value,
+ sourceType = sourceType,
+ animate = roundableState.targetView.isShown
+ )
+ }
+
+ /**
+ * Request the roundness [value] for a specific [sourceType].
+ *
+ * The top/bottom roundness of a [Roundable] can be defined by different [sourceType]. In case
+ * more origins require different roundness, for the same property, the maximum value will
+ * always be chosen.
+ *
+ * @param top top value between 0f and 1f.
+ * @param bottom bottom value between 0f and 1f.
+ * @param sourceType the source from which the request for roundness comes.
+ * @param animate true if it should animate to that value.
+ * @return Whether the roundness was changed.
+ */
+ @JvmDefault
+ fun requestRoundness(
+ @FloatRange(from = 0.0, to = 1.0) top: Float,
+ @FloatRange(from = 0.0, to = 1.0) bottom: Float,
+ sourceType: SourceType,
+ animate: Boolean,
+ ): Boolean {
+ val hasTopChanged =
+ requestTopRoundness(value = top, sourceType = sourceType, animate = animate)
+ val hasBottomChanged =
+ requestBottomRoundness(value = bottom, sourceType = sourceType, animate = animate)
+ return hasTopChanged || hasBottomChanged
+ }
+
+ /**
+ * Request the roundness [value] for a specific [sourceType]. Animate the roundness if the view
+ * is shown.
+ *
+ * The top/bottom roundness of a [Roundable] can be defined by different [sourceType]. In case
+ * more origins require different roundness, for the same property, the maximum value will
+ * always be chosen.
+ *
+ * @param top top value between 0f and 1f.
+ * @param bottom bottom value between 0f and 1f.
+ * @param sourceType the source from which the request for roundness comes.
+ * @return Whether the roundness was changed.
+ */
+ @JvmDefault
+ fun requestRoundness(
+ @FloatRange(from = 0.0, to = 1.0) top: Float,
+ @FloatRange(from = 0.0, to = 1.0) bottom: Float,
+ sourceType: SourceType,
+ ): Boolean {
+ return requestRoundness(
+ top = top,
+ bottom = bottom,
+ sourceType = sourceType,
+ animate = roundableState.targetView.isShown,
+ )
+ }
+
+ /**
+ * Request the roundness 0f for a [SourceType]. Animate the roundness if the view is shown.
+ *
+ * The top/bottom roundness of a [Roundable] can be defined by different [sourceType]. In case
+ * more origins require different roundness, for the same property, the maximum value will
+ * always be chosen.
+ *
+ * @param sourceType the source from which the request for roundness comes.
+ */
+ @JvmDefault
+ fun requestRoundnessReset(sourceType: SourceType) {
+ requestRoundness(top = 0f, bottom = 0f, sourceType = sourceType)
+ }
+
/** Apply the roundness changes, usually means invalidate the [RoundableState.targetView]. */
@JvmDefault
- fun applyRoundness() {
+ fun applyRoundnessAndInvalidate() {
roundableState.targetView.invalidate()
}
@@ -227,7 +343,7 @@
/** Set the current top roundness */
internal fun setTopRoundness(
value: Float,
- animated: Boolean = targetView.isShown,
+ animated: Boolean,
) {
PropertyAnimator.setProperty(targetView, topAnimatable, value, DURATION, animated)
}
@@ -235,11 +351,19 @@
/** Set the current bottom roundness */
internal fun setBottomRoundness(
value: Float,
- animated: Boolean = targetView.isShown,
+ animated: Boolean,
) {
PropertyAnimator.setProperty(targetView, bottomAnimatable, value, DURATION, animated)
}
+ fun debugString() = buildString {
+ append("TargetView: ${targetView.hashCode()} ")
+ append("Top: $topRoundness ")
+ append(topRoundnessMap.map { "${it.key} ${it.value}" })
+ append(" Bottom: $bottomRoundness ")
+ append(bottomRoundnessMap.map { "${it.key} ${it.value}" })
+ }
+
companion object {
private val DURATION: AnimationProperties =
AnimationProperties()
@@ -252,7 +376,7 @@
override fun setValue(view: View, value: Float) {
roundable.roundableState.topRoundness = value
- roundable.applyRoundness()
+ roundable.applyRoundnessAndInvalidate()
}
},
R.id.top_roundess_animator_tag,
@@ -267,7 +391,7 @@
override fun setValue(view: View, value: Float) {
roundable.roundableState.bottomRoundness = value
- roundable.applyRoundness()
+ roundable.applyRoundnessAndInvalidate()
}
},
R.id.bottom_roundess_animator_tag,
@@ -277,7 +401,31 @@
}
}
-enum class SourceType {
+/**
+ * Interface used to define the owner of a roundness. Usually the [SourceType] is defined as a
+ * private property of a class.
+ */
+interface SourceType {
+ companion object {
+ /**
+ * This is the most convenient way to define a new [SourceType].
+ *
+ * For example:
+ *
+ * ```kotlin
+ * private val SECTION = SourceType.from("Section")
+ * ```
+ */
+ @JvmStatic
+ fun from(name: String) =
+ object : SourceType {
+ override fun toString() = name
+ }
+ }
+}
+
+@Deprecated("Use SourceType.from() instead", ReplaceWith("SourceType.from()"))
+enum class LegacySourceType : SourceType {
DefaultValue,
OnDismissAnimation,
OnScroll,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationView.java
index d29298a..fbe88df 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationView.java
@@ -39,9 +39,13 @@
import com.android.systemui.statusbar.NotificationShelf;
import com.android.systemui.statusbar.notification.FakeShadowView;
import com.android.systemui.statusbar.notification.NotificationUtils;
+import com.android.systemui.statusbar.notification.SourceType;
import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout;
import com.android.systemui.statusbar.notification.stack.StackStateAnimator;
+import java.util.HashSet;
+import java.util.Set;
+
/**
* Base class for both {@link ExpandableNotificationRow} and {@link NotificationShelf}
* to implement dimming/activating on Keyguard for the double-tap gesture
@@ -91,6 +95,7 @@
= new PathInterpolator(0.6f, 0, 0.5f, 1);
private static final Interpolator ACTIVATE_INVERSE_ALPHA_INTERPOLATOR
= new PathInterpolator(0, 0, 0.5f, 1);
+ private final Set<SourceType> mOnDetachResetRoundness = new HashSet<>();
private int mTintedRippleColor;
private int mNormalRippleColor;
private Gefingerpoken mTouchHandler;
@@ -134,6 +139,7 @@
private boolean mDismissed;
private boolean mRefocusOnDismiss;
private AccessibilityManager mAccessibilityManager;
+ protected boolean mUseRoundnessSourceTypes;
public ActivatableNotificationView(Context context, AttributeSet attrs) {
super(context, attrs);
@@ -613,9 +619,9 @@
protected void resetAllContentAlphas() {}
@Override
- public void applyRoundness() {
- super.applyRoundness();
+ public void applyRoundnessAndInvalidate() {
applyBackgroundRoundness(getTopCornerRadius(), getBottomCornerRadius());
+ super.applyRoundnessAndInvalidate();
}
@Override
@@ -775,6 +781,33 @@
mAccessibilityManager = accessibilityManager;
}
+ /**
+ * Enable the support for rounded corner based on the SourceType
+ * @param enabled true if is supported
+ */
+ public void useRoundnessSourceTypes(boolean enabled) {
+ mUseRoundnessSourceTypes = enabled;
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+ if (mUseRoundnessSourceTypes && !mOnDetachResetRoundness.isEmpty()) {
+ for (SourceType sourceType : mOnDetachResetRoundness) {
+ requestRoundnessReset(sourceType);
+ }
+ mOnDetachResetRoundness.clear();
+ }
+ }
+
+ /**
+ * SourceType which should be reset when this View is detached
+ * @param sourceType will be reset on View detached
+ */
+ public void addOnDetachResetRoundness(SourceType sourceType) {
+ mOnDetachResetRoundness.add(sourceType);
+ }
+
public interface OnActivatedListener {
void onActivated(ActivatableNotificationView view);
void onActivationReset(ActivatableNotificationView view);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
index d7d5ac9..c7c1634 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
@@ -91,6 +91,7 @@
import com.android.systemui.statusbar.notification.AboveShelfChangedListener;
import com.android.systemui.statusbar.notification.FeedbackIcon;
import com.android.systemui.statusbar.notification.LaunchAnimationParameters;
+import com.android.systemui.statusbar.notification.LegacySourceType;
import com.android.systemui.statusbar.notification.NotificationFadeAware;
import com.android.systemui.statusbar.notification.NotificationLaunchAnimatorController;
import com.android.systemui.statusbar.notification.NotificationUtils;
@@ -143,6 +144,9 @@
private static final int MENU_VIEW_INDEX = 0;
public static final float DEFAULT_HEADER_VISIBLE_AMOUNT = 1.0f;
private static final long RECENTLY_ALERTED_THRESHOLD_MS = TimeUnit.SECONDS.toMillis(30);
+ private static final SourceType BASE_VALUE = SourceType.from("BaseValue");
+ private static final SourceType FROM_PARENT = SourceType.from("FromParent(ENR)");
+ private static final SourceType PINNED = SourceType.from("Pinned");
// We don't correctly track dark mode until the content views are inflated, so always update
// the background on first content update just in case it happens to be during a theme change.
@@ -150,6 +154,7 @@
private boolean mNotificationTranslationFinished = false;
private boolean mIsSnoozed;
private boolean mIsFaded;
+ private boolean mAnimatePinnedRoundness = false;
/**
* Listener for when {@link ExpandableNotificationRow} is laid out.
@@ -376,7 +381,7 @@
private float mTopRoundnessDuringLaunchAnimation;
private float mBottomRoundnessDuringLaunchAnimation;
- private boolean mIsNotificationGroupCornerEnabled;
+ private float mSmallRoundness;
/**
* Returns whether the given {@code statusBarNotification} is a system notification.
@@ -844,7 +849,9 @@
}
onAttachedChildrenCountChanged();
row.setIsChildInGroup(false, null);
- row.requestBottomRoundness(0.0f, /* animate = */ false, SourceType.DefaultValue);
+ if (!mUseRoundnessSourceTypes) {
+ row.requestBottomRoundness(0.0f, LegacySourceType.DefaultValue, /* animate = */ false);
+ }
}
/**
@@ -860,7 +867,10 @@
if (child.keepInParentForDismissAnimation()) {
mChildrenContainer.removeNotification(child);
child.setIsChildInGroup(false, null);
- child.requestBottomRoundness(0.0f, /* animate = */ false, SourceType.DefaultValue);
+ if (!mUseRoundnessSourceTypes) {
+ LegacySourceType sourceType = LegacySourceType.DefaultValue;
+ child.requestBottomRoundness(0f, sourceType, /* animate = */ false);
+ }
child.setKeepInParentForDismissAnimation(false);
logKeepInParentChildDetached(child);
childCountChanged = true;
@@ -915,6 +925,9 @@
mNotificationParent.updateBackgroundForGroupState();
}
updateBackgroundClipping();
+ if (mUseRoundnessSourceTypes) {
+ updateBaseRoundness();
+ }
}
@Override
@@ -1033,6 +1046,16 @@
if (isAboveShelf() != wasAboveShelf) {
mAboveShelfChangedListener.onAboveShelfStateChanged(!wasAboveShelf);
}
+ if (mUseRoundnessSourceTypes) {
+ if (pinned) {
+ // Should be animated if someone explicitly set it to 0 and the row is shown.
+ boolean animated = mAnimatePinnedRoundness && isShown();
+ requestRoundness(/* top = */ 1f, /* bottom = */ 1f, PINNED, animated);
+ } else {
+ requestRoundnessReset(PINNED);
+ mAnimatePinnedRoundness = true;
+ }
+ }
}
@Override
@@ -1607,6 +1630,8 @@
super(context, attrs);
mImageResolver = new NotificationInlineImageResolver(context,
new NotificationInlineImageCache());
+ float radius = getResources().getDimension(R.dimen.notification_corner_radius_small);
+ mSmallRoundness = radius / getMaxRadius();
initDimens();
}
@@ -1839,7 +1864,7 @@
mChildrenContainer.setIsLowPriority(mIsLowPriority);
mChildrenContainer.setContainingNotification(ExpandableNotificationRow.this);
mChildrenContainer.onNotificationUpdated();
- mChildrenContainer.enableNotificationGroupCorner(mIsNotificationGroupCornerEnabled);
+ mChildrenContainer.useRoundnessSourceTypes(mUseRoundnessSourceTypes);
mTranslateableViews.add(mChildrenContainer);
});
@@ -2271,7 +2296,7 @@
@Override
public float getTopRoundness() {
- if (mExpandAnimationRunning) {
+ if (!mUseRoundnessSourceTypes && mExpandAnimationRunning) {
return mTopRoundnessDuringLaunchAnimation;
}
@@ -2280,7 +2305,7 @@
@Override
public float getBottomRoundness() {
- if (mExpandAnimationRunning) {
+ if (!mUseRoundnessSourceTypes && mExpandAnimationRunning) {
return mBottomRoundnessDuringLaunchAnimation;
}
@@ -3436,17 +3461,24 @@
}
@Override
- public void applyRoundness() {
- super.applyRoundness();
+ public void applyRoundnessAndInvalidate() {
applyChildrenRoundness();
+ super.applyRoundnessAndInvalidate();
}
private void applyChildrenRoundness() {
if (mIsSummaryWithChildren) {
- mChildrenContainer.requestBottomRoundness(
- getBottomRoundness(),
- /* animate = */ false,
- SourceType.DefaultValue);
+ if (mUseRoundnessSourceTypes) {
+ mChildrenContainer.requestRoundness(
+ /* top = */ getTopRoundness(),
+ /* bottom = */ getBottomRoundness(),
+ FROM_PARENT);
+ } else {
+ mChildrenContainer.requestBottomRoundness(
+ getBottomRoundness(),
+ LegacySourceType.DefaultValue,
+ /* animate = */ false);
+ }
}
}
@@ -3605,6 +3637,7 @@
} else {
pw.println("no viewState!!!");
}
+ pw.println("Roundness: " + getRoundableState().debugString());
if (mIsSummaryWithChildren) {
pw.println();
@@ -3649,14 +3682,38 @@
return mTargetPoint;
}
+ /** Update the minimum roundness based on current state */
+ private void updateBaseRoundness() {
+ if (isChildInGroup()) {
+ requestRoundnessReset(BASE_VALUE);
+ } else {
+ requestRoundness(mSmallRoundness, mSmallRoundness, BASE_VALUE);
+ }
+ }
+
/**
- * Enable the support for rounded corner in notification group
+ * Enable the support for rounded corner based on the SourceType
* @param enabled true if is supported
*/
- public void enableNotificationGroupCorner(boolean enabled) {
- mIsNotificationGroupCornerEnabled = enabled;
+ @Override
+ public void useRoundnessSourceTypes(boolean enabled) {
+ super.useRoundnessSourceTypes(enabled);
if (mChildrenContainer != null) {
- mChildrenContainer.enableNotificationGroupCorner(mIsNotificationGroupCornerEnabled);
+ mChildrenContainer.useRoundnessSourceTypes(mUseRoundnessSourceTypes);
+ }
+ }
+
+ @Override
+ public String toString() {
+ String roundableStateDebug = "RoundableState = " + getRoundableState().debugString();
+ return "ExpandableNotificationRow:" + hashCode() + " { " + roundableStateDebug + " }";
+ }
+
+ @Override
+ protected void onAttachedToWindow() {
+ super.onAttachedToWindow();
+ if (mUseRoundnessSourceTypes) {
+ updateBaseRoundness();
}
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowController.java
index 8a400d5..d113860 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowController.java
@@ -255,8 +255,8 @@
mStatusBarStateController.removeCallback(mStatusBarStateListener);
}
});
- mView.enableNotificationGroupCorner(
- mFeatureFlags.isEnabled(Flags.NOTIFICATION_GROUP_CORNER));
+ mView.useRoundnessSourceTypes(
+ mFeatureFlags.isEnabled(Flags.USE_ROUNDNESS_SOURCETYPES));
}
private final StatusBarStateController.StateListener mStatusBarStateListener =
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableOutlineView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableOutlineView.java
index 2324627..0213b96 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableOutlineView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableOutlineView.java
@@ -219,9 +219,9 @@
}
@Override
- public void applyRoundness() {
+ public void applyRoundnessAndInvalidate() {
invalidateOutline();
- super.applyRoundness();
+ super.applyRoundnessAndInvalidate();
}
protected void setBackgroundTop(int backgroundTop) {
@@ -233,7 +233,7 @@
public void onDensityOrFontScaleChanged() {
initDimens();
- applyRoundness();
+ applyRoundnessAndInvalidate();
}
@Override
@@ -241,7 +241,7 @@
int previousHeight = getActualHeight();
super.setActualHeight(actualHeight, notifyListeners);
if (previousHeight != actualHeight) {
- applyRoundness();
+ applyRoundnessAndInvalidate();
}
}
@@ -250,7 +250,7 @@
int previousAmount = getClipTopAmount();
super.setClipTopAmount(clipTopAmount);
if (previousAmount != clipTopAmount) {
- applyRoundness();
+ applyRoundnessAndInvalidate();
}
}
@@ -259,14 +259,14 @@
int previousAmount = getClipBottomAmount();
super.setClipBottomAmount(clipBottomAmount);
if (previousAmount != clipBottomAmount) {
- applyRoundness();
+ applyRoundnessAndInvalidate();
}
}
protected void setOutlineAlpha(float alpha) {
if (alpha != mOutlineAlpha) {
mOutlineAlpha = alpha;
- applyRoundness();
+ applyRoundnessAndInvalidate();
}
}
@@ -280,7 +280,7 @@
setOutlineRect(rect.left, rect.top, rect.right, rect.bottom);
} else {
mCustomOutline = false;
- applyRoundness();
+ applyRoundnessAndInvalidate();
}
}
@@ -340,7 +340,7 @@
// Outlines need to be at least 1 dp
mOutlineRect.bottom = (int) Math.max(top, mOutlineRect.bottom);
mOutlineRect.right = (int) Math.max(left, mOutlineRect.right);
- applyRoundness();
+ applyRoundnessAndInvalidate();
}
public Path getCustomClipPath(View child) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationHeaderViewWrapper.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationHeaderViewWrapper.java
index f13e48d..1f664cb 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationHeaderViewWrapper.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationHeaderViewWrapper.java
@@ -71,6 +71,8 @@
private View mFeedbackIcon;
private boolean mIsLowPriority;
private boolean mTransformLowPriorityTitle;
+ private boolean mUseRoundnessSourceTypes;
+ private RoundnessChangedListener mRoundnessChangedListener;
protected NotificationHeaderViewWrapper(Context ctx, View view, ExpandableNotificationRow row) {
super(ctx, view, row);
@@ -117,6 +119,20 @@
return mRoundableState;
}
+ @Override
+ public void applyRoundnessAndInvalidate() {
+ if (mUseRoundnessSourceTypes && mRoundnessChangedListener != null) {
+ // We cannot apply the rounded corner to this View, so our parents (in drawChild()) will
+ // clip our canvas. So we should invalidate our parent.
+ mRoundnessChangedListener.applyRoundnessAndInvalidate();
+ }
+ Roundable.super.applyRoundnessAndInvalidate();
+ }
+
+ public void setOnRoundnessChangedListener(RoundnessChangedListener listener) {
+ mRoundnessChangedListener = listener;
+ }
+
protected void resolveHeaderViews() {
mIcon = mView.findViewById(com.android.internal.R.id.icon);
mHeaderText = mView.findViewById(com.android.internal.R.id.header_text);
@@ -343,4 +359,23 @@
}
}
}
+
+ /**
+ * Enable the support for rounded corner based on the SourceType
+ *
+ * @param enabled true if is supported
+ */
+ public void useRoundnessSourceTypes(boolean enabled) {
+ mUseRoundnessSourceTypes = enabled;
+ }
+
+ /**
+ * Interface that handle the Roundness changes
+ */
+ public interface RoundnessChangedListener {
+ /**
+ * This method will be called when this class call applyRoundnessAndInvalidate()
+ */
+ void applyRoundnessAndInvalidate();
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java
index d43ca823..4a8e2db 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java
@@ -47,6 +47,7 @@
import com.android.systemui.statusbar.NotificationGroupingUtil;
import com.android.systemui.statusbar.NotificationShelf;
import com.android.systemui.statusbar.notification.FeedbackIcon;
+import com.android.systemui.statusbar.notification.LegacySourceType;
import com.android.systemui.statusbar.notification.NotificationFadeAware;
import com.android.systemui.statusbar.notification.NotificationUtils;
import com.android.systemui.statusbar.notification.Roundable;
@@ -83,6 +84,7 @@
return mAnimationFilter;
}
}.setDuration(200);
+ private static final SourceType FROM_PARENT = SourceType.from("FromParent(NCC)");
private final List<View> mDividers = new ArrayList<>();
private final List<ExpandableNotificationRow> mAttachedChildren = new ArrayList<>();
@@ -131,7 +133,7 @@
private int mUntruncatedChildCount;
private boolean mContainingNotificationIsFaded = false;
private RoundableState mRoundableState;
- private boolean mIsNotificationGroupCornerEnabled;
+ private boolean mUseRoundnessSourceTypes;
public NotificationChildrenContainer(Context context) {
this(context, null);
@@ -313,9 +315,12 @@
row.setContentTransformationAmount(0, false /* isLastChild */);
row.setNotificationFaded(mContainingNotificationIsFaded);
- // This is a workaround, the NotificationShelf should be the owner of `OnScroll` roundness.
- // Here we should reset the `OnScroll` roundness only on top-level rows.
- NotificationShelf.resetOnScrollRoundness(row);
+ if (!mUseRoundnessSourceTypes) {
+ // This is a workaround, the NotificationShelf should be the owner of `OnScroll`
+ // roundness.
+ // Here we should reset the `OnScroll` roundness only on top-level rows.
+ NotificationShelf.resetLegacyOnScrollRoundness(row);
+ }
// It doesn't make sense to keep old animations around, lets cancel them!
ExpandableViewState viewState = row.getViewState();
@@ -323,6 +328,10 @@
viewState.cancelAnimations(row);
row.cancelAppearDrawing();
}
+
+ if (mUseRoundnessSourceTypes) {
+ applyRoundnessAndInvalidate();
+ }
}
private void ensureRemovedFromTransientContainer(View v) {
@@ -356,6 +365,11 @@
if (!row.isRemoved()) {
mGroupingUtil.restoreChildNotification(row);
}
+
+ if (mUseRoundnessSourceTypes) {
+ row.requestRoundnessReset(FROM_PARENT);
+ applyRoundnessAndInvalidate();
+ }
}
/**
@@ -382,6 +396,10 @@
getContext(),
mNotificationHeader,
mContainingNotification);
+ mNotificationHeaderWrapper.useRoundnessSourceTypes(mUseRoundnessSourceTypes);
+ if (mUseRoundnessSourceTypes) {
+ mNotificationHeaderWrapper.setOnRoundnessChangedListener(this::invalidate);
+ }
addView(mNotificationHeader, 0);
invalidate();
} else {
@@ -419,6 +437,12 @@
getContext(),
mNotificationHeaderLowPriority,
mContainingNotification);
+ mNotificationHeaderWrapperLowPriority.useRoundnessSourceTypes(
+ mUseRoundnessSourceTypes
+ );
+ if (mUseRoundnessSourceTypes) {
+ mNotificationHeaderWrapper.setOnRoundnessChangedListener(this::invalidate);
+ }
addView(mNotificationHeaderLowPriority, 0);
invalidate();
} else {
@@ -841,7 +865,7 @@
isCanvasChanged = true;
canvas.save();
- if (mIsNotificationGroupCornerEnabled && translation != 0f) {
+ if (mUseRoundnessSourceTypes && translation != 0f) {
clipPath.offset(translation, 0f);
canvas.clipPath(clipPath);
clipPath.offset(-translation, 0f);
@@ -1392,24 +1416,28 @@
}
@Override
- public void applyRoundness() {
- Roundable.super.applyRoundness();
+ public void applyRoundnessAndInvalidate() {
boolean last = true;
for (int i = mAttachedChildren.size() - 1; i >= 0; i--) {
ExpandableNotificationRow child = mAttachedChildren.get(i);
if (child.getVisibility() == View.GONE) {
continue;
}
- child.requestTopRoundness(
- /* value = */ 0f,
- /* animate = */ isShown(),
- SourceType.DefaultValue);
- child.requestBottomRoundness(
- /* value = */ last ? getBottomRoundness() : 0f,
- /* animate = */ isShown(),
- SourceType.DefaultValue);
+ if (mUseRoundnessSourceTypes) {
+ child.requestRoundness(
+ /* top = */ 0f,
+ /* bottom = */ last ? getBottomRoundness() : 0f,
+ FROM_PARENT);
+ } else {
+ child.requestRoundness(
+ /* top = */ 0f,
+ /* bottom = */ last ? getBottomRoundness() : 0f,
+ LegacySourceType.DefaultValue,
+ /* animate = */ isShown());
+ }
last = false;
}
+ Roundable.super.applyRoundnessAndInvalidate();
}
public void setHeaderVisibleAmount(float headerVisibleAmount) {
@@ -1467,10 +1495,17 @@
}
/**
- * Enable the support for rounded corner in notification group
+ * Enable the support for rounded corner based on the SourceType
+ *
* @param enabled true if is supported
*/
- public void enableNotificationGroupCorner(boolean enabled) {
- mIsNotificationGroupCornerEnabled = enabled;
+ public void useRoundnessSourceTypes(boolean enabled) {
+ mUseRoundnessSourceTypes = enabled;
+ }
+
+ @Override
+ public String toString() {
+ String roundableStateDebug = "RoundableState = " + getRoundableState().debugString();
+ return "NotificationChildrenContainer:" + hashCode() + " { " + roundableStateDebug + " }";
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationRoundnessManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationRoundnessManager.java
index 6810055..fde8c4d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationRoundnessManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationRoundnessManager.java
@@ -25,6 +25,9 @@
import com.android.systemui.R;
import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.dump.DumpManager;
+import com.android.systemui.flags.FeatureFlags;
+import com.android.systemui.flags.Flags;
+import com.android.systemui.statusbar.notification.LegacySourceType;
import com.android.systemui.statusbar.notification.NotificationSectionsFeatureManager;
import com.android.systemui.statusbar.notification.Roundable;
import com.android.systemui.statusbar.notification.SourceType;
@@ -45,6 +48,7 @@
public class NotificationRoundnessManager implements Dumpable {
private static final String TAG = "NotificationRoundnessManager";
+ private static final SourceType DISMISS_ANIMATION = SourceType.from("DismissAnimation");
private final ExpandableView[] mFirstInSectionViews;
private final ExpandableView[] mLastInSectionViews;
@@ -63,12 +67,14 @@
private ExpandableView mSwipedView = null;
private Roundable mViewBeforeSwipedView = null;
private Roundable mViewAfterSwipedView = null;
+ private boolean mUseRoundnessSourceTypes;
@Inject
NotificationRoundnessManager(
NotificationSectionsFeatureManager sectionsFeatureManager,
NotificationRoundnessLogger notifLogger,
- DumpManager dumpManager) {
+ DumpManager dumpManager,
+ FeatureFlags featureFlags) {
int numberOfSections = sectionsFeatureManager.getNumberOfBuckets();
mFirstInSectionViews = new ExpandableView[numberOfSections];
mLastInSectionViews = new ExpandableView[numberOfSections];
@@ -76,6 +82,7 @@
mTmpLastInSectionViews = new ExpandableView[numberOfSections];
mNotifLogger = notifLogger;
mDumpManager = dumpManager;
+ mUseRoundnessSourceTypes = featureFlags.isEnabled(Flags.USE_ROUNDNESS_SOURCETYPES);
mDumpManager.registerDumpable(TAG, this);
}
@@ -94,6 +101,7 @@
}
public void updateView(ExpandableView view, boolean animate) {
+ if (mUseRoundnessSourceTypes) return;
boolean changed = updateViewWithoutCallback(view, animate);
if (changed) {
mRoundingChangedCallback.run();
@@ -110,6 +118,7 @@
boolean updateViewWithoutCallback(
ExpandableView view,
boolean animate) {
+ if (mUseRoundnessSourceTypes) return false;
if (view == null
|| view == mViewBeforeSwipedView
|| view == mViewAfterSwipedView) {
@@ -118,13 +127,13 @@
final boolean isTopChanged = view.requestTopRoundness(
getRoundnessDefaultValue(view, true /* top */),
- animate,
- SourceType.DefaultValue);
+ LegacySourceType.DefaultValue,
+ animate);
final boolean isBottomChanged = view.requestBottomRoundness(
getRoundnessDefaultValue(view, /* top = */ false),
- animate,
- SourceType.DefaultValue);
+ LegacySourceType.DefaultValue,
+ animate);
final boolean isFirstInSection = isFirstInSection(view);
final boolean isLastInSection = isLastInSection(view);
@@ -139,6 +148,7 @@
}
private boolean isFirstInSection(ExpandableView view) {
+ if (mUseRoundnessSourceTypes) return false;
for (int i = 0; i < mFirstInSectionViews.length; i++) {
if (view == mFirstInSectionViews[i]) {
return true;
@@ -148,6 +158,7 @@
}
private boolean isLastInSection(ExpandableView view) {
+ if (mUseRoundnessSourceTypes) return false;
for (int i = mLastInSectionViews.length - 1; i >= 0; i--) {
if (view == mLastInSectionViews[i]) {
return true;
@@ -160,9 +171,6 @@
Roundable viewBefore,
ExpandableView viewSwiped,
Roundable viewAfter) {
- final boolean animate = true;
- final SourceType source = SourceType.OnDismissAnimation;
-
// This method requires you to change the roundness of the current View targets and reset
// the roundness of the old View targets (if any) to 0f.
// To avoid conflicts, it generates a set of old Views and removes the current Views
@@ -172,31 +180,34 @@
if (mSwipedView != null) oldViews.add(mSwipedView);
if (mViewAfterSwipedView != null) oldViews.add(mViewAfterSwipedView);
+ final SourceType source;
+ if (mUseRoundnessSourceTypes) {
+ source = DISMISS_ANIMATION;
+ } else {
+ source = LegacySourceType.OnDismissAnimation;
+ }
+
mViewBeforeSwipedView = viewBefore;
if (viewBefore != null) {
oldViews.remove(viewBefore);
- viewBefore.requestTopRoundness(0f, animate, source);
- viewBefore.requestBottomRoundness(1f, animate, source);
+ viewBefore.requestRoundness(/* top = */ 0f, /* bottom = */ 1f, source);
}
mSwipedView = viewSwiped;
if (viewSwiped != null) {
oldViews.remove(viewSwiped);
- viewSwiped.requestTopRoundness(1f, animate, source);
- viewSwiped.requestBottomRoundness(1f, animate, source);
+ viewSwiped.requestRoundness(/* top = */ 1f, /* bottom = */ 1f, source);
}
mViewAfterSwipedView = viewAfter;
if (viewAfter != null) {
oldViews.remove(viewAfter);
- viewAfter.requestTopRoundness(1f, animate, source);
- viewAfter.requestBottomRoundness(0f, animate, source);
+ viewAfter.requestRoundness(/* top = */ 1f, /* bottom = */ 0f, source);
}
// After setting the current Views, reset the views that are still present in the set.
for (Roundable oldView : oldViews) {
- oldView.requestTopRoundness(0f, animate, source);
- oldView.requestBottomRoundness(0f, animate, source);
+ oldView.requestRoundnessReset(source);
}
}
@@ -204,7 +215,23 @@
mIsClearAllInProgress = isClearingAll;
}
+ /**
+ * Check if "Clear all" notifications is in progress.
+ */
+ public boolean isClearAllInProgress() {
+ return mIsClearAllInProgress;
+ }
+
+ /**
+ * Check if we can request the `Pulsing` roundness for notification.
+ */
+ public boolean shouldRoundNotificationPulsing() {
+ return mRoundForPulsingViews;
+ }
+
private float getRoundnessDefaultValue(Roundable view, boolean top) {
+ if (mUseRoundnessSourceTypes) return 0f;
+
if (view == null) {
return 0f;
}
@@ -250,6 +277,7 @@
}
public void setExpanded(float expandedHeight, float appearFraction) {
+ if (mUseRoundnessSourceTypes) return;
mExpanded = expandedHeight != 0.0f;
mAppearFraction = appearFraction;
if (mTrackedHeadsUp != null) {
@@ -258,6 +286,7 @@
}
public void updateRoundedChildren(NotificationSection[] sections) {
+ if (mUseRoundnessSourceTypes) return;
boolean anyChanged = false;
for (int i = 0; i < sections.length; i++) {
mTmpFirstInSectionViews[i] = mFirstInSectionViews[i];
@@ -280,6 +309,7 @@
NotificationSection[] sections,
ExpandableView[] oldViews,
boolean first) {
+ if (mUseRoundnessSourceTypes) return false;
boolean anyChanged = false;
for (ExpandableView oldView : oldViews) {
if (oldView != null) {
@@ -313,6 +343,7 @@
NotificationSection[] sections,
ExpandableView[] oldViews,
boolean first) {
+ if (mUseRoundnessSourceTypes) return false;
boolean anyChanged = false;
for (NotificationSection section : sections) {
ExpandableView newView =
@@ -339,6 +370,15 @@
mAnimatedChildren = animatedChildren;
}
+ /**
+ * Check if the view should be animated
+ * @param view target view
+ * @return true, if is in the AnimatedChildren set
+ */
+ public boolean isAnimatedChild(ExpandableView view) {
+ return mAnimatedChildren.contains(view);
+ }
+
public void setOnRoundingChangedCallback(Runnable roundingChangedCallback) {
mRoundingChangedCallback = roundingChangedCallback;
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManager.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManager.kt
index a1b77ac..070b439 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManager.kt
@@ -19,8 +19,11 @@
import android.util.Log
import android.view.View
import com.android.internal.annotations.VisibleForTesting
+import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.flags.Flags
import com.android.systemui.media.controls.ui.KeyguardMediaController
import com.android.systemui.statusbar.notification.NotificationSectionsFeatureManager
+import com.android.systemui.statusbar.notification.SourceType
import com.android.systemui.statusbar.notification.collection.render.MediaContainerController
import com.android.systemui.statusbar.notification.collection.render.SectionHeaderController
import com.android.systemui.statusbar.notification.dagger.AlertingHeader
@@ -44,12 +47,16 @@
private val keyguardMediaController: KeyguardMediaController,
private val sectionsFeatureManager: NotificationSectionsFeatureManager,
private val mediaContainerController: MediaContainerController,
+ private val notificationRoundnessManager: NotificationRoundnessManager,
@IncomingHeader private val incomingHeaderController: SectionHeaderController,
@PeopleHeader private val peopleHeaderController: SectionHeaderController,
@AlertingHeader private val alertingHeaderController: SectionHeaderController,
- @SilentHeader private val silentHeaderController: SectionHeaderController
+ @SilentHeader private val silentHeaderController: SectionHeaderController,
+ featureFlags: FeatureFlags
) : SectionProvider {
+ private val useRoundnessSourceTypes = featureFlags.isEnabled(Flags.USE_ROUNDNESS_SOURCETYPES)
+
private val configurationListener = object : ConfigurationController.ConfigurationListener {
override fun onLocaleListChanged() {
reinflateViews()
@@ -177,11 +184,49 @@
size = sections.size,
operation = SectionBounds::addNotif
)
+
+ // Build a set of the old first/last Views of the sections
+ val oldFirstChildren = sections.mapNotNull { it.firstVisibleChild }.toSet().toMutableSet()
+ val oldLastChildren = sections.mapNotNull { it.lastVisibleChild }.toSet().toMutableSet()
+
// Update each section with the associated boundary, tracking if there was a change
val changed = sections.fold(false) { changed, section ->
val bounds = sectionBounds[section.bucket] ?: SectionBounds.None
- bounds.updateSection(section) || changed
+ val isSectionChanged = bounds.updateSection(section)
+ isSectionChanged || changed
}
+
+ if (useRoundnessSourceTypes) {
+ val newFirstChildren = sections.mapNotNull { it.firstVisibleChild }
+ val newLastChildren = sections.mapNotNull { it.lastVisibleChild }
+
+ // Update the roundness of Views that weren't already in the first/last position
+ newFirstChildren.forEach { firstChild ->
+ val wasFirstChild = oldFirstChildren.remove(firstChild)
+ if (!wasFirstChild) {
+ val notAnimatedChild = !notificationRoundnessManager.isAnimatedChild(firstChild)
+ val animated = firstChild.isShown && notAnimatedChild
+ firstChild.requestTopRoundness(1f, SECTION, animated)
+ }
+ }
+ newLastChildren.forEach { lastChild ->
+ val wasLastChild = oldLastChildren.remove(lastChild)
+ if (!wasLastChild) {
+ val notAnimatedChild = !notificationRoundnessManager.isAnimatedChild(lastChild)
+ val animated = lastChild.isShown && notAnimatedChild
+ lastChild.requestBottomRoundness(1f, SECTION, animated)
+ }
+ }
+
+ // The Views left in the set are no longer in the first/last position
+ oldFirstChildren.forEach { noMoreFirstChild ->
+ noMoreFirstChild.requestTopRoundness(0f, SECTION)
+ }
+ oldLastChildren.forEach { noMoreLastChild ->
+ noMoreLastChild.requestBottomRoundness(0f, SECTION)
+ }
+ }
+
if (DEBUG) {
logSections(sections)
}
@@ -215,5 +260,6 @@
companion object {
private const val TAG = "NotifSectionsManager"
private const val DEBUG = false
+ private val SECTION = SourceType.from("Section")
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
index 7c3e52c..21e2bd8 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
@@ -191,10 +191,13 @@
private final boolean mDebugLines;
private Paint mDebugPaint;
- /** Used to track the Y positions that were already used to draw debug text labels. */
+ /**
+ * Used to track the Y positions that were already used to draw debug text labels.
+ */
private Set<Integer> mDebugTextUsedYPositions;
private final boolean mDebugRemoveAnimation;
private final boolean mSimplifiedAppearFraction;
+ private final boolean mUseRoundnessSourceTypes;
private int mContentHeight;
private float mIntrinsicContentHeight;
@@ -355,15 +358,14 @@
private final Rect mQsHeaderBound = new Rect();
private boolean mContinuousShadowUpdate;
private boolean mContinuousBackgroundUpdate;
- private final ViewTreeObserver.OnPreDrawListener mShadowUpdater
- = () -> {
- updateViewShadows();
- return true;
- };
+ private final ViewTreeObserver.OnPreDrawListener mShadowUpdater = () -> {
+ updateViewShadows();
+ return true;
+ };
private final ViewTreeObserver.OnPreDrawListener mBackgroundUpdater = () -> {
- updateBackground();
- return true;
- };
+ updateBackground();
+ return true;
+ };
private final Comparator<ExpandableView> mViewPositionComparator = (view, otherView) -> {
float endY = view.getTranslationY() + view.getActualHeight();
float otherEndY = otherView.getTranslationY() + otherView.getActualHeight();
@@ -419,7 +421,8 @@
*/
private int mMaxDisplayedNotifications = -1;
private float mKeyguardBottomPadding = -1;
- @VisibleForTesting int mStatusBarHeight;
+ @VisibleForTesting
+ int mStatusBarHeight;
private int mMinInteractionHeight;
private final Rect mClipRect = new Rect();
private boolean mIsClipped;
@@ -568,6 +571,8 @@
@Nullable
private OnClickListener mManageButtonClickListener;
+ @Nullable
+ private OnNotificationRemovedListener mOnNotificationRemovedListener;
public NotificationStackScrollLayout(Context context, AttributeSet attrs) {
super(context, attrs, 0, 0);
@@ -576,6 +581,7 @@
mDebugLines = featureFlags.isEnabled(Flags.NSSL_DEBUG_LINES);
mDebugRemoveAnimation = featureFlags.isEnabled(Flags.NSSL_DEBUG_REMOVE_ANIMATION);
mSimplifiedAppearFraction = featureFlags.isEnabled(Flags.SIMPLIFIED_APPEAR_FRACTION);
+ mUseRoundnessSourceTypes = featureFlags.isEnabled(Flags.USE_ROUNDNESS_SOURCETYPES);
mSectionsManager = Dependency.get(NotificationSectionsManager.class);
mScreenOffAnimationController =
Dependency.get(ScreenOffAnimationController.class);
@@ -741,7 +747,7 @@
}
private void logHunSkippedForUnexpectedState(ExpandableNotificationRow enr,
- boolean expected, boolean actual) {
+ boolean expected, boolean actual) {
if (mLogger == null) return;
mLogger.hunSkippedForUnexpectedState(enr.getEntry(), expected, actual);
}
@@ -868,13 +874,13 @@
/**
* Draws round rects for each background section.
- *
+ * <p>
* We want to draw a round rect for each background section as defined by {@link #mSections}.
* However, if two sections are directly adjacent with no gap between them (e.g. on the
* lockscreen where the shelf can appear directly below the high priority section, or while
* scrolling the shade so that the top of the shelf is right at the bottom of the high priority
* section), we don't want to round the adjacent corners.
- *
+ * <p>
* Since {@link Canvas} doesn't provide a way to draw a half-rounded rect, this means that we
* need to coalesce the backgrounds for adjacent sections and draw them as a single round rect.
* This method tracks the top of each rect we need to draw, then iterates through the visible
@@ -883,7 +889,7 @@
* the current section. When we're done iterating we will always have one rect left to draw.
*/
private void drawBackgroundRects(Canvas canvas, int left, int right, int top,
- int animationYOffset) {
+ int animationYOffset) {
int backgroundRectTop = top;
int lastSectionBottom =
mSections[0].getCurrentBounds().bottom + animationYOffset;
@@ -974,7 +980,7 @@
@ShadeViewRefactor(RefactorComponent.SHADE_VIEW)
void initView(Context context, NotificationSwipeHelper swipeHelper,
- NotificationStackSizeCalculator notificationStackSizeCalculator) {
+ NotificationStackSizeCalculator notificationStackSizeCalculator) {
mScroller = new OverScroller(getContext());
mSwipeHelper = swipeHelper;
mNotificationStackSizeCalculator = notificationStackSizeCalculator;
@@ -1304,18 +1310,19 @@
/**
* @return Whether we should skip stack height updates.
* True when
- * 1) Unlock hint is running
- * 2) Swiping up on lockscreen or flinging down after swipe up
+ * 1) Unlock hint is running
+ * 2) Swiping up on lockscreen or flinging down after swipe up
*/
private boolean shouldSkipHeightUpdate() {
return mAmbientState.isOnKeyguard()
&& (mAmbientState.isUnlockHintRunning()
- || mAmbientState.isSwipingUp()
- || mAmbientState.isFlingingAfterSwipeUpOnLockscreen());
+ || mAmbientState.isSwipingUp()
+ || mAmbientState.isFlingingAfterSwipeUpOnLockscreen());
}
/**
* Apply expansion fraction to the y position and height of the notifications panel.
+ *
* @param listenerNeedsAnimation does the listener need to animate?
*/
private void updateStackPosition(boolean listenerNeedsAnimation) {
@@ -1708,7 +1715,7 @@
*/
@ShadeViewRefactor(RefactorComponent.COORDINATOR)
ExpandableView getChildAtPosition(float touchX, float touchY,
- boolean requireMinHeight, boolean ignoreDecors) {
+ boolean requireMinHeight, boolean ignoreDecors) {
// find the view under the pointer, accounting for GONE views
final int count = getChildCount();
for (int childIdx = 0; childIdx < count; childIdx++) {
@@ -1868,9 +1875,9 @@
public void dismissViewAnimated(
View child, Consumer<Boolean> endRunnable, int delay, long duration) {
if (child instanceof SectionHeaderView) {
- ((StackScrollerDecorView) child).setContentVisible(
- false /* visible */, true /* animate */, endRunnable);
- return;
+ ((StackScrollerDecorView) child).setContentVisible(
+ false /* visible */, true /* animate */, endRunnable);
+ return;
}
mSwipeHelper.dismissChild(
child,
@@ -2032,10 +2039,11 @@
/**
* Scrolls by the given delta, overscrolling if needed. If called during a fling and the delta
* would cause us to exceed the provided maximum overscroll, springs back instead.
- *
+ * <p>
* This method performs the determination of whether we're exceeding the overscroll and clamps
* the scroll amount if so. The actual scrolling/overscrolling happens in
* {@link #onCustomOverScrolled(int, boolean)}
+ *
* @param deltaY The (signed) number of pixels to scroll.
* @param scrollY The current scroll position (absolute scrolling only).
* @param scrollRangeY The maximum allowable scroll position (absolute scrolling only).
@@ -2098,7 +2106,7 @@
*/
@ShadeViewRefactor(RefactorComponent.STATE_RESOLVER)
public void setOverScrollAmount(float amount, boolean onTop, boolean animate,
- boolean cancelAnimators) {
+ boolean cancelAnimators) {
setOverScrollAmount(amount, onTop, animate, cancelAnimators, isRubberbanded(onTop));
}
@@ -2114,7 +2122,7 @@
*/
@ShadeViewRefactor(RefactorComponent.STATE_RESOLVER)
public void setOverScrollAmount(float amount, boolean onTop, boolean animate,
- boolean cancelAnimators, boolean isRubberbanded) {
+ boolean cancelAnimators, boolean isRubberbanded) {
if (cancelAnimators) {
mStateAnimator.cancelOverScrollAnimators(onTop);
}
@@ -2123,7 +2131,7 @@
@ShadeViewRefactor(RefactorComponent.STATE_RESOLVER)
private void setOverScrollAmountInternal(float amount, boolean onTop, boolean animate,
- boolean isRubberbanded) {
+ boolean isRubberbanded) {
amount = Math.max(0, amount);
if (animate) {
mStateAnimator.animateOverScrollToAmount(amount, onTop, isRubberbanded);
@@ -2179,7 +2187,7 @@
* Scrolls to the given position, overscrolling if needed. If called during a fling and the
* position exceeds the provided maximum overscroll, springs back instead.
*
- * @param scrollY The target scroll position.
+ * @param scrollY The target scroll position.
* @param clampedY Whether this value was clamped by the calling method, meaning we've reached
* the overscroll limit.
*/
@@ -2288,8 +2296,8 @@
if (row.isSummaryWithChildren() && row.areChildrenExpanded()) {
List<ExpandableNotificationRow> notificationChildren =
row.getAttachedChildren();
- for (int childIndex = 0; childIndex < notificationChildren.size();
- childIndex++) {
+ int childrenSize = notificationChildren.size();
+ for (int childIndex = 0; childIndex < childrenSize; childIndex++) {
ExpandableNotificationRow rowChild = notificationChildren.get(childIndex);
if (rowChild.getTranslationY() + rowTranslation >= translationY) {
return rowChild;
@@ -2365,10 +2373,9 @@
/**
* Calculate the gap height between two different views
*
- * @param previous the previousView
- * @param current the currentView
+ * @param previous the previousView
+ * @param current the currentView
* @param visibleIndex the visible index in the list
- *
* @return the gap height needed before the current view
*/
public float calculateGapHeight(
@@ -2376,7 +2383,7 @@
ExpandableView current,
int visibleIndex
) {
- return mStackScrollAlgorithm.getGapHeightForChild(mSectionsManager, visibleIndex, current,
+ return mStackScrollAlgorithm.getGapHeightForChild(mSectionsManager, visibleIndex, current,
previous, mAmbientState.getFractionToShade(), mAmbientState.isOnKeyguard());
}
@@ -2642,15 +2649,15 @@
return mScrolledToTopOnFirstDown
&& !mExpandedInThisMotion
&& (initialVelocity > mMinimumVelocity
- || (topOverScroll > mMinTopOverScrollToEscape && initialVelocity > 0));
+ || (topOverScroll > mMinTopOverScrollToEscape && initialVelocity > 0));
}
/**
* Updates the top padding of the notifications, taking {@link #getIntrinsicPadding()} into
* account.
*
- * @param qsHeight the top padding imposed by the quick settings panel
- * @param animate whether to animate the change
+ * @param qsHeight the top padding imposed by the quick settings panel
+ * @param animate whether to animate the change
*/
@ShadeViewRefactor(RefactorComponent.COORDINATOR)
public void updateTopPadding(float qsHeight, boolean animate) {
@@ -2724,21 +2731,34 @@
}
-
@ShadeViewRefactor(RefactorComponent.SHADE_VIEW)
public void setChildTransferInProgress(boolean childTransferInProgress) {
Assert.isMainThread();
mChildTransferInProgress = childTransferInProgress;
}
+ /**
+ * Set the remove notification listener
+ * @param listener callback for notification removed
+ */
+ public void setOnNotificationRemovedListener(OnNotificationRemovedListener listener) {
+ mOnNotificationRemovedListener = listener;
+ }
+
@ShadeViewRefactor(RefactorComponent.SHADE_VIEW)
@Override
public void onViewRemoved(View child) {
super.onViewRemoved(child);
// we only call our internal methods if this is actually a removal and not just a
// notification which becomes a child notification
+ ExpandableView expandableView = (ExpandableView) child;
if (!mChildTransferInProgress) {
- onViewRemovedInternal((ExpandableView) child, this);
+ onViewRemovedInternal(expandableView, this);
+ }
+ if (mOnNotificationRemovedListener != null) {
+ mOnNotificationRemovedListener.onNotificationRemoved(
+ expandableView,
+ mChildTransferInProgress);
}
}
@@ -2998,8 +3018,10 @@
mAnimateNextSectionBoundsChange = false;
}
mAmbientState.setLastVisibleBackgroundChild(lastChild);
- // TODO: Refactor SectionManager and put the RoundnessManager there.
- mController.getNotificationRoundnessManager().updateRoundedChildren(mSections);
+ if (!mUseRoundnessSourceTypes) {
+ // TODO: Refactor SectionManager and put the RoundnessManager there.
+ mController.getNotificationRoundnessManager().updateRoundedChildren(mSections);
+ }
mAnimateBottomOnLayout = false;
invalidate();
}
@@ -3206,7 +3228,7 @@
// Only animate if we still have pinned heads up, otherwise we just have the
// regular collapse animation of the lock screen
|| (mKeyguardBypassEnabled && onKeyguard()
- && mInHeadsUpPinnedMode);
+ && mInHeadsUpPinnedMode);
if (performDisappearAnimation && !isHeadsUp) {
type = row.wasJustClicked()
? AnimationEvent.ANIMATION_TYPE_HEADS_UP_DISAPPEAR_CLICK
@@ -3351,7 +3373,7 @@
AnimationEvent animEvent = duration == null
? new AnimationEvent(child, AnimationEvent.ANIMATION_TYPE_CHANGE_POSITION)
: new AnimationEvent(
- child, AnimationEvent.ANIMATION_TYPE_CHANGE_POSITION, duration);
+ child, AnimationEvent.ANIMATION_TYPE_CHANGE_POSITION, duration);
mAnimationEvents.add(animEvent);
}
mChildrenChangingPositions.clear();
@@ -3442,7 +3464,9 @@
@ShadeViewRefactor(RefactorComponent.LAYOUT_ALGORITHM)
protected StackScrollAlgorithm createStackScrollAlgorithm(Context context) {
- return new StackScrollAlgorithm(context, this);
+ StackScrollAlgorithm stackScrollAlgorithm = new StackScrollAlgorithm(context, this);
+ stackScrollAlgorithm.useRoundnessSourceTypes(mUseRoundnessSourceTypes);
+ return stackScrollAlgorithm;
}
/**
@@ -3772,7 +3796,7 @@
}
private void logEmptySpaceClick(MotionEvent ev, boolean isTouchBelowLastNotification,
- int statusBarState, boolean touchIsClick) {
+ int statusBarState, boolean touchIsClick) {
if (mLogger == null) {
return;
}
@@ -3957,7 +3981,9 @@
mOnEmptySpaceClickListener = listener;
}
- /** @hide */
+ /**
+ * @hide
+ */
@Override
@ShadeViewRefactor(RefactorComponent.INPUT)
public boolean performAccessibilityActionInternal(int action, Bundle arguments) {
@@ -4728,7 +4754,9 @@
return touchY > mTopPadding + mStackTranslation;
}
- /** @hide */
+ /**
+ * @hide
+ */
@Override
@ShadeViewRefactor(RefactorComponent.SHADE_VIEW)
public void onInitializeAccessibilityEventInternal(AccessibilityEvent event) {
@@ -5353,7 +5381,9 @@
return canChildBeCleared(row) && matchesSelection(row, selection);
}
- /** Register a {@link View.OnClickListener} to be invoked when the Manage button is clicked. */
+ /**
+ * Register a {@link View.OnClickListener} to be invoked when the Manage button is clicked.
+ */
public void setManageButtonClickListener(@Nullable OnClickListener listener) {
mManageButtonClickListener = listener;
if (mFooterView != null) {
@@ -5418,6 +5448,7 @@
/**
* Set how far the wake up is when waking up from pulsing. This is a height and will adjust the
* notification positions accordingly.
+ *
* @param height the new wake up height
* @return the overflow how much the height is further than he lowest notification
*/
@@ -5649,7 +5680,7 @@
* Set rounded rect clipping bounds on this view.
*/
public void setRoundedClippingBounds(int left, int top, int right, int bottom, int topRadius,
- int bottomRadius) {
+ int bottomRadius) {
if (mRoundedRectClippingLeft == left && mRoundedRectClippingRight == right
&& mRoundedRectClippingBottom == bottom && mRoundedRectClippingTop == top
&& mBgCornerRadii[0] == topRadius && mBgCornerRadii[5] == bottomRadius) {
@@ -5710,7 +5741,7 @@
mLaunchingNotification = launching;
mLaunchingNotificationNeedsToBeClipped = mLaunchAnimationParams != null
&& (mLaunchAnimationParams.getStartRoundedTopClipping() > 0
- || mLaunchAnimationParams.getParentStartRoundedTopClipping() > 0);
+ || mLaunchAnimationParams.getParentStartRoundedTopClipping() > 0);
if (!mLaunchingNotificationNeedsToBeClipped || !mLaunchingNotification) {
mLaunchedNotificationClipPath.reset();
}
@@ -5748,7 +5779,7 @@
mLaunchAnimationParams.getProgress(0,
NotificationLaunchAnimatorController.ANIMATION_DURATION_TOP_ROUNDING));
int top = (int) Math.min(MathUtils.lerp(mRoundedRectClippingTop,
- mLaunchAnimationParams.getTop(), expandProgress),
+ mLaunchAnimationParams.getTop(), expandProgress),
mRoundedRectClippingTop);
float topRadius = mLaunchAnimationParams.getTopCornerRadius();
float bottomRadius = mLaunchAnimationParams.getBottomCornerRadius();
@@ -5872,25 +5903,25 @@
@ShadeViewRefactor(RefactorComponent.SHADE_VIEW)
public interface OnOverscrollTopChangedListener {
- /**
- * Notifies a listener that the overscroll has changed.
- *
- * @param amount the amount of overscroll, in pixels
- * @param isRubberbanded if true, this is a rubberbanded overscroll; if false, this is an
- * unrubberbanded motion to directly expand overscroll view (e.g
- * expand
- * QS)
- */
- void onOverscrollTopChanged(float amount, boolean isRubberbanded);
+ /**
+ * Notifies a listener that the overscroll has changed.
+ *
+ * @param amount the amount of overscroll, in pixels
+ * @param isRubberbanded if true, this is a rubberbanded overscroll; if false, this is an
+ * unrubberbanded motion to directly expand overscroll view (e.g
+ * expand
+ * QS)
+ */
+ void onOverscrollTopChanged(float amount, boolean isRubberbanded);
- /**
- * Notify a listener that the scroller wants to escape from the scrolling motion and
- * start a fling animation to the expanded or collapsed overscroll view (e.g expand the QS)
- *
- * @param velocity The velocity that the Scroller had when over flinging
- * @param open Should the fling open or close the overscroll view.
- */
- void flingTopOverscroll(float velocity, boolean open);
+ /**
+ * Notify a listener that the scroller wants to escape from the scrolling motion and
+ * start a fling animation to the expanded or collapsed overscroll view (e.g expand the QS)
+ *
+ * @param velocity The velocity that the Scroller had when over flinging
+ * @param open Should the fling open or close the overscroll view.
+ */
+ void flingTopOverscroll(float velocity, boolean open);
}
@ShadeViewRefactor(RefactorComponent.SHADE_VIEW)
@@ -6252,7 +6283,9 @@
}
};
- public HeadsUpTouchHelper.Callback getHeadsUpCallback() { return mHeadsUpCallback; }
+ public HeadsUpTouchHelper.Callback getHeadsUpCallback() {
+ return mHeadsUpCallback;
+ }
void onGroupExpandChanged(ExpandableNotificationRow changedRow, boolean expanded) {
boolean animated = mAnimationsEnabled && (mIsExpanded || changedRow.isPinned());
@@ -6357,15 +6390,25 @@
return mLastSentExpandedHeight;
}
- /** Enum for selecting some or all notification rows (does not included non-notif views). */
+ /**
+ * Enum for selecting some or all notification rows (does not included non-notif views).
+ */
@Retention(SOURCE)
@IntDef({ROWS_ALL, ROWS_HIGH_PRIORITY, ROWS_GENTLE})
- @interface SelectedRows {}
- /** All rows representing notifs. */
+ @interface SelectedRows {
+ }
+
+ /**
+ * All rows representing notifs.
+ */
public static final int ROWS_ALL = 0;
- /** Only rows where entry.isHighPriority() is true. */
+ /**
+ * Only rows where entry.isHighPriority() is true.
+ */
public static final int ROWS_HIGH_PRIORITY = 1;
- /** Only rows where entry.isHighPriority() is false. */
+ /**
+ * Only rows where entry.isHighPriority() is false.
+ */
public static final int ROWS_GENTLE = 2;
interface ClearAllListener {
@@ -6380,4 +6423,16 @@
void onAnimationEnd(
List<ExpandableNotificationRow> viewsToRemove, @SelectedRows int selectedRows);
}
+
+ /**
+ *
+ */
+ public interface OnNotificationRemovedListener {
+ /**
+ *
+ * @param child
+ * @param isTransferInProgress
+ */
+ void onNotificationRemoved(ExpandableView child, boolean isTransferInProgress);
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
index 2d6d0a9c..4bcc0b6 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
@@ -182,10 +182,12 @@
private NotificationStackScrollLayout mView;
private boolean mFadeNotificationsOnDismiss;
private NotificationSwipeHelper mSwipeHelper;
- @Nullable private Boolean mHistoryEnabled;
+ @Nullable
+ private Boolean mHistoryEnabled;
private int mBarState;
private HeadsUpAppearanceController mHeadsUpAppearanceController;
private final FeatureFlags mFeatureFlags;
+ private final boolean mUseRoundnessSourceTypes;
private final NotificationTargetsHelper mNotificationTargetsHelper;
private View mLongPressedView;
@@ -391,7 +393,7 @@
if (item != null) {
Point origin = provider.getRevealAnimationOrigin();
mNotificationGutsManager.openGuts(row, origin.x, origin.y, item);
- } else {
+ } else {
Log.e(TAG, "Provider has shouldShowGutsOnSnapOpen, but provided no "
+ "menu item in menuItemtoExposeOnSnap. Skipping.");
}
@@ -420,7 +422,7 @@
@Override
public void onSnooze(StatusBarNotification sbn,
- NotificationSwipeActionHelper.SnoozeOption snoozeOption) {
+ NotificationSwipeActionHelper.SnoozeOption snoozeOption) {
mCentralSurfaces.setNotificationSnoozed(sbn, snoozeOption);
}
@@ -544,7 +546,7 @@
@Override
public boolean updateSwipeProgress(View animView, boolean dismissable,
- float swipeProgress) {
+ float swipeProgress) {
// Returning true prevents alpha fading.
return !mFadeNotificationsOnDismiss;
}
@@ -584,16 +586,22 @@
@Override
public void onHeadsUpPinned(NotificationEntry entry) {
- mNotificationRoundnessManager.updateView(entry.getRow(), false /* animate */);
+ if (!mUseRoundnessSourceTypes) {
+ mNotificationRoundnessManager.updateView(
+ entry.getRow(),
+ /* animate = */ false);
+ }
}
@Override
public void onHeadsUpUnPinned(NotificationEntry entry) {
- ExpandableNotificationRow row = entry.getRow();
- // update the roundedness posted, because we might be animating away the
- // headsup soon, so no need to set the roundedness to 0 and then back to 1.
- row.post(() -> mNotificationRoundnessManager.updateView(row,
- true /* animate */));
+ if (!mUseRoundnessSourceTypes) {
+ ExpandableNotificationRow row = entry.getRow();
+ // update the roundedness posted, because we might be animating away the
+ // headsup soon, so no need to set the roundedness to 0 and then back to 1.
+ row.post(() -> mNotificationRoundnessManager.updateView(row,
+ true /* animate */));
+ }
}
@Override
@@ -603,8 +611,10 @@
mView.setNumHeadsUp(numEntries);
mView.setTopHeadsUpEntry(topEntry);
generateHeadsUpAnimation(entry, isHeadsUp);
- ExpandableNotificationRow row = entry.getRow();
- mNotificationRoundnessManager.updateView(row, true /* animate */);
+ if (!mUseRoundnessSourceTypes) {
+ ExpandableNotificationRow row = entry.getRow();
+ mNotificationRoundnessManager.updateView(row, true /* animate */);
+ }
}
};
@@ -697,6 +707,7 @@
mSeenNotificationsProvider = seenNotificationsProvider;
mShadeController = shadeController;
mFeatureFlags = featureFlags;
+ mUseRoundnessSourceTypes = featureFlags.isEnabled(Flags.USE_ROUNDNESS_SOURCETYPES);
mNotificationTargetsHelper = notificationTargetsHelper;
updateResources();
}
@@ -763,8 +774,10 @@
mLockscreenUserManager.addUserChangedListener(mLockscreenUserChangeListener);
mFadeNotificationsOnDismiss = mFeatureFlags.isEnabled(Flags.NOTIFICATION_DISMISSAL_FADE);
- mNotificationRoundnessManager.setOnRoundingChangedCallback(mView::invalidate);
- mView.addOnExpandedHeightChangedListener(mNotificationRoundnessManager::setExpanded);
+ if (!mUseRoundnessSourceTypes) {
+ mNotificationRoundnessManager.setOnRoundingChangedCallback(mView::invalidate);
+ mView.addOnExpandedHeightChangedListener(mNotificationRoundnessManager::setExpanded);
+ }
mVisibilityLocationProviderDelegator.setDelegate(this::isInVisibleLocation);
@@ -997,9 +1010,11 @@
Log.wtf(TAG, "isHistoryEnabled failed to initialize its value");
return false;
}
- mHistoryEnabled = historyEnabled =
- Settings.Secure.getIntForUser(mView.getContext().getContentResolver(),
- Settings.Secure.NOTIFICATION_HISTORY_ENABLED, 0, UserHandle.USER_CURRENT) == 1;
+ mHistoryEnabled = historyEnabled = Settings.Secure.getIntForUser(
+ mView.getContext().getContentResolver(),
+ Settings.Secure.NOTIFICATION_HISTORY_ENABLED,
+ 0,
+ UserHandle.USER_CURRENT) == 1;
}
return historyEnabled;
}
@@ -1029,7 +1044,7 @@
}
public void setOverScrollAmount(float amount, boolean onTop, boolean animate,
- boolean cancelAnimators) {
+ boolean cancelAnimators) {
mView.setOverScrollAmount(amount, onTop, animate, cancelAnimators);
}
@@ -1207,7 +1222,7 @@
/**
* Update whether we should show the empty shade view ("no notifications" in the shade).
- *
+ * <p>
* When in split mode, notifications are always visible regardless of the state of the
* QuickSettings panel. That being the case, empty view is always shown if the other conditions
* are true.
@@ -1233,7 +1248,7 @@
/**
* @return true if {@link StatusBarStateController} is in transition to the KEYGUARD
- * and false otherwise.
+ * and false otherwise.
*/
private boolean isInTransitionToKeyguard() {
final int currentState = mStatusBarStateController.getState();
@@ -1265,7 +1280,9 @@
mView.setExpandedHeight(expandedHeight);
}
- /** Sets the QS header. Used to check if a touch is within its bounds. */
+ /**
+ * Sets the QS header. Used to check if a touch is within its bounds.
+ */
public void setQsHeader(ViewGroup view) {
mView.setQsHeader(view);
}
@@ -1328,7 +1345,7 @@
public RemoteInputController.Delegate createDelegate() {
return new RemoteInputController.Delegate() {
public void setRemoteInputActive(NotificationEntry entry,
- boolean remoteInputActive) {
+ boolean remoteInputActive) {
mHeadsUpManager.setRemoteInputActive(entry, remoteInputActive);
entry.notifyHeightChanged(true /* needsAnimation */);
updateFooter();
@@ -1459,7 +1476,7 @@
}
private void onAnimationEnd(List<ExpandableNotificationRow> viewsToRemove,
- @SelectedRows int selectedRows) {
+ @SelectedRows int selectedRows) {
if (selectedRows == ROWS_ALL) {
mNotifCollection.dismissAllNotifications(
mLockscreenUserManager.getCurrentUserId());
@@ -1502,8 +1519,8 @@
/**
* @return the inset during the full shade transition, that needs to be added to the position
- * of the quick settings edge. This is relevant for media, that is transitioning
- * from the keyguard host to the quick settings one.
+ * of the quick settings edge. This is relevant for media, that is transitioning
+ * from the keyguard host to the quick settings one.
*/
public int getFullShadeTransitionInset() {
MediaContainerView view = mKeyguardMediaController.getSinglePaneContainer();
@@ -1517,10 +1534,10 @@
/**
* @param fraction The fraction of lockscreen to shade transition.
* 0f for all other states.
- *
- * Once the lockscreen to shade transition completes and the shade is 100% open,
- * LockscreenShadeTransitionController resets amount and fraction to 0, where they remain
- * until the next lockscreen-to-shade transition.
+ * <p>
+ * Once the lockscreen to shade transition completes and the shade is 100% open,
+ * LockscreenShadeTransitionController resets amount and fraction to 0, where
+ * they remain until the next lockscreen-to-shade transition.
*/
public void setTransitionToFullShadeAmount(float fraction) {
mView.setFractionToShade(fraction);
@@ -1533,7 +1550,9 @@
mView.setExtraTopInsetForFullShadeTransition(overScrollAmount);
}
- /** */
+ /**
+ *
+ */
public void setWillExpand(boolean willExpand) {
mView.setWillExpand(willExpand);
}
@@ -1549,7 +1568,7 @@
* Set rounded rect clipping bounds on this view.
*/
public void setRoundedClippingBounds(int left, int top, int right, int bottom, int topRadius,
- int bottomRadius) {
+ int bottomRadius) {
mView.setRoundedClippingBounds(left, top, right, bottom, topRadius, bottomRadius);
}
@@ -1569,6 +1588,15 @@
}
/**
+ * Set the remove notification listener
+ * @param listener callback for notification removed
+ */
+ public void setOnNotificationRemovedListener(
+ NotificationStackScrollLayout.OnNotificationRemovedListener listener) {
+ mView.setOnNotificationRemovedListener(listener);
+ }
+
+ /**
* Enum for UiEvent logged from this class
*/
enum NotificationPanelEvent implements UiEventLogger.UiEventEnum {
@@ -1578,10 +1606,13 @@
@UiEvent(doc = "User dismissed all silent notifications from notification panel.")
DISMISS_SILENT_NOTIFICATIONS_PANEL(314);
private final int mId;
+
NotificationPanelEvent(int id) {
mId = id;
}
- @Override public int getId() {
+
+ @Override
+ public int getId() {
return mId;
}
@@ -1722,8 +1753,12 @@
@Override
public void bindRow(ExpandableNotificationRow row) {
row.setHeadsUpAnimatingAwayListener(animatingAway -> {
- mNotificationRoundnessManager.updateView(row, false);
- mHeadsUpAppearanceController.updateHeader(row.getEntry());
+ if (!mUseRoundnessSourceTypes) {
+ mNotificationRoundnessManager.updateView(row, false);
+ }
+ NotificationEntry entry = row.getEntry();
+ mHeadsUpAppearanceController.updateHeader(entry);
+ mHeadsUpAppearanceController.updateHeadsUpAndPulsingRoundness(entry);
});
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSwipeHelper.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSwipeHelper.java
index ee57411..aaf9300 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSwipeHelper.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSwipeHelper.java
@@ -20,6 +20,7 @@
import static com.android.internal.jank.InteractionJankMonitor.CUJ_NOTIFICATION_SHADE_ROW_SWIPE;
import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
import android.animation.ValueAnimator;
import android.content.res.Resources;
import android.graphics.Rect;
@@ -34,9 +35,11 @@
import com.android.systemui.SwipeHelper;
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.flags.FeatureFlags;
+import com.android.systemui.flags.Flags;
import com.android.systemui.plugins.FalsingManager;
import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin;
import com.android.systemui.plugins.statusbar.NotificationSwipeActionHelper;
+import com.android.systemui.statusbar.notification.SourceType;
import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
import com.android.systemui.statusbar.notification.row.ExpandableView;
@@ -49,6 +52,7 @@
@VisibleForTesting
protected static final long COVER_MENU_DELAY = 4000;
private static final String TAG = "NotificationSwipeHelper";
+ private static final SourceType SWIPE_DISMISS = SourceType.from("SwipeDismiss");
private final Runnable mFalsingCheck;
private View mTranslatingParentView;
private View mMenuExposedView;
@@ -64,13 +68,21 @@
private WeakReference<NotificationMenuRowPlugin> mCurrMenuRowRef;
private boolean mIsExpanded;
private boolean mPulsing;
+ private final NotificationRoundnessManager mNotificationRoundnessManager;
+ private final boolean mUseRoundnessSourceTypes;
NotificationSwipeHelper(
- Resources resources, ViewConfiguration viewConfiguration,
- FalsingManager falsingManager, FeatureFlags featureFlags,
- int swipeDirection, NotificationCallback callback,
- NotificationMenuRowPlugin.OnMenuEventListener menuListener) {
+ Resources resources,
+ ViewConfiguration viewConfiguration,
+ FalsingManager falsingManager,
+ FeatureFlags featureFlags,
+ int swipeDirection,
+ NotificationCallback callback,
+ NotificationMenuRowPlugin.OnMenuEventListener menuListener,
+ NotificationRoundnessManager notificationRoundnessManager) {
super(swipeDirection, callback, resources, viewConfiguration, falsingManager, featureFlags);
+ mNotificationRoundnessManager = notificationRoundnessManager;
+ mUseRoundnessSourceTypes = featureFlags.isEnabled(Flags.USE_ROUNDNESS_SOURCETYPES);
mMenuListener = menuListener;
mCallback = callback;
mFalsingCheck = () -> resetExposedMenuView(true /* animate */, true /* force */);
@@ -304,6 +316,33 @@
handleMenuCoveredOrDismissed();
}
+ @Override
+ protected void prepareDismissAnimation(View view, Animator anim) {
+ super.prepareDismissAnimation(view, anim);
+
+ if (mUseRoundnessSourceTypes
+ && view instanceof ExpandableNotificationRow
+ && mNotificationRoundnessManager.isClearAllInProgress()) {
+ ExpandableNotificationRow row = (ExpandableNotificationRow) view;
+ anim.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationStart(Animator animation) {
+ row.requestRoundness(/* top = */ 1f, /* bottom = */ 1f, SWIPE_DISMISS);
+ }
+
+ @Override
+ public void onAnimationCancel(Animator animation) {
+ row.requestRoundnessReset(SWIPE_DISMISS);
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ row.requestRoundnessReset(SWIPE_DISMISS);
+ }
+ });
+ }
+ }
+
@VisibleForTesting
protected void superDismissChild(final View view, float velocity, boolean useAccelerateInterpolator) {
super.dismissChild(view, velocity, useAccelerateInterpolator);
@@ -521,14 +560,17 @@
private int mSwipeDirection;
private NotificationCallback mNotificationCallback;
private NotificationMenuRowPlugin.OnMenuEventListener mOnMenuEventListener;
+ private NotificationRoundnessManager mNotificationRoundnessManager;
@Inject
Builder(@Main Resources resources, ViewConfiguration viewConfiguration,
- FalsingManager falsingManager, FeatureFlags featureFlags) {
+ FalsingManager falsingManager, FeatureFlags featureFlags,
+ NotificationRoundnessManager notificationRoundnessManager) {
mResources = resources;
mViewConfiguration = viewConfiguration;
mFalsingManager = falsingManager;
mFeatureFlags = featureFlags;
+ mNotificationRoundnessManager = notificationRoundnessManager;
}
Builder setSwipeDirection(int swipeDirection) {
@@ -549,7 +591,8 @@
NotificationSwipeHelper build() {
return new NotificationSwipeHelper(mResources, mViewConfiguration, mFalsingManager,
- mFeatureFlags, mSwipeDirection, mNotificationCallback, mOnMenuEventListener);
+ mFeatureFlags, mSwipeDirection, mNotificationCallback, mOnMenuEventListener,
+ mNotificationRoundnessManager);
}
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationTargetsHelper.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationTargetsHelper.kt
index 991a14b..548d1a1 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationTargetsHelper.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationTargetsHelper.kt
@@ -20,8 +20,7 @@
constructor(
featureFlags: FeatureFlags,
) {
- private val isNotificationGroupCornerEnabled =
- featureFlags.isEnabled(Flags.NOTIFICATION_GROUP_CORNER)
+ private val useRoundnessSourceTypes = featureFlags.isEnabled(Flags.USE_ROUNDNESS_SOURCETYPES)
/**
* This method looks for views that can be rounded (and implement [Roundable]) during a
@@ -48,7 +47,7 @@
if (notificationParent != null && childrenContainer != null) {
// We are inside a notification group
- if (!isNotificationGroupCornerEnabled) {
+ if (!useRoundnessSourceTypes) {
return RoundableTargets(null, null, null)
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java
index d8c6878..aff7b4c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java
@@ -31,6 +31,7 @@
import com.android.systemui.animation.ShadeInterpolation;
import com.android.systemui.statusbar.EmptyShadeView;
import com.android.systemui.statusbar.NotificationShelf;
+import com.android.systemui.statusbar.notification.LegacySourceType;
import com.android.systemui.statusbar.notification.SourceType;
import com.android.systemui.statusbar.notification.row.ActivatableNotificationView;
import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
@@ -51,6 +52,7 @@
private static final String TAG = "StackScrollAlgorithm";
private static final Boolean DEBUG = false;
+ private static final SourceType STACK_SCROLL_ALGO = SourceType.from("StackScrollAlgorithm");
private final ViewGroup mHostView;
private float mPaddingBetweenElements;
@@ -70,6 +72,7 @@
private float mQuickQsOffsetHeight;
private float mSmallCornerRadius;
private float mLargeCornerRadius;
+ private boolean mUseRoundnessSourceTypes;
public StackScrollAlgorithm(
Context context,
@@ -129,7 +132,7 @@
}
private void updateAlphaState(StackScrollAlgorithmState algorithmState,
- AmbientState ambientState) {
+ AmbientState ambientState) {
for (ExpandableView view : algorithmState.visibleChildren) {
final ViewState viewState = view.getViewState();
@@ -229,7 +232,7 @@
}
private void getNotificationChildrenStates(StackScrollAlgorithmState algorithmState,
- AmbientState ambientState) {
+ AmbientState ambientState) {
int childCount = algorithmState.visibleChildren.size();
for (int i = 0; i < childCount; i++) {
ExpandableView v = algorithmState.visibleChildren.get(i);
@@ -241,7 +244,7 @@
}
private void updateSpeedBumpState(StackScrollAlgorithmState algorithmState,
- int speedBumpIndex) {
+ int speedBumpIndex) {
int childCount = algorithmState.visibleChildren.size();
int belowSpeedBump = speedBumpIndex;
for (int i = 0; i < childCount; i++) {
@@ -268,7 +271,7 @@
}
private void updateClipping(StackScrollAlgorithmState algorithmState,
- AmbientState ambientState) {
+ AmbientState ambientState) {
float drawStart = ambientState.isOnKeyguard() ? 0
: ambientState.getStackY() - ambientState.getScrollY();
float clipStart = 0;
@@ -314,7 +317,7 @@
* Updates the dimmed, activated and hiding sensitive states of the children.
*/
private void updateDimmedActivatedHideSensitive(AmbientState ambientState,
- StackScrollAlgorithmState algorithmState) {
+ StackScrollAlgorithmState algorithmState) {
boolean dimmed = ambientState.isDimmed();
boolean hideSensitive = ambientState.isHideSensitive();
View activatedChild = ambientState.getActivatedChild();
@@ -408,7 +411,7 @@
}
private int updateNotGoneIndex(StackScrollAlgorithmState state, int notGoneIndex,
- ExpandableView v) {
+ ExpandableView v) {
ExpandableViewState viewState = v.getViewState();
viewState.notGoneIndex = notGoneIndex;
state.visibleChildren.add(v);
@@ -434,7 +437,7 @@
* @param ambientState The current ambient state
*/
protected void updatePositionsForState(StackScrollAlgorithmState algorithmState,
- AmbientState ambientState) {
+ AmbientState ambientState) {
if (!ambientState.isOnKeyguard()
|| (ambientState.isBypassEnabled() && ambientState.isPulseExpanding())) {
algorithmState.mCurrentYPosition += mNotificationScrimPadding;
@@ -448,7 +451,7 @@
}
private void setLocation(ExpandableViewState expandableViewState, float currentYPosition,
- int i) {
+ int i) {
expandableViewState.location = ExpandableViewState.LOCATION_MAIN_AREA;
if (currentYPosition <= 0) {
expandableViewState.location = ExpandableViewState.LOCATION_HIDDEN_TOP;
@@ -496,9 +499,13 @@
}
@VisibleForTesting
- void maybeUpdateHeadsUpIsVisible(ExpandableViewState viewState, boolean isShadeExpanded,
- boolean mustStayOnScreen, boolean topVisible, float viewEnd, float hunMax) {
-
+ void maybeUpdateHeadsUpIsVisible(
+ ExpandableViewState viewState,
+ boolean isShadeExpanded,
+ boolean mustStayOnScreen,
+ boolean topVisible,
+ float viewEnd,
+ float hunMax) {
if (isShadeExpanded && mustStayOnScreen && topVisible) {
viewState.headsUpIsVisible = viewEnd < hunMax;
}
@@ -676,7 +683,7 @@
}
private void updatePulsingStates(StackScrollAlgorithmState algorithmState,
- AmbientState ambientState) {
+ AmbientState ambientState) {
int childCount = algorithmState.visibleChildren.size();
for (int i = 0; i < childCount; i++) {
View child = algorithmState.visibleChildren.get(i);
@@ -693,7 +700,7 @@
}
private void updateHeadsUpStates(StackScrollAlgorithmState algorithmState,
- AmbientState ambientState) {
+ AmbientState ambientState) {
int childCount = algorithmState.visibleChildren.size();
// Move the tracked heads up into position during the appear animation, by interpolating
@@ -777,7 +784,7 @@
*/
@VisibleForTesting
void clampHunToTop(float quickQsOffsetHeight, float stackTranslation, float collapsedHeight,
- ExpandableViewState viewState) {
+ ExpandableViewState viewState) {
final float newTranslation = Math.max(quickQsOffsetHeight + stackTranslation,
viewState.getYTranslation());
@@ -792,7 +799,7 @@
// Pin HUN to bottom of expanded QS
// while the rest of notifications are scrolled offscreen.
private void clampHunToMaxTranslation(AmbientState ambientState, ExpandableNotificationRow row,
- ExpandableViewState childState) {
+ ExpandableViewState childState) {
float maxHeadsUpTranslation = ambientState.getMaxHeadsUpTranslation();
final float maxShelfPosition = ambientState.getInnerHeight() + ambientState.getTopPadding()
+ ambientState.getStackTranslation();
@@ -807,14 +814,19 @@
// Animate pinned HUN bottom corners to and from original roundness.
final float originalCornerRadius =
row.isLastInSection() ? 1f : (mSmallCornerRadius / mLargeCornerRadius);
- final float roundness = computeCornerRoundnessForPinnedHun(mHostView.getHeight(),
+ final float bottomValue = computeCornerRoundnessForPinnedHun(mHostView.getHeight(),
ambientState.getStackY(), getMaxAllowedChildHeight(row), originalCornerRadius);
- row.requestBottomRoundness(roundness, /* animate = */ false, SourceType.OnScroll);
+ if (mUseRoundnessSourceTypes) {
+ row.requestBottomRoundness(bottomValue, STACK_SCROLL_ALGO);
+ row.addOnDetachResetRoundness(STACK_SCROLL_ALGO);
+ } else {
+ row.requestBottomRoundness(bottomValue, LegacySourceType.OnScroll);
+ }
}
@VisibleForTesting
float computeCornerRoundnessForPinnedHun(float hostViewHeight, float stackY,
- float viewMaxHeight, float originalCornerRadius) {
+ float viewMaxHeight, float originalCornerRadius) {
// Compute y where corner roundness should be in its original unpinned state.
// We use view max height because the pinned collapsed HUN expands to max height
@@ -844,7 +856,7 @@
* @param ambientState The ambient state of the algorithm
*/
private void updateZValuesForState(StackScrollAlgorithmState algorithmState,
- AmbientState ambientState) {
+ AmbientState ambientState) {
int childCount = algorithmState.visibleChildren.size();
float childrenOnTop = 0.0f;
@@ -876,9 +888,9 @@
* previous HUNs whose Z positions are greater than 0.
*/
protected float updateChildZValue(int i, float childrenOnTop,
- StackScrollAlgorithmState algorithmState,
- AmbientState ambientState,
- boolean isTopHun) {
+ StackScrollAlgorithmState algorithmState,
+ AmbientState ambientState,
+ boolean isTopHun) {
ExpandableView child = algorithmState.visibleChildren.get(i);
ExpandableViewState childViewState = child.getViewState();
float baseZ = ambientState.getBaseZHeight();
@@ -950,6 +962,14 @@
this.mIsExpanded = isExpanded;
}
+ /**
+ * Enable the support for rounded corner based on the SourceType
+ * @param enabled true if is supported
+ */
+ public void useRoundnessSourceTypes(boolean enabled) {
+ mUseRoundnessSourceTypes = enabled;
+ }
+
public static class StackScrollAlgorithmState {
/**
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpAppearanceController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpAppearanceController.java
index 484441a..c217ab3 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpAppearanceController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpAppearanceController.java
@@ -19,11 +19,14 @@
import static com.android.systemui.statusbar.phone.fragment.dagger.StatusBarFragmentModule.OPERATOR_NAME_FRAME_VIEW;
import android.graphics.Rect;
+import android.util.MathUtils;
import android.view.View;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.widget.ViewClippingUtil;
import com.android.systemui.R;
+import com.android.systemui.flags.FeatureFlags;
+import com.android.systemui.flags.Flags;
import com.android.systemui.plugins.DarkIconDispatcher;
import com.android.systemui.plugins.statusbar.StatusBarStateController;
import com.android.systemui.shade.NotificationPanelViewController;
@@ -32,8 +35,10 @@
import com.android.systemui.statusbar.HeadsUpStatusBarView;
import com.android.systemui.statusbar.StatusBarState;
import com.android.systemui.statusbar.notification.NotificationWakeUpCoordinator;
+import com.android.systemui.statusbar.notification.SourceType;
import com.android.systemui.statusbar.notification.collection.NotificationEntry;
import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
+import com.android.systemui.statusbar.notification.stack.NotificationRoundnessManager;
import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController;
import com.android.systemui.statusbar.phone.fragment.dagger.StatusBarFragmentScope;
import com.android.systemui.statusbar.policy.Clock;
@@ -51,6 +56,7 @@
/**
* Controls the appearance of heads up notifications in the icon area and the header itself.
+ * It also controls the roundness of the heads up notifications and the pulsing notifications.
*/
@StatusBarFragmentScope
public class HeadsUpAppearanceController extends ViewController<HeadsUpStatusBarView>
@@ -59,12 +65,17 @@
NotificationWakeUpCoordinator.WakeUpListener {
public static final int CONTENT_FADE_DURATION = 110;
public static final int CONTENT_FADE_DELAY = 100;
+
+ private static final SourceType HEADS_UP = SourceType.from("HeadsUp");
+ private static final SourceType PULSING = SourceType.from("Pulsing");
private final NotificationIconAreaController mNotificationIconAreaController;
private final HeadsUpManagerPhone mHeadsUpManager;
private final NotificationStackScrollLayoutController mStackScrollerController;
private final DarkIconDispatcher mDarkIconDispatcher;
private final NotificationPanelViewController mNotificationPanelViewController;
+ private final NotificationRoundnessManager mNotificationRoundnessManager;
+ private final boolean mUseRoundnessSourceTypes;
private final Consumer<ExpandableNotificationRow>
mSetTrackingHeadsUp = this::setTrackingHeadsUp;
private final BiConsumer<Float, Float> mSetExpandedHeight = this::setAppearFraction;
@@ -105,11 +116,15 @@
CommandQueue commandQueue,
NotificationStackScrollLayoutController stackScrollerController,
NotificationPanelViewController notificationPanelViewController,
+ NotificationRoundnessManager notificationRoundnessManager,
+ FeatureFlags featureFlags,
HeadsUpStatusBarView headsUpStatusBarView,
Clock clockView,
@Named(OPERATOR_NAME_FRAME_VIEW) Optional<View> operatorNameViewOptional) {
super(headsUpStatusBarView);
mNotificationIconAreaController = notificationIconAreaController;
+ mNotificationRoundnessManager = notificationRoundnessManager;
+ mUseRoundnessSourceTypes = featureFlags.isEnabled(Flags.USE_ROUNDNESS_SOURCETYPES);
mHeadsUpManager = headsUpManager;
// We may be mid-HUN-expansion when this controller is re-created (for example, if the user
@@ -179,6 +194,7 @@
public void onHeadsUpPinned(NotificationEntry entry) {
updateTopEntry();
updateHeader(entry);
+ updateHeadsUpAndPulsingRoundness(entry);
}
private void updateTopEntry() {
@@ -316,6 +332,7 @@
public void onHeadsUpUnPinned(NotificationEntry entry) {
updateTopEntry();
updateHeader(entry);
+ updateHeadsUpAndPulsingRoundness(entry);
}
public void setAppearFraction(float expandedHeight, float appearFraction) {
@@ -346,7 +363,9 @@
ExpandableNotificationRow previousTracked = mTrackedChild;
mTrackedChild = trackedChild;
if (previousTracked != null) {
- updateHeader(previousTracked.getEntry());
+ NotificationEntry entry = previousTracked.getEntry();
+ updateHeader(entry);
+ updateHeadsUpAndPulsingRoundness(entry);
}
}
@@ -357,6 +376,7 @@
private void updateHeadsUpHeaders() {
mHeadsUpManager.getAllEntries().forEach(entry -> {
updateHeader(entry);
+ updateHeadsUpAndPulsingRoundness(entry);
});
}
@@ -370,6 +390,31 @@
row.setHeaderVisibleAmount(headerVisibleAmount);
}
+ /**
+ * Update the HeadsUp and the Pulsing roundness based on current state
+ * @param entry target notification
+ */
+ public void updateHeadsUpAndPulsingRoundness(NotificationEntry entry) {
+ if (mUseRoundnessSourceTypes) {
+ ExpandableNotificationRow row = entry.getRow();
+ boolean isTrackedChild = row == mTrackedChild;
+ if (row.isPinned() || row.isHeadsUpAnimatingAway() || isTrackedChild) {
+ float roundness = MathUtils.saturate(1f - mAppearFraction);
+ row.requestRoundness(roundness, roundness, HEADS_UP);
+ } else {
+ row.requestRoundnessReset(HEADS_UP);
+ }
+ if (mNotificationRoundnessManager.shouldRoundNotificationPulsing()) {
+ if (row.showingPulsing()) {
+ row.requestRoundness(/* top = */ 1f, /* bottom = */ 1f, PULSING);
+ } else {
+ row.requestRoundnessReset(PULSING);
+ }
+ }
+ }
+ }
+
+
@Override
public void onDarkChanged(ArrayList<Rect> areas, float darkIntensity, int tint) {
mView.onDarkChanged(areas, darkIntensity, tint);
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..e7afc50 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
@@ -29,6 +30,7 @@
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,8 +56,7 @@
@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
@@ -63,15 +66,18 @@
impl: MobileConnectionsRepositoryImpl
): 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/ui/MobileUiAdapter.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/MobileUiAdapter.kt
index c7e0ce1..d9487bf 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,9 +52,9 @@
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 }
@@ -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/wifi/ui/WifiUiAdapter.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/WifiUiAdapter.kt
index b816364..5223760 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/WifiUiAdapter.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/WifiUiAdapter.kt
@@ -24,6 +24,7 @@
import com.android.systemui.statusbar.phone.StatusBarIconController
import com.android.systemui.statusbar.phone.StatusBarLocation
import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags
+import com.android.systemui.statusbar.pipeline.wifi.ui.model.WifiIcon
import com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel.LocationBasedWifiViewModel
import com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel.WifiViewModel
import javax.inject.Inject
@@ -73,7 +74,9 @@
// Note that this flow may still run if
// [statusBarPipelineFlags.runNewWifiIconBackend] is true because we may
// want to get the logging data without rendering.
- if (wifiIcon != null && statusBarPipelineFlags.useNewWifiIcon()) {
+ if (
+ wifiIcon is WifiIcon.Visible && statusBarPipelineFlags.useNewWifiIcon()
+ ) {
iconController.setNewWifiIcon()
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/binder/WifiViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/binder/WifiViewBinder.kt
index 345f8cb..f5b5950 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/binder/WifiViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/binder/WifiViewBinder.kt
@@ -30,6 +30,7 @@
import com.android.systemui.statusbar.StatusBarIconView.STATE_DOT
import com.android.systemui.statusbar.StatusBarIconView.STATE_HIDDEN
import com.android.systemui.statusbar.StatusBarIconView.STATE_ICON
+import com.android.systemui.statusbar.pipeline.wifi.ui.model.WifiIcon
import com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel.LocationBasedWifiViewModel
import kotlinx.coroutines.InternalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
@@ -92,8 +93,10 @@
launch {
viewModel.wifiIcon.collect { wifiIcon ->
- view.isVisible = wifiIcon != null
- wifiIcon?.let { IconViewBinder.bind(wifiIcon, iconView) }
+ view.isVisible = wifiIcon is WifiIcon.Visible
+ if (wifiIcon is WifiIcon.Visible) {
+ IconViewBinder.bind(wifiIcon.icon, iconView)
+ }
}
}
@@ -135,7 +138,7 @@
return object : Binding {
override fun getShouldIconBeVisible(): Boolean {
- return viewModel.wifiIcon.value != null
+ return viewModel.wifiIcon.value is WifiIcon.Visible
}
override fun onVisibilityStateChanged(@StatusBarIconView.VisibleState state: Int) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/model/WifiIcon.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/model/WifiIcon.kt
new file mode 100644
index 0000000..e491d2b
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/model/WifiIcon.kt
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.wifi.ui.model
+
+import android.annotation.DrawableRes
+import com.android.systemui.common.shared.model.ContentDescription
+import com.android.systemui.common.shared.model.Icon
+import com.android.systemui.log.table.Diffable
+import com.android.systemui.log.table.TableRowLogger
+
+/** Represents the various states of the wifi icon. */
+sealed interface WifiIcon : Diffable<WifiIcon> {
+ /** Represents a wifi icon that should be hidden (not visible). */
+ object Hidden : WifiIcon {
+ override fun toString() = "hidden"
+ }
+
+ /**
+ * Represents a visible wifi icon that uses [res] as its image and [contentDescription] as its
+ * description.
+ */
+ class Visible(
+ @DrawableRes res: Int,
+ val contentDescription: ContentDescription.Loaded,
+ ) : WifiIcon {
+ val icon = Icon.Resource(res, contentDescription)
+
+ override fun toString() = contentDescription.description.toString()
+ }
+
+ override fun logDiffs(prevVal: WifiIcon, row: TableRowLogger) {
+ if (prevVal.toString() != toString()) {
+ row.logChange(COL_ICON, toString())
+ }
+ }
+
+ override fun logFull(row: TableRowLogger) {
+ row.logChange(COL_ICON, toString())
+ }
+}
+
+private const val COL_ICON = "wifiIcon"
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/HomeWifiViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/HomeWifiViewModel.kt
index 95ab251..a29c9b9 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/HomeWifiViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/HomeWifiViewModel.kt
@@ -17,8 +17,8 @@
package com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel
import android.graphics.Color
-import com.android.systemui.common.shared.model.Icon
import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags
+import com.android.systemui.statusbar.pipeline.wifi.ui.model.WifiIcon
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow
@@ -28,7 +28,7 @@
*/
class HomeWifiViewModel(
statusBarPipelineFlags: StatusBarPipelineFlags,
- wifiIcon: StateFlow<Icon.Resource?>,
+ wifiIcon: StateFlow<WifiIcon>,
isActivityInViewVisible: Flow<Boolean>,
isActivityOutViewVisible: Flow<Boolean>,
isActivityContainerVisible: Flow<Boolean>,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/KeyguardWifiViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/KeyguardWifiViewModel.kt
index 86535d6..1e190fb 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/KeyguardWifiViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/KeyguardWifiViewModel.kt
@@ -17,15 +17,15 @@
package com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel
import android.graphics.Color
-import com.android.systemui.common.shared.model.Icon
import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags
+import com.android.systemui.statusbar.pipeline.wifi.ui.model.WifiIcon
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow
/** A view model for the wifi icon shown on keyguard (lockscreen). */
class KeyguardWifiViewModel(
statusBarPipelineFlags: StatusBarPipelineFlags,
- wifiIcon: StateFlow<Icon.Resource?>,
+ wifiIcon: StateFlow<WifiIcon>,
isActivityInViewVisible: Flow<Boolean>,
isActivityOutViewVisible: Flow<Boolean>,
isActivityContainerVisible: Flow<Boolean>,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/LocationBasedWifiViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/LocationBasedWifiViewModel.kt
index 7cbdf5d..e35a8fe 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/LocationBasedWifiViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/LocationBasedWifiViewModel.kt
@@ -17,8 +17,8 @@
package com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel
import android.graphics.Color
-import com.android.systemui.common.shared.model.Icon
import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags
+import com.android.systemui.statusbar.pipeline.wifi.ui.model.WifiIcon
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.flowOf
@@ -33,8 +33,8 @@
statusBarPipelineFlags: StatusBarPipelineFlags,
debugTint: Int,
- /** The wifi icon that should be displayed. Null if we shouldn't display any icon. */
- val wifiIcon: StateFlow<Icon.Resource?>,
+ /** The wifi icon that should be displayed. */
+ val wifiIcon: StateFlow<WifiIcon>,
/** True if the activity in view should be visible. */
val isActivityInViewVisible: Flow<Boolean>,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/QsWifiViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/QsWifiViewModel.kt
index fd54c5f..18e62b2 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/QsWifiViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/QsWifiViewModel.kt
@@ -17,15 +17,15 @@
package com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel
import android.graphics.Color
-import com.android.systemui.common.shared.model.Icon
import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags
+import com.android.systemui.statusbar.pipeline.wifi.ui.model.WifiIcon
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow
/** A view model for the wifi icon shown in quick settings (when the shade is pulled down). */
class QsWifiViewModel(
statusBarPipelineFlags: StatusBarPipelineFlags,
- wifiIcon: StateFlow<Icon.Resource?>,
+ wifiIcon: StateFlow<WifiIcon>,
isActivityInViewVisible: Flow<Boolean>,
isActivityOutViewVisible: Flow<Boolean>,
isActivityContainerVisible: Flow<Boolean>,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModel.kt
index 0782bbb..ec7ba65 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModel.kt
@@ -17,16 +17,18 @@
package com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel
import android.content.Context
-import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import androidx.annotation.VisibleForTesting
import com.android.settingslib.AccessibilityContentDescriptions.WIFI_CONNECTION_STRENGTH
import com.android.settingslib.AccessibilityContentDescriptions.WIFI_NO_CONNECTION
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.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.statusbar.pipeline.dagger.WifiTableLog
+import com.android.systemui.statusbar.pipeline.wifi.ui.model.WifiIcon
+import com.android.systemui.log.table.TableLogBuffer
+import com.android.systemui.log.table.logDiffsForTable
import com.android.systemui.statusbar.connectivity.WifiIcons.WIFI_FULL_ICONS
import com.android.systemui.statusbar.connectivity.WifiIcons.WIFI_NO_INTERNET_ICONS
import com.android.systemui.statusbar.connectivity.WifiIcons.WIFI_NO_NETWORK
@@ -71,50 +73,39 @@
connectivityConstants: ConnectivityConstants,
private val context: Context,
logger: ConnectivityPipelineLogger,
+ @WifiTableLog wifiTableLogBuffer: TableLogBuffer,
interactor: WifiInteractor,
@Application private val scope: CoroutineScope,
statusBarPipelineFlags: StatusBarPipelineFlags,
wifiConstants: WifiConstants,
) {
- /**
- * Returns the drawable resource ID to use for the wifi icon based on the given network.
- * Null if we can't compute the icon.
- */
- @DrawableRes
- private fun WifiNetworkModel.iconResId(): Int? {
+ /** Returns the icon to use based on the given network. */
+ private fun WifiNetworkModel.icon(): WifiIcon {
return when (this) {
- is WifiNetworkModel.CarrierMerged -> null
- is WifiNetworkModel.Inactive -> WIFI_NO_NETWORK
- is WifiNetworkModel.Active ->
- when {
- this.level == null -> null
- this.isValidated -> WIFI_FULL_ICONS[this.level]
- else -> WIFI_NO_INTERNET_ICONS[this.level]
- }
- }
- }
-
- /**
- * Returns the content description for the wifi icon based on the given network.
- * Null if we can't compute the content description.
- */
- private fun WifiNetworkModel.contentDescription(): ContentDescription? {
- return when (this) {
- is WifiNetworkModel.CarrierMerged -> null
- is WifiNetworkModel.Inactive ->
+ is WifiNetworkModel.CarrierMerged -> WifiIcon.Hidden
+ is WifiNetworkModel.Inactive -> WifiIcon.Visible(
+ res = WIFI_NO_NETWORK,
ContentDescription.Loaded(
"${context.getString(WIFI_NO_CONNECTION)},${context.getString(NO_INTERNET)}"
)
+ )
is WifiNetworkModel.Active ->
when (this.level) {
- null -> null
+ null -> WifiIcon.Hidden
else -> {
val levelDesc = context.getString(WIFI_CONNECTION_STRENGTH[this.level])
when {
- this.isValidated -> ContentDescription.Loaded(levelDesc)
+ this.isValidated ->
+ WifiIcon.Visible(
+ WIFI_FULL_ICONS[this.level],
+ ContentDescription.Loaded(levelDesc)
+ )
else ->
- ContentDescription.Loaded(
- "$levelDesc,${context.getString(NO_INTERNET)}"
+ WifiIcon.Visible(
+ WIFI_NO_INTERNET_ICONS[this.level],
+ ContentDescription.Loaded(
+ "$levelDesc,${context.getString(NO_INTERNET)}"
+ )
)
}
}
@@ -122,8 +113,8 @@
}
}
- /** The wifi icon that should be displayed. Null if we shouldn't display any icon. */
- private val wifiIcon: StateFlow<Icon.Resource?> =
+ /** The wifi icon that should be displayed. */
+ private val wifiIcon: StateFlow<WifiIcon> =
combine(
interactor.isEnabled,
interactor.isDefault,
@@ -131,22 +122,29 @@
interactor.wifiNetwork,
) { isEnabled, isDefault, isForceHidden, wifiNetwork ->
if (!isEnabled || isForceHidden || wifiNetwork is WifiNetworkModel.CarrierMerged) {
- return@combine null
+ return@combine WifiIcon.Hidden
}
- val iconResId = wifiNetwork.iconResId() ?: return@combine null
- val icon = Icon.Resource(iconResId, wifiNetwork.contentDescription())
+ val icon = wifiNetwork.icon()
return@combine when {
isDefault -> icon
wifiConstants.alwaysShowIconIfEnabled -> icon
!connectivityConstants.hasDataCapabilities -> icon
wifiNetwork is WifiNetworkModel.Active && wifiNetwork.isValidated -> icon
- else -> null
+ else -> WifiIcon.Hidden
}
}
- .logOutputChange(logger, "icon") { icon -> icon?.contentDescription.toString() }
- .stateIn(scope, started = SharingStarted.WhileSubscribed(), initialValue = null)
+ .logDiffsForTable(
+ wifiTableLogBuffer,
+ columnPrefix = "",
+ initialValue = WifiIcon.Hidden,
+ )
+ .stateIn(
+ scope,
+ started = SharingStarted.WhileSubscribed(),
+ initialValue = WifiIcon.Hidden
+ )
/** The wifi activity status. Null if we shouldn't display the activity status. */
private val activity: Flow<WifiActivityModel?> =
diff --git a/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayController.kt b/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayController.kt
index a9d05d1..ea40208 100644
--- a/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayController.kt
+++ b/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayController.kt
@@ -105,8 +105,9 @@
*
* This method handles inflating and attaching the view, then delegates to [updateView] to
* display the correct information in the view.
+ * @param onViewTimeout a runnable that runs after the view timeout.
*/
- fun displayView(newInfo: T) {
+ fun displayView(newInfo: T, onViewTimeout: Runnable? = null) {
val currentDisplayInfo = displayInfo
// Update our list of active devices by removing it if necessary, then adding back at the
@@ -173,7 +174,10 @@
cancelViewTimeout?.run()
}
cancelViewTimeout = mainExecutor.executeDelayed(
- { removeView(id, REMOVAL_REASON_TIMEOUT) },
+ {
+ removeView(id, REMOVAL_REASON_TIMEOUT)
+ onViewTimeout?.run()
+ },
timeout.toLong()
)
}
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/src/com/android/systemui/wmshell/WMShell.java b/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java
index 02738d5..8ef98f0 100644
--- a/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java
+++ b/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java
@@ -253,6 +253,12 @@
splitScreen.onFinishedWakingUp();
}
});
+ mCommandQueue.addCallback(new CommandQueue.Callbacks() {
+ @Override
+ public void goToFullscreenFromSplit() {
+ splitScreen.goToFullscreenFromSplit();
+ }
+ });
}
@VisibleForTesting
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPasswordViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPasswordViewControllerTest.kt
index ffd95f4..d20be56 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPasswordViewControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPasswordViewControllerTest.kt
@@ -19,6 +19,7 @@
import android.testing.AndroidTestingRunner
import android.testing.TestableLooper
import android.view.inputmethod.InputMethodManager
+import android.widget.EditText
import androidx.test.filters.SmallTest
import com.android.internal.util.LatencyTracker
import com.android.internal.widget.LockPatternUtils
@@ -43,6 +44,8 @@
@Mock
private lateinit var keyguardPasswordView: KeyguardPasswordView
@Mock
+ private lateinit var passwordEntry: EditText
+ @Mock
lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor
@Mock
lateinit var securityMode: KeyguardSecurityModel.SecurityMode
@@ -81,6 +84,9 @@
).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(
keyguardPasswordView,
keyguardUpdateMonitor,
@@ -103,7 +109,10 @@
Mockito.`when`(keyguardViewController.isBouncerShowing).thenReturn(true)
Mockito.`when`(keyguardPasswordView.isShown).thenReturn(true)
keyguardPasswordViewController.onResume(KeyguardSecurityView.VIEW_REVEALED)
- keyguardPasswordView.post { verify(keyguardPasswordView).requestFocus() }
+ keyguardPasswordView.post {
+ verify(keyguardPasswordView).requestFocus()
+ verify(keyguardPasswordView).showKeyboard()
+ }
}
@Test
@@ -115,6 +124,15 @@
}
@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)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaControlPanelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaControlPanelTest.kt
index 761773b..fdef344 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaControlPanelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaControlPanelTest.kt
@@ -702,7 +702,7 @@
}
@Test
- fun bind_seekBarDisabled_noActions_seekBarVisibilityIsSetToGone() {
+ fun bind_seekBarDisabled_noActions_seekBarVisibilityIsSetToInvisible() {
useRealConstraintSets()
val state = mediaData.copy(semanticActions = MediaButton())
@@ -711,7 +711,7 @@
player.bindPlayer(state, PACKAGE)
- assertThat(expandedSet.getVisibility(seekBar.id)).isEqualTo(ConstraintSet.GONE)
+ assertThat(expandedSet.getVisibility(seekBar.id)).isEqualTo(ConstraintSet.INVISIBLE)
}
@Test
@@ -741,7 +741,7 @@
}
@Test
- fun seekBarChangesToDisabledAfterBind_noActions_seekBarChangesToGone() {
+ fun seekBarChangesToDisabledAfterBind_noActions_seekBarChangesToInvisible() {
useRealConstraintSets()
val state = mediaData.copy(semanticActions = MediaButton())
@@ -752,7 +752,7 @@
getEnabledChangeListener().onEnabledChanged(enabled = false)
- assertThat(expandedSet.getVisibility(seekBar.id)).isEqualTo(ConstraintSet.GONE)
+ assertThat(expandedSet.getVisibility(seekBar.id)).isEqualTo(ConstraintSet.INVISIBLE)
}
@Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinatorTest.kt
index 4437394..311740e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinatorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinatorTest.kt
@@ -265,6 +265,8 @@
@Test
fun commandQueueCallback_transferToReceiverSucceeded_triggersCorrectChip() {
+ displayReceiverTriggered()
+ reset(vibratorHelper)
commandQueueCallback.updateMediaTapToTransferSenderDisplay(
StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_RECEIVER_SUCCEEDED,
routeInfo,
@@ -278,13 +280,15 @@
.isEqualTo(ChipStateSender.TRANSFER_TO_RECEIVER_SUCCEEDED.getExpectedStateText())
assertThat(chipbarView.getLoadingIcon().visibility).isEqualTo(View.GONE)
assertThat(chipbarView.getUndoButton().visibility).isEqualTo(View.GONE)
- assertThat(uiEventLoggerFake.eventId(0))
+ // Event index 1 since initially displaying the triggered chip would also log an event.
+ assertThat(uiEventLoggerFake.eventId(1))
.isEqualTo(MediaTttSenderUiEvents.MEDIA_TTT_SENDER_TRANSFER_TO_RECEIVER_SUCCEEDED.id)
verify(vibratorHelper, never()).vibrate(any<VibrationEffect>())
}
@Test
fun transferToReceiverSucceeded_nullUndoCallback_noUndo() {
+ displayReceiverTriggered()
commandQueueCallback.updateMediaTapToTransferSenderDisplay(
StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_RECEIVER_SUCCEEDED,
routeInfo,
@@ -297,6 +301,7 @@
@Test
fun transferToReceiverSucceeded_withUndoRunnable_undoVisible() {
+ displayReceiverTriggered()
commandQueueCallback.updateMediaTapToTransferSenderDisplay(
StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_RECEIVER_SUCCEEDED,
routeInfo,
@@ -313,6 +318,7 @@
@Test
fun transferToReceiverSucceeded_undoButtonClick_switchesToTransferToThisDeviceTriggered() {
var undoCallbackCalled = false
+ displayReceiverTriggered()
commandQueueCallback.updateMediaTapToTransferSenderDisplay(
StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_RECEIVER_SUCCEEDED,
routeInfo,
@@ -325,8 +331,9 @@
getChipbarView().getUndoButton().performClick()
- // Event index 1 since initially displaying the succeeded chip would also log an event
- assertThat(uiEventLoggerFake.eventId(1))
+ // Event index 2 since initially displaying the triggered and succeeded chip would also log
+ // events.
+ assertThat(uiEventLoggerFake.eventId(2))
.isEqualTo(MediaTttSenderUiEvents.MEDIA_TTT_SENDER_UNDO_TRANSFER_TO_RECEIVER_CLICKED.id)
assertThat(undoCallbackCalled).isTrue()
assertThat(getChipbarView().getChipText())
@@ -335,6 +342,8 @@
@Test
fun commandQueueCallback_transferToThisDeviceSucceeded_triggersCorrectChip() {
+ displayThisDeviceTriggered()
+ reset(vibratorHelper)
commandQueueCallback.updateMediaTapToTransferSenderDisplay(
StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_THIS_DEVICE_SUCCEEDED,
routeInfo,
@@ -348,13 +357,15 @@
.isEqualTo(ChipStateSender.TRANSFER_TO_THIS_DEVICE_SUCCEEDED.getExpectedStateText())
assertThat(chipbarView.getLoadingIcon().visibility).isEqualTo(View.GONE)
assertThat(chipbarView.getUndoButton().visibility).isEqualTo(View.GONE)
- assertThat(uiEventLoggerFake.eventId(0))
+ // Event index 1 since initially displaying the triggered chip would also log an event.
+ assertThat(uiEventLoggerFake.eventId(1))
.isEqualTo(MediaTttSenderUiEvents.MEDIA_TTT_SENDER_TRANSFER_TO_THIS_DEVICE_SUCCEEDED.id)
verify(vibratorHelper, never()).vibrate(any<VibrationEffect>())
}
@Test
fun transferToThisDeviceSucceeded_nullUndoCallback_noUndo() {
+ displayThisDeviceTriggered()
commandQueueCallback.updateMediaTapToTransferSenderDisplay(
StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_THIS_DEVICE_SUCCEEDED,
routeInfo,
@@ -367,6 +378,7 @@
@Test
fun transferToThisDeviceSucceeded_withUndoRunnable_undoVisible() {
+ displayThisDeviceTriggered()
commandQueueCallback.updateMediaTapToTransferSenderDisplay(
StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_THIS_DEVICE_SUCCEEDED,
routeInfo,
@@ -383,6 +395,7 @@
@Test
fun transferToThisDeviceSucceeded_undoButtonClick_switchesToTransferToThisDeviceTriggered() {
var undoCallbackCalled = false
+ displayThisDeviceTriggered()
commandQueueCallback.updateMediaTapToTransferSenderDisplay(
StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_THIS_DEVICE_SUCCEEDED,
routeInfo,
@@ -395,8 +408,9 @@
getChipbarView().getUndoButton().performClick()
- // Event index 1 since initially displaying the succeeded chip would also log an event
- assertThat(uiEventLoggerFake.eventId(1))
+ // Event index 2 since initially displaying the triggered and succeeded chip would also log
+ // events.
+ assertThat(uiEventLoggerFake.eventId(2))
.isEqualTo(
MediaTttSenderUiEvents.MEDIA_TTT_SENDER_UNDO_TRANSFER_TO_THIS_DEVICE_CLICKED.id
)
@@ -407,6 +421,8 @@
@Test
fun commandQueueCallback_transferToReceiverFailed_triggersCorrectChip() {
+ displayReceiverTriggered()
+ reset(vibratorHelper)
commandQueueCallback.updateMediaTapToTransferSenderDisplay(
StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_RECEIVER_FAILED,
routeInfo,
@@ -421,7 +437,8 @@
assertThat(chipbarView.getLoadingIcon().visibility).isEqualTo(View.GONE)
assertThat(chipbarView.getUndoButton().visibility).isEqualTo(View.GONE)
assertThat(chipbarView.getErrorIcon().visibility).isEqualTo(View.VISIBLE)
- assertThat(uiEventLoggerFake.eventId(0))
+ // Event index 1 since initially displaying the triggered chip would also log an event.
+ assertThat(uiEventLoggerFake.eventId(1))
.isEqualTo(MediaTttSenderUiEvents.MEDIA_TTT_SENDER_TRANSFER_TO_RECEIVER_FAILED.id)
verify(vibratorHelper).vibrate(any<VibrationEffect>())
}
@@ -429,6 +446,12 @@
@Test
fun commandQueueCallback_transferToThisDeviceFailed_triggersCorrectChip() {
commandQueueCallback.updateMediaTapToTransferSenderDisplay(
+ StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_THIS_DEVICE_TRIGGERED,
+ routeInfo,
+ null
+ )
+ reset(vibratorHelper)
+ commandQueueCallback.updateMediaTapToTransferSenderDisplay(
StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_THIS_DEVICE_FAILED,
routeInfo,
null
@@ -442,7 +465,8 @@
assertThat(chipbarView.getLoadingIcon().visibility).isEqualTo(View.GONE)
assertThat(chipbarView.getUndoButton().visibility).isEqualTo(View.GONE)
assertThat(chipbarView.getErrorIcon().visibility).isEqualTo(View.VISIBLE)
- assertThat(uiEventLoggerFake.eventId(0))
+ // Event index 1 since initially displaying the triggered chip would also log an event.
+ assertThat(uiEventLoggerFake.eventId(1))
.isEqualTo(MediaTttSenderUiEvents.MEDIA_TTT_SENDER_TRANSFER_TO_THIS_DEVICE_FAILED.id)
verify(vibratorHelper).vibrate(any<VibrationEffect>())
}
@@ -517,6 +541,166 @@
}
@Test
+ fun commandQueueCallback_receiverTriggeredThenAlmostStart_invalidTransitionLogged() {
+ commandQueueCallback.updateMediaTapToTransferSenderDisplay(
+ StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_RECEIVER_TRIGGERED,
+ routeInfo,
+ null
+ )
+ verify(windowManager).addView(any(), any())
+ reset(windowManager)
+
+ commandQueueCallback.updateMediaTapToTransferSenderDisplay(
+ StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_ALMOST_CLOSE_TO_START_CAST,
+ routeInfo,
+ null
+ )
+
+ verify(logger).logInvalidStateTransitionError(any(), any())
+ verify(windowManager, never()).addView(any(), any())
+ }
+
+ @Test
+ fun commandQueueCallback_thisDeviceTriggeredThenAlmostEnd_invalidTransitionLogged() {
+ commandQueueCallback.updateMediaTapToTransferSenderDisplay(
+ StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_THIS_DEVICE_TRIGGERED,
+ routeInfo,
+ null
+ )
+ verify(windowManager).addView(any(), any())
+ reset(windowManager)
+
+ commandQueueCallback.updateMediaTapToTransferSenderDisplay(
+ StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_ALMOST_CLOSE_TO_END_CAST,
+ routeInfo,
+ null
+ )
+
+ verify(logger).logInvalidStateTransitionError(any(), any())
+ verify(windowManager, never()).addView(any(), any())
+ }
+
+ @Test
+ fun commandQueueCallback_receiverSucceededThenReceiverTriggered_invalidTransitionLogged() {
+ displayReceiverTriggered()
+ commandQueueCallback.updateMediaTapToTransferSenderDisplay(
+ StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_RECEIVER_SUCCEEDED,
+ routeInfo,
+ null
+ )
+ reset(windowManager)
+
+ commandQueueCallback.updateMediaTapToTransferSenderDisplay(
+ StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_RECEIVER_TRIGGERED,
+ routeInfo,
+ null
+ )
+
+ verify(logger).logInvalidStateTransitionError(any(), any())
+ verify(windowManager, never()).addView(any(), any())
+ }
+
+ @Test
+ fun commandQueueCallback_thisDeviceSucceededThenThisDeviceTriggered_invalidTransitionLogged() {
+ displayThisDeviceTriggered()
+ commandQueueCallback.updateMediaTapToTransferSenderDisplay(
+ StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_THIS_DEVICE_SUCCEEDED,
+ routeInfo,
+ null
+ )
+ reset(windowManager)
+
+ commandQueueCallback.updateMediaTapToTransferSenderDisplay(
+ StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_THIS_DEVICE_TRIGGERED,
+ routeInfo,
+ null
+ )
+
+ verify(logger).logInvalidStateTransitionError(any(), any())
+ verify(windowManager, never()).addView(any(), any())
+ }
+
+ @Test
+ fun commandQueueCallback_almostStartThenReceiverSucceeded_invalidTransitionLogged() {
+ commandQueueCallback.updateMediaTapToTransferSenderDisplay(
+ StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_ALMOST_CLOSE_TO_START_CAST,
+ routeInfo,
+ null
+ )
+ verify(windowManager).addView(any(), any())
+ reset(windowManager)
+
+ commandQueueCallback.updateMediaTapToTransferSenderDisplay(
+ StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_RECEIVER_SUCCEEDED,
+ routeInfo,
+ null
+ )
+
+ verify(logger).logInvalidStateTransitionError(any(), any())
+ verify(windowManager, never()).addView(any(), any())
+ }
+
+ @Test
+ fun commandQueueCallback_almostEndThenThisDeviceSucceeded_invalidTransitionLogged() {
+ commandQueueCallback.updateMediaTapToTransferSenderDisplay(
+ StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_ALMOST_CLOSE_TO_END_CAST,
+ routeInfo,
+ null
+ )
+ verify(windowManager).addView(any(), any())
+ reset(windowManager)
+
+ commandQueueCallback.updateMediaTapToTransferSenderDisplay(
+ StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_THIS_DEVICE_SUCCEEDED,
+ routeInfo,
+ null
+ )
+
+ verify(logger).logInvalidStateTransitionError(any(), any())
+ verify(windowManager, never()).addView(any(), any())
+ }
+
+ @Test
+ fun commandQueueCallback_AlmostStartThenReceiverFailed_invalidTransitionLogged() {
+ commandQueueCallback.updateMediaTapToTransferSenderDisplay(
+ StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_ALMOST_CLOSE_TO_START_CAST,
+ routeInfo,
+ null
+ )
+ verify(windowManager).addView(any(), any())
+ reset(windowManager)
+
+ commandQueueCallback.updateMediaTapToTransferSenderDisplay(
+ StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_RECEIVER_FAILED,
+ routeInfo,
+ null
+ )
+
+ verify(logger).logInvalidStateTransitionError(any(), any())
+ verify(windowManager, never()).addView(any(), any())
+ }
+
+ @Test
+ fun commandQueueCallback_almostEndThenThisDeviceFailed_invalidTransitionLogged() {
+ commandQueueCallback.updateMediaTapToTransferSenderDisplay(
+ StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_ALMOST_CLOSE_TO_END_CAST,
+ routeInfo,
+ null
+ )
+ verify(windowManager).addView(any(), any())
+ reset(windowManager)
+
+ commandQueueCallback.updateMediaTapToTransferSenderDisplay(
+ StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_THIS_DEVICE_FAILED,
+ routeInfo,
+ null
+ )
+
+ verify(logger).logInvalidStateTransitionError(any(), any())
+ verify(windowManager, never()).addView(any(), any())
+ }
+
+ @Test
fun receivesNewStateFromCommandQueue_isLogged() {
commandQueueCallback.updateMediaTapToTransferSenderDisplay(
StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_ALMOST_CLOSE_TO_START_CAST,
@@ -575,6 +759,7 @@
@Test
fun transferToReceiverSucceededThenFarFromReceiver_viewStillDisplayedButDoesTimeOut() {
+ displayReceiverTriggered()
commandQueueCallback.updateMediaTapToTransferSenderDisplay(
StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_RECEIVER_SUCCEEDED,
routeInfo,
@@ -598,6 +783,7 @@
@Test
fun transferToThisDeviceSucceededThenFarFromReceiver_viewStillDisplayedButDoesTimeOut() {
+ displayThisDeviceTriggered()
commandQueueCallback.updateMediaTapToTransferSenderDisplay(
StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_THIS_DEVICE_SUCCEEDED,
routeInfo,
@@ -621,6 +807,7 @@
@Test
fun transferToReceiverSucceeded_thenUndo_thenFar_viewStillDisplayedButDoesTimeOut() {
+ displayReceiverTriggered()
commandQueueCallback.updateMediaTapToTransferSenderDisplay(
StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_RECEIVER_SUCCEEDED,
routeInfo,
@@ -660,6 +847,7 @@
@Test
fun transferToThisDeviceSucceeded_thenUndo_thenFar_viewStillDisplayedButDoesTimeOut() {
+ displayThisDeviceTriggered()
commandQueueCallback.updateMediaTapToTransferSenderDisplay(
StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_THIS_DEVICE_SUCCEEDED,
routeInfo,
@@ -717,6 +905,26 @@
private fun ChipStateSender.getExpectedStateText(): String? {
return this.getChipTextString(context, OTHER_DEVICE_NAME).loadText(context)
}
+
+ // display receiver triggered state helper method to make sure we start from a valid state
+ // transition (FAR_FROM_RECEIVER -> TRANSFER_TO_RECEIVER_TRIGGERED).
+ private fun displayReceiverTriggered() {
+ commandQueueCallback.updateMediaTapToTransferSenderDisplay(
+ StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_RECEIVER_TRIGGERED,
+ routeInfo,
+ null
+ )
+ }
+
+ // display this device triggered state helper method to make sure we start from a valid state
+ // transition (FAR_FROM_RECEIVER -> TRANSFER_TO_THIS_DEVICE_TRIGGERED).
+ private fun displayThisDeviceTriggered() {
+ commandQueueCallback.updateMediaTapToTransferSenderDisplay(
+ StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_THIS_DEVICE_TRIGGERED,
+ routeInfo,
+ null
+ )
+ }
}
private const val APP_NAME = "Fake app name"
diff --git a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskControllerTest.kt
index 9758842..4a9c750 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskControllerTest.kt
@@ -16,16 +16,21 @@
package com.android.systemui.notetask
import android.app.KeyguardManager
+import android.content.ComponentName
import android.content.Context
import android.content.Intent
+import android.content.pm.PackageManager
import android.os.UserManager
import android.test.suitebuilder.annotation.SmallTest
-import android.view.KeyEvent
import androidx.test.runner.AndroidJUnit4
import com.android.systemui.SysuiTestCase
import com.android.systemui.notetask.NoteTaskIntentResolver.Companion.NOTES_ACTION
+import com.android.systemui.notetask.shortcut.CreateNoteTaskShortcutActivity
+import com.android.systemui.util.mockito.argumentCaptor
+import com.android.systemui.util.mockito.eq
import com.android.systemui.util.mockito.whenever
import com.android.wm.shell.bubbles.Bubbles
+import com.google.common.truth.Truth.assertThat
import java.util.Optional
import org.junit.Before
import org.junit.Test
@@ -48,6 +53,7 @@
private val notesIntent = Intent(NOTES_ACTION)
@Mock lateinit var context: Context
+ @Mock lateinit var packageManager: PackageManager
@Mock lateinit var noteTaskIntentResolver: NoteTaskIntentResolver
@Mock lateinit var bubbles: Bubbles
@Mock lateinit var optionalBubbles: Optional<Bubbles>
@@ -60,6 +66,7 @@
fun setUp() {
MockitoAnnotations.initMocks(this)
+ whenever(context.packageManager).thenReturn(packageManager)
whenever(noteTaskIntentResolver.resolveIntent()).thenReturn(notesIntent)
whenever(optionalBubbles.orElse(null)).thenReturn(bubbles)
whenever(optionalKeyguardManager.orElse(null)).thenReturn(keyguardManager)
@@ -78,89 +85,125 @@
)
}
+ // region showNoteTask
@Test
- fun handleSystemKey_keyguardIsLocked_shouldStartActivity() {
+ fun showNoteTask_keyguardIsLocked_shouldStartActivity() {
whenever(keyguardManager.isKeyguardLocked).thenReturn(true)
- createNoteTaskController().handleSystemKey(KeyEvent.KEYCODE_VIDEO_APP_1)
+ createNoteTaskController().showNoteTask(isInMultiWindowMode = false)
verify(context).startActivity(notesIntent)
verify(bubbles, never()).showAppBubble(notesIntent)
}
@Test
- fun handleSystemKey_keyguardIsUnlocked_shouldStartBubbles() {
+ fun showNoteTask_keyguardIsUnlocked_shouldStartBubbles() {
whenever(keyguardManager.isKeyguardLocked).thenReturn(false)
- createNoteTaskController().handleSystemKey(KeyEvent.KEYCODE_VIDEO_APP_1)
+ createNoteTaskController().showNoteTask(isInMultiWindowMode = false)
verify(bubbles).showAppBubble(notesIntent)
verify(context, never()).startActivity(notesIntent)
}
@Test
- fun handleSystemKey_receiveInvalidSystemKey_shouldDoNothing() {
- createNoteTaskController().handleSystemKey(KeyEvent.KEYCODE_UNKNOWN)
+ fun showNoteTask_isInMultiWindowMode_shouldStartActivity() {
+ whenever(keyguardManager.isKeyguardLocked).thenReturn(false)
- verify(context, never()).startActivity(notesIntent)
+ createNoteTaskController().showNoteTask(isInMultiWindowMode = true)
+
+ verify(context).startActivity(notesIntent)
verify(bubbles, never()).showAppBubble(notesIntent)
}
@Test
- fun handleSystemKey_bubblesIsNull_shouldDoNothing() {
+ fun showNoteTask_bubblesIsNull_shouldDoNothing() {
whenever(optionalBubbles.orElse(null)).thenReturn(null)
- createNoteTaskController().handleSystemKey(KeyEvent.KEYCODE_VIDEO_APP_1)
+ createNoteTaskController().showNoteTask(isInMultiWindowMode = false)
verify(context, never()).startActivity(notesIntent)
verify(bubbles, never()).showAppBubble(notesIntent)
}
@Test
- fun handleSystemKey_keyguardManagerIsNull_shouldDoNothing() {
+ fun showNoteTask_keyguardManagerIsNull_shouldDoNothing() {
whenever(optionalKeyguardManager.orElse(null)).thenReturn(null)
- createNoteTaskController().handleSystemKey(KeyEvent.KEYCODE_VIDEO_APP_1)
+ createNoteTaskController().showNoteTask(isInMultiWindowMode = false)
verify(context, never()).startActivity(notesIntent)
verify(bubbles, never()).showAppBubble(notesIntent)
}
@Test
- fun handleSystemKey_userManagerIsNull_shouldDoNothing() {
+ fun showNoteTask_userManagerIsNull_shouldDoNothing() {
whenever(optionalUserManager.orElse(null)).thenReturn(null)
- createNoteTaskController().handleSystemKey(KeyEvent.KEYCODE_VIDEO_APP_1)
+ createNoteTaskController().showNoteTask(isInMultiWindowMode = false)
verify(context, never()).startActivity(notesIntent)
verify(bubbles, never()).showAppBubble(notesIntent)
}
@Test
- fun handleSystemKey_intentResolverReturnsNull_shouldDoNothing() {
+ fun showNoteTask_intentResolverReturnsNull_shouldDoNothing() {
whenever(noteTaskIntentResolver.resolveIntent()).thenReturn(null)
- createNoteTaskController().handleSystemKey(KeyEvent.KEYCODE_VIDEO_APP_1)
+ createNoteTaskController().showNoteTask(isInMultiWindowMode = false)
verify(context, never()).startActivity(notesIntent)
verify(bubbles, never()).showAppBubble(notesIntent)
}
@Test
- fun handleSystemKey_flagDisabled_shouldDoNothing() {
- createNoteTaskController(isEnabled = false).handleSystemKey(KeyEvent.KEYCODE_VIDEO_APP_1)
+ fun showNoteTask_flagDisabled_shouldDoNothing() {
+ createNoteTaskController(isEnabled = false).showNoteTask()
verify(context, never()).startActivity(notesIntent)
verify(bubbles, never()).showAppBubble(notesIntent)
}
@Test
- fun handleSystemKey_userIsLocked_shouldDoNothing() {
+ fun showNoteTask_userIsLocked_shouldDoNothing() {
whenever(userManager.isUserUnlocked).thenReturn(false)
- createNoteTaskController().handleSystemKey(KeyEvent.KEYCODE_VIDEO_APP_1)
+ createNoteTaskController().showNoteTask(isInMultiWindowMode = false)
verify(context, never()).startActivity(notesIntent)
verify(bubbles, never()).showAppBubble(notesIntent)
}
+ // endregion
+
+ // region setNoteTaskShortcutEnabled
+ @Test
+ fun setNoteTaskShortcutEnabled_setTrue() {
+ createNoteTaskController().setNoteTaskShortcutEnabled(value = true)
+
+ val argument = argumentCaptor<ComponentName>()
+ verify(context.packageManager)
+ .setComponentEnabledSetting(
+ argument.capture(),
+ eq(PackageManager.COMPONENT_ENABLED_STATE_ENABLED),
+ eq(PackageManager.DONT_KILL_APP),
+ )
+ val expected = ComponentName(context, CreateNoteTaskShortcutActivity::class.java)
+ assertThat(argument.value.flattenToString()).isEqualTo(expected.flattenToString())
+ }
+
+ @Test
+ fun setNoteTaskShortcutEnabled_setFalse() {
+ createNoteTaskController().setNoteTaskShortcutEnabled(value = false)
+
+ val argument = argumentCaptor<ComponentName>()
+ verify(context.packageManager)
+ .setComponentEnabledSetting(
+ argument.capture(),
+ eq(PackageManager.COMPONENT_ENABLED_STATE_DISABLED),
+ eq(PackageManager.DONT_KILL_APP),
+ )
+ val expected = ComponentName(context, CreateNoteTaskShortcutActivity::class.java)
+ assertThat(argument.value.flattenToString()).isEqualTo(expected.flattenToString())
+ }
+ // endregion
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskInitializerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskInitializerTest.kt
index 334089c..538131a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskInitializerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskInitializerTest.kt
@@ -16,10 +16,10 @@
package com.android.systemui.notetask
import android.test.suitebuilder.annotation.SmallTest
+import android.view.KeyEvent
import androidx.test.runner.AndroidJUnit4
import com.android.systemui.SysuiTestCase
import com.android.systemui.statusbar.CommandQueue
-import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.whenever
import com.android.wm.shell.bubbles.Bubbles
import java.util.Optional
@@ -45,6 +45,7 @@
@Mock lateinit var commandQueue: CommandQueue
@Mock lateinit var bubbles: Bubbles
@Mock lateinit var optionalBubbles: Optional<Bubbles>
+ @Mock lateinit var noteTaskController: NoteTaskController
@Before
fun setUp() {
@@ -57,12 +58,13 @@
private fun createNoteTaskInitializer(isEnabled: Boolean = true): NoteTaskInitializer {
return NoteTaskInitializer(
optionalBubbles = optionalBubbles,
- lazyNoteTaskController = mock(),
+ noteTaskController = noteTaskController,
commandQueue = commandQueue,
isEnabled = isEnabled,
)
}
+ // region initializer
@Test
fun initialize_shouldAddCallbacks() {
createNoteTaskInitializer().initialize()
@@ -85,4 +87,35 @@
verify(commandQueue, never()).addCallback(any())
}
+
+ @Test
+ fun initialize_flagEnabled_shouldEnableShortcut() {
+ createNoteTaskInitializer().initialize()
+
+ verify(noteTaskController).setNoteTaskShortcutEnabled(true)
+ }
+
+ @Test
+ fun initialize_flagDisabled_shouldDisableShortcut() {
+ createNoteTaskInitializer(isEnabled = false).initialize()
+
+ verify(noteTaskController).setNoteTaskShortcutEnabled(false)
+ }
+ // endregion
+
+ // region handleSystemKey
+ @Test
+ fun handleSystemKey_receiveValidSystemKey_shouldShowNoteTask() {
+ createNoteTaskInitializer().callbacks.handleSystemKey(KeyEvent.KEYCODE_VIDEO_APP_1)
+
+ verify(noteTaskController).showNoteTask()
+ }
+
+ @Test
+ fun handleSystemKey_receiveInvalidSystemKey_shouldDoNothing() {
+ createNoteTaskInitializer().callbacks.handleSystemKey(KeyEvent.KEYCODE_UNKNOWN)
+
+ verify(noteTaskController, never()).showNoteTask()
+ }
+ // endregion
}
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 5e9c1aa..906c20b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/QSSecurityFooterTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QSSecurityFooterTest.java
@@ -703,28 +703,32 @@
public void testParentalControls() {
// Make sure the security footer is visible, so that the images are updated.
when(mSecurityController.isProfileOwnerOfOrganizationOwnedDevice()).thenReturn(true);
-
when(mSecurityController.isParentalControlsEnabled()).thenReturn(true);
+ // We use the default icon when there is no admin icon.
+ when(mSecurityController.getIcon(any())).thenReturn(null);
+ mFooter.refreshState();
+ TestableLooper.get(this).processAllMessages();
+ assertEquals(mContext.getString(R.string.quick_settings_disclosure_parental_controls),
+ mFooterText.getText());
+ assertEquals(DEFAULT_ICON_ID, mPrimaryFooterIcon.getLastImageResource());
+
Drawable testDrawable = new VectorDrawable();
when(mSecurityController.getIcon(any())).thenReturn(testDrawable);
assertNotNull(mSecurityController.getIcon(null));
mFooter.refreshState();
-
TestableLooper.get(this).processAllMessages();
assertEquals(mContext.getString(R.string.quick_settings_disclosure_parental_controls),
mFooterText.getText());
assertEquals(View.VISIBLE, mPrimaryFooterIcon.getVisibility());
-
assertEquals(testDrawable, mPrimaryFooterIcon.getDrawable());
// 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());
}
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/notification/RoundableTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/RoundableTest.kt
new file mode 100644
index 0000000..89faa239
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/RoundableTest.kt
@@ -0,0 +1,164 @@
+package com.android.systemui.statusbar.notification
+
+import android.view.View
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.util.mockito.mock
+import org.junit.Assert.assertEquals
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.mockito.Mockito.atLeastOnce
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+
+@SmallTest
+@RunWith(JUnit4::class)
+class RoundableTest : SysuiTestCase() {
+ val targetView: View = mock()
+ val roundable = FakeRoundable(targetView)
+
+ @Test
+ fun defaultConfig_shouldNotHaveRoundedCorner() {
+ // the expected default value for the roundness is top = 0f, bottom = 0f
+ assertEquals(0f, roundable.roundableState.topRoundness)
+ assertEquals(0f, roundable.roundableState.bottomRoundness)
+ assertEquals(false, roundable.hasRoundedCorner())
+ }
+
+ @Test
+ fun applyRoundnessAndInvalidate_should_invalidate_targetView() {
+ roundable.applyRoundnessAndInvalidate()
+
+ verify(targetView, times(1)).invalidate()
+ }
+
+ @Test
+ fun requestTopRoundness_update_and_invalidate_targetView() {
+ roundable.requestTopRoundness(value = 1f, sourceType = SOURCE1)
+
+ assertEquals(1f, roundable.roundableState.topRoundness)
+ verify(targetView, times(1)).invalidate()
+ }
+
+ @Test
+ fun requestBottomRoundness_update_and_invalidate_targetView() {
+ roundable.requestBottomRoundness(value = 1f, sourceType = SOURCE1)
+
+ assertEquals(1f, roundable.roundableState.bottomRoundness)
+ verify(targetView, times(1)).invalidate()
+ }
+
+ @Test
+ fun requestRoundness_update_and_invalidate_targetView() {
+ roundable.requestRoundness(top = 1f, bottom = 1f, sourceType = SOURCE1)
+
+ assertEquals(1f, roundable.roundableState.topRoundness)
+ assertEquals(1f, roundable.roundableState.bottomRoundness)
+ verify(targetView, atLeastOnce()).invalidate()
+ }
+
+ @Test
+ fun requestRoundnessReset_update_and_invalidate_targetView() {
+ roundable.requestRoundness(1f, 1f, SOURCE1)
+ assertEquals(1f, roundable.roundableState.topRoundness)
+ assertEquals(1f, roundable.roundableState.bottomRoundness)
+
+ roundable.requestRoundnessReset(sourceType = SOURCE1)
+
+ assertEquals(0f, roundable.roundableState.topRoundness)
+ assertEquals(0f, roundable.roundableState.bottomRoundness)
+ verify(targetView, atLeastOnce()).invalidate()
+ }
+
+ @Test
+ fun hasRoundedCorner_return_true_ifRoundnessIsGreaterThenZero() {
+ roundable.requestRoundness(top = 1f, bottom = 1f, sourceType = SOURCE1)
+ assertEquals(true, roundable.hasRoundedCorner())
+
+ roundable.requestRoundness(top = 1f, bottom = 0f, sourceType = SOURCE1)
+ assertEquals(true, roundable.hasRoundedCorner())
+
+ roundable.requestRoundness(top = 0f, bottom = 1f, sourceType = SOURCE1)
+ assertEquals(true, roundable.hasRoundedCorner())
+
+ roundable.requestRoundness(top = 0f, bottom = 0f, sourceType = SOURCE1)
+ assertEquals(false, roundable.hasRoundedCorner())
+ }
+
+ @Test
+ fun roundness_take_maxValue_onMultipleSources_first_lower() {
+ roundable.requestRoundness(0.1f, 0.1f, SOURCE1)
+ assertEquals(0.1f, roundable.roundableState.topRoundness)
+ assertEquals(0.1f, roundable.roundableState.bottomRoundness)
+
+ roundable.requestRoundness(0.2f, 0.2f, SOURCE2)
+ // SOURCE1 has 0.1f - SOURCE2 has 0.2f
+ assertEquals(0.2f, roundable.roundableState.topRoundness)
+ assertEquals(0.2f, roundable.roundableState.bottomRoundness)
+ }
+
+ @Test
+ fun roundness_take_maxValue_onMultipleSources_first_higher() {
+ roundable.requestRoundness(0.5f, 0.5f, SOURCE1)
+ assertEquals(0.5f, roundable.roundableState.topRoundness)
+ assertEquals(0.5f, roundable.roundableState.bottomRoundness)
+
+ roundable.requestRoundness(0.1f, 0.1f, SOURCE2)
+ // SOURCE1 has 0.5f - SOURCE2 has 0.1f
+ assertEquals(0.5f, roundable.roundableState.topRoundness)
+ assertEquals(0.5f, roundable.roundableState.bottomRoundness)
+ }
+
+ @Test
+ fun roundness_take_maxValue_onMultipleSources_first_higher_second_step() {
+ roundable.requestRoundness(0.1f, 0.1f, SOURCE1)
+ assertEquals(0.1f, roundable.roundableState.topRoundness)
+ assertEquals(0.1f, roundable.roundableState.bottomRoundness)
+
+ roundable.requestRoundness(0.2f, 0.2f, SOURCE2)
+ // SOURCE1 has 0.1f - SOURCE2 has 0.2f
+ assertEquals(0.2f, roundable.roundableState.topRoundness)
+ assertEquals(0.2f, roundable.roundableState.bottomRoundness)
+
+ roundable.requestRoundness(0.3f, 0.3f, SOURCE1)
+ // SOURCE1 has 0.3f - SOURCE2 has 0.2f
+ assertEquals(0.3f, roundable.roundableState.topRoundness)
+ assertEquals(0.3f, roundable.roundableState.bottomRoundness)
+ }
+
+ @Test
+ fun roundness_take_maxValue_onMultipleSources_first_lower_second_step() {
+ roundable.requestRoundness(0.5f, 0.5f, SOURCE1)
+ assertEquals(0.5f, roundable.roundableState.topRoundness)
+ assertEquals(0.5f, roundable.roundableState.bottomRoundness)
+
+ roundable.requestRoundness(0.2f, 0.2f, SOURCE2)
+ // SOURCE1 has 0.5f - SOURCE2 has 0.2f
+ assertEquals(0.5f, roundable.roundableState.topRoundness)
+ assertEquals(0.5f, roundable.roundableState.bottomRoundness)
+
+ roundable.requestRoundness(0.1f, 0.1f, SOURCE1)
+ // SOURCE1 has 0.1f - SOURCE2 has 0.2f
+ assertEquals(0.2f, roundable.roundableState.topRoundness)
+ assertEquals(0.2f, roundable.roundableState.bottomRoundness)
+ }
+
+ class FakeRoundable(
+ targetView: View,
+ radius: Float = MAX_RADIUS,
+ ) : Roundable {
+ override val roundableState =
+ RoundableState(
+ targetView = targetView,
+ roundable = this,
+ maxRadius = radius,
+ )
+ }
+
+ companion object {
+ private const val MAX_RADIUS = 10f
+ private val SOURCE1 = SourceType.from("Source1")
+ private val SOURCE2 = SourceType.from("Source2")
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java
index 088d165..59d4720 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java
@@ -213,8 +213,7 @@
SourceType sourceType
) throws Exception {
ExpandableNotificationRow row = createRow();
- row.requestTopRoundness(topRoundness, false, sourceType);
- row.requestBottomRoundness(bottomRoundness, /*animate = */ false, sourceType);
+ row.requestRoundness(topRoundness, bottomRoundness, sourceType, /*animate = */ false);
assertEquals(topRoundness, row.getTopRoundness(), /* delta = */ 0f);
assertEquals(bottomRoundness, row.getBottomRoundness(), /* delta = */ 0f);
return row;
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainerTest.java
index 438b528..fd1944e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainerTest.java
@@ -25,7 +25,7 @@
import androidx.test.filters.SmallTest;
import com.android.systemui.SysuiTestCase;
-import com.android.systemui.statusbar.notification.SourceType;
+import com.android.systemui.statusbar.notification.LegacySourceType;
import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
import com.android.systemui.statusbar.notification.row.NotificationTestHelper;
@@ -158,7 +158,7 @@
ExpandableNotificationRow row = mNotificationTestHelper.createRowWithRoundness(
/* topRoundness = */ 1f,
/* bottomRoundness = */ 1f,
- /* sourceType = */ SourceType.OnScroll);
+ /* sourceType = */ LegacySourceType.OnScroll);
mChildrenContainer.addNotification(row, 0);
@@ -171,11 +171,11 @@
ExpandableNotificationRow row1 = mNotificationTestHelper.createRowWithRoundness(
/* topRoundness = */ 1f,
/* bottomRoundness = */ 1f,
- /* sourceType = */ SourceType.DefaultValue);
+ /* sourceType = */ LegacySourceType.DefaultValue);
ExpandableNotificationRow row2 = mNotificationTestHelper.createRowWithRoundness(
/* topRoundness = */ 1f,
/* bottomRoundness = */ 1f,
- /* sourceType = */ SourceType.OnDismissAnimation);
+ /* sourceType = */ LegacySourceType.OnDismissAnimation);
mChildrenContainer.addNotification(row1, 0);
mChildrenContainer.addNotification(row2, 0);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationRoundnessManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationRoundnessManagerTest.java
index 8c8b644..bd0a556 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationRoundnessManagerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationRoundnessManagerTest.java
@@ -35,6 +35,7 @@
import com.android.systemui.R;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.dump.DumpManager;
+import com.android.systemui.flags.FeatureFlags;
import com.android.systemui.statusbar.notification.NotificationSectionsFeatureManager;
import com.android.systemui.statusbar.notification.collection.NotificationEntry;
import com.android.systemui.statusbar.notification.logging.NotificationRoundnessLogger;
@@ -73,7 +74,8 @@
mRoundnessManager = new NotificationRoundnessManager(
new NotificationSectionsFeatureManager(new DeviceConfigProxy(), mContext),
mLogger,
- mock(DumpManager.class));
+ mock(DumpManager.class),
+ mock(FeatureFlags.class));
allowTestableLooperAsMainThread();
NotificationTestHelper testHelper = new NotificationTestHelper(
mContext,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManagerTest.java
index ecc0224..30da08e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManagerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManagerTest.java
@@ -30,6 +30,7 @@
import androidx.test.filters.SmallTest;
import com.android.systemui.SysuiTestCase;
+import com.android.systemui.flags.FeatureFlags;
import com.android.systemui.media.controls.ui.KeyguardMediaController;
import com.android.systemui.plugins.statusbar.StatusBarStateController;
import com.android.systemui.statusbar.StatusBarState;
@@ -59,10 +60,12 @@
@Mock private KeyguardMediaController mKeyguardMediaController;
@Mock private NotificationSectionsFeatureManager mSectionsFeatureManager;
@Mock private MediaContainerController mMediaContainerController;
+ @Mock private NotificationRoundnessManager mNotificationRoundnessManager;
@Mock private SectionHeaderController mIncomingHeaderController;
@Mock private SectionHeaderController mPeopleHeaderController;
@Mock private SectionHeaderController mAlertingHeaderController;
@Mock private SectionHeaderController mSilentHeaderController;
+ @Mock private FeatureFlags mFeatureFlag;
private NotificationSectionsManager mSectionsManager;
@@ -89,10 +92,12 @@
mKeyguardMediaController,
mSectionsFeatureManager,
mMediaContainerController,
+ mNotificationRoundnessManager,
mIncomingHeaderController,
mPeopleHeaderController,
mAlertingHeaderController,
- mSilentHeaderController
+ mSilentHeaderController,
+ mFeatureFlag
);
// Required in order for the header inflation to work properly
when(mNssl.generateLayoutParams(any(AttributeSet.class)))
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationShelfTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationShelfTest.kt
index bda2336..9d759c4 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationShelfTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationShelfTest.kt
@@ -9,7 +9,7 @@
import com.android.systemui.animation.ShadeInterpolation
import com.android.systemui.statusbar.NotificationShelf
import com.android.systemui.statusbar.StatusBarIconView
-import com.android.systemui.statusbar.notification.SourceType
+import com.android.systemui.statusbar.notification.LegacySourceType
import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow
import com.android.systemui.statusbar.notification.row.ExpandableView
import com.android.systemui.statusbar.notification.row.NotificationTestHelper
@@ -314,9 +314,9 @@
val row: ExpandableNotificationRow = notificationTestHelper.createRowWithRoundness(
/* topRoundness = */ 1f,
/* bottomRoundness = */ 1f,
- /* sourceType = */ SourceType.OnScroll)
+ /* sourceType = */ LegacySourceType.OnScroll)
- NotificationShelf.resetOnScrollRoundness(row)
+ NotificationShelf.resetLegacyOnScrollRoundness(row)
assertEquals(0f, row.topRoundness)
assertEquals(0f, row.bottomRoundness)
@@ -327,14 +327,14 @@
val row1: ExpandableNotificationRow = notificationTestHelper.createRowWithRoundness(
/* topRoundness = */ 1f,
/* bottomRoundness = */ 1f,
- /* sourceType = */ SourceType.DefaultValue)
+ /* sourceType = */ LegacySourceType.DefaultValue)
val row2: ExpandableNotificationRow = notificationTestHelper.createRowWithRoundness(
/* topRoundness = */ 1f,
/* bottomRoundness = */ 1f,
- /* sourceType = */ SourceType.OnDismissAnimation)
+ /* sourceType = */ LegacySourceType.OnDismissAnimation)
- NotificationShelf.resetOnScrollRoundness(row1)
- NotificationShelf.resetOnScrollRoundness(row2)
+ NotificationShelf.resetLegacyOnScrollRoundness(row1)
+ NotificationShelf.resetLegacyOnScrollRoundness(row2)
assertEquals(1f, row1.topRoundness)
assertEquals(1f, row1.bottomRoundness)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationSwipeHelperTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationSwipeHelperTest.java
index 4ea1c71..680a323 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationSwipeHelperTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationSwipeHelperTest.java
@@ -74,6 +74,7 @@
private NotificationSwipeHelper mSwipeHelper;
private NotificationSwipeHelper.NotificationCallback mCallback;
private NotificationMenuRowPlugin.OnMenuEventListener mListener;
+ private NotificationRoundnessManager mNotificationRoundnessManager;
private View mView;
private MotionEvent mEvent;
private NotificationMenuRowPlugin mMenuRow;
@@ -92,10 +93,17 @@
public void setUp() throws Exception {
mCallback = mock(NotificationSwipeHelper.NotificationCallback.class);
mListener = mock(NotificationMenuRowPlugin.OnMenuEventListener.class);
+ mNotificationRoundnessManager = mock(NotificationRoundnessManager.class);
mFeatureFlags = mock(FeatureFlags.class);
mSwipeHelper = spy(new NotificationSwipeHelper(
- mContext.getResources(), ViewConfiguration.get(mContext),
- new FalsingManagerFake(), mFeatureFlags, SwipeHelper.X, mCallback, mListener));
+ mContext.getResources(),
+ ViewConfiguration.get(mContext),
+ new FalsingManagerFake(),
+ mFeatureFlags,
+ SwipeHelper.X,
+ mCallback,
+ mListener,
+ mNotificationRoundnessManager));
mView = mock(View.class);
mEvent = mock(MotionEvent.class);
mMenuRow = mock(NotificationMenuRowPlugin.class);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationTargetsHelperTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationTargetsHelperTest.kt
index a2e9230..81a3f12 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationTargetsHelperTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationTargetsHelperTest.kt
@@ -35,7 +35,7 @@
) =
NotificationTargetsHelper(
FakeFeatureFlags().apply {
- set(Flags.NOTIFICATION_GROUP_CORNER, notificationGroupCorner)
+ set(Flags.USE_ROUNDNESS_SOURCETYPES, notificationGroupCorner)
}
)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ConfigurationControllerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ConfigurationControllerImplTest.kt
index 038af8f..6155e3c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ConfigurationControllerImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ConfigurationControllerImplTest.kt
@@ -27,6 +27,7 @@
import com.android.systemui.statusbar.policy.ConfigurationController.ConfigurationListener
import com.google.common.truth.Truth.assertThat
import org.junit.Before
+import org.junit.Ignore
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mockito.doAnswer
@@ -295,6 +296,7 @@
}
@Test
+ @Ignore("b/261408895")
fun equivalentConfigObject_listenerNotNotified() {
val config = mContext.resources.configuration
val listener = createAndAddListener()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/HeadsUpAppearanceControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/HeadsUpAppearanceControllerTest.java
index 103b7b42..9727b6c5 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/HeadsUpAppearanceControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/HeadsUpAppearanceControllerTest.java
@@ -32,6 +32,7 @@
import androidx.test.filters.SmallTest;
import com.android.systemui.SysuiTestCase;
+import com.android.systemui.flags.FeatureFlags;
import com.android.systemui.plugins.DarkIconDispatcher;
import com.android.systemui.plugins.statusbar.StatusBarStateController;
import com.android.systemui.shade.NotificationPanelViewController;
@@ -40,6 +41,7 @@
import com.android.systemui.statusbar.notification.NotificationWakeUpCoordinator;
import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
import com.android.systemui.statusbar.notification.row.NotificationTestHelper;
+import com.android.systemui.statusbar.notification.stack.NotificationRoundnessManager;
import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController;
import com.android.systemui.statusbar.policy.Clock;
import com.android.systemui.statusbar.policy.KeyguardStateController;
@@ -71,6 +73,8 @@
private NotificationWakeUpCoordinator mWakeUpCoordinator;
private KeyguardStateController mKeyguardStateController;
private CommandQueue mCommandQueue;
+ private NotificationRoundnessManager mNotificationRoundnessManager;
+ private FeatureFlags mFeatureFlag;
@Before
public void setUp() throws Exception {
@@ -89,6 +93,8 @@
mWakeUpCoordinator = mock(NotificationWakeUpCoordinator.class);
mKeyguardStateController = mock(KeyguardStateController.class);
mCommandQueue = mock(CommandQueue.class);
+ mNotificationRoundnessManager = mock(NotificationRoundnessManager.class);
+ mFeatureFlag = mock(FeatureFlags.class);
mHeadsUpAppearanceController = new HeadsUpAppearanceController(
mock(NotificationIconAreaController.class),
mHeadsUpManager,
@@ -100,6 +106,8 @@
mCommandQueue,
mStackScrollerController,
mPanelView,
+ mNotificationRoundnessManager,
+ mFeatureFlag,
mHeadsUpStatusBarView,
new Clock(mContext, null),
Optional.of(mOperatorNameView));
@@ -182,6 +190,8 @@
mCommandQueue,
mStackScrollerController,
mPanelView,
+ mNotificationRoundnessManager,
+ mFeatureFlag,
mHeadsUpStatusBarView,
new Clock(mContext, null),
Optional.empty());
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/MobileConnectionRepositoryTest.kt
index 5ce51bb..aa7ab0d 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/MobileConnectionRepositoryTest.kt
@@ -31,6 +31,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.NETWORK_TYPE_LTE
import android.telephony.TelephonyManager.NETWORK_TYPE_UNKNOWN
import androidx.test.filters.SmallTest
@@ -221,6 +222,21 @@
}
@Test
+ fun testFlowForSubId_dataConnectionState_unknown() =
+ runBlocking(IMMEDIATE) {
+ var latest: MobileSubscriptionModel? = null
+ val job = underTest.subscriptionModelFlow.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
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/view/ModernStatusBarWifiViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/view/ModernStatusBarWifiViewTest.kt
index 5c16e129..3d9fd96 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/view/ModernStatusBarWifiViewTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/view/ModernStatusBarWifiViewTest.kt
@@ -25,6 +25,7 @@
import com.android.systemui.R
import com.android.systemui.SysuiTestCase
import com.android.systemui.lifecycle.InstantTaskExecutorRule
+import com.android.systemui.log.table.TableLogBuffer
import com.android.systemui.statusbar.StatusBarIconView.STATE_DOT
import com.android.systemui.statusbar.StatusBarIconView.STATE_HIDDEN
import com.android.systemui.statusbar.StatusBarIconView.STATE_ICON
@@ -64,6 +65,7 @@
private lateinit var statusBarPipelineFlags: StatusBarPipelineFlags
@Mock
private lateinit var logger: ConnectivityPipelineLogger
+ @Mock private lateinit var tableLogBuffer: TableLogBuffer
@Mock
private lateinit var connectivityConstants: ConnectivityConstants
@Mock
@@ -103,6 +105,7 @@
connectivityConstants,
context,
logger,
+ tableLogBuffer,
interactor,
scope,
statusBarPipelineFlags,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelIconParameterizedTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelIconParameterizedTest.kt
index 3001b81..12b93819 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelIconParameterizedTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelIconParameterizedTest.kt
@@ -23,6 +23,7 @@
import com.android.settingslib.AccessibilityContentDescriptions.WIFI_NO_CONNECTION
import com.android.systemui.SysuiTestCase
import com.android.systemui.common.shared.model.ContentDescription.Companion.loadContentDescription
+import com.android.systemui.log.table.TableLogBuffer
import com.android.systemui.statusbar.connectivity.WifiIcons.WIFI_FULL_ICONS
import com.android.systemui.statusbar.connectivity.WifiIcons.WIFI_NO_INTERNET_ICONS
import com.android.systemui.statusbar.connectivity.WifiIcons.WIFI_NO_NETWORK
@@ -40,6 +41,7 @@
import com.android.systemui.statusbar.pipeline.wifi.domain.interactor.WifiInteractor
import com.android.systemui.statusbar.pipeline.wifi.domain.interactor.WifiInteractorImpl
import com.android.systemui.statusbar.pipeline.wifi.shared.WifiConstants
+import com.android.systemui.statusbar.pipeline.wifi.ui.model.WifiIcon
import com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel.WifiViewModel.Companion.NO_INTERNET
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.CoroutineScope
@@ -67,6 +69,7 @@
@Mock private lateinit var statusBarPipelineFlags: StatusBarPipelineFlags
@Mock private lateinit var logger: ConnectivityPipelineLogger
+ @Mock private lateinit var tableLogBuffer: TableLogBuffer
@Mock private lateinit var connectivityConstants: ConnectivityConstants
@Mock private lateinit var wifiConstants: WifiConstants
private lateinit var airplaneModeRepository: FakeAirplaneModeRepository
@@ -123,6 +126,7 @@
connectivityConstants,
context,
logger,
+ tableLogBuffer,
interactor,
scope,
statusBarPipelineFlags,
@@ -137,15 +141,21 @@
yield()
// THEN we get the expected icon
- assertThat(iconFlow.value?.res).isEqualTo(testCase.expected?.iconResource)
- val expectedContentDescription =
- if (testCase.expected == null) {
- null
- } else {
- testCase.expected.contentDescription.invoke(context)
+ val actualIcon = iconFlow.value
+ when (testCase.expected) {
+ null -> {
+ assertThat(actualIcon).isInstanceOf(WifiIcon.Hidden::class.java)
}
- assertThat(iconFlow.value?.contentDescription?.loadContentDescription(context))
- .isEqualTo(expectedContentDescription)
+ else -> {
+ assertThat(actualIcon).isInstanceOf(WifiIcon.Visible::class.java)
+ val actualIconVisible = actualIcon as WifiIcon.Visible
+ assertThat(actualIconVisible.icon.res).isEqualTo(testCase.expected.iconResource)
+ val expectedContentDescription =
+ testCase.expected.contentDescription.invoke(context)
+ assertThat(actualIconVisible.contentDescription.loadContentDescription(context))
+ .isEqualTo(expectedContentDescription)
+ }
+ }
job.cancel()
}
@@ -174,7 +184,7 @@
val isDefault: Boolean = false,
val network: WifiNetworkModel,
- /** The expected output. Null if we expect the output to be null. */
+ /** The expected output. Null if we expect the output to be hidden. */
val expected: Expected?
) {
override fun toString(): String {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelTest.kt
index 6a6b2a8..7502020 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelTest.kt
@@ -18,7 +18,7 @@
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
-import com.android.systemui.common.shared.model.Icon
+import com.android.systemui.log.table.TableLogBuffer
import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags
import com.android.systemui.statusbar.pipeline.airplane.data.repository.FakeAirplaneModeRepository
import com.android.systemui.statusbar.pipeline.airplane.domain.interactor.AirplaneModeInteractor
@@ -34,6 +34,7 @@
import com.android.systemui.statusbar.pipeline.wifi.domain.interactor.WifiInteractorImpl
import com.android.systemui.statusbar.pipeline.wifi.shared.WifiConstants
import com.android.systemui.statusbar.pipeline.wifi.shared.model.WifiActivityModel
+import com.android.systemui.statusbar.pipeline.wifi.ui.model.WifiIcon
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@@ -59,6 +60,7 @@
@Mock private lateinit var statusBarPipelineFlags: StatusBarPipelineFlags
@Mock private lateinit var logger: ConnectivityPipelineLogger
+ @Mock private lateinit var tableLogBuffer: TableLogBuffer
@Mock private lateinit var connectivityConstants: ConnectivityConstants
@Mock private lateinit var wifiConstants: WifiConstants
private lateinit var airplaneModeRepository: FakeAirplaneModeRepository
@@ -103,21 +105,21 @@
@Test
fun wifiIcon_allLocationViewModelsReceiveSameData() = runBlocking(IMMEDIATE) {
- var latestHome: Icon? = null
+ var latestHome: WifiIcon? = null
val jobHome = underTest
.home
.wifiIcon
.onEach { latestHome = it }
.launchIn(this)
- var latestKeyguard: Icon? = null
+ var latestKeyguard: WifiIcon? = null
val jobKeyguard = underTest
.keyguard
.wifiIcon
.onEach { latestKeyguard = it }
.launchIn(this)
- var latestQs: Icon? = null
+ var latestQs: WifiIcon? = null
val jobQs = underTest
.qs
.wifiIcon
@@ -133,7 +135,7 @@
)
yield()
- assertThat(latestHome).isInstanceOf(Icon.Resource::class.java)
+ assertThat(latestHome).isInstanceOf(WifiIcon.Visible::class.java)
assertThat(latestHome).isEqualTo(latestKeyguard)
assertThat(latestKeyguard).isEqualTo(latestQs)
@@ -541,6 +543,7 @@
connectivityConstants,
context,
logger,
+ tableLogBuffer,
interactor,
scope,
statusBarPipelineFlags,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/MemoryTrackingTestCase.java b/packages/SystemUI/tests/utils/src/com/android/systemui/MemoryTrackingTestCase.java
new file mode 100644
index 0000000..3767fbe
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/MemoryTrackingTestCase.java
@@ -0,0 +1,63 @@
+/*
+ * 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;
+
+import android.os.Debug;
+import android.util.Log;
+
+import org.junit.AfterClass;
+import org.junit.Before;
+
+import java.io.File;
+import java.io.IOException;
+
+/**
+ * Convenience class for grabbing a heap dump after a test class is run.
+ *
+ * To use:
+ * - locally edit your test class to inherit from MemoryTrackingTestCase instead of SysuiTestCase
+ * - Watch the logcat with tag MEMORY to see the path to the .ahprof file
+ * - adb pull /path/to/something.ahprof
+ * - Download ahat from https://sites.google.com/corp/google.com/ahat/home
+ * - java -jar ~/Downloads/ahat-1.7.2.jar something.hprof
+ * - Watch output for next steps
+ * - Profit and fix leaks!
+ */
+public class MemoryTrackingTestCase extends SysuiTestCase {
+ private static File sFilesDir = null;
+ private static String sLatestTestClassName = null;
+
+ @Before public void grabFilesDir() {
+ if (sFilesDir == null) {
+ sFilesDir = mContext.getFilesDir();
+ }
+ sLatestTestClassName = getClass().getName();
+ }
+
+ @AfterClass
+ public static void dumpHeap() throws IOException {
+ if (sFilesDir == null) {
+ Log.e("MEMORY", "Somehow no test cases??");
+ return;
+ }
+ mockitoTearDown();
+ Log.w("MEMORY", "about to dump heap");
+ File path = new File(sFilesDir, sLatestTestClassName + ".ahprof");
+ Debug.dumpHprofData(path.getPath());
+ Log.w("MEMORY", "did it! Location: " + path);
+ }
+}
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/proto/src/OWNERS b/proto/src/OWNERS
index abd08de..ccff624 100644
--- a/proto/src/OWNERS
+++ b/proto/src/OWNERS
@@ -2,3 +2,4 @@
per-file wifi.proto = file:/wifi/OWNERS
per-file camera.proto = file:/services/core/java/com/android/server/camera/OWNERS
per-file system_messages.proto = file:/core/res/OWNERS
+per-file altitude.proto = file:/location/OWNERS
diff --git a/proto/src/altitude.proto b/proto/src/altitude.proto
new file mode 100644
index 0000000..1010f67
--- /dev/null
+++ b/proto/src/altitude.proto
@@ -0,0 +1,54 @@
+/*
+ * 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.
+ */
+
+syntax = "proto2";
+
+package com.android.internal.location.altitude;
+
+option java_package = "com.android.internal.location.altitude";
+option java_multiple_files = true;
+
+// Defines parameters for a spherically projected geoid map and corresponding
+// tile management.
+message MapParamsProto {
+ // Defines the resolution of the map in terms of an S2 level.
+ optional int32 map_s2_level = 1;
+ // Defines the resolution of the tiles in cache in terms of an S2 level.
+ optional int32 cache_tile_s2_level = 2;
+ // Defines the resolution of the tiles on disk in terms of an S2 level.
+ optional int32 disk_tile_s2_level = 3;
+ // Defines the `a` coefficient in the expression `a * map_value + b` used to
+ // calculate a geoid height in meters.
+ optional double model_a_meters = 4;
+ // Defines the `b` coefficient in the expression `a * map_value + b` used to
+ // calculate a geoid height in meters.
+ optional double model_b_meters = 5;
+ // Defines the root mean square error in meters of the geoid height.
+ optional double model_rmse_meters = 6;
+}
+
+// A single tile associating values in the unit interval [0, 1] to map cells.
+message S2TileProto {
+ // The S2 token associated with the common parent of all map cells in this
+ // tile.
+ optional string tile_key = 1;
+
+ // Encoded data that merge into a value in the unit interval [0, 1] for each
+ // map cell in this tile.
+ optional bytes byte_buffer = 2;
+ optional bytes byte_jpeg = 3;
+ optional bytes byte_png = 4;
+}
diff --git a/services/api/current.txt b/services/api/current.txt
index 834ed2f..18b1df3 100644
--- a/services/api/current.txt
+++ b/services/api/current.txt
@@ -59,6 +59,7 @@
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();
}
public static interface PackageManagerLocal.UnfilteredSnapshot extends java.lang.AutoCloseable {
@@ -98,6 +99,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();
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/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/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/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index 7566bab..efe14f4 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -18324,7 +18324,7 @@
public void waitForBroadcastBarrier(@Nullable PrintWriter pw) {
enforceCallingPermission(permission.DUMP, "waitForBroadcastBarrier()");
- BroadcastLoopers.waitForIdle(pw);
+ BroadcastLoopers.waitForBarrier(pw);
for (BroadcastQueue queue : mBroadcastQueues) {
queue.waitForBarrier(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..672392d 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;
@@ -551,48 +557,63 @@
* 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 isLPQueueEligible =
+ consecutiveHighPriorityCount >= maxHighPriorityDispatchLimit
+ && nextLPRecord.enqueueTime <= nextHPRecord.enqueueTime
+ && !blockedOnOrderedDispatch(nextLPRecord, nextLPRecordIndex);
+ return isLPQueueEligible ? lowPriorityQueue : highPriorityQueue;
+ }
+
+ private static boolean isQueueEmpty(@Nullable ArrayDeque<SomeArgs> queue) {
+ return (queue == null || queue.isEmpty());
}
/**
@@ -600,13 +621,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..e9340e9 100644
--- a/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java
+++ b/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java
@@ -220,10 +220,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 +280,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;
}
@@ -729,10 +740,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 +749,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 +790,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 +845,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 +961,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 +981,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 +1010,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 +1028,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;
@@ -1442,10 +1499,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
diff --git a/services/core/java/com/android/server/am/UserController.java b/services/core/java/com/android/server/am/UserController.java
index a7e95fb..aefa2f5 100644
--- a/services/core/java/com/android/server/am/UserController.java
+++ b/services/core/java/com/android/server/am/UserController.java
@@ -949,9 +949,8 @@
int stopUser(final int userId, final boolean force, boolean allowDelayedLocking,
final IStopUserCallback stopUserCallback, KeyEvictedCallback keyEvictedCallback) {
checkCallingPermission(INTERACT_ACROSS_USERS_FULL, "stopUser");
- if (userId < 0 || userId == UserHandle.USER_SYSTEM) {
- throw new IllegalArgumentException("Can't stop system user " + userId);
- }
+ Preconditions.checkArgument(userId >= 0, "Invalid user id %d", userId);
+
enforceShellRestriction(UserManager.DISALLOW_DEBUGGING_FEATURES, userId);
synchronized (mLock) {
return stopUsersLU(userId, force, allowDelayedLocking, stopUserCallback,
diff --git a/services/core/java/com/android/server/appop/AppOpsCheckingServiceImpl.java b/services/core/java/com/android/server/appop/AppOpsCheckingServiceImpl.java
index 8d1da71..587fb04 100644
--- a/services/core/java/com/android/server/appop/AppOpsCheckingServiceImpl.java
+++ b/services/core/java/com/android/server/appop/AppOpsCheckingServiceImpl.java
@@ -218,7 +218,7 @@
}
@Override
- public boolean arePackageModesDefault(String packageMode, @UserIdInt int userId) {
+ public boolean arePackageModesDefault(@NonNull String packageMode, @UserIdInt int userId) {
synchronized (mLock) {
ArrayMap<String, SparseIntArray> packageModes = mUserPackageModes.get(userId, null);
if (packageModes == null) {
@@ -490,15 +490,16 @@
}
@Override
- public SparseBooleanArray evalForegroundUidOps(int uid, SparseBooleanArray foregroundOps) {
+ public SparseBooleanArray evalForegroundUidOps(int uid,
+ @Nullable SparseBooleanArray foregroundOps) {
synchronized (mLock) {
return evalForegroundOps(mUidModes.get(uid), foregroundOps);
}
}
@Override
- public SparseBooleanArray evalForegroundPackageOps(String packageName,
- SparseBooleanArray foregroundOps, @UserIdInt int userId) {
+ public SparseBooleanArray evalForegroundPackageOps(@NonNull String packageName,
+ @Nullable SparseBooleanArray foregroundOps, @UserIdInt int userId) {
synchronized (mLock) {
ArrayMap<String, SparseIntArray> packageModes = mUserPackageModes.get(userId, null);
return evalForegroundOps(packageModes == null ? null : packageModes.get(packageName),
@@ -537,8 +538,8 @@
}
@Override
- public boolean dumpListeners(int dumpOp, int dumpUid, String dumpPackage,
- PrintWriter printWriter) {
+ public boolean dumpListeners(int dumpOp, int dumpUid, @Nullable String dumpPackage,
+ @NonNull PrintWriter printWriter) {
boolean needSep = false;
if (mOpModeWatchers.size() > 0) {
boolean printedHeader = false;
diff --git a/services/core/java/com/android/server/appop/AppOpsCheckingServiceInterface.java b/services/core/java/com/android/server/appop/AppOpsCheckingServiceInterface.java
index d8d0d48..ef3e368 100644
--- a/services/core/java/com/android/server/appop/AppOpsCheckingServiceInterface.java
+++ b/services/core/java/com/android/server/appop/AppOpsCheckingServiceInterface.java
@@ -103,7 +103,7 @@
* @param packageName package name.
* @param userId user id associated with the package.
*/
- boolean arePackageModesDefault(String packageName, @UserIdInt int userId);
+ boolean arePackageModesDefault(@NonNull String packageName, @UserIdInt int userId);
/**
* Stop tracking app-op modes for all uid and packages.
@@ -184,7 +184,7 @@
* @param foregroundOps boolean array where app-ops that have MODE_FOREGROUND are marked true.
* @return foregroundOps.
*/
- SparseBooleanArray evalForegroundUidOps(int uid, SparseBooleanArray foregroundOps);
+ SparseBooleanArray evalForegroundUidOps(int uid, @Nullable SparseBooleanArray foregroundOps);
/**
* Go over the list of app-ops for the package name and mark app-ops with MODE_FOREGROUND in
@@ -194,8 +194,8 @@
* @param userId user id associated with the package.
* @return foregroundOps.
*/
- SparseBooleanArray evalForegroundPackageOps(String packageName,
- SparseBooleanArray foregroundOps, @UserIdInt int userId);
+ SparseBooleanArray evalForegroundPackageOps(@NonNull String packageName,
+ @Nullable SparseBooleanArray foregroundOps, @UserIdInt int userId);
/**
* Dump op mode and package mode listeners and their details.
@@ -205,5 +205,6 @@
* @param dumpPackage if not null and if dumpOp is -1, dumps watchers for the package name.
* @param printWriter writer to dump to.
*/
- boolean dumpListeners(int dumpOp, int dumpUid, String dumpPackage, PrintWriter printWriter);
+ boolean dumpListeners(int dumpOp, int dumpUid, @Nullable String dumpPackage,
+ @NonNull PrintWriter printWriter);
}
diff --git a/services/core/java/com/android/server/appop/OnOpModeChangedListener.java b/services/core/java/com/android/server/appop/OnOpModeChangedListener.java
index 5ebe811..1d1a9e7 100644
--- a/services/core/java/com/android/server/appop/OnOpModeChangedListener.java
+++ b/services/core/java/com/android/server/appop/OnOpModeChangedListener.java
@@ -22,7 +22,7 @@
* Listener for mode changes, encapsulates methods that should be triggered in the event of a mode
* change.
*/
-abstract class OnOpModeChangedListener {
+public abstract class OnOpModeChangedListener {
// Constant meaning that any UID should be matched when dispatching callbacks
private static final int UID_ANY = -2;
diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java
index 519429f..aa8ee3d 100644
--- a/services/core/java/com/android/server/audio/AudioService.java
+++ b/services/core/java/com/android/server/audio/AudioService.java
@@ -86,6 +86,7 @@
import android.media.AudioFocusInfo;
import android.media.AudioFocusRequest;
import android.media.AudioFormat;
+import android.media.AudioHalVersionInfo;
import android.media.AudioManager;
import android.media.AudioManagerInternal;
import android.media.AudioPlaybackConfiguration;
@@ -257,6 +258,9 @@
/** Debug communication route */
protected static final boolean DEBUG_COMM_RTE = false;
+ /** Debug log sound fx (touchsounds...) in dumpsys */
+ protected static final boolean DEBUG_LOG_SOUND_FX = false;
+
/** How long to delay before persisting a change in volume/ringer mode. */
private static final int PERSIST_DELAY = 500;
@@ -378,6 +382,7 @@
private static final int MSG_ROTATION_UPDATE = 48;
private static final int MSG_FOLD_UPDATE = 49;
private static final int MSG_RESET_SPATIALIZER = 50;
+ private static final int MSG_NO_LOG_FOR_PLAYER_I = 51;
// start of messages handled under wakelock
// these messages can only be queued, i.e. sent with queueMsgUnderWakeLock(),
@@ -1016,7 +1021,7 @@
PowerManager pm = (PowerManager)context.getSystemService(Context.POWER_SERVICE);
mAudioEventWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "handleAudioEvent");
- mSfxHelper = new SoundEffectsHelper(mContext);
+ mSfxHelper = new SoundEffectsHelper(mContext, playerBase -> ignorePlayerLogs(playerBase));
final boolean headTrackingDefault = mContext.getResources().getBoolean(
com.android.internal.R.bool.config_spatial_audio_head_tracking_enabled_default);
@@ -1500,6 +1505,18 @@
}
//-----------------------------------------------------------------
+ // Communicate to PlayackActivityMonitor whether to log or not
+ // the sound FX activity (useful for removing touch sounds in the activity logs)
+ void ignorePlayerLogs(@NonNull PlayerBase playerToIgnore) {
+ if (DEBUG_LOG_SOUND_FX) {
+ return;
+ }
+ sendMsg(mAudioHandler, MSG_NO_LOG_FOR_PLAYER_I, SENDMSG_REPLACE,
+ /*arg1, piid of the player*/ playerToIgnore.getPlayerIId(),
+ /*arg2 ignored*/ 0, /*obj ignored*/ null, /*delay*/ 0);
+ }
+
+ //-----------------------------------------------------------------
// monitoring requests for volume range initialization
@Override // AudioSystemAdapter.OnVolRangeInitRequestListener
public void onVolumeRangeInitRequestFromNative() {
@@ -6622,9 +6639,13 @@
return AudioSystem.STREAM_RING;
} else if (wasStreamActiveRecently(
AudioSystem.STREAM_NOTIFICATION, sStreamOverrideDelayMs)) {
- if (DEBUG_VOL)
- Log.v(TAG, "getActiveStreamType: Forcing STREAM_NOTIFICATION stream active");
- return AudioSystem.STREAM_NOTIFICATION;
+ if (DEBUG_VOL) {
+ Log.v(
+ TAG,
+ "getActiveStreamType: Forcing STREAM_NOTIFICATION stream"
+ + " active");
+ }
+ return AudioSystem.STREAM_NOTIFICATION;
} else {
if (DEBUG_VOL) {
Log.v(TAG, "getActiveStreamType: Forcing DEFAULT_VOL_STREAM_NO_PLAYBACK("
@@ -8721,6 +8742,10 @@
// fold parameter format: "device_folded=x" where x is one of on, off
mAudioSystem.setParameters((String) msg.obj);
break;
+
+ case MSG_NO_LOG_FOR_PLAYER_I:
+ mPlaybackMonitor.ignorePlayerIId(msg.arg1);
+ break;
}
}
}
@@ -11299,15 +11324,16 @@
return mMediaFocusControl.sendFocusLoss(focusLoser);
}
- private static final String[] HAL_VERSIONS =
- new String[] {"7.1", "7.0", "6.0", "5.0", "4.0", "2.0"};
-
- /** @see AudioManager#getHalVersion */
- public @Nullable String getHalVersion() {
- for (String version : HAL_VERSIONS) {
+ /**
+ * @see AudioManager#getHalVersion
+ */
+ public @Nullable AudioHalVersionInfo getHalVersion() {
+ for (AudioHalVersionInfo version : AudioHalVersionInfo.VERSIONS) {
try {
+ // TODO: check AIDL service.
+ String versionStr = version.getMajorVersion() + "." + version.getMinorVersion();
HwBinder.getService(
- String.format("android.hardware.audio@%s::IDevicesFactory", version),
+ String.format("android.hardware.audio@%s::IDevicesFactory", versionStr),
"default");
return version;
} catch (NoSuchElementException e) {
diff --git a/services/core/java/com/android/server/audio/PlaybackActivityMonitor.java b/services/core/java/com/android/server/audio/PlaybackActivityMonitor.java
index 89dfc17..f35931ca 100644
--- a/services/core/java/com/android/server/audio/PlaybackActivityMonitor.java
+++ b/services/core/java/com/android/server/audio/PlaybackActivityMonitor.java
@@ -192,6 +192,18 @@
}
//=================================================================
+ // Player to ignore (only handling single player, designed for ignoring
+ // in the logs one specific player such as the touch sounds player)
+ @GuardedBy("mPlayerLock")
+ private ArrayList<Integer> mDoNotLogPiidList = new ArrayList<>();
+
+ /*package*/ void ignorePlayerIId(int doNotLogPiid) {
+ synchronized (mPlayerLock) {
+ mDoNotLogPiidList.add(doNotLogPiid);
+ }
+ }
+
+ //=================================================================
// Track players and their states
// methods playerAttributes, playerEvent, releasePlayer are all oneway calls
// into AudioService. They trigger synchronous dispatchPlaybackChange() which updates
@@ -315,14 +327,18 @@
Log.v(TAG, TextUtils.formatSimple("playerEvent(piid=%d, event=%s, eventValue=%d)",
piid, AudioPlaybackConfiguration.playerStateToString(event), eventValue));
}
-
- final boolean change;
+ boolean change;
synchronized(mPlayerLock) {
final AudioPlaybackConfiguration apc = mPlayers.get(new Integer(piid));
if (apc == null) {
return;
}
+ final boolean doNotLog = mDoNotLogPiidList.contains(piid);
+ if (doNotLog && event != AudioPlaybackConfiguration.PLAYER_STATE_RELEASED) {
+ // do not log nor dispatch events for "ignored" players other than the release
+ return;
+ }
sEventLogger.enqueue(new PlayerEvent(piid, event, eventValue));
if (event == AudioPlaybackConfiguration.PLAYER_UPDATE_PORT_ID) {
@@ -339,7 +355,8 @@
}
}
}
- if (apc.getPlayerType() == AudioPlaybackConfiguration.PLAYER_TYPE_JAM_SOUNDPOOL) {
+ if (apc.getPlayerType() == AudioPlaybackConfiguration.PLAYER_TYPE_JAM_SOUNDPOOL
+ && event != AudioPlaybackConfiguration.PLAYER_STATE_RELEASED) {
// FIXME SoundPool not ready for state reporting
return;
}
@@ -351,9 +368,15 @@
Log.e(TAG, "Error handling event " + event);
change = false;
}
- if (change && event == AudioPlaybackConfiguration.PLAYER_STATE_STARTED) {
- mDuckingManager.checkDuck(apc);
- mFadingManager.checkFade(apc);
+ if (change) {
+ if (event == AudioPlaybackConfiguration.PLAYER_STATE_STARTED) {
+ mDuckingManager.checkDuck(apc);
+ mFadingManager.checkFade(apc);
+ }
+ if (doNotLog) {
+ // do not dispatch events for "ignored" players
+ change = false;
+ }
}
}
if (change) {
@@ -436,6 +459,10 @@
mEventHandler.sendMessage(
mEventHandler.obtainMessage(MSG_L_CLEAR_PORTS_FOR_PIID, piid, /*arg2=*/0));
+ if (change && mDoNotLogPiidList.contains(piid)) {
+ // do not dispatch a change for a "do not log" player
+ change = false;
+ }
}
}
if (change) {
@@ -581,6 +608,9 @@
for (Integer piidInt : piidIntList) {
final AudioPlaybackConfiguration apc = mPlayers.get(piidInt);
if (apc != null) {
+ if (mDoNotLogPiidList.contains(apc.getPlayerInterfaceId())) {
+ pw.print("(not logged)");
+ }
apc.dump(pw);
}
}
diff --git a/services/core/java/com/android/server/audio/SoundEffectsHelper.java b/services/core/java/com/android/server/audio/SoundEffectsHelper.java
index 79b54eb..8c4efba 100644
--- a/services/core/java/com/android/server/audio/SoundEffectsHelper.java
+++ b/services/core/java/com/android/server/audio/SoundEffectsHelper.java
@@ -25,6 +25,7 @@
import android.media.MediaPlayer;
import android.media.MediaPlayer.OnCompletionListener;
import android.media.MediaPlayer.OnErrorListener;
+import android.media.PlayerBase;
import android.media.SoundPool;
import android.os.Environment;
import android.os.Handler;
@@ -47,6 +48,7 @@
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import java.util.function.Consumer;
/**
* A helper class for managing sound effects loading / unloading
@@ -109,11 +111,14 @@
private final int[] mEffects = new int[AudioManager.NUM_SOUND_EFFECTS]; // indexes in mResources
private SoundPool mSoundPool;
private SoundPoolLoader mSoundPoolLoader;
+ /** callback to provide handle to the player of the sound effects */
+ private final Consumer<PlayerBase> mPlayerAvailableCb;
- SoundEffectsHelper(Context context) {
+ SoundEffectsHelper(Context context, Consumer<PlayerBase> playerAvailableCb) {
mContext = context;
mSfxAttenuationDb = mContext.getResources().getInteger(
com.android.internal.R.integer.config_soundEffectVolumeDb);
+ mPlayerAvailableCb = playerAvailableCb;
startWorker();
}
@@ -189,6 +194,7 @@
.setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
.build())
.build();
+ mPlayerAvailableCb.accept(mSoundPool);
loadSoundAssets();
mSoundPoolLoader = new SoundPoolLoader();
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..ee53b60 100644
--- a/services/core/java/com/android/server/display/LocalDisplayAdapter.java
+++ b/services/core/java/com/android/server/display/LocalDisplayAdapter.java
@@ -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);
}
}
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/PersistentDataStore.java b/services/core/java/com/android/server/display/PersistentDataStore.java
index f30a84f..e7601bc 100644
--- a/services/core/java/com/android/server/display/PersistentDataStore.java
+++ b/services/core/java/com/android/server/display/PersistentDataStore.java
@@ -619,7 +619,7 @@
private static final class DisplayState {
private int mColorMode;
- private float mBrightness;
+ private float mBrightness = Float.NaN;
private int mWidth;
private int mHeight;
private float mRefreshRate;
@@ -700,7 +700,11 @@
break;
case TAG_BRIGHTNESS_VALUE:
String brightness = parser.nextText();
- mBrightness = Float.parseFloat(brightness);
+ try {
+ mBrightness = Float.parseFloat(brightness);
+ } catch (NumberFormatException e) {
+ mBrightness = Float.NaN;
+ }
break;
case TAG_BRIGHTNESS_CONFIGURATIONS:
mDisplayBrightnessConfigurations.loadFromXml(parser);
@@ -727,7 +731,9 @@
serializer.endTag(null, TAG_COLOR_MODE);
serializer.startTag(null, TAG_BRIGHTNESS_VALUE);
- serializer.text(Float.toString(mBrightness));
+ if (!Float.isNaN(mBrightness)) {
+ serializer.text(Float.toString(mBrightness));
+ }
serializer.endTag(null, TAG_BRIGHTNESS_VALUE);
serializer.startTag(null, TAG_BRIGHTNESS_CONFIGURATIONS);
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/hdmi/ArcTerminationActionFromAvr.java b/services/core/java/com/android/server/hdmi/ArcTerminationActionFromAvr.java
index 4855be6..ccb2633 100644
--- a/services/core/java/com/android/server/hdmi/ArcTerminationActionFromAvr.java
+++ b/services/core/java/com/android/server/hdmi/ArcTerminationActionFromAvr.java
@@ -15,6 +15,8 @@
*/
package com.android.server.hdmi;
+import android.hardware.hdmi.HdmiControlManager;
+import android.hardware.hdmi.IHdmiControlCallback;
import android.hardware.tv.cec.V1_0.SendMessageResult;
/**
@@ -33,6 +35,10 @@
super(source);
}
+ ArcTerminationActionFromAvr(HdmiCecLocalDevice source, IHdmiControlCallback callback) {
+ super(source, callback);
+ }
+
@Override
boolean start() {
mState = STATE_WAITING_FOR_INITIATE_ARC_RESPONSE;
@@ -47,10 +53,19 @@
return false;
}
switch (cmd.getOpcode()) {
+ case Constants.MESSAGE_FEATURE_ABORT:
+ int originalOpcode = cmd.getParams()[0] & 0xFF;
+ if (originalOpcode == Constants.MESSAGE_TERMINATE_ARC) {
+ mState = STATE_ARC_TERMINATED;
+ audioSystem().processArcTermination();
+ finishWithCallback(HdmiControlManager.RESULT_TARGET_NOT_AVAILABLE);
+ return true;
+ }
+ return false;
case Constants.MESSAGE_REPORT_ARC_TERMINATED:
mState = STATE_ARC_TERMINATED;
audioSystem().processArcTermination();
- finish();
+ finishWithCallback(HdmiControlManager.RESULT_SUCCESS);
return true;
}
return false;
@@ -79,7 +94,7 @@
audioSystem().setArcStatus(false);
}
HdmiLogger.debug("Terminate ARC was not successfully sent.");
- finish();
+ finishWithCallback(HdmiControlManager.RESULT_TARGET_NOT_AVAILABLE);
}
});
}
@@ -88,6 +103,6 @@
// Disable ARC if TV didn't respond with <Report ARC Terminated> in time.
audioSystem().setArcStatus(false);
HdmiLogger.debug("handleTerminateArcTimeout");
- finish();
+ finishWithCallback(HdmiControlManager.RESULT_TIMEOUT);
}
}
diff --git a/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceAudioSystem.java b/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceAudioSystem.java
index ccaa9255d..a026c4b 100644
--- a/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceAudioSystem.java
+++ b/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceAudioSystem.java
@@ -458,8 +458,16 @@
HdmiLogger.debug("ARC is not established between TV and AVR device");
return Constants.ABORT_NOT_IN_CORRECT_MODE;
} else {
- removeAction(ArcTerminationActionFromAvr.class);
- addAndStartAction(new ArcTerminationActionFromAvr(this));
+ if (!getActions(ArcTerminationActionFromAvr.class).isEmpty()
+ && !getActions(ArcTerminationActionFromAvr.class).get(0).mCallbacks.isEmpty()) {
+ IHdmiControlCallback callback =
+ getActions(ArcTerminationActionFromAvr.class).get(0).mCallbacks.get(0);
+ removeAction(ArcTerminationActionFromAvr.class);
+ addAndStartAction(new ArcTerminationActionFromAvr(this, callback));
+ } else {
+ removeAction(ArcTerminationActionFromAvr.class);
+ addAndStartAction(new ArcTerminationActionFromAvr(this));
+ }
return Constants.HANDLED;
}
}
diff --git a/services/core/java/com/android/server/hdmi/HdmiControlService.java b/services/core/java/com/android/server/hdmi/HdmiControlService.java
index 43cd71a..2f15e57 100644
--- a/services/core/java/com/android/server/hdmi/HdmiControlService.java
+++ b/services/core/java/com/android/server/hdmi/HdmiControlService.java
@@ -878,16 +878,30 @@
Slog.w(TAG, "Device type doesn't support ARC.");
return;
}
+ boolean isArcEnabled = false;
if (settingValue == SOUNDBAR_MODE_DISABLED && audioSystem != null) {
- if (audioSystem.isArcEnabled()) {
- audioSystem.addAndStartAction(new ArcTerminationActionFromAvr(audioSystem));
- }
+ isArcEnabled = audioSystem.isArcEnabled();
if (isSystemAudioActivated()) {
audioSystem.terminateSystemAudioMode();
}
+ if (isArcEnabled) {
+ if (audioSystem.hasAction(ArcTerminationActionFromAvr.class)) {
+ audioSystem.removeAction(ArcTerminationActionFromAvr.class);
+ }
+ audioSystem.addAndStartAction(new ArcTerminationActionFromAvr(audioSystem,
+ new IHdmiControlCallback.Stub() {
+ @Override
+ public void onComplete(int result) {
+ mAddressAllocated = false;
+ initializeCecLocalDevices(INITIATED_BY_SOUNDBAR_MODE);
+ }
+ }));
+ }
}
- mAddressAllocated = false;
- initializeCecLocalDevices(INITIATED_BY_SOUNDBAR_MODE);
+ if (!isArcEnabled) {
+ mAddressAllocated = false;
+ initializeCecLocalDevices(INITIATED_BY_SOUNDBAR_MODE);
+ }
}
/**
diff --git a/services/core/java/com/android/server/input/KeyboardLayoutManager.java b/services/core/java/com/android/server/input/KeyboardLayoutManager.java
index 85d6197..1bb14aa 100644
--- a/services/core/java/com/android/server/input/KeyboardLayoutManager.java
+++ b/services/core/java/com/android/server/input/KeyboardLayoutManager.java
@@ -145,7 +145,7 @@
@Override
public void onInputDeviceChanged(int deviceId) {
final InputDevice inputDevice = getInputDevice(deviceId);
- if (inputDevice == null) {
+ if (inputDevice == null || inputDevice.isVirtual() || !inputDevice.isFullKeyboard()) {
return;
}
synchronized (mDataStore) {
diff --git a/services/core/java/com/android/server/notification/SnoozeHelper.java b/services/core/java/com/android/server/notification/SnoozeHelper.java
index 4bbd40d..5f8572b 100644
--- a/services/core/java/com/android/server/notification/SnoozeHelper.java
+++ b/services/core/java/com/android/server/notification/SnoozeHelper.java
@@ -59,6 +59,9 @@
static final int CONCURRENT_SNOOZE_LIMIT = 500;
+ // A safe size for strings to be put in persistent storage, to avoid breaking the XML write.
+ static final int MAX_STRING_LENGTH = 1000;
+
protected static final String XML_TAG_NAME = "snoozed-notifications";
private static final String XML_SNOOZED_NOTIFICATION = "notification";
@@ -200,7 +203,7 @@
scheduleRepost(key, duration);
Long activateAt = System.currentTimeMillis() + duration;
synchronized (mLock) {
- mPersistedSnoozedNotifications.put(key, activateAt);
+ mPersistedSnoozedNotifications.put(getTrimmedString(key), activateAt);
}
}
@@ -210,7 +213,10 @@
protected void snooze(NotificationRecord record, String contextId) {
if (contextId != null) {
synchronized (mLock) {
- mPersistedSnoozedNotificationsWithContext.put(record.getKey(), contextId);
+ mPersistedSnoozedNotificationsWithContext.put(
+ getTrimmedString(record.getKey()),
+ getTrimmedString(contextId)
+ );
}
}
snooze(record);
@@ -225,6 +231,13 @@
}
}
+ private String getTrimmedString(String key) {
+ if (key != null && key.length() > MAX_STRING_LENGTH) {
+ return key.substring(0, MAX_STRING_LENGTH);
+ }
+ return key;
+ }
+
protected boolean cancel(int userId, String pkg, String tag, int id) {
synchronized (mLock) {
final Set<Map.Entry<String, NotificationRecord>> records =
@@ -293,10 +306,12 @@
}
protected void repost(String key, int userId, boolean muteOnReturn) {
+ final String trimmedKey = getTrimmedString(key);
+
NotificationRecord record;
synchronized (mLock) {
- mPersistedSnoozedNotifications.remove(key);
- mPersistedSnoozedNotificationsWithContext.remove(key);
+ mPersistedSnoozedNotifications.remove(trimmedKey);
+ mPersistedSnoozedNotificationsWithContext.remove(trimmedKey);
record = mSnoozedNotifications.remove(key);
}
diff --git a/services/core/java/com/android/server/pm/PackageManagerLocal.java b/services/core/java/com/android/server/pm/PackageManagerLocal.java
index 21c2f2c..fad61b8 100644
--- a/services/core/java/com/android/server/pm/PackageManagerLocal.java
+++ b/services/core/java/com/android/server/pm/PackageManagerLocal.java
@@ -167,14 +167,16 @@
PackageState getPackageState(@NonNull String packageName);
/**
- * Iterates on all states. This should only be used when either the target package name
- * is not known or the large majority of the states are expected to be used.
- *
+ * Returns a map of all {@link PackageState PackageStates} on the device.
+ * <p>
* This will cause app visibility filtering to be invoked on each state on the device,
- * which can be expensive.
+ * which can be expensive. Prefer {@link #getPackageState(String)} if possible.
*
- * @param consumer Block to accept each state as it becomes available post-filtering.
+ * @return Mapping of package name to {@link PackageState}.
*/
+ @NonNull
+ Map<String, PackageState> getPackageStates();
+
void forAllPackageStates(@NonNull Consumer<PackageState> consumer);
@Override
diff --git a/services/core/java/com/android/server/pm/PackageManagerServiceUtils.java b/services/core/java/com/android/server/pm/PackageManagerServiceUtils.java
index 77334e5..a72ae56 100644
--- a/services/core/java/com/android/server/pm/PackageManagerServiceUtils.java
+++ b/services/core/java/com/android/server/pm/PackageManagerServiceUtils.java
@@ -142,6 +142,11 @@
public static final Predicate<PackageStateInternal> REMOVE_IF_NULL_PKG =
pkgSetting -> pkgSetting.getPkg() == null;
+ // This is a horrible hack to workaround b/240373119, specifically for fixing the T branch.
+ // A proper fix should be implemented in master instead.
+ public static final ThreadLocal<Boolean> DISABLE_ENFORCE_INTENTS_TO_MATCH_INTENT_FILTERS =
+ ThreadLocal.withInitial(() -> false);
+
/**
* Components of apps targeting Android T and above will stop receiving intents from
* external callers that do not match its declared intent filters.
@@ -1093,6 +1098,8 @@
PlatformCompat compat, ComponentResolverApi resolver,
List<ResolveInfo> resolveInfos, boolean isReceiver,
Intent intent, String resolvedType, int filterCallingUid) {
+ if (DISABLE_ENFORCE_INTENTS_TO_MATCH_INTENT_FILTERS.get()) return;
+
final Printer logPrinter = DEBUG_INTENT_MATCHING
? new LogPrinter(Log.VERBOSE, TAG, Log.LOG_ID_SYSTEM)
: null;
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/local/PackageManagerLocalImpl.java b/services/core/java/com/android/server/pm/local/PackageManagerLocalImpl.java
index 4ff0d59..024b63e 100644
--- a/services/core/java/com/android/server/pm/local/PackageManagerLocalImpl.java
+++ b/services/core/java/com/android/server/pm/local/PackageManagerLocalImpl.java
@@ -22,6 +22,7 @@
import android.annotation.UserIdInt;
import android.os.Binder;
import android.os.UserHandle;
+import android.util.ArrayMap;
import com.android.server.pm.Computer;
import com.android.server.pm.PackageManagerLocal;
@@ -30,7 +31,6 @@
import com.android.server.pm.snapshot.PackageDataSnapshot;
import java.io.IOException;
-import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
@@ -143,7 +143,7 @@
private final int mUserId;
@Nullable
- private ArrayList<PackageState> mFilteredPackageStates;
+ private Map<String, PackageState> mFilteredPackageStates;
@Nullable
private final UnfilteredSnapshotImpl mParentSnapshot;
@@ -179,25 +179,36 @@
return mSnapshot.getPackageStateFiltered(packageName, mCallingUid, mUserId);
}
+ @NonNull
@Override
- public void forAllPackageStates(@NonNull Consumer<PackageState> consumer) {
+ public Map<String, PackageState> getPackageStates() {
checkClosed();
if (mFilteredPackageStates == null) {
var packageStates = mSnapshot.getPackageStates();
- var filteredPackageStates = new ArrayList<PackageState>();
+ var filteredPackageStates = new ArrayMap<String, PackageState>();
for (int index = 0, size = packageStates.size(); index < size; index++) {
var packageState = packageStates.valueAt(index);
if (!mSnapshot.shouldFilterApplication(packageState, mCallingUid, mUserId)) {
- filteredPackageStates.add(packageState);
+ filteredPackageStates.put(packageStates.keyAt(index), packageState);
}
}
- mFilteredPackageStates = filteredPackageStates;
+ mFilteredPackageStates = Collections.unmodifiableMap(filteredPackageStates);
}
- for (int index = 0, size = mFilteredPackageStates.size(); index < size; index++) {
- var packageState = mFilteredPackageStates.get(index);
- consumer.accept(packageState);
+ 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/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/policy/DeviceStateProviderImpl.java b/services/core/java/com/android/server/policy/DeviceStateProviderImpl.java
index d2e0502..91bb677 100644
--- a/services/core/java/com/android/server/policy/DeviceStateProviderImpl.java
+++ b/services/core/java/com/android/server/policy/DeviceStateProviderImpl.java
@@ -378,13 +378,14 @@
try {
conditionSatisfied = mStateConditions.get(state).getAsBoolean();
} catch (IllegalStateException e) {
- // Failed to compute the current state based on current available data. Return
+ // Failed to compute the current state based on current available data. Continue
// with the expectation that notifyDeviceStateChangedIfNeeded() will be called
- // when a callback with the missing data is triggered.
+ // when a callback with the missing data is triggered. May trigger another state
+ // change if another state is satisfied currently.
if (DEBUG) {
Slog.d(TAG, "Unable to check current state", e);
}
- return;
+ continue;
}
if (conditionSatisfied) {
diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java
index 9d58a73..2f0f88a 100644
--- a/services/core/java/com/android/server/policy/PhoneWindowManager.java
+++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java
@@ -2920,6 +2920,27 @@
return key_consumed;
}
break;
+ case KeyEvent.KEYCODE_DPAD_UP:
+ if (down && event.isMetaPressed() && event.isCtrlPressed() && repeatCount == 0) {
+ StatusBarManagerInternal statusbar = getStatusBarManagerInternal();
+ if (statusbar != null) {
+ statusbar.goToFullscreenFromSplit();
+ }
+ 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());
@@ -3574,6 +3595,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/statusbar/StatusBarManagerInternal.java b/services/core/java/com/android/server/statusbar/StatusBarManagerInternal.java
index 434cd78..392fda9 100644
--- a/services/core/java/com/android/server/statusbar/StatusBarManagerInternal.java
+++ b/services/core/java/com/android/server/statusbar/StatusBarManagerInternal.java
@@ -205,4 +205,16 @@
* @see com.android.internal.statusbar.IStatusBar#showRearDisplayDialog
*/
void showRearDisplayDialog(int currentBaseState);
+
+ /**
+ * 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 006d888..8d71d9c 100644
--- a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java
+++ b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java
@@ -726,6 +726,24 @@
} catch (RemoteException ex) { }
}
}
+
+ @Override
+ public void goToFullscreenFromSplit() {
+ if (mBar != null) {
+ try {
+ mBar.goToFullscreenFromSplit();
+ } 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/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 e099aac..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;
}
@@ -5433,25 +5434,20 @@
*/
private void postApplyAnimation(boolean visible, boolean fromTransition) {
final boolean usingShellTransitions = mTransitionController.isShellTransitionsEnabled();
- final boolean delayed = isAnimating(PARENTS | CHILDREN,
+ final boolean delayed = !usingShellTransitions && isAnimating(PARENTS | CHILDREN,
ANIMATION_TYPE_APP_TRANSITION | ANIMATION_TYPE_WINDOW_ANIMATION
| ANIMATION_TYPE_RECENTS);
- if (!delayed) {
+ if (!delayed && !usingShellTransitions) {
// We aren't delayed anything, but exiting windows rely on the animation finished
// callback being called in case the ActivityRecord was pretending to be delayed,
// which we might have done because we were in closing/opening apps list.
- if (!usingShellTransitions) {
- onAnimationFinished(ANIMATION_TYPE_APP_TRANSITION, null /* AnimationAdapter */);
- if (visible) {
- // The token was made immediately visible, there will be no entrance animation.
- // We need to inform the client the enter animation was finished.
- mEnteringAnimation = true;
- mWmService.mActivityManagerAppTransitionNotifier.onAppTransitionFinishedLocked(
- token);
- }
- } else {
- // update wallpaper target
- setAppLayoutChanges(FINISH_LAYOUT_REDO_WALLPAPER, "ActivityRecord");
+ onAnimationFinished(ANIMATION_TYPE_APP_TRANSITION, null /* AnimationAdapter */);
+ if (visible) {
+ // The token was made immediately visible, there will be no entrance animation.
+ // We need to inform the client the enter animation was finished.
+ mEnteringAnimation = true;
+ mWmService.mActivityManagerAppTransitionNotifier.onAppTransitionFinishedLocked(
+ token);
}
}
@@ -5460,8 +5456,8 @@
// updated.
// If we're becoming invisible, update the client visibility if we are not running an
// animation. Otherwise, we'll update client visibility in onAnimationFinished.
- if (visible || !isAnimating(PARENTS, ANIMATION_TYPE_APP_TRANSITION | ANIMATION_TYPE_RECENTS)
- || usingShellTransitions) {
+ if (visible || usingShellTransitions
+ || !isAnimating(PARENTS, ANIMATION_TYPE_APP_TRANSITION | ANIMATION_TYPE_RECENTS)) {
setClientVisible(visible);
}
@@ -6549,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) {
@@ -7442,8 +7438,10 @@
} else if (!show && mLastSurfaceShowing) {
getSyncTransaction().hide(mSurfaceControl);
}
- if (show) {
- mActivityRecordInputSink.applyChangesToSurfaceIfChanged(getSyncTransaction());
+ // Input sink surface is not a part of animation, so just apply in a steady state
+ // (non-sync) with pending transaction.
+ if (show && mSyncState == SYNC_STATE_NONE) {
+ mActivityRecordInputSink.applyChangesToSurfaceIfChanged(getPendingTransaction());
}
}
if (mThumbnail != 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/ActivityTaskSupervisor.java b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java
index 3145ab3..8a247cf 100644
--- a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java
+++ b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java
@@ -145,6 +145,7 @@
import com.android.server.am.ActivityManagerService;
import com.android.server.am.HostingRecord;
import com.android.server.am.UserState;
+import com.android.server.pm.PackageManagerServiceUtils;
import com.android.server.utils.Slogf;
import com.android.server.wm.ActivityMetricsLogger.LaunchingState;
@@ -2619,12 +2620,17 @@
// ActivityStarter will acquire the lock where the places need, so execute the request
// outside of the lock.
try {
+ // We need to temporarily disable the explicit intent filter matching enforcement
+ // because Task does not store the resolved type of the intent data, causing filter
+ // mismatch in certain cases. (b/240373119)
+ PackageManagerServiceUtils.DISABLE_ENFORCE_INTENTS_TO_MATCH_INTENT_FILTERS.set(true);
return mService.getActivityStartController().startActivityInPackage(taskCallingUid,
callingPid, callingUid, callingPackage, callingFeatureId, intent, null, null,
null, 0, 0, options, userId, task, "startActivityFromRecents",
false /* validateIncomingUser */, null /* originatingPendingIntent */,
false /* allowBackgroundActivityStart */);
} finally {
+ PackageManagerServiceUtils.DISABLE_ENFORCE_INTENTS_TO_MATCH_INTENT_FILTERS.set(false);
synchronized (mService.mGlobalLock) {
mService.continueWindowLayout();
}
diff --git a/services/core/java/com/android/server/wm/BackNavigationController.java b/services/core/java/com/android/server/wm/BackNavigationController.java
index 8680f1b..b16602e 100644
--- a/services/core/java/com/android/server/wm/BackNavigationController.java
+++ b/services/core/java/com/android/server/wm/BackNavigationController.java
@@ -810,7 +810,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 9ac1762..b419f36 100644
--- a/services/core/java/com/android/server/wm/DisplayPolicy.java
+++ b/services/core/java/com/android/server/wm/DisplayPolicy.java
@@ -57,7 +57,6 @@
import static android.view.WindowManager.LayoutParams.TYPE_VOICE_INTERACTION;
import static android.view.WindowManager.LayoutParams.TYPE_VOICE_INTERACTION_STARTING;
import static android.view.WindowManager.LayoutParams.TYPE_WALLPAPER;
-import static android.view.WindowManager.TRANSIT_WAKE;
import static android.view.WindowManagerGlobal.ADD_OKAY;
import static android.view.WindowManagerPolicyConstants.ACTION_HDMI_PLUGGED;
import static android.view.WindowManagerPolicyConstants.ALT_BAR_BOTTOM;
@@ -789,13 +788,7 @@
if (!mDisplayContent.isDefaultDisplay) {
return;
}
- if (mAwake && mDisplayContent.mTransitionController.isShellTransitionsEnabled()
- && !mDisplayContent.mTransitionController.isCollecting()) {
- // Start a transition for waking. This is needed for showWhenLocked activities.
- mDisplayContent.mTransitionController.requestTransitionIfNeeded(TRANSIT_WAKE,
- 0 /* flags */, null /* trigger */, mDisplayContent);
- }
- mService.mAtmService.mKeyguardController.updateDeferWakeTransition(
+ mService.mAtmService.mKeyguardController.updateDeferTransitionForAod(
mAwake /* waiting */);
}
}
diff --git a/services/core/java/com/android/server/wm/DisplayRotation.java b/services/core/java/com/android/server/wm/DisplayRotation.java
index eaa08fd..185e06e 100644
--- a/services/core/java/com/android/server/wm/DisplayRotation.java
+++ b/services/core/java/com/android/server/wm/DisplayRotation.java
@@ -1537,6 +1537,7 @@
private int mHalfFoldSavedRotation = -1; // No saved rotation
private DeviceStateController.FoldState mFoldState =
DeviceStateController.FoldState.UNKNOWN;
+ private boolean mInHalfFoldTransition = false;
boolean overrideFrozenRotation() {
return mFoldState == DeviceStateController.FoldState.HALF_FOLDED;
@@ -1544,6 +1545,7 @@
boolean shouldRevertOverriddenRotation() {
return mFoldState == DeviceStateController.FoldState.OPEN // When transitioning to open.
+ && mInHalfFoldTransition
&& mHalfFoldSavedRotation != -1 // Ignore if we've already reverted.
&& mUserRotationMode
== WindowManagerPolicy.USER_ROTATION_LOCKED; // Ignore if we're unlocked.
@@ -1552,6 +1554,7 @@
int revertOverriddenRotation() {
int savedRotation = mHalfFoldSavedRotation;
mHalfFoldSavedRotation = -1;
+ mInHalfFoldTransition = false;
return savedRotation;
}
@@ -1577,16 +1580,11 @@
mService.updateRotation(false /* alwaysSendConfiguration */,
false /* forceRelayout */);
} else {
- // Revert the rotation to our saved value if we transition from HALF_FOLDED.
- if (mHalfFoldSavedRotation != -1) {
- mRotation = mHalfFoldSavedRotation;
- }
- // Tell the device to update its orientation (mFoldState is still HALF_FOLDED here
- // so we will override USER_ROTATION_LOCKED and allow a rotation).
+ mInHalfFoldTransition = true;
+ mFoldState = newState;
+ // Tell the device to update its orientation.
mService.updateRotation(false /* alwaysSendConfiguration */,
false /* forceRelayout */);
- // Once we are rotated, set mFoldstate, effectively removing the lock override.
- mFoldState = newState;
}
}
}
@@ -1683,6 +1681,7 @@
private static class RotationHistory {
private static final int MAX_SIZE = 8;
+ private static final int NO_FOLD_CONTROLLER = -2;
private static class Record {
final @Surface.Rotation int mFromRotation;
final @Surface.Rotation int mToRotation;
@@ -1694,6 +1693,9 @@
final String mLastOrientationSource;
final @ActivityInfo.ScreenOrientation int mSourceOrientation;
final long mTimestamp = System.currentTimeMillis();
+ final int mHalfFoldSavedRotation;
+ final boolean mInHalfFoldTransition;
+ final DeviceStateController.FoldState mFoldState;
Record(DisplayRotation dr, int fromRotation, int toRotation) {
mFromRotation = fromRotation;
@@ -1719,6 +1721,15 @@
mLastOrientationSource = null;
mSourceOrientation = SCREEN_ORIENTATION_UNSET;
}
+ if (dr.mFoldController != null) {
+ mHalfFoldSavedRotation = dr.mFoldController.mHalfFoldSavedRotation;
+ mInHalfFoldTransition = dr.mFoldController.mInHalfFoldTransition;
+ mFoldState = dr.mFoldController.mFoldState;
+ } else {
+ mHalfFoldSavedRotation = NO_FOLD_CONTROLLER;
+ mInHalfFoldTransition = false;
+ mFoldState = DeviceStateController.FoldState.UNKNOWN;
+ }
}
void dump(String prefix, PrintWriter pw) {
@@ -1735,6 +1746,12 @@
if (mNonDefaultRequestingTaskDisplayArea != null) {
pw.println(prefix + " requestingTda=" + mNonDefaultRequestingTaskDisplayArea);
}
+ if (mHalfFoldSavedRotation != NO_FOLD_CONTROLLER) {
+ pw.println(prefix + " halfFoldSavedRotation="
+ + mHalfFoldSavedRotation
+ + " mInHalfFoldTransition=" + mInHalfFoldTransition
+ + " mFoldState=" + mFoldState);
+ }
}
}
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/KeyguardController.java b/services/core/java/com/android/server/wm/KeyguardController.java
index 1567fa7..e9badef 100644
--- a/services/core/java/com/android/server/wm/KeyguardController.java
+++ b/services/core/java/com/android/server/wm/KeyguardController.java
@@ -37,6 +37,7 @@
import static android.view.WindowManagerPolicyConstants.KEYGUARD_GOING_AWAY_FLAG_TO_SHADE;
import static android.view.WindowManagerPolicyConstants.KEYGUARD_GOING_AWAY_FLAG_WITH_WALLPAPER;
+import static com.android.server.policy.WindowManagerPolicy.FINISH_LAYOUT_REDO_WALLPAPER;
import static com.android.server.wm.ActivityTaskManagerDebugConfig.TAG_ATM;
import static com.android.server.wm.ActivityTaskManagerDebugConfig.TAG_WITH_CLASS_NAME;
import static com.android.server.wm.ActivityTaskSupervisor.PRESERVE_WINDOWS;
@@ -176,7 +177,7 @@
final boolean keyguardChanged = (keyguardShowing != state.mKeyguardShowing)
|| (state.mKeyguardGoingAway && keyguardShowing && !aodRemoved);
if (aodRemoved) {
- updateDeferWakeTransition(false /* waiting */);
+ updateDeferTransitionForAod(false /* waiting */);
}
if (!keyguardChanged && !aodChanged) {
setWakeTransitionReady();
@@ -533,24 +534,25 @@
private final Runnable mResetWaitTransition = () -> {
synchronized (mWindowManager.mGlobalLock) {
- updateDeferWakeTransition(false /* waiting */);
+ updateDeferTransitionForAod(false /* waiting */);
}
};
- void updateDeferWakeTransition(boolean waiting) {
+ // Defer transition until AOD dismissed.
+ void updateDeferTransitionForAod(boolean waiting) {
if (waiting == mWaitingForWakeTransition) {
return;
}
- if (!mWindowManager.mAtmService.getTransitionController().isShellTransitionsEnabled()) {
+ if (!mService.getTransitionController().isCollecting()) {
return;
}
- // if aod is showing, defer the wake transition until aod state changed.
+ // if AOD is showing, defer the wake transition until AOD state changed.
if (waiting && isAodShowing(DEFAULT_DISPLAY)) {
mWaitingForWakeTransition = true;
mWindowManager.mAtmService.getTransitionController().deferTransitionReady();
mWindowManager.mH.postDelayed(mResetWaitTransition, DEFER_WAKE_TRANSITION_TIMEOUT_MS);
} else if (!waiting) {
- // dismiss the deferring if the aod state change or cancel awake.
+ // dismiss the deferring if the AOD state change or cancel awake.
mWaitingForWakeTransition = false;
mWindowManager.mAtmService.getTransitionController().continueTransitionReady();
mWindowManager.mH.removeCallbacks(mResetWaitTransition);
@@ -648,6 +650,12 @@
mRequestDismissKeyguard = lastDismissKeyguardActivity != mDismissingKeyguardActivity
&& !mOccluded && !mKeyguardGoingAway
&& mDismissingKeyguardActivity != null;
+ if (mOccluded && mKeyguardShowing && !display.isSleeping() && !top.fillsParent()
+ && display.mWallpaperController.getWallpaperTarget() == null) {
+ // The occluding activity may be translucent or not fill screen. Then let wallpaper
+ // to check whether it should set itself as target to avoid blank background.
+ display.pendingLayoutChanges |= FINISH_LAYOUT_REDO_WALLPAPER;
+ }
if (mTopTurnScreenOnActivity != lastTurnScreenOnActivity
&& mTopTurnScreenOnActivity != null
@@ -657,10 +665,18 @@
mTopTurnScreenOnActivity.setCurrentLaunchCanTurnScreenOn(false);
}
+ boolean hasChange = false;
if (lastOccluded != mOccluded) {
controller.handleOccludedChanged(mDisplayId, mTopOccludesActivity);
+ hasChange = true;
} else if (!lastKeyguardGoingAway && mKeyguardGoingAway) {
controller.handleKeyguardGoingAwayChanged(display);
+ hasChange = true;
+ }
+ // Collect the participates for shell transition, so that transition won't happen too
+ // early since the transition was set ready.
+ if (hasChange && top != null && (mOccluded || mKeyguardGoingAway)) {
+ display.mTransitionController.collect(top);
}
}
diff --git a/services/core/java/com/android/server/wm/LetterboxUiController.java b/services/core/java/com/android/server/wm/LetterboxUiController.java
index bb4c482..bcea6f4 100644
--- a/services/core/java/com/android/server/wm/LetterboxUiController.java
+++ b/services/core/java/com/android/server/wm/LetterboxUiController.java
@@ -673,6 +673,12 @@
+ getHorizontalPositionMultiplier(mActivityRecord.getParent().getConfiguration()));
pw.println(prefix + " letterboxVerticalPositionMultiplier="
+ getVerticalPositionMultiplier(mActivityRecord.getParent().getConfiguration()));
+ pw.println(prefix + " letterboxPositionForHorizontalReachability="
+ + LetterboxConfiguration.letterboxHorizontalReachabilityPositionToString(
+ mLetterboxConfiguration.getLetterboxPositionForHorizontalReachability()));
+ pw.println(prefix + " letterboxPositionForVerticalReachability="
+ + LetterboxConfiguration.letterboxVerticalReachabilityPositionToString(
+ mLetterboxConfiguration.getLetterboxPositionForVerticalReachability()));
pw.println(prefix + " fixedOrientationLetterboxAspectRatio="
+ mLetterboxConfiguration.getFixedOrientationLetterboxAspectRatio());
pw.println(prefix + " defaultMinAspectRatioForUnresizableApps="
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..1ee4d6b 100644
--- a/services/core/java/com/android/server/wm/RootWindowContainer.java
+++ b/services/core/java/com/android/server/wm/RootWindowContainer.java
@@ -2619,7 +2619,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 +2644,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..a74a4b0 100644
--- a/services/core/java/com/android/server/wm/Task.java
+++ b/services/core/java/com/android/server/wm/Task.java
@@ -2167,7 +2167,7 @@
}
private boolean shouldStartChangeTransition(int prevWinMode, @NonNull Rect prevBounds) {
- if (!isLeafTask() || !canStartChangeTransition()) {
+ if (!(isLeafTask() || mCreatedByOrganizer) || !canStartChangeTransition()) {
return false;
}
final int newWinMode = getWindowingMode();
@@ -2455,7 +2455,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 +2768,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 +3077,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 +5726,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..66b868a 100644
--- a/services/core/java/com/android/server/wm/TaskFragment.java
+++ b/services/core/java/com/android/server/wm/TaskFragment.java
@@ -346,7 +346,7 @@
}
void process(ActivityRecord start, boolean preserveWindow) {
- if (start == null || !start.mVisibleRequested) {
+ if (start == null || !start.isVisibleRequested()) {
return;
}
reset(preserveWindow);
@@ -1356,7 +1356,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 +1370,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 +1421,7 @@
// Do over!
mTaskSupervisor.scheduleResumeTopActivities();
}
- if (!next.mVisibleRequested || next.stopped) {
+ if (!next.isVisibleRequested() || next.stopped) {
next.setVisibility(true);
}
next.completeResumeLocked();
@@ -1732,7 +1732,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 +1742,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
diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java
index 2b11d54..2737456 100644
--- a/services/core/java/com/android/server/wm/Transition.java
+++ b/services/core/java/com/android/server/wm/Transition.java
@@ -639,8 +639,8 @@
if (target.asDisplayContent() != null || target.asActivityRecord() != null) {
t.setCrop(targetLeash, null /* crop */);
} else {
- // Crop to the requested bounds.
- final Rect clipRect = target.getRequestedOverrideBounds();
+ // Crop to the resolved override bounds.
+ final Rect clipRect = target.getResolvedOverrideBounds();
t.setWindowCrop(targetLeash, clipRect.width(), clipRect.height());
}
t.setCornerRadius(targetLeash, 0);
@@ -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.
@@ -1617,8 +1617,11 @@
WindowContainer<?> ancestor = findCommonAncestor(sortedTargets, changes, topApp);
- // make leash based on highest (z-order) direct child of ancestor with a participant.
- WindowContainer leashReference = sortedTargets.get(0);
+ // Make leash based on highest (z-order) direct child of ancestor with a participant.
+ // TODO(b/261418859): Handle the case when the target contains window containers which
+ // belong to a different display. As a workaround we use topApp, from which wallpaper
+ // window container is removed, instead of sortedTargets here.
+ WindowContainer leashReference = topApp;
while (leashReference.getParent() != ancestor) {
leashReference = leashReference.getParent();
}
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/WindowManagerShellCommand.java b/services/core/java/com/android/server/wm/WindowManagerShellCommand.java
index 2838304..46a30fb 100644
--- a/services/core/java/com/android/server/wm/WindowManagerShellCommand.java
+++ b/services/core/java/com/android/server/wm/WindowManagerShellCommand.java
@@ -1221,6 +1221,12 @@
pw.println("Default position for vertical reachability: "
+ LetterboxConfiguration.letterboxVerticalReachabilityPositionToString(
mLetterboxConfiguration.getDefaultPositionForVerticalReachability()));
+ pw.println("Current position for horizontal reachability:"
+ + LetterboxConfiguration.letterboxHorizontalReachabilityPositionToString(
+ mLetterboxConfiguration.getLetterboxPositionForHorizontalReachability()));
+ pw.println("Current position for vertical reachability:"
+ + LetterboxConfiguration.letterboxVerticalReachabilityPositionToString(
+ mLetterboxConfiguration.getLetterboxPositionForVerticalReachability()));
pw.println("Is education enabled: "
+ mLetterboxConfiguration.getIsEducationEnabled());
pw.println("Is using split screen aspect ratio as aspect ratio for unresizable apps: "
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..c6578ef 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;
}
@@ -2101,7 +2101,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 +2123,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 +2170,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 +2743,7 @@
+ " exiting=" + mAnimatingExit + " destroying=" + mDestroying);
if (mActivityRecord != null) {
Slog.i(TAG_WM, " mActivityRecord.visibleRequested="
- + mActivityRecord.mVisibleRequested);
+ + mActivityRecord.isVisibleRequested());
}
}
}
@@ -3219,7 +3219,7 @@
}
return !mActivityRecord.getTask().getRootTask().shouldIgnoreInput()
- && mActivityRecord.mVisibleRequested;
+ && mActivityRecord.isVisibleRequested();
}
/**
@@ -3875,7 +3875,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 +4734,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 +5195,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/permission/java/com/android/server/permission/access/appop/AppOpsCheckingServiceCompatImpl.kt b/services/permission/java/com/android/server/permission/access/appop/AppOpsCheckingServiceCompatImpl.kt
new file mode 100644
index 0000000..a565feb
--- /dev/null
+++ b/services/permission/java/com/android/server/permission/access/appop/AppOpsCheckingServiceCompatImpl.kt
@@ -0,0 +1,144 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.permission.access.appop
+
+import android.util.ArraySet
+import android.util.SparseBooleanArray
+import android.util.SparseIntArray
+import com.android.server.appop.AppOpsCheckingServiceInterface
+import com.android.server.appop.OnOpModeChangedListener
+import com.android.server.permission.access.AccessCheckingService
+import java.io.PrintWriter
+
+class AppOpsCheckingServiceCompatImpl(
+ private val accessCheckingService: AccessCheckingService
+) : AppOpsCheckingServiceInterface {
+ override fun getNonDefaultUidModes(uid: Int): SparseIntArray {
+ TODO("Not yet implemented")
+ }
+
+ override fun getUidMode(uid: Int, op: Int): Int {
+ TODO("Not yet implemented")
+ }
+
+ override fun setUidMode(uid: Int, op: Int, mode: Int): Boolean {
+ TODO("Not yet implemented")
+ }
+
+ override fun getPackageMode(packageName: String, op: Int, userId: Int): Int {
+ TODO("Not yet implemented")
+ }
+
+ override fun setPackageMode(packageName: String, op: Int, mode: Int, userId: Int) {
+ TODO("Not yet implemented")
+ }
+
+ override fun removePackage(packageName: String, userId: Int): Boolean {
+ TODO("Not yet implemented")
+ }
+
+ override fun removeUid(uid: Int) {
+ TODO("Not yet implemented")
+ }
+
+ override fun areUidModesDefault(uid: Int): Boolean {
+ TODO("Not yet implemented")
+ }
+
+ override fun arePackageModesDefault(packageName: String, userId: Int): Boolean {
+ TODO("Not yet implemented")
+ }
+
+ override fun clearAllModes() {
+ TODO("Not yet implemented")
+ }
+
+ override fun startWatchingOpModeChanged(changedListener: OnOpModeChangedListener, op: Int) {
+ TODO("Not yet implemented")
+ }
+
+ override fun startWatchingPackageModeChanged(
+ changedListener: OnOpModeChangedListener,
+ packageName: String
+ ) {
+ TODO("Not yet implemented")
+ }
+
+ override fun removeListener(changedListener: OnOpModeChangedListener) {
+ TODO("Not yet implemented")
+ }
+
+ override fun getOpModeChangedListeners(op: Int): ArraySet<OnOpModeChangedListener> {
+ TODO("Not yet implemented")
+ }
+
+ override fun getPackageModeChangedListeners(
+ packageName: String
+ ): ArraySet<OnOpModeChangedListener> {
+ TODO("Not yet implemented")
+ }
+
+ override fun notifyWatchersOfChange(op: Int, uid: Int) {
+ TODO("Not yet implemented")
+ }
+
+ override fun notifyOpChanged(
+ changedListener: OnOpModeChangedListener,
+ op: Int,
+ uid: Int,
+ packageName: String?
+ ) {
+ TODO("Not yet implemented")
+ }
+
+ override fun notifyOpChangedForAllPkgsInUid(
+ op: Int,
+ uid: Int,
+ onlyForeground: Boolean,
+ callbackToIgnore: OnOpModeChangedListener?
+ ) {
+ TODO("Not yet implemented")
+ }
+
+ override fun evalForegroundUidOps(
+ uid: Int,
+ foregroundOps: SparseBooleanArray?
+ ): SparseBooleanArray {
+ TODO("Not yet implemented")
+ }
+
+ override fun evalForegroundPackageOps(
+ packageName: String,
+ foregroundOps: SparseBooleanArray?,
+ userId: Int
+ ): SparseBooleanArray {
+ TODO("Not yet implemented")
+ }
+
+ override fun dumpListeners(
+ dumpOp: Int,
+ dumpUid: Int,
+ dumpPackage: String?,
+ printWriter: PrintWriter
+ ): Boolean {
+ TODO("Not yet implemented")
+ }
+
+ companion object {
+ private val LOG_TAG = AppOpsCheckingServiceCompatImpl::class.java.simpleName
+ }
+}
diff --git a/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/PackageManagerLocalSnapshotTest.kt b/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/PackageManagerLocalSnapshotTest.kt
index faa2352..5f26d6f 100644
--- a/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/PackageManagerLocalSnapshotTest.kt
+++ b/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/PackageManagerLocalSnapshotTest.kt
@@ -53,6 +53,11 @@
snapshot.use {
val packageStates = it.packageStates
+ // Check for unmodifiable
+ assertFailsWith(UnsupportedOperationException::class) {
+ it.packageStates.clear()
+ }
+
// Check contents
assertThat(packageStates).containsExactly(
packageStateAll.packageName, packageStateAll,
@@ -78,9 +83,14 @@
assertThat(filteredOne.getPackageState(packageStateUser10.packageName)).isNull()
filteredThree.use {
- val statesList = mutableListOf<PackageState>()
- assertThat(it.forAllPackageStates { statesList += it })
- assertThat(statesList).containsExactly(packageStateAll, packageStateUser10)
+ // Check for unmodifiable
+ assertFailsWith(UnsupportedOperationException::class) {
+ it.packageStates.clear()
+ }
+ assertThat(it.packageStates).containsExactly(
+ packageStateAll.packageName, packageStateAll,
+ packageStateUser10.packageName, packageStateUser10,
+ )
}
// Call after child close, parent open fails
@@ -96,7 +106,7 @@
// Call after close fails
assertClosedFailure { snapshot.packageStates }
- assertClosedFailure { filteredOne.forAllPackageStates {} }
+ assertClosedFailure { filteredOne.packageStates }
assertClosedFailure {
filteredTwo.getPackageState(packageStateAll.packageName)
}
@@ -116,9 +126,15 @@
.isEqualTo(packageStateUser0)
assertThat(it.getPackageState(packageStateUser10.packageName)).isNull()
- val statesList = mutableListOf<PackageState>()
- assertThat(it.forAllPackageStates { statesList += it })
- assertThat(statesList).containsExactly(packageStateAll, packageStateUser0)
+ // Check for unmodifiable
+ assertFailsWith(UnsupportedOperationException::class) {
+ it.packageStates.clear()
+ }
+
+ assertThat(it.packageStates).containsExactly(
+ packageStateAll.packageName, packageStateAll,
+ packageStateUser0.packageName, packageStateUser0,
+ )
}
// Call after close fails
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..de09b19 100644
--- a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueModernImplTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueModernImplTest.java
@@ -592,6 +592,73 @@
}
/**
+ * 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));
+
+ // mix of broadcasts, with more than 2 normal
+ queue.enqueueOrReplaceBroadcast(
+ makeBroadcastRecord(new Intent(Intent.ACTION_BOOT_COMPLETED)
+ .addFlags(Intent.FLAG_RECEIVER_OFFLOAD)), 0);
+ queue.enqueueOrReplaceBroadcast(
+ makeBroadcastRecord(new Intent(Intent.ACTION_TIMEZONE_CHANGED)), 0);
+ queue.enqueueOrReplaceBroadcast(
+ makeBroadcastRecord(new Intent(Intent.ACTION_PACKAGE_CHANGED)
+ .addFlags(Intent.FLAG_RECEIVER_OFFLOAD)), 0);
+ queue.enqueueOrReplaceBroadcast(
+ makeBroadcastRecord(new Intent(Intent.ACTION_ALARM_CHANGED)), 0);
+ queue.enqueueOrReplaceBroadcast(
+ makeBroadcastRecord(new Intent(Intent.ACTION_TIME_TICK)), 0);
+ queue.enqueueOrReplaceBroadcast(
+ makeBroadcastRecord(new Intent(Intent.ACTION_LOCALE_CHANGED)
+ .addFlags(Intent.FLAG_RECEIVER_FOREGROUND)), 0);
+ queue.enqueueOrReplaceBroadcast(
+ makeBroadcastRecord(new Intent(Intent.ACTION_APPLICATION_PREFERENCES),
+ optInteractive), 0);
+ queue.enqueueOrReplaceBroadcast(
+ makeBroadcastRecord(new Intent(AppWidgetManager.ACTION_APPWIDGET_UPDATE),
+ optInteractive), 0);
+ queue.enqueueOrReplaceBroadcast(
+ makeBroadcastRecord(new Intent(Intent.ACTION_INPUT_METHOD_CHANGED)
+ .addFlags(Intent.FLAG_RECEIVER_FOREGROUND)), 0);
+ queue.enqueueOrReplaceBroadcast(
+ makeBroadcastRecord(new Intent(Intent.ACTION_NEW_OUTGOING_CALL),
+ optInteractive), 0);
+
+ 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 sending a broadcast that removes any matching pending
* broadcasts is applied as expected.
*/
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..f105971 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,
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/NetworkManagementServiceTest.java b/services/tests/servicestests/src/com/android/server/NetworkManagementServiceTest.java
new file mode 100644
index 0000000..cbe6d26
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/NetworkManagementServiceTest.java
@@ -0,0 +1,339 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server;
+
+import static android.net.ConnectivityManager.FIREWALL_CHAIN_DOZABLE;
+import static android.net.ConnectivityManager.FIREWALL_CHAIN_LOW_POWER_STANDBY;
+import static android.net.ConnectivityManager.FIREWALL_CHAIN_POWERSAVE;
+import static android.net.ConnectivityManager.FIREWALL_CHAIN_RESTRICTED;
+import static android.net.ConnectivityManager.FIREWALL_CHAIN_STANDBY;
+import static android.util.DebugUtils.valueToString;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+
+import android.annotation.NonNull;
+import android.content.Context;
+import android.net.ConnectivityManager;
+import android.net.INetd;
+import android.net.INetdUnsolicitedEventListener;
+import android.net.LinkAddress;
+import android.net.NetworkPolicyManager;
+import android.os.BatteryStats;
+import android.os.Binder;
+import android.os.IBinder;
+import android.os.Process;
+import android.os.RemoteException;
+import android.test.suitebuilder.annotation.SmallTest;
+import android.util.ArrayMap;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.internal.app.IBatteryStats;
+import com.android.server.NetworkManagementService.Dependencies;
+import com.android.server.net.BaseNetworkObserver;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.function.BiFunction;
+
+/**
+ * Tests for {@link NetworkManagementService}.
+ */
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class NetworkManagementServiceTest {
+ private NetworkManagementService mNMService;
+ @Mock private Context mContext;
+ @Mock private ConnectivityManager mCm;
+ @Mock private IBatteryStats.Stub mBatteryStatsService;
+ @Mock private INetd.Stub mNetdService;
+
+ private static final int TEST_UID = 111;
+
+ @NonNull
+ @Captor
+ private ArgumentCaptor<INetdUnsolicitedEventListener> mUnsolListenerCaptor;
+
+ private final MockDependencies mDeps = new MockDependencies();
+
+ private final class MockDependencies extends Dependencies {
+ @Override
+ public IBinder getService(String name) {
+ switch (name) {
+ case BatteryStats.SERVICE_NAME:
+ return mBatteryStatsService;
+ default:
+ throw new UnsupportedOperationException("Unknown service " + name);
+ }
+ }
+
+ @Override
+ public void registerLocalService(NetworkManagementInternal nmi) {
+ }
+
+ @Override
+ public INetd getNetd() {
+ return mNetdService;
+ }
+
+ @Override
+ public int getCallingUid() {
+ return Process.SYSTEM_UID;
+ }
+ }
+
+ @Before
+ public void setUp() throws Exception {
+ MockitoAnnotations.initMocks(this);
+ doNothing().when(mNetdService)
+ .registerUnsolicitedEventListener(mUnsolListenerCaptor.capture());
+ doReturn(Context.CONNECTIVITY_SERVICE).when(mContext).getSystemServiceName(
+ eq(ConnectivityManager.class));
+ doReturn(mCm).when(mContext).getSystemService(eq(Context.CONNECTIVITY_SERVICE));
+ // Start the service and wait until it connects to our socket.
+ mNMService = NetworkManagementService.create(mContext, mDeps);
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ mNMService.shutdown();
+ }
+
+ private static <T> T expectSoon(T mock) {
+ return verify(mock, timeout(200));
+ }
+
+ /**
+ * Tests that network observers work properly.
+ */
+ @Test
+ public void testNetworkObservers() throws Exception {
+ BaseNetworkObserver observer = mock(BaseNetworkObserver.class);
+ doReturn(new Binder()).when(observer).asBinder(); // Used by registerObserver.
+ mNMService.registerObserver(observer);
+
+ // Forget everything that happened to the mock so far, so we can explicitly verify
+ // everything that happens and does not happen to it from now on.
+
+ INetdUnsolicitedEventListener unsolListener = mUnsolListenerCaptor.getValue();
+ reset(observer);
+ // Now call unsolListener methods and ensure that the observer methods are
+ // called. After every method we expect a callback soon after; to ensure that
+ // invalid messages don't cause any callbacks, we call verifyNoMoreInteractions at the end.
+
+ /**
+ * Interface changes.
+ */
+ unsolListener.onInterfaceAdded("rmnet12");
+ expectSoon(observer).interfaceAdded("rmnet12");
+
+ unsolListener.onInterfaceRemoved("eth1");
+ expectSoon(observer).interfaceRemoved("eth1");
+
+ unsolListener.onInterfaceChanged("clat4", true);
+ expectSoon(observer).interfaceStatusChanged("clat4", true);
+
+ unsolListener.onInterfaceLinkStateChanged("rmnet0", false);
+ expectSoon(observer).interfaceLinkStateChanged("rmnet0", false);
+
+ /**
+ * Bandwidth control events.
+ */
+ unsolListener.onQuotaLimitReached("data", "rmnet_usb0");
+ expectSoon(observer).limitReached("data", "rmnet_usb0");
+
+ /**
+ * Interface class activity.
+ */
+ unsolListener.onInterfaceClassActivityChanged(true, 1, 1234, TEST_UID);
+ expectSoon(observer).interfaceClassDataActivityChanged(1, true, 1234, TEST_UID);
+
+ unsolListener.onInterfaceClassActivityChanged(false, 9, 5678, TEST_UID);
+ expectSoon(observer).interfaceClassDataActivityChanged(9, false, 5678, TEST_UID);
+
+ unsolListener.onInterfaceClassActivityChanged(false, 9, 4321, TEST_UID);
+ expectSoon(observer).interfaceClassDataActivityChanged(9, false, 4321, TEST_UID);
+
+ /**
+ * IP address changes.
+ */
+ unsolListener.onInterfaceAddressUpdated("fe80::1/64", "wlan0", 128, 253);
+ expectSoon(observer).addressUpdated("wlan0", new LinkAddress("fe80::1/64", 128, 253));
+
+ unsolListener.onInterfaceAddressRemoved("fe80::1/64", "wlan0", 128, 253);
+ expectSoon(observer).addressRemoved("wlan0", new LinkAddress("fe80::1/64", 128, 253));
+
+ unsolListener.onInterfaceAddressRemoved("2001:db8::1/64", "wlan0", 1, 0);
+ expectSoon(observer).addressRemoved("wlan0", new LinkAddress("2001:db8::1/64", 1, 0));
+
+ /**
+ * DNS information broadcasts.
+ */
+ unsolListener.onInterfaceDnsServerInfo("rmnet_usb0", 3600, new String[]{"2001:db8::1"});
+ expectSoon(observer).interfaceDnsServerInfo("rmnet_usb0", 3600,
+ new String[]{"2001:db8::1"});
+
+ unsolListener.onInterfaceDnsServerInfo("wlan0", 14400,
+ new String[]{"2001:db8::1", "2001:db8::2"});
+ expectSoon(observer).interfaceDnsServerInfo("wlan0", 14400,
+ new String[]{"2001:db8::1", "2001:db8::2"});
+
+ // We don't check for negative lifetimes, only for parse errors.
+ unsolListener.onInterfaceDnsServerInfo("wlan0", -3600, new String[]{"::1"});
+ expectSoon(observer).interfaceDnsServerInfo("wlan0", -3600,
+ new String[]{"::1"});
+
+ // No syntax checking on the addresses.
+ unsolListener.onInterfaceDnsServerInfo("wlan0", 600,
+ new String[]{"", "::", "", "foo", "::1"});
+ expectSoon(observer).interfaceDnsServerInfo("wlan0", 600,
+ new String[]{"", "::", "", "foo", "::1"});
+
+ // Make sure nothing else was called.
+ verifyNoMoreInteractions(observer);
+ }
+
+ @Test
+ public void testFirewallEnabled() {
+ mNMService.setFirewallEnabled(true);
+ assertTrue(mNMService.isFirewallEnabled());
+
+ mNMService.setFirewallEnabled(false);
+ assertFalse(mNMService.isFirewallEnabled());
+ }
+
+ @Test
+ public void testNetworkRestrictedDefault() {
+ assertFalse(mNMService.isNetworkRestricted(TEST_UID));
+ }
+
+ @Test
+ public void testMeteredNetworkRestrictions() throws RemoteException {
+ // Make sure the mocked netd method returns true.
+ doReturn(true).when(mNetdService).bandwidthEnableDataSaver(anyBoolean());
+
+ // Restrict usage of mobile data in background
+ mNMService.setUidOnMeteredNetworkDenylist(TEST_UID, true);
+ assertTrue("Should be true since mobile data usage is restricted",
+ mNMService.isNetworkRestricted(TEST_UID));
+ verify(mCm).addUidToMeteredNetworkDenyList(TEST_UID);
+
+ mNMService.setDataSaverModeEnabled(true);
+ verify(mNetdService).bandwidthEnableDataSaver(true);
+
+ mNMService.setUidOnMeteredNetworkDenylist(TEST_UID, false);
+ assertTrue("Should be true since data saver is on and the uid is not allowlisted",
+ mNMService.isNetworkRestricted(TEST_UID));
+ verify(mCm).removeUidFromMeteredNetworkDenyList(TEST_UID);
+
+ mNMService.setUidOnMeteredNetworkAllowlist(TEST_UID, true);
+ assertFalse("Should be false since data saver is on and the uid is allowlisted",
+ mNMService.isNetworkRestricted(TEST_UID));
+ verify(mCm).addUidToMeteredNetworkAllowList(TEST_UID);
+
+ // remove uid from allowlist and turn datasaver off again
+ mNMService.setUidOnMeteredNetworkAllowlist(TEST_UID, false);
+ verify(mCm).removeUidFromMeteredNetworkAllowList(TEST_UID);
+ mNMService.setDataSaverModeEnabled(false);
+ verify(mNetdService).bandwidthEnableDataSaver(false);
+ assertFalse("Network should not be restricted when data saver is off",
+ mNMService.isNetworkRestricted(TEST_UID));
+ }
+
+ @Test
+ public void testFirewallChains() {
+ final ArrayMap<Integer, ArrayMap<Integer, Boolean>> expected = new ArrayMap<>();
+ // Dozable chain
+ final ArrayMap<Integer, Boolean> isRestrictedForDozable = new ArrayMap<>();
+ isRestrictedForDozable.put(NetworkPolicyManager.FIREWALL_RULE_DEFAULT, true);
+ isRestrictedForDozable.put(INetd.FIREWALL_RULE_ALLOW, false);
+ isRestrictedForDozable.put(INetd.FIREWALL_RULE_DENY, true);
+ expected.put(FIREWALL_CHAIN_DOZABLE, isRestrictedForDozable);
+ // Powersaver chain
+ final ArrayMap<Integer, Boolean> isRestrictedForPowerSave = new ArrayMap<>();
+ isRestrictedForPowerSave.put(NetworkPolicyManager.FIREWALL_RULE_DEFAULT, true);
+ isRestrictedForPowerSave.put(INetd.FIREWALL_RULE_ALLOW, false);
+ isRestrictedForPowerSave.put(INetd.FIREWALL_RULE_DENY, true);
+ expected.put(FIREWALL_CHAIN_POWERSAVE, isRestrictedForPowerSave);
+ // Standby chain
+ final ArrayMap<Integer, Boolean> isRestrictedForStandby = new ArrayMap<>();
+ isRestrictedForStandby.put(NetworkPolicyManager.FIREWALL_RULE_DEFAULT, false);
+ isRestrictedForStandby.put(INetd.FIREWALL_RULE_ALLOW, false);
+ isRestrictedForStandby.put(INetd.FIREWALL_RULE_DENY, true);
+ expected.put(FIREWALL_CHAIN_STANDBY, isRestrictedForStandby);
+ // Restricted mode chain
+ final ArrayMap<Integer, Boolean> isRestrictedForRestrictedMode = new ArrayMap<>();
+ isRestrictedForRestrictedMode.put(NetworkPolicyManager.FIREWALL_RULE_DEFAULT, true);
+ isRestrictedForRestrictedMode.put(INetd.FIREWALL_RULE_ALLOW, false);
+ isRestrictedForRestrictedMode.put(INetd.FIREWALL_RULE_DENY, true);
+ expected.put(FIREWALL_CHAIN_RESTRICTED, isRestrictedForRestrictedMode);
+ // Low Power Standby chain
+ final ArrayMap<Integer, Boolean> isRestrictedForLowPowerStandby = new ArrayMap<>();
+ isRestrictedForLowPowerStandby.put(NetworkPolicyManager.FIREWALL_RULE_DEFAULT, true);
+ isRestrictedForLowPowerStandby.put(INetd.FIREWALL_RULE_ALLOW, false);
+ isRestrictedForLowPowerStandby.put(INetd.FIREWALL_RULE_DENY, true);
+ expected.put(FIREWALL_CHAIN_LOW_POWER_STANDBY, isRestrictedForLowPowerStandby);
+
+ final int[] chains = {
+ FIREWALL_CHAIN_STANDBY,
+ FIREWALL_CHAIN_POWERSAVE,
+ FIREWALL_CHAIN_DOZABLE,
+ FIREWALL_CHAIN_RESTRICTED,
+ FIREWALL_CHAIN_LOW_POWER_STANDBY
+ };
+ final int[] states = {
+ INetd.FIREWALL_RULE_ALLOW,
+ INetd.FIREWALL_RULE_DENY,
+ NetworkPolicyManager.FIREWALL_RULE_DEFAULT
+ };
+ BiFunction<Integer, Integer, String> errorMsg = (chain, state) -> {
+ return String.format("Unexpected value for chain: %s and state: %s",
+ valueToString(INetd.class, "FIREWALL_CHAIN_", chain),
+ valueToString(INetd.class, "FIREWALL_RULE_", state));
+ };
+ for (int chain : chains) {
+ final ArrayMap<Integer, Boolean> expectedValues = expected.get(chain);
+ mNMService.setFirewallChainEnabled(chain, true);
+ verify(mCm).setFirewallChainEnabled(chain, true /* enabled */);
+ for (int state : states) {
+ mNMService.setFirewallUidRule(chain, TEST_UID, state);
+ assertEquals(errorMsg.apply(chain, state),
+ expectedValues.get(state), mNMService.isNetworkRestricted(TEST_UID));
+ }
+ mNMService.setFirewallChainEnabled(chain, false);
+ verify(mCm).setFirewallChainEnabled(chain, false /* enabled */);
+ }
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java b/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java
index 6e446f0..d47f063 100644
--- a/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java
+++ b/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java
@@ -638,6 +638,39 @@
/* keyEvictedCallback= */ mKeyEvictedCallback, /* expectLocking= */ true);
}
+ @Test
+ public void testStopUser_invalidUser() {
+ int userId = -1;
+
+ assertThrows(IllegalArgumentException.class,
+ () -> mUserController.stopUser(userId, /* force= */ true,
+ /* allowDelayedLocking= */ true, /* stopUserCallback= */ null,
+ /* keyEvictedCallback= */ null));
+ }
+
+ @Test
+ public void testStopUser_systemUser() {
+ int userId = UserHandle.USER_SYSTEM;
+
+ int r = mUserController.stopUser(userId, /* force= */ true,
+ /* allowDelayedLocking= */ true, /* stopUserCallback= */ null,
+ /* keyEvictedCallback= */ null);
+
+ assertThat(r).isEqualTo(ActivityManager.USER_OP_ERROR_IS_SYSTEM);
+ }
+
+ @Test
+ public void testStopUser_currentUser() {
+ setUpUser(TEST_USER_ID1, /* flags= */ 0);
+ mUserController.startUser(TEST_USER_ID1, /* foreground= */ true);
+
+ int r = mUserController.stopUser(TEST_USER_ID1, /* force= */ true,
+ /* allowDelayedLocking= */ true, /* stopUserCallback= */ null,
+ /* keyEvictedCallback= */ null);
+
+ assertThat(r).isEqualTo(ActivityManager.USER_OP_IS_CURRENT);
+ }
+
/**
* Test conditional delayed locking with mDelayUserDataLocking true.
*/
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..ca8e45a 100644
--- a/services/tests/servicestests/src/com/android/server/display/DisplayManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/display/DisplayManagerServiceTest.java
@@ -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/display/PersistentDataStoreTest.java b/services/tests/servicestests/src/com/android/server/display/PersistentDataStoreTest.java
index 3b0a22f..35a677e 100644
--- a/services/tests/servicestests/src/com/android/server/display/PersistentDataStoreTest.java
+++ b/services/tests/servicestests/src/com/android/server/display/PersistentDataStoreTest.java
@@ -344,6 +344,40 @@
assertEquals(85.3f, newDataStore.getUserPreferredRefreshRate(testDisplayDevice), 0.1f);
}
+ @Test
+ public void testBrightnessInitialisesWithInvalidFloat() {
+ final String uniqueDisplayId = "test:123";
+ DisplayDevice testDisplayDevice = new DisplayDevice(null, null, uniqueDisplayId, null) {
+ @Override
+ public boolean hasStableUniqueId() {
+ return true;
+ }
+
+ @Override
+ public DisplayDeviceInfo getDisplayDeviceInfoLocked() {
+ return null;
+ }
+ };
+
+ // Set any value which initialises Display state
+ float refreshRate = 85.3f;
+ mDataStore.loadIfNeeded();
+ mDataStore.setUserPreferredRefreshRate(testDisplayDevice, refreshRate);
+
+ final ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ mInjector.setWriteStream(baos);
+ mDataStore.saveIfNeeded();
+ mTestLooper.dispatchAll();
+ assertTrue(mInjector.wasWriteSuccessful());
+ TestInjector newInjector = new TestInjector();
+ PersistentDataStore newDataStore = new PersistentDataStore(newInjector);
+ ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
+ newInjector.setReadStream(bais);
+ newDataStore.loadIfNeeded();
+ assertTrue(Float.isNaN(mDataStore.getBrightness(testDisplayDevice)));
+ }
+
+
public class TestInjector extends PersistentDataStore.Injector {
private InputStream mReadStream;
private OutputStream mWriteStream;
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/ArcTerminationActionFromAvrTest.java b/services/tests/servicestests/src/com/android/server/hdmi/ArcTerminationActionFromAvrTest.java
index 09cd47a..2cb46da 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/ArcTerminationActionFromAvrTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/ArcTerminationActionFromAvrTest.java
@@ -24,6 +24,8 @@
import android.content.Context;
import android.content.ContextWrapper;
+import android.hardware.hdmi.HdmiControlManager;
+import android.hardware.hdmi.IHdmiControlCallback;
import android.hardware.tv.cec.V1_0.SendMessageResult;
import android.media.AudioManager;
import android.os.Looper;
@@ -52,6 +54,7 @@
private Context mContextSpy;
private HdmiCecLocalDeviceAudioSystem mHdmiCecLocalDeviceAudioSystem;
private FakePowerManagerWrapper mPowerManager;
+ private TestCallback mCallback;
private ArcTerminationActionFromAvr mAction;
private FakeNativeWrapper mNativeWrapper;
@@ -112,7 +115,9 @@
}
};
mHdmiCecLocalDeviceAudioSystem.init();
- mAction = new ArcTerminationActionFromAvr(mHdmiCecLocalDeviceAudioSystem);
+ mCallback = new TestCallback();
+ mAction = new ArcTerminationActionFromAvr(mHdmiCecLocalDeviceAudioSystem,
+ mCallback);
mLocalDevices.add(mHdmiCecLocalDeviceAudioSystem);
hdmiControlService.onBootPhase(PHASE_SYSTEM_SERVICES_READY);
@@ -121,6 +126,20 @@
mTestLooper.dispatchAll();
}
+ private static class TestCallback extends IHdmiControlCallback.Stub {
+ private final ArrayList<Integer> mCallbackResult = new ArrayList<Integer>();
+
+ @Override
+ public void onComplete(int result) {
+ mCallbackResult.add(result);
+ }
+
+ private int getResult() {
+ assertThat(mCallbackResult.size()).isEqualTo(1);
+ return mCallbackResult.get(0);
+ }
+ }
+
@Test
public void testSendMessage_sendFailed() {
mNativeWrapper.setMessageSendResult(Constants.MESSAGE_TERMINATE_ARC,
@@ -133,6 +152,7 @@
assertThat(mNativeWrapper.getResultMessages()).contains(terminateArc);
assertThat(mHdmiCecLocalDeviceAudioSystem.isArcEnabled()).isFalse();
+ assertThat(mCallback.getResult()).isEqualTo(HdmiControlManager.RESULT_TARGET_NOT_AVAILABLE);
}
@Test
@@ -149,6 +169,7 @@
mTestLooper.dispatchAll();
assertThat(mHdmiCecLocalDeviceAudioSystem.isArcEnabled()).isFalse();
+ assertThat(mCallback.getResult()).isEqualTo(HdmiControlManager.RESULT_TIMEOUT);
}
@Test
@@ -167,5 +188,28 @@
mTestLooper.dispatchAll();
assertThat(mHdmiCecLocalDeviceAudioSystem.isArcEnabled()).isFalse();
+ assertThat(mCallback.getResult()).isEqualTo(HdmiControlManager.RESULT_SUCCESS);
+ }
+
+ @Test
+ public void testReportArcTerminated_featureAbort() {
+ mHdmiCecLocalDeviceAudioSystem.addAndStartAction(mAction);
+ mTestLooper.dispatchAll();
+ HdmiCecMessage terminateArc = HdmiCecMessageBuilder.buildTerminateArc(
+ Constants.ADDR_AUDIO_SYSTEM, Constants.ADDR_TV);
+
+ assertThat(mNativeWrapper.getResultMessages()).contains(terminateArc);
+
+ HdmiCecMessage arcTerminatedResponse = HdmiCecMessageBuilder.buildFeatureAbortCommand(
+ Constants.ADDR_TV,
+ Constants.ADDR_AUDIO_SYSTEM,
+ Constants.MESSAGE_TERMINATE_ARC,
+ Constants.ABORT_REFUSED);
+
+ mNativeWrapper.onCecMessage(arcTerminatedResponse);
+ mTestLooper.dispatchAll();
+
+ assertThat(mHdmiCecLocalDeviceAudioSystem.isArcEnabled()).isFalse();
+ assertThat(mCallback.getResult()).isEqualTo(HdmiControlManager.RESULT_TARGET_NOT_AVAILABLE);
}
}
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceAudioSystemTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceAudioSystemTest.java
index 7c6c990..de2c218 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceAudioSystemTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceAudioSystemTest.java
@@ -535,6 +535,25 @@
}
@Test
+ public void handleRequestArcTerminate_callbackIsPreserved() throws Exception {
+ TestCallback callback = new TestCallback();
+
+ mHdmiCecLocalDeviceAudioSystem.setArcStatus(true);
+ assertThat(mHdmiCecLocalDeviceAudioSystem.isArcEnabled()).isTrue();
+ mHdmiCecLocalDeviceAudioSystem.addAndStartAction(
+ new ArcTerminationActionFromAvr(mHdmiCecLocalDeviceAudioSystem, callback));
+
+ HdmiCecMessage message =
+ HdmiCecMessageBuilder.buildRequestArcTermination(ADDR_TV, ADDR_AUDIO_SYSTEM);
+ assertThat(mHdmiCecLocalDeviceAudioSystem.handleRequestArcTermination(message))
+ .isEqualTo(Constants.HANDLED);
+
+ mTestLooper.dispatchAll();
+ assertThat(mHdmiCecLocalDeviceAudioSystem.getActions(
+ ArcTerminationActionFromAvr.class).get(0).mCallbacks.get(0)).isEqualTo(callback);
+ }
+
+ @Test
public void handleRequestArcInit_arcIsNotSupported() throws Exception {
HdmiCecMessage message =
HdmiCecMessageBuilder.buildRequestArcInitiation(ADDR_TV, ADDR_AUDIO_SYSTEM);
@@ -880,4 +899,13 @@
assertThat(mNativeWrapper.getResultMessages()).doesNotContain(
systemAudioModeRequest_fromAudioSystem);
}
+
+ private static class TestCallback extends IHdmiControlCallback.Stub {
+ private final ArrayList<Integer> mCallbackResult = new ArrayList<Integer>();
+
+ @Override
+ public void onComplete(int result) {
+ mCallbackResult.add(result);
+ }
+ }
}
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiControlServiceTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiControlServiceTest.java
index ef2b212..49a0a9a52 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiControlServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiControlServiceTest.java
@@ -1036,6 +1036,7 @@
@Test
public void setSoundbarMode_enabled_addAudioSystemLocalDevice() {
+ mHdmiControlServiceSpy.setPowerStatus(HdmiControlManager.POWER_STATUS_ON);
// Initialize the local devices excluding the audio system.
mHdmiControlServiceSpy.clearCecLocalDevices();
mLocalDevices.remove(mAudioSystemDeviceSpy);
@@ -1053,6 +1054,7 @@
@Test
public void setSoundbarMode_disabled_removeAudioSystemLocalDevice() {
+ mHdmiControlServiceSpy.setPowerStatus(HdmiControlManager.POWER_STATUS_ON);
// Initialize the local devices excluding the audio system.
mHdmiControlServiceSpy.clearCecLocalDevices();
mLocalDevices.remove(mAudioSystemDeviceSpy);
@@ -1073,6 +1075,10 @@
HdmiControlManager.CEC_SETTING_NAME_SOUNDBAR_MODE,
HdmiControlManager.SOUNDBAR_MODE_DISABLED);
mTestLooper.dispatchAll();
+
+ // Wait for ArcTerminationActionFromAvr timeout for the logical address allocation to start.
+ mTestLooper.moveTimeForward(HdmiConfig.TIMEOUT_MS);
+ mTestLooper.dispatchAll();
assertThat(mHdmiControlServiceSpy.audioSystem()).isNull();
}
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/pm/CrossProfileAppsServiceImplTest.java b/services/tests/servicestests/src/com/android/server/pm/CrossProfileAppsServiceImplTest.java
new file mode 100644
index 0000000..c81fbb4
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/pm/CrossProfileAppsServiceImplTest.java
@@ -0,0 +1,781 @@
+package com.android.server.pm;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.nullable;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.doReturn;
+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 static org.testng.Assert.assertThrows;
+
+import android.Manifest;
+import android.app.ActivityManager;
+import android.app.ActivityManagerInternal;
+import android.app.ActivityOptions;
+import android.app.AppOpsManager;
+import android.app.IApplicationThread;
+import android.app.admin.DevicePolicyManagerInternal;
+import android.content.AttributionSourceState;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.PermissionChecker;
+import android.content.pm.ActivityInfo;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.CrossProfileAppsInternal;
+import android.content.pm.IPackageManager;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManagerInternal;
+import android.content.pm.PermissionInfo;
+import android.content.pm.ResolveInfo;
+import android.os.Binder;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.permission.PermissionCheckerManager;
+import android.permission.PermissionManager;
+import android.platform.test.annotations.Presubmit;
+import android.util.SparseArray;
+
+import com.android.internal.util.FunctionalUtils.ThrowingRunnable;
+import com.android.internal.util.FunctionalUtils.ThrowingSupplier;
+import com.android.server.LocalServices;
+import com.android.server.pm.permission.PermissionManagerService;
+import com.android.server.wm.ActivityTaskManagerInternal;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Build/Install/Run:
+ * atest FrameworksServicesTests:com.android.server.pm.CrossProfileAppsServiceImplTest
+ */
+@Presubmit
+@RunWith(MockitoJUnitRunner.class)
+public class CrossProfileAppsServiceImplTest {
+ private static final String PACKAGE_ONE = "com.one";
+ private static final String FEATURE_ID = "feature.one";
+ private static final int PACKAGE_ONE_UID = 1111;
+ private static final ComponentName ACTIVITY_COMPONENT =
+ new ComponentName("com.one", "test");
+
+ private static final String PACKAGE_TWO = "com.two";
+ private static final int PACKAGE_TWO_UID = 2222;
+
+ private static final int PRIMARY_USER = 0;
+ private static final int PROFILE_OF_PRIMARY_USER = 10;
+ private static final int SECONDARY_USER = 11;
+
+ @Mock
+ private Context mContext;
+ @Mock
+ private UserManager mUserManager;
+ @Mock
+ private PackageManager mPackageManager;
+ @Mock
+ private PackageManagerInternal mPackageManagerInternal;
+ @Mock
+ private AppOpsManager mAppOpsManager;
+ @Mock
+ private ActivityManagerInternal mActivityManagerInternal;
+ @Mock
+ private ActivityTaskManagerInternal mActivityTaskManagerInternal;
+ @Mock
+ private IPackageManager mIPackageManager;
+ @Mock
+ private DevicePolicyManagerInternal mDevicePolicyManagerInternal;
+
+ private TestInjector mTestInjector;
+ private ActivityInfo mActivityInfo;
+ private CrossProfileAppsServiceImpl mCrossProfileAppsServiceImpl;
+ private IApplicationThread mIApplicationThread;
+
+ private SparseArray<Boolean> mUserEnabled = new SparseArray<>();
+
+ @Before
+ public void initCrossProfileAppsServiceImpl() {
+ mTestInjector = new TestInjector();
+ LocalServices.removeServiceForTest(CrossProfileAppsInternal.class);
+ mCrossProfileAppsServiceImpl = new CrossProfileAppsServiceImpl(mContext, mTestInjector);
+ when(mContext.getPackageManager()).thenReturn(mPackageManager);
+ }
+
+ @Before
+ public void setupEnabledProfiles() {
+ mUserEnabled.put(PRIMARY_USER, true);
+ mUserEnabled.put(PROFILE_OF_PRIMARY_USER, true);
+ mUserEnabled.put(SECONDARY_USER, true);
+
+ when(mUserManager.getEnabledProfileIds(anyInt())).thenAnswer(
+ invocation -> {
+ List<Integer> users = new ArrayList<>();
+ final int targetUser = invocation.getArgument(0);
+ users.add(targetUser);
+
+ int profileUserId = -1;
+ if (targetUser == PRIMARY_USER) {
+ profileUserId = PROFILE_OF_PRIMARY_USER;
+ } else if (targetUser == PROFILE_OF_PRIMARY_USER) {
+ profileUserId = PRIMARY_USER;
+ }
+
+ if (profileUserId != -1 && mUserEnabled.get(profileUserId)) {
+ users.add(profileUserId);
+ }
+ return users.stream().mapToInt(i -> i).toArray();
+ });
+ }
+
+ @Before
+ public void setupCaller() {
+ mTestInjector.setCallingUid(PACKAGE_ONE_UID);
+ mTestInjector.setCallingUserId(PRIMARY_USER);
+ }
+
+ @Before
+ public void setupPackage() throws Exception {
+ // PACKAGE_ONE are installed in all users.
+ mockAppsInstalled(PACKAGE_ONE, PRIMARY_USER, true);
+ mockAppsInstalled(PACKAGE_ONE, PROFILE_OF_PRIMARY_USER, true);
+ mockAppsInstalled(PACKAGE_ONE, SECONDARY_USER, true);
+
+ // Packages are resolved to their corresponding UID.
+ doAnswer(invocation -> {
+ final int uid = invocation.getArgument(0);
+ final String packageName = invocation.getArgument(1);
+ if (uid == PACKAGE_ONE_UID && PACKAGE_ONE.equals(packageName)) {
+ return null;
+ } else if (uid ==PACKAGE_TWO_UID && PACKAGE_TWO.equals(packageName)) {
+ return null;
+ }
+ throw new SecurityException("Not matching");
+ }).when(mAppOpsManager).checkPackage(anyInt(), anyString());
+
+ // The intent is resolved to the ACTIVITY_COMPONENT.
+ mockActivityLaunchIntentResolvedTo(ACTIVITY_COMPONENT);
+ }
+
+ @Test
+ public void getTargetUserProfiles_fromPrimaryUser_installed() throws Exception {
+ List<UserHandle> targetProfiles =
+ mCrossProfileAppsServiceImpl.getTargetUserProfiles(PACKAGE_ONE);
+ assertThat(targetProfiles).containsExactly(UserHandle.of(PROFILE_OF_PRIMARY_USER));
+ }
+
+ @Test
+ public void getTargetUserProfiles_fromPrimaryUser_notInstalled() throws Exception {
+ mockAppsInstalled(PACKAGE_ONE, PROFILE_OF_PRIMARY_USER, false);
+
+ List<UserHandle> targetProfiles =
+ mCrossProfileAppsServiceImpl.getTargetUserProfiles(PACKAGE_ONE);
+ assertThat(targetProfiles).isEmpty();
+ }
+
+ @Test
+ public void getTargetUserProfiles_fromPrimaryUser_userNotEnabled() throws Exception {
+ mUserEnabled.put(PROFILE_OF_PRIMARY_USER, false);
+
+ List<UserHandle> targetProfiles =
+ mCrossProfileAppsServiceImpl.getTargetUserProfiles(PACKAGE_ONE);
+ assertThat(targetProfiles).isEmpty();
+ }
+
+ @Test
+ public void getTargetUserProfiles_fromSecondaryUser() throws Exception {
+ mTestInjector.setCallingUserId(SECONDARY_USER);
+
+ List<UserHandle> targetProfiles =
+ mCrossProfileAppsServiceImpl.getTargetUserProfiles(PACKAGE_ONE);
+ assertThat(targetProfiles).isEmpty();
+ }
+
+ @Test
+ public void getTargetUserProfiles_fromProfile_installed() throws Exception {
+ mTestInjector.setCallingUserId(PROFILE_OF_PRIMARY_USER);
+
+ List<UserHandle> targetProfiles =
+ mCrossProfileAppsServiceImpl.getTargetUserProfiles(PACKAGE_ONE);
+ assertThat(targetProfiles).containsExactly(UserHandle.of(PRIMARY_USER));
+ }
+
+ @Test
+ public void getTargetUserProfiles_fromProfile_notInstalled() throws Exception {
+ mTestInjector.setCallingUserId(PROFILE_OF_PRIMARY_USER);
+ mockAppsInstalled(PACKAGE_ONE, PRIMARY_USER, false);
+
+ List<UserHandle> targetProfiles =
+ mCrossProfileAppsServiceImpl.getTargetUserProfiles(PACKAGE_ONE);
+ assertThat(targetProfiles).isEmpty();
+ }
+
+ @Test(expected = SecurityException.class)
+ public void getTargetUserProfiles_fakeCaller() throws Exception {
+ mCrossProfileAppsServiceImpl.getTargetUserProfiles(PACKAGE_TWO);
+ }
+
+ @Test
+ public void startActivityAsUser_currentUser() throws Exception {
+ assertThrows(
+ SecurityException.class,
+ () ->
+ mCrossProfileAppsServiceImpl.startActivityAsUser(
+ mIApplicationThread,
+ PACKAGE_ONE,
+ FEATURE_ID,
+ ACTIVITY_COMPONENT,
+ UserHandle.of(PRIMARY_USER).getIdentifier(),
+ true,
+ /* targetTask */ null,
+ /* options */ null));
+
+ verify(mActivityTaskManagerInternal, never())
+ .startActivityAsUser(
+ nullable(IApplicationThread.class),
+ anyString(),
+ nullable(String.class),
+ any(Intent.class),
+ nullable(IBinder.class),
+ anyInt(),
+ nullable(Bundle.class),
+ anyInt());
+ }
+
+ @Test
+ public void startAnyActivityAsUser_currentUser() {
+ assertThrows(
+ SecurityException.class,
+ () ->
+ mCrossProfileAppsServiceImpl.startActivityAsUser(
+ mIApplicationThread,
+ PACKAGE_ONE,
+ FEATURE_ID,
+ ACTIVITY_COMPONENT,
+ UserHandle.of(PRIMARY_USER).getIdentifier(),
+ false,
+ /* targetTask */ null,
+ /* options */ null));
+
+ verify(mActivityTaskManagerInternal, never())
+ .startActivityAsUser(
+ nullable(IApplicationThread.class),
+ anyString(),
+ nullable(String.class),
+ any(Intent.class),
+ nullable(IBinder.class),
+ anyInt(),
+ nullable(Bundle.class),
+ anyInt());
+ }
+
+ @Test
+ public void startActivityAsUser_profile_notInstalled() throws Exception {
+ mockAppsInstalled(PACKAGE_ONE, PROFILE_OF_PRIMARY_USER, false);
+
+ assertThrows(
+ SecurityException.class,
+ () ->
+ mCrossProfileAppsServiceImpl.startActivityAsUser(
+ mIApplicationThread,
+ PACKAGE_ONE,
+ FEATURE_ID,
+ ACTIVITY_COMPONENT,
+ UserHandle.of(PROFILE_OF_PRIMARY_USER).getIdentifier(),
+ true,
+ /* targetTask */ null,
+ /* options */ null));
+
+ verify(mActivityTaskManagerInternal, never())
+ .startActivityAsUser(
+ nullable(IApplicationThread.class),
+ anyString(),
+ nullable(String.class),
+ any(Intent.class),
+ nullable(IBinder.class),
+ anyInt(),
+ nullable(Bundle.class),
+ anyInt());
+ }
+
+ @Test
+ public void startAnyActivityAsUser_profile_notInstalled() {
+ mockAppsInstalled(PACKAGE_ONE, PROFILE_OF_PRIMARY_USER, false);
+
+ assertThrows(
+ SecurityException.class,
+ () ->
+ mCrossProfileAppsServiceImpl.startActivityAsUser(
+ mIApplicationThread,
+ PACKAGE_ONE,
+ FEATURE_ID,
+ ACTIVITY_COMPONENT,
+ UserHandle.of(PROFILE_OF_PRIMARY_USER).getIdentifier(),
+ false,
+ /* targetTask */ null,
+ /* options */ null));
+
+ verify(mActivityTaskManagerInternal, never())
+ .startActivityAsUser(
+ nullable(IApplicationThread.class),
+ anyString(),
+ nullable(String.class),
+ any(Intent.class),
+ nullable(IBinder.class),
+ anyInt(),
+ nullable(Bundle.class),
+ anyInt());
+ }
+
+ @Test
+ public void startActivityAsUser_profile_fakeCaller() throws Exception {
+ assertThrows(
+ SecurityException.class,
+ () ->
+ mCrossProfileAppsServiceImpl.startActivityAsUser(
+ mIApplicationThread,
+ PACKAGE_TWO,
+ FEATURE_ID,
+ ACTIVITY_COMPONENT,
+ UserHandle.of(PROFILE_OF_PRIMARY_USER).getIdentifier(),
+ true,
+ /* targetTask */ null,
+ /* options */ null));
+
+ verify(mActivityTaskManagerInternal, never())
+ .startActivityAsUser(
+ nullable(IApplicationThread.class),
+ anyString(),
+ nullable(String.class),
+ any(Intent.class),
+ nullable(IBinder.class),
+ anyInt(),
+ nullable(Bundle.class),
+ anyInt());
+ }
+
+ @Test
+ public void startAnyActivityAsUser_profile_fakeCaller() {
+ assertThrows(
+ SecurityException.class,
+ () ->
+ mCrossProfileAppsServiceImpl.startActivityAsUser(
+ mIApplicationThread,
+ PACKAGE_TWO,
+ FEATURE_ID,
+ ACTIVITY_COMPONENT,
+ UserHandle.of(PROFILE_OF_PRIMARY_USER).getIdentifier(),
+ false,
+ /* targetTask */ null,
+ /* options */ null));
+
+ verify(mActivityTaskManagerInternal, never())
+ .startActivityAsUser(
+ nullable(IApplicationThread.class),
+ anyString(),
+ nullable(String.class),
+ any(Intent.class),
+ nullable(IBinder.class),
+ anyInt(),
+ nullable(Bundle.class),
+ anyInt());
+ }
+
+ @Test
+ public void startActivityAsUser_profile_notExported() throws Exception {
+ mActivityInfo.exported = false;
+
+ assertThrows(
+ SecurityException.class,
+ () ->
+ mCrossProfileAppsServiceImpl.startActivityAsUser(
+ mIApplicationThread,
+ PACKAGE_ONE,
+ FEATURE_ID,
+ ACTIVITY_COMPONENT,
+ UserHandle.of(PROFILE_OF_PRIMARY_USER).getIdentifier(),
+ true,
+ /* targetTask */ null,
+ /* options */ null));
+
+ verify(mActivityTaskManagerInternal, never())
+ .startActivityAsUser(
+ nullable(IApplicationThread.class),
+ anyString(),
+ nullable(String.class),
+ any(Intent.class),
+ nullable(IBinder.class),
+ anyInt(),
+ nullable(Bundle.class),
+ anyInt());
+ }
+
+ @Test
+ public void startAnyActivityAsUser_profile_notExported() {
+ try {
+ when(mPackageManager.getPermissionInfo(anyString(), anyInt()))
+ .thenReturn(new PermissionInfo());
+ } catch (PackageManager.NameNotFoundException ignored) {
+ }
+ mActivityInfo.exported = false;
+
+
+ // There's a bug in static mocking if the APK is large - so here is the next best thing...
+ doReturn(Context.PERMISSION_CHECKER_SERVICE).when(mContext)
+ .getSystemServiceName(PermissionCheckerManager.class);
+ PermissionCheckerManager permissionCheckerManager = mock(PermissionCheckerManager.class);
+ doReturn(PermissionChecker.PERMISSION_HARD_DENIED).when(permissionCheckerManager)
+ .checkPermission(eq(Manifest.permission.INTERACT_ACROSS_PROFILES), any(
+ AttributionSourceState.class), anyString(), anyBoolean(), anyBoolean(),
+ anyBoolean(), anyInt());
+ doReturn(permissionCheckerManager).when(mContext).getSystemService(
+ Context.PERMISSION_CHECKER_SERVICE);
+
+ assertThrows(
+ SecurityException.class,
+ () ->
+ mCrossProfileAppsServiceImpl.startActivityAsUser(
+ mIApplicationThread,
+ PACKAGE_ONE,
+ FEATURE_ID,
+ ACTIVITY_COMPONENT,
+ UserHandle.of(PROFILE_OF_PRIMARY_USER).getIdentifier(),
+ false,
+ /* targetTask */ null,
+ /* options */ null));
+
+ verify(mActivityTaskManagerInternal, never())
+ .startActivityAsUser(
+ nullable(IApplicationThread.class),
+ anyString(),
+ nullable(String.class),
+ any(Intent.class),
+ nullable(IBinder.class),
+ anyInt(),
+ nullable(Bundle.class),
+ anyInt());
+ }
+
+ @Test
+ public void startActivityAsUser_profile_anotherPackage() throws Exception {
+ assertThrows(
+ SecurityException.class,
+ () ->
+ mCrossProfileAppsServiceImpl.startActivityAsUser(
+ mIApplicationThread,
+ PACKAGE_ONE,
+ FEATURE_ID,
+ new ComponentName(PACKAGE_TWO, "test"),
+ UserHandle.of(PROFILE_OF_PRIMARY_USER).getIdentifier(),
+ true,
+ /* targetTask */ null,
+ /* options */ null));
+
+ verify(mActivityTaskManagerInternal, never())
+ .startActivityAsUser(
+ nullable(IApplicationThread.class),
+ anyString(),
+ nullable(String.class),
+ any(Intent.class),
+ nullable(IBinder.class),
+ anyInt(),
+ nullable(Bundle.class),
+ anyInt());
+ }
+
+ @Test
+ public void startAnyActivityAsUser_profile_anotherPackage() {
+ assertThrows(
+ SecurityException.class,
+ () ->
+ mCrossProfileAppsServiceImpl.startActivityAsUser(
+ mIApplicationThread,
+ PACKAGE_ONE,
+ FEATURE_ID,
+ new ComponentName(PACKAGE_TWO, "test"),
+ UserHandle.of(PROFILE_OF_PRIMARY_USER).getIdentifier(),
+ false,
+ /* targetTask */ null,
+ /* options */ null));
+
+ verify(mActivityTaskManagerInternal, never())
+ .startActivityAsUser(
+ nullable(IApplicationThread.class),
+ anyString(),
+ nullable(String.class),
+ any(Intent.class),
+ nullable(IBinder.class),
+ anyInt(),
+ nullable(Bundle.class),
+ anyInt());
+ }
+
+ @Test
+ public void startActivityAsUser_secondaryUser() throws Exception {
+ assertThrows(
+ SecurityException.class,
+ () ->
+ mCrossProfileAppsServiceImpl.startActivityAsUser(
+ mIApplicationThread,
+ PACKAGE_ONE,
+ FEATURE_ID,
+ ACTIVITY_COMPONENT,
+ UserHandle.of(SECONDARY_USER).getIdentifier(),
+ true,
+ /* targetTask */ null,
+ /* options */ null));
+
+ verify(mActivityTaskManagerInternal, never())
+ .startActivityAsUser(
+ nullable(IApplicationThread.class),
+ anyString(),
+ nullable(String.class),
+ any(Intent.class),
+ nullable(IBinder.class),
+ anyInt(),
+ nullable(Bundle.class),
+ anyInt());
+ }
+
+ @Test
+ public void startAnyActivityAsUser_secondaryUser() {
+ assertThrows(
+ SecurityException.class,
+ () ->
+ mCrossProfileAppsServiceImpl.startActivityAsUser(
+ mIApplicationThread,
+ PACKAGE_ONE,
+ FEATURE_ID,
+ ACTIVITY_COMPONENT,
+ UserHandle.of(SECONDARY_USER).getIdentifier(),
+ false,
+ /* targetTask */ null,
+ /* options */ null));
+
+ verify(mActivityTaskManagerInternal, never())
+ .startActivityAsUser(
+ nullable(IApplicationThread.class),
+ anyString(),
+ nullable(String.class),
+ any(Intent.class),
+ nullable(IBinder.class),
+ anyInt(),
+ nullable(Bundle.class),
+ anyInt());
+ }
+
+ @Test
+ public void startActivityAsUser_fromProfile_success() throws Exception {
+ mTestInjector.setCallingUserId(PROFILE_OF_PRIMARY_USER);
+
+ mCrossProfileAppsServiceImpl.startActivityAsUser(
+ mIApplicationThread,
+ PACKAGE_ONE,
+ FEATURE_ID,
+ ACTIVITY_COMPONENT,
+ UserHandle.of(PRIMARY_USER).getIdentifier(),
+ true,
+ /* targetTask */ null,
+ /* options */ null);
+
+ verify(mActivityTaskManagerInternal)
+ .startActivityAsUser(
+ nullable(IApplicationThread.class),
+ eq(PACKAGE_ONE),
+ eq(FEATURE_ID),
+ any(Intent.class),
+ nullable(IBinder.class),
+ anyInt(),
+ nullable(Bundle.class),
+ eq(PRIMARY_USER));
+ }
+
+ @Test
+ public void startActivityAsUser_sameTask_fromProfile_success() throws Exception {
+ mTestInjector.setCallingUserId(PROFILE_OF_PRIMARY_USER);
+
+ Bundle options = ActivityOptions.makeOpenCrossProfileAppsAnimation().toBundle();
+ Binder targetTask = new Binder();
+ mCrossProfileAppsServiceImpl.startActivityAsUser(
+ mIApplicationThread,
+ PACKAGE_ONE,
+ FEATURE_ID,
+ ACTIVITY_COMPONENT,
+ UserHandle.of(PRIMARY_USER).getIdentifier(),
+ true,
+ targetTask,
+ options);
+ verify(mActivityTaskManagerInternal)
+ .startActivityAsUser(
+ nullable(IApplicationThread.class),
+ eq(PACKAGE_ONE),
+ eq(FEATURE_ID),
+ any(Intent.class),
+ eq(targetTask),
+ anyInt(),
+ eq(options),
+ eq(PRIMARY_USER));
+ }
+
+ private void mockAppsInstalled(String packageName, int user, boolean installed) {
+ when(mPackageManagerInternal.getPackageInfo(
+ eq(packageName),
+ anyLong(),
+ anyInt(),
+ eq(user)))
+ .thenReturn(installed ? createInstalledPackageInfo() : null);
+ }
+
+ private PackageInfo createInstalledPackageInfo() {
+ PackageInfo packageInfo = new PackageInfo();
+ packageInfo.applicationInfo = new ApplicationInfo();
+ packageInfo.applicationInfo.enabled = true;
+ return packageInfo;
+ }
+
+ private void mockActivityLaunchIntentResolvedTo(ComponentName componentName) {
+ ResolveInfo resolveInfo = new ResolveInfo();
+ ActivityInfo activityInfo = new ActivityInfo();
+ activityInfo.packageName = componentName.getPackageName();
+ activityInfo.name = componentName.getClassName();
+ activityInfo.exported = true;
+ resolveInfo.activityInfo = activityInfo;
+ mActivityInfo = activityInfo;
+
+ when(mPackageManagerInternal.queryIntentActivities(
+ any(Intent.class), nullable(String.class), anyLong(), anyInt(), anyInt()))
+ .thenReturn(Collections.singletonList(resolveInfo));
+ }
+
+ private class TestInjector implements CrossProfileAppsServiceImpl.Injector {
+ private int mCallingUid;
+ private int mCallingUserId;
+ private int mCallingPid;
+
+ public void setCallingUid(int uid) {
+ mCallingUid = uid;
+ }
+
+ public void setCallingPid(int pid) {
+ mCallingPid = pid;
+ }
+
+ public void setCallingUserId(int userId) {
+ mCallingUserId = userId;
+ }
+
+ @Override
+ public int getCallingUid() {
+ return mCallingUid;
+ }
+
+ @Override
+ public int getCallingPid() {
+ return mCallingPid;
+ }
+
+ @Override
+ public int getCallingUserId() {
+ return mCallingUserId;
+ }
+
+ @Override
+ public UserHandle getCallingUserHandle() {
+ return UserHandle.of(mCallingUserId);
+ }
+
+ @Override
+ public long clearCallingIdentity() {
+ return 0;
+ }
+
+ @Override
+ public void restoreCallingIdentity(long token) {
+ }
+
+ @Override
+ public void withCleanCallingIdentity(ThrowingRunnable action) {
+ action.run();
+ }
+
+ @Override
+ public <T> T withCleanCallingIdentity(ThrowingSupplier<T> action) {
+ return action.get();
+ }
+
+ @Override
+ public UserManager getUserManager() {
+ return mUserManager;
+ }
+
+ @Override
+ public PackageManagerInternal getPackageManagerInternal() {
+ return mPackageManagerInternal;
+ }
+
+ @Override
+ public PackageManager getPackageManager() {
+ return mPackageManager;
+ }
+
+ @Override
+ public AppOpsManager getAppOpsManager() {
+ return mAppOpsManager;
+ }
+
+ @Override
+ public ActivityManagerInternal getActivityManagerInternal() {
+ return mActivityManagerInternal;
+ }
+
+ @Override
+ public ActivityTaskManagerInternal getActivityTaskManagerInternal() {
+ return mActivityTaskManagerInternal;
+ }
+
+ @Override
+ public IPackageManager getIPackageManager() {
+ return mIPackageManager;
+ }
+
+ @Override
+ public DevicePolicyManagerInternal getDevicePolicyManagerInternal() {
+ return mDevicePolicyManagerInternal;
+ }
+
+ @Override
+ public void sendBroadcastAsUser(Intent intent, UserHandle user) {
+ mContext.sendBroadcastAsUser(intent, user);
+ }
+
+ @Override
+ public int checkComponentPermission(
+ String permission, int uid, int owningUid, boolean exported) {
+ return ActivityManager.checkComponentPermission(permission, uid, owningUid, exported);
+ }
+
+ @Override
+ public void killUid(int uid) {
+ PermissionManagerService.killUid(
+ UserHandle.getAppId(uid),
+ UserHandle.getUserId(uid),
+ PermissionManager.KILL_APP_REASON_PERMISSIONS_REVOKED);
+ }
+ }
+}
\ No newline at end of file
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/uiservicestests/src/com/android/server/notification/SnoozeHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/SnoozeHelperTest.java
index a03a1b4..ebcb498 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/SnoozeHelperTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/SnoozeHelperTest.java
@@ -234,6 +234,37 @@
}
@Test
+ public void testScheduleRepostsForLongTagPersistedNotification() throws Exception {
+ String longTag = "A".repeat(66000);
+ NotificationRecord r = getNotificationRecord("pkg", 1, longTag, UserHandle.SYSTEM);
+ mSnoozeHelper.snooze(r, 0);
+
+ // We store the full key in temp storage.
+ ArgumentCaptor<PendingIntent> captor = ArgumentCaptor.forClass(PendingIntent.class);
+ verify(mAm).setExactAndAllowWhileIdle(anyInt(), anyLong(), captor.capture());
+ assertEquals(66010, captor.getValue().getIntent().getStringExtra(EXTRA_KEY).length());
+
+ TypedXmlSerializer serializer = Xml.newFastSerializer();
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ serializer.setOutput(new BufferedOutputStream(baos), "utf-8");
+ serializer.startDocument(null, true);
+ mSnoozeHelper.writeXml(serializer);
+ serializer.endDocument();
+ serializer.flush();
+
+ TypedXmlPullParser parser = Xml.newFastPullParser();
+ parser.setInput(new BufferedInputStream(
+ new ByteArrayInputStream(baos.toByteArray())), "utf-8");
+ mSnoozeHelper.readXml(parser, 4);
+
+ mSnoozeHelper.scheduleRepostsForPersistedNotifications(5);
+
+ // We trim the key in persistent storage.
+ verify(mAm, times(2)).setExactAndAllowWhileIdle(anyInt(), anyLong(), captor.capture());
+ assertEquals(1000, captor.getValue().getIntent().getStringExtra(EXTRA_KEY).length());
+ }
+
+ @Test
public void testSnoozeForTime() throws Exception {
NotificationRecord r = getNotificationRecord("pkg", 1, "one", UserHandle.SYSTEM);
mSnoozeHelper.snooze(r, 1000);
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/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/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..435c39c 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
@@ -720,7 +720,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 +738,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 +813,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 +838,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_",