Merge "Add TestApis to get currently registered AudioMixes" into main
diff --git a/AconfigFlags.bp b/AconfigFlags.bp
index e6e835b..1c6df75 100644
--- a/AconfigFlags.bp
+++ b/AconfigFlags.bp
@@ -24,6 +24,7 @@
":android.chre.flags-aconfig-java{.generated_srcjars}",
":android.companion.flags-aconfig-java{.generated_srcjars}",
":android.companion.virtual.flags-aconfig-java{.generated_srcjars}",
+ ":android.companion.virtualdevice.flags-aconfig-java{.generated_srcjars}",
":android.content.flags-aconfig-java{.generated_srcjars}",
":android.content.pm.flags-aconfig-java{.generated_srcjars}",
":android.content.res.flags-aconfig-java{.generated_srcjars}",
@@ -35,6 +36,7 @@
":android.hardware.radio.flags-aconfig-java{.generated_srcjars}",
":android.hardware.usb.flags-aconfig-java{.generated_srcjars}",
":android.location.flags-aconfig-java{.generated_srcjars}",
+ ":android.media.codec-aconfig-java{.generated_srcjars}",
":android.media.tv.flags-aconfig-java{.generated_srcjars}",
":android.multiuser.flags-aconfig-java{.generated_srcjars}",
":android.net.platform.flags-aconfig-java{.generated_srcjars}",
@@ -54,6 +56,7 @@
":android.service.notification.flags-aconfig-java{.generated_srcjars}",
":android.service.voice.flags-aconfig-java{.generated_srcjars}",
":android.speech.flags-aconfig-java{.generated_srcjars}",
+ ":android.systemserver.flags-aconfig-java{.generated_srcjars}",
":android.tracing.flags-aconfig-java{.generated_srcjars}",
":android.view.accessibility.flags-aconfig-java{.generated_srcjars}",
":android.view.contentcapture.flags-aconfig-java{.generated_srcjars}",
@@ -68,6 +71,7 @@
":com.android.hardware.input-aconfig-java{.generated_srcjars}",
":com.android.input.flags-aconfig-java{.generated_srcjars}",
":com.android.internal.foldables.flags-aconfig-java{.generated_srcjars}",
+ ":com.android.internal.pm.pkg.component.flags-aconfig-java{.generated_srcjars}",
":com.android.media.flags.bettertogether-aconfig-java{.generated_srcjars}",
":com.android.media.flags.editing-aconfig-java{.generated_srcjars}",
":com.android.net.thread.flags-aconfig-java{.generated_srcjars}",
@@ -1137,3 +1141,35 @@
aconfig_declarations: "android.app.wearable.flags-aconfig",
defaults: ["framework-minus-apex-aconfig-java-defaults"],
}
+
+aconfig_declarations {
+ name: "com.android.internal.pm.pkg.component.flags-aconfig",
+ package: "com.android.internal.pm.pkg.component.flags",
+ srcs: ["core/java/com/android/internal/pm/pkg/component/flags/flags.aconfig"],
+}
+
+java_aconfig_library {
+ name: "com.android.internal.pm.pkg.component.flags-aconfig-java",
+ aconfig_declarations: "com.android.internal.pm.pkg.component.flags-aconfig",
+ defaults: ["framework-minus-apex-aconfig-java-defaults"],
+}
+
+java_aconfig_library {
+ name: "com.android.internal.pm.pkg.component.flags-aconfig-java-host",
+ aconfig_declarations: "com.android.internal.pm.pkg.component.flags-aconfig",
+ host_supported: true,
+ defaults: ["framework-minus-apex-aconfig-java-defaults"],
+}
+
+// System Server
+aconfig_declarations {
+ name: "android.systemserver.flags-aconfig",
+ package: "android.server",
+ srcs: ["services/java/com/android/server/flags.aconfig"],
+}
+
+java_aconfig_library {
+ name: "android.systemserver.flags-aconfig-java",
+ aconfig_declarations: "android.systemserver.flags-aconfig",
+ defaults: ["framework-minus-apex-aconfig-java-defaults"],
+}
diff --git a/MEMORY_OWNERS b/MEMORY_OWNERS
new file mode 100644
index 0000000..89ce5140
--- /dev/null
+++ b/MEMORY_OWNERS
@@ -0,0 +1,6 @@
+surenb@google.com
+tjmercier@google.com
+kaleshsingh@google.com
+jyescas@google.com
+carlosgalo@google.com
+jji@google.com
diff --git a/apex/jobscheduler/framework/java/android/app/job/JobInfo.java b/apex/jobscheduler/framework/java/android/app/job/JobInfo.java
index 324d8ca..7284f47 100644
--- a/apex/jobscheduler/framework/java/android/app/job/JobInfo.java
+++ b/apex/jobscheduler/framework/java/android/app/job/JobInfo.java
@@ -23,7 +23,6 @@
import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VPN;
import static android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED;
import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
-import static android.text.format.DateUtils.HOUR_IN_MILLIS;
import static android.util.TimeUtils.formatDuration;
import android.annotation.BytesLong;
@@ -50,9 +49,7 @@
import android.os.Parcel;
import android.os.Parcelable;
import android.os.PersistableBundle;
-import android.os.Process;
import android.os.Trace;
-import android.os.UserHandle;
import android.util.ArraySet;
import android.util.Log;
@@ -206,6 +203,8 @@
/* Minimum flex for a periodic job, in milliseconds. */
private static final long MIN_FLEX_MILLIS = 5 * 60 * 1000L; // 5 minutes
+ private static final long MIN_ALLOWED_TIME_WINDOW_MILLIS = MIN_PERIOD_MILLIS;
+
/**
* Minimum backoff interval for a job, in milliseconds
* @hide
@@ -1881,11 +1880,12 @@
}
/**
- * Set deadline which is the maximum scheduling latency. The job will be run by this
- * deadline even if other requirements (including a delay set through
- * {@link #setMinimumLatency(long)}) are not met.
+ * Set a deadline after which all other functional requested constraints will be ignored.
+ * After the deadline has passed, the job can run even if other requirements (including
+ * a delay set through {@link #setMinimumLatency(long)}) are not met.
* {@link JobParameters#isOverrideDeadlineExpired()} will return {@code true} if the job's
- * deadline has passed.
+ * deadline has passed. The job's execution may be delayed beyond the set deadline by
+ * other factors such as Doze mode and system health signals.
*
* <p>
* Because it doesn't make sense setting this property on a periodic job, doing so will
@@ -1894,30 +1894,23 @@
*
* <p class="note">
* Since a job will run once the deadline has passed regardless of the status of other
- * constraints, setting a deadline of 0 with other constraints makes those constraints
- * meaningless when it comes to execution decisions. Avoid doing this.
- * </p>
- *
- * <p>
- * Short deadlines hinder the system's ability to optimize scheduling behavior and may
- * result in running jobs at inopportune times. Therefore, starting in Android version
- * {@link android.os.Build.VERSION_CODES#VANILLA_ICE_CREAM}, minimum time windows will be
- * enforced to help make it easier to better optimize job execution. Time windows are
+ * constraints, setting a deadline of 0 (or a {@link #setMinimumLatency(long) delay} equal
+ * to the deadline) with other constraints makes those constraints
+ * meaningless when it comes to execution decisions. Since doing so is indicative of an
+ * error in the logic, starting in Android version
+ * {@link android.os.Build.VERSION_CODES#VANILLA_ICE_CREAM}, jobs with extremely short
+ * time windows will fail to build. Time windows are
* defined as the time between a job's {@link #setMinimumLatency(long) minimum latency}
* and its deadline. If the minimum latency is not set, it is assumed to be 0.
- * The following minimums will be enforced:
- * <ul>
- * <li>
- * Jobs with {@link #PRIORITY_DEFAULT} or higher priorities have a minimum time
- * window of one hour.
- * </li>
- * <li>Jobs with {@link #PRIORITY_LOW} have a minimum time window of 6 hours.</li>
- * <li>Jobs with {@link #PRIORITY_MIN} have a minimum time window of 12 hours.</li>
- * </ul>
*
* Work that must happen immediately should use {@link #setExpedited(boolean)} or
* {@link #setUserInitiated(boolean)} in the appropriate manner.
*
+ * <p>
+ * This API aimed to guarantee execution of the job by the deadline only on Android version
+ * {@link android.os.Build.VERSION_CODES#LOLLIPOP}. That aim and guarantee has not existed
+ * since {@link android.os.Build.VERSION_CODES#M}.
+ *
* @see JobInfo#getMaxExecutionDelayMillis()
*/
public Builder setOverrideDeadline(long maxExecutionDelayMillis) {
@@ -2347,35 +2340,36 @@
throw new IllegalArgumentException("Invalid priority level provided: " + mPriority);
}
- if (enforceMinimumTimeWindows
- && Flags.enforceMinimumTimeWindows()
- // TODO(312197030): remove exemption for the system
- && !UserHandle.isCore(Process.myUid())
- && hasLateConstraint && !isPeriodic) {
- final long windowStart = hasEarlyConstraint ? minLatencyMillis : 0;
- if (mPriority >= PRIORITY_DEFAULT) {
- if (maxExecutionDelayMillis - windowStart < HOUR_IN_MILLIS) {
- throw new IllegalArgumentException(
- getPriorityString(mPriority)
- + " cannot have a time window less than 1 hour."
- + " Delay=" + windowStart
- + ", deadline=" + maxExecutionDelayMillis);
- }
- } else if (mPriority >= PRIORITY_LOW) {
- if (maxExecutionDelayMillis - windowStart < 6 * HOUR_IN_MILLIS) {
- throw new IllegalArgumentException(
- getPriorityString(mPriority)
- + " cannot have a time window less than 6 hours."
- + " Delay=" + windowStart
- + ", deadline=" + maxExecutionDelayMillis);
- }
+ final boolean hasFunctionalConstraint = networkRequest != null
+ || constraintFlags != 0
+ || (triggerContentUris != null && triggerContentUris.length > 0);
+ if (hasLateConstraint && !isPeriodic) {
+ if (!hasFunctionalConstraint) {
+ Log.w(TAG, "Job '" + service.flattenToShortString() + "#" + jobId + "'"
+ + " has a deadline with no functional constraints."
+ + " The deadline won't improve job execution latency."
+ + " Consider removing the deadline.");
} else {
- if (maxExecutionDelayMillis - windowStart < 12 * HOUR_IN_MILLIS) {
- throw new IllegalArgumentException(
- getPriorityString(mPriority)
- + " cannot have a time window less than 12 hours."
- + " Delay=" + windowStart
- + ", deadline=" + maxExecutionDelayMillis);
+ final long windowStart = hasEarlyConstraint ? minLatencyMillis : 0;
+ if (maxExecutionDelayMillis - windowStart < MIN_ALLOWED_TIME_WINDOW_MILLIS) {
+ if (enforceMinimumTimeWindows
+ && Flags.enforceMinimumTimeWindows()) {
+ throw new IllegalArgumentException("Jobs with a deadline and"
+ + " functional constraints cannot have a time window less than "
+ + MIN_ALLOWED_TIME_WINDOW_MILLIS + " ms."
+ + " Job '" + service.flattenToShortString() + "#" + jobId + "'"
+ + " has delay=" + windowStart
+ + ", deadline=" + maxExecutionDelayMillis);
+ } else {
+ Log.w(TAG, "Job '" + service.flattenToShortString() + "#" + jobId + "'"
+ + " has a deadline with functional constraints and an extremely"
+ + " short time window of "
+ + (maxExecutionDelayMillis - windowStart) + " ms"
+ + " (delay=" + windowStart
+ + ", deadline=" + maxExecutionDelayMillis + ")."
+ + " The functional constraints are not likely to be satisfied when"
+ + " the job runs.");
+ }
}
}
}
diff --git a/apex/jobscheduler/service/aconfig/job.aconfig b/apex/jobscheduler/service/aconfig/job.aconfig
index 5d65d9d..8a5206f 100644
--- a/apex/jobscheduler/service/aconfig/job.aconfig
+++ b/apex/jobscheduler/service/aconfig/job.aconfig
@@ -1,6 +1,20 @@
package: "com.android.server.job"
flag {
+ name: "batch_active_bucket_jobs"
+ namespace: "backstage_power"
+ description: "Include jobs in the ACTIVE bucket in the job batching effort. Don't let them run as freely as they're ready."
+ bug: "299329948"
+}
+
+flag {
+ name: "batch_connectivity_jobs_per_network"
+ namespace: "backstage_power"
+ description: "Have JobScheduler attempt to delay the start of some connectivity jobs until there are several ready or the network is active"
+ bug: "28382445"
+}
+
+flag {
name: "do_not_force_rush_execution_at_boot"
namespace: "backstage_power"
description: "Don't force rush job execution right after boot completion"
@@ -20,10 +34,3 @@
description: "Throw an exception if an unsupported app uses JobInfo.setBias"
bug: "300477393"
}
-
-flag {
- name: "batch_jobs_on_network_activation"
- namespace: "backstage_power"
- description: "Have JobScheduler attempt to delay the start of some connectivity jobs until the network is actually active"
- bug: "318394184"
-}
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 6550f26..012ede2 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/JobConcurrencyManager.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/JobConcurrencyManager.java
@@ -96,7 +96,6 @@
static final String CONFIG_KEY_PREFIX_CONCURRENCY = "concurrency_";
private static final String KEY_CONCURRENCY_LIMIT = CONFIG_KEY_PREFIX_CONCURRENCY + "limit";
- @VisibleForTesting
static final int DEFAULT_CONCURRENCY_LIMIT;
static {
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 57467e3..e6ee975 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java
@@ -65,6 +65,7 @@
import android.content.pm.ProviderInfo;
import android.content.pm.ServiceInfo;
import android.net.Network;
+import android.net.NetworkCapabilities;
import android.net.Uri;
import android.os.BatteryManager;
import android.os.BatteryManagerInternal;
@@ -89,6 +90,7 @@
import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.IndentingPrintWriter;
+import android.util.KeyValueListParser;
import android.util.Log;
import android.util.Pair;
import android.util.Slog;
@@ -316,7 +318,8 @@
private final List<JobRestriction> mJobRestrictions;
@GuardedBy("mLock")
- private final BatteryStateTracker mBatteryStateTracker;
+ @VisibleForTesting
+ final BatteryStateTracker mBatteryStateTracker;
@GuardedBy("mLock")
private final SparseArray<String> mCloudMediaProviderPackages = new SparseArray<>();
@@ -538,7 +541,9 @@
apiQuotaScheduleUpdated = true;
}
break;
+ case Constants.KEY_MIN_READY_CPU_ONLY_JOBS_COUNT:
case Constants.KEY_MIN_READY_NON_ACTIVE_JOBS_COUNT:
+ case Constants.KEY_MAX_CPU_ONLY_JOB_BATCH_DELAY_MS:
case Constants.KEY_MAX_NON_ACTIVE_JOB_BATCH_DELAY_MS:
mConstants.updateBatchingConstantsLocked();
break;
@@ -554,6 +559,8 @@
case Constants.KEY_CONN_CONGESTION_DELAY_FRAC:
case Constants.KEY_CONN_PREFETCH_RELAX_FRAC:
case Constants.KEY_CONN_LOW_SIGNAL_STRENGTH_RELAX_FRAC:
+ case Constants.KEY_CONN_MAX_CONNECTIVITY_JOB_BATCH_DELAY_MS:
+ case Constants.KEY_CONN_TRANSPORT_BATCH_THRESHOLD:
case Constants.KEY_CONN_USE_CELL_SIGNAL_STRENGTH:
case Constants.KEY_CONN_UPDATE_ALL_JOBS_MIN_INTERVAL_MS:
mConstants.updateConnectivityConstantsLocked();
@@ -602,6 +609,8 @@
sc.onConstantsUpdatedLocked();
}
}
+
+ mHandler.sendEmptyMessage(MSG_CHECK_JOB);
}
@Override
@@ -646,8 +655,12 @@
*/
public static class Constants {
// Key names stored in the settings value.
+ private static final String KEY_MIN_READY_CPU_ONLY_JOBS_COUNT =
+ "min_ready_cpu_only_jobs_count";
private static final String KEY_MIN_READY_NON_ACTIVE_JOBS_COUNT =
"min_ready_non_active_jobs_count";
+ private static final String KEY_MAX_CPU_ONLY_JOB_BATCH_DELAY_MS =
+ "max_cpu_only_job_batch_delay_ms";
private static final String KEY_MAX_NON_ACTIVE_JOB_BATCH_DELAY_MS =
"max_non_active_job_batch_delay_ms";
private static final String KEY_HEAVY_USE_FACTOR = "heavy_use_factor";
@@ -665,6 +678,10 @@
"conn_update_all_jobs_min_interval_ms";
private static final String KEY_CONN_LOW_SIGNAL_STRENGTH_RELAX_FRAC =
"conn_low_signal_strength_relax_frac";
+ private static final String KEY_CONN_TRANSPORT_BATCH_THRESHOLD =
+ "conn_transport_batch_threshold";
+ private static final String KEY_CONN_MAX_CONNECTIVITY_JOB_BATCH_DELAY_MS =
+ "conn_max_connectivity_job_batch_delay_ms";
private static final String KEY_PREFETCH_FORCE_BATCH_RELAX_THRESHOLD_MS =
"prefetch_force_batch_relax_threshold_ms";
// This has been enabled for 3+ full releases. We're unlikely to disable it.
@@ -713,7 +730,11 @@
private static final String KEY_MAX_NUM_PERSISTED_JOB_WORK_ITEMS =
"max_num_persisted_job_work_items";
- private static final int DEFAULT_MIN_READY_NON_ACTIVE_JOBS_COUNT = 5;
+ private static final int DEFAULT_MIN_READY_CPU_ONLY_JOBS_COUNT =
+ Math.min(3, JobConcurrencyManager.DEFAULT_CONCURRENCY_LIMIT / 3);
+ private static final int DEFAULT_MIN_READY_NON_ACTIVE_JOBS_COUNT =
+ Math.min(5, JobConcurrencyManager.DEFAULT_CONCURRENCY_LIMIT / 3);
+ private static final long DEFAULT_MAX_CPU_ONLY_JOB_BATCH_DELAY_MS = 31 * MINUTE_IN_MILLIS;
private static final long DEFAULT_MAX_NON_ACTIVE_JOB_BATCH_DELAY_MS = 31 * MINUTE_IN_MILLIS;
private static final float DEFAULT_HEAVY_USE_FACTOR = .9f;
private static final float DEFAULT_MODERATE_USE_FACTOR = .5f;
@@ -725,6 +746,15 @@
private static final boolean DEFAULT_CONN_USE_CELL_SIGNAL_STRENGTH = true;
private static final long DEFAULT_CONN_UPDATE_ALL_JOBS_MIN_INTERVAL_MS = MINUTE_IN_MILLIS;
private static final float DEFAULT_CONN_LOW_SIGNAL_STRENGTH_RELAX_FRAC = 0.5f;
+ private static final SparseIntArray DEFAULT_CONN_TRANSPORT_BATCH_THRESHOLD =
+ new SparseIntArray();
+ private static final long DEFAULT_CONN_MAX_CONNECTIVITY_JOB_BATCH_DELAY_MS =
+ 31 * MINUTE_IN_MILLIS;
+ static {
+ DEFAULT_CONN_TRANSPORT_BATCH_THRESHOLD.put(
+ NetworkCapabilities.TRANSPORT_CELLULAR,
+ Math.min(3, JobConcurrencyManager.DEFAULT_CONCURRENCY_LIMIT / 3));
+ }
private static final long DEFAULT_PREFETCH_FORCE_BATCH_RELAX_THRESHOLD_MS = HOUR_IN_MILLIS;
private static final boolean DEFAULT_ENABLE_API_QUOTAS = true;
private static final int DEFAULT_API_QUOTA_SCHEDULE_COUNT = 250;
@@ -762,11 +792,23 @@
static final int DEFAULT_MAX_NUM_PERSISTED_JOB_WORK_ITEMS = 100_000;
/**
- * Minimum # of non-ACTIVE jobs for which the JMS will be happy running some work early.
+ * Minimum # of jobs that have to be ready for JS to be happy running work.
+ * Only valid if {@link Flags#batchActiveBucketJobs()} is true.
+ */
+ int MIN_READY_CPU_ONLY_JOBS_COUNT = DEFAULT_MIN_READY_CPU_ONLY_JOBS_COUNT;
+
+ /**
+ * Minimum # of non-ACTIVE jobs that have to be ready for JS to be happy running work.
*/
int MIN_READY_NON_ACTIVE_JOBS_COUNT = DEFAULT_MIN_READY_NON_ACTIVE_JOBS_COUNT;
/**
+ * Don't batch a CPU-only job if it's been delayed due to force batching attempts for
+ * at least this amount of time.
+ */
+ long MAX_CPU_ONLY_JOB_BATCH_DELAY_MS = DEFAULT_MAX_CPU_ONLY_JOB_BATCH_DELAY_MS;
+
+ /**
* Don't batch a non-ACTIVE job if it's been delayed due to force batching attempts for
* at least this amount of time.
*/
@@ -822,6 +864,17 @@
*/
public float CONN_LOW_SIGNAL_STRENGTH_RELAX_FRAC =
DEFAULT_CONN_LOW_SIGNAL_STRENGTH_RELAX_FRAC;
+ /**
+ * The minimum batch requirement per each transport type before allowing a network to run
+ * on a network with that transport.
+ */
+ public SparseIntArray CONN_TRANSPORT_BATCH_THRESHOLD = new SparseIntArray();
+ /**
+ * Don't batch a connectivity job if it's been delayed due to force batching attempts for
+ * at least this amount of time.
+ */
+ public long CONN_MAX_CONNECTIVITY_JOB_BATCH_DELAY_MS =
+ DEFAULT_CONN_MAX_CONNECTIVITY_JOB_BATCH_DELAY_MS;
/**
* The amount of time within which we would consider the app to be launching relatively soon
@@ -972,11 +1025,31 @@
public boolean USE_TARE_POLICY = EconomyManager.DEFAULT_ENABLE_POLICY_JOB_SCHEDULER
&& EconomyManager.DEFAULT_ENABLE_TARE_MODE == EconomyManager.ENABLED_MODE_ON;
+ public Constants() {
+ copyTransportBatchThresholdDefaults();
+ }
+
private void updateBatchingConstantsLocked() {
- MIN_READY_NON_ACTIVE_JOBS_COUNT = DeviceConfig.getInt(
+ // The threshold should be in the range
+ // [0, DEFAULT_CONCURRENCY_LIMIT / 3].
+ MIN_READY_CPU_ONLY_JOBS_COUNT =
+ Math.max(0, Math.min(JobConcurrencyManager.DEFAULT_CONCURRENCY_LIMIT / 3,
+ DeviceConfig.getInt(
+ DeviceConfig.NAMESPACE_JOB_SCHEDULER,
+ KEY_MIN_READY_CPU_ONLY_JOBS_COUNT,
+ DEFAULT_MIN_READY_CPU_ONLY_JOBS_COUNT)));
+ // The threshold should be in the range
+ // [0, DEFAULT_CONCURRENCY_LIMIT / 3].
+ MIN_READY_NON_ACTIVE_JOBS_COUNT =
+ Math.max(0, Math.min(JobConcurrencyManager.DEFAULT_CONCURRENCY_LIMIT / 3,
+ DeviceConfig.getInt(
+ DeviceConfig.NAMESPACE_JOB_SCHEDULER,
+ KEY_MIN_READY_NON_ACTIVE_JOBS_COUNT,
+ DEFAULT_MIN_READY_NON_ACTIVE_JOBS_COUNT)));
+ MAX_CPU_ONLY_JOB_BATCH_DELAY_MS = DeviceConfig.getLong(
DeviceConfig.NAMESPACE_JOB_SCHEDULER,
- KEY_MIN_READY_NON_ACTIVE_JOBS_COUNT,
- DEFAULT_MIN_READY_NON_ACTIVE_JOBS_COUNT);
+ KEY_MAX_CPU_ONLY_JOB_BATCH_DELAY_MS,
+ DEFAULT_MAX_CPU_ONLY_JOB_BATCH_DELAY_MS);
MAX_NON_ACTIVE_JOB_BATCH_DELAY_MS = DeviceConfig.getLong(
DeviceConfig.NAMESPACE_JOB_SCHEDULER,
KEY_MAX_NON_ACTIVE_JOB_BATCH_DELAY_MS,
@@ -1024,6 +1097,46 @@
DeviceConfig.NAMESPACE_JOB_SCHEDULER,
KEY_CONN_LOW_SIGNAL_STRENGTH_RELAX_FRAC,
DEFAULT_CONN_LOW_SIGNAL_STRENGTH_RELAX_FRAC);
+ final String batchThresholdConfigString = DeviceConfig.getString(
+ DeviceConfig.NAMESPACE_JOB_SCHEDULER,
+ KEY_CONN_TRANSPORT_BATCH_THRESHOLD,
+ null);
+ final KeyValueListParser parser = new KeyValueListParser(',');
+ CONN_TRANSPORT_BATCH_THRESHOLD.clear();
+ try {
+ parser.setString(batchThresholdConfigString);
+
+ for (int t = parser.size() - 1; t >= 0; --t) {
+ final String transportString = parser.keyAt(t);
+ try {
+ final int transport = Integer.parseInt(transportString);
+ // The threshold should be in the range
+ // [0, DEFAULT_CONCURRENCY_LIMIT / 3].
+ CONN_TRANSPORT_BATCH_THRESHOLD.put(transport, Math.max(0,
+ Math.min(JobConcurrencyManager.DEFAULT_CONCURRENCY_LIMIT / 3,
+ parser.getInt(transportString, 1))));
+ } catch (NumberFormatException e) {
+ Slog.e(TAG, "Bad transport string", e);
+ }
+ }
+ } catch (IllegalArgumentException e) {
+ Slog.wtf(TAG, "Bad string for " + KEY_CONN_TRANSPORT_BATCH_THRESHOLD, e);
+ // Use the defaults.
+ copyTransportBatchThresholdDefaults();
+ }
+ CONN_MAX_CONNECTIVITY_JOB_BATCH_DELAY_MS = Math.max(0, Math.min(24 * HOUR_IN_MILLIS,
+ DeviceConfig.getLong(
+ DeviceConfig.NAMESPACE_JOB_SCHEDULER,
+ KEY_CONN_MAX_CONNECTIVITY_JOB_BATCH_DELAY_MS,
+ DEFAULT_CONN_MAX_CONNECTIVITY_JOB_BATCH_DELAY_MS)));
+ }
+
+ private void copyTransportBatchThresholdDefaults() {
+ for (int i = DEFAULT_CONN_TRANSPORT_BATCH_THRESHOLD.size() - 1; i >= 0; --i) {
+ CONN_TRANSPORT_BATCH_THRESHOLD.put(
+ DEFAULT_CONN_TRANSPORT_BATCH_THRESHOLD.keyAt(i),
+ DEFAULT_CONN_TRANSPORT_BATCH_THRESHOLD.valueAt(i));
+ }
}
private void updatePersistingConstantsLocked() {
@@ -1168,8 +1281,11 @@
void dump(IndentingPrintWriter pw) {
pw.println("Settings:");
pw.increaseIndent();
+ pw.print(KEY_MIN_READY_CPU_ONLY_JOBS_COUNT, MIN_READY_CPU_ONLY_JOBS_COUNT).println();
pw.print(KEY_MIN_READY_NON_ACTIVE_JOBS_COUNT,
MIN_READY_NON_ACTIVE_JOBS_COUNT).println();
+ pw.print(KEY_MAX_CPU_ONLY_JOB_BATCH_DELAY_MS,
+ MAX_CPU_ONLY_JOB_BATCH_DELAY_MS).println();
pw.print(KEY_MAX_NON_ACTIVE_JOB_BATCH_DELAY_MS,
MAX_NON_ACTIVE_JOB_BATCH_DELAY_MS).println();
pw.print(KEY_HEAVY_USE_FACTOR, HEAVY_USE_FACTOR).println();
@@ -1185,6 +1301,10 @@
.println();
pw.print(KEY_CONN_LOW_SIGNAL_STRENGTH_RELAX_FRAC, CONN_LOW_SIGNAL_STRENGTH_RELAX_FRAC)
.println();
+ pw.print(KEY_CONN_TRANSPORT_BATCH_THRESHOLD, CONN_TRANSPORT_BATCH_THRESHOLD.toString())
+ .println();
+ pw.print(KEY_CONN_MAX_CONNECTIVITY_JOB_BATCH_DELAY_MS,
+ CONN_MAX_CONNECTIVITY_JOB_BATCH_DELAY_MS).println();
pw.print(KEY_PREFETCH_FORCE_BATCH_RELAX_THRESHOLD_MS,
PREFETCH_FORCE_BATCH_RELAX_THRESHOLD_MS).println();
@@ -2835,9 +2955,9 @@
mJobPackageTracker.notePending(job);
}
- void noteJobsPending(List<JobStatus> jobs) {
- for (int i = jobs.size() - 1; i >= 0; i--) {
- noteJobPending(jobs.get(i));
+ void noteJobsPending(ArraySet<JobStatus> jobs) {
+ for (int i = jobs.size() - 1; i >= 0; --i) {
+ noteJobPending(jobs.valueAt(i));
}
}
@@ -3463,7 +3583,7 @@
}
final class ReadyJobQueueFunctor implements Consumer<JobStatus> {
- final ArrayList<JobStatus> newReadyJobs = new ArrayList<>();
+ final ArraySet<JobStatus> newReadyJobs = new ArraySet<>();
@Override
public void accept(JobStatus job) {
@@ -3491,9 +3611,27 @@
* policies on when we want to execute jobs.
*/
final class MaybeReadyJobQueueFunctor implements Consumer<JobStatus> {
- int forceBatchedCount;
- int unbatchedCount;
+ /**
+ * Set of jobs that will be force batched, mapped by network. A {@code null} network is
+ * reserved/intended for CPU-only (non-networked) jobs.
+ * The set may include already running jobs.
+ */
+ @VisibleForTesting
+ final ArrayMap<Network, ArraySet<JobStatus>> mBatches = new ArrayMap<>();
+ /** List of all jobs that could run if allowed. Already running jobs are excluded. */
+ @VisibleForTesting
final List<JobStatus> runnableJobs = new ArrayList<>();
+ /**
+ * Convenience holder of all jobs ready to run that won't be force batched.
+ * Already running jobs are excluded.
+ */
+ final ArraySet<JobStatus> mUnbatchedJobs = new ArraySet<>();
+ /**
+ * Count of jobs that won't be force batched, mapped by network. A {@code null} network is
+ * reserved/intended for CPU-only (non-networked) jobs.
+ * The set may include already running jobs.
+ */
+ final ArrayMap<Network, Integer> mUnbatchedJobCount = new ArrayMap<>();
public MaybeReadyJobQueueFunctor() {
reset();
@@ -3540,27 +3678,77 @@
shouldForceBatchJob = false;
} else {
final long nowElapsed = sElapsedRealtimeClock.millis();
- final boolean batchDelayExpired = job.getFirstForceBatchedTimeElapsed() > 0
- && nowElapsed - job.getFirstForceBatchedTimeElapsed()
- >= mConstants.MAX_NON_ACTIVE_JOB_BATCH_DELAY_MS;
- shouldForceBatchJob =
- mConstants.MIN_READY_NON_ACTIVE_JOBS_COUNT > 1
- && job.getEffectiveStandbyBucket() != ACTIVE_INDEX
- && job.getEffectiveStandbyBucket() != EXEMPTED_INDEX
- && !batchDelayExpired;
+ final long timeUntilDeadlineMs = job.hasDeadlineConstraint()
+ ? job.getLatestRunTimeElapsed() - nowElapsed
+ : Long.MAX_VALUE;
+ // Differentiate behavior based on whether the job needs network or not.
+ if (Flags.batchConnectivityJobsPerNetwork()
+ && job.hasConnectivityConstraint()) {
+ // For connectivity jobs, let them run immediately if the network is already
+ // active (in a state for job run), otherwise, only run them if there are
+ // enough to meet the batching requirement or the job has been waiting
+ // long enough.
+ final boolean batchDelayExpired =
+ job.getFirstForceBatchedTimeElapsed() > 0
+ && nowElapsed - job.getFirstForceBatchedTimeElapsed()
+ >= mConstants.CONN_MAX_CONNECTIVITY_JOB_BATCH_DELAY_MS;
+ shouldForceBatchJob = !batchDelayExpired
+ && job.getEffectiveStandbyBucket() != EXEMPTED_INDEX
+ && timeUntilDeadlineMs
+ > mConstants.CONN_MAX_CONNECTIVITY_JOB_BATCH_DELAY_MS / 2
+ && !mConnectivityController.isNetworkInStateForJobRunLocked(job);
+ } else {
+ final boolean batchDelayExpired;
+ final boolean batchingEnabled;
+ if (Flags.batchActiveBucketJobs()) {
+ batchingEnabled = mConstants.MIN_READY_CPU_ONLY_JOBS_COUNT > 1
+ && timeUntilDeadlineMs
+ > mConstants.MAX_CPU_ONLY_JOB_BATCH_DELAY_MS / 2
+ // Active UIDs' jobs were by default treated as in the ACTIVE
+ // bucket, so we must explicitly exclude them when batching
+ // ACTIVE jobs.
+ && !job.uidActive
+ && !job.getJob().isExemptedFromAppStandby();
+ batchDelayExpired = job.getFirstForceBatchedTimeElapsed() > 0
+ && nowElapsed - job.getFirstForceBatchedTimeElapsed()
+ >= mConstants.MAX_CPU_ONLY_JOB_BATCH_DELAY_MS;
+ } else {
+ batchingEnabled = mConstants.MIN_READY_NON_ACTIVE_JOBS_COUNT > 1
+ && job.getEffectiveStandbyBucket() != ACTIVE_INDEX;
+ batchDelayExpired = job.getFirstForceBatchedTimeElapsed() > 0
+ && nowElapsed - job.getFirstForceBatchedTimeElapsed()
+ >= mConstants.MAX_NON_ACTIVE_JOB_BATCH_DELAY_MS;
+ }
+ shouldForceBatchJob = batchingEnabled
+ && job.getEffectiveStandbyBucket() != EXEMPTED_INDEX
+ && !batchDelayExpired;
+ }
}
+ // If connectivity job batching isn't enabled, treat every job as
+ // a non-connectivity job since that mimics the old behavior.
+ final Network network =
+ Flags.batchConnectivityJobsPerNetwork() ? job.network : null;
+ ArraySet<JobStatus> batch = mBatches.get(network);
+ if (batch == null) {
+ batch = new ArraySet<>();
+ mBatches.put(network, batch);
+ }
+ batch.add(job);
+
if (shouldForceBatchJob) {
- // Force batching non-ACTIVE jobs. Don't include them in the other counts.
- forceBatchedCount++;
if (job.getFirstForceBatchedTimeElapsed() == 0) {
job.setFirstForceBatchedTimeElapsed(sElapsedRealtimeClock.millis());
}
} else {
- unbatchedCount++;
+ mUnbatchedJobCount.put(network,
+ mUnbatchedJobCount.getOrDefault(job.network, 0) + 1);
}
if (!isRunning) {
runnableJobs.add(job);
+ if (!shouldForceBatchJob) {
+ mUnbatchedJobs.add(job);
+ }
}
} else {
if (isRunning) {
@@ -3600,34 +3788,131 @@
@GuardedBy("mLock")
@VisibleForTesting
void postProcessLocked() {
- if (unbatchedCount > 0
- || forceBatchedCount >= mConstants.MIN_READY_NON_ACTIVE_JOBS_COUNT) {
- if (DEBUG) {
- Slog.d(TAG, "maybeQueueReadyJobsForExecutionLocked: Running jobs.");
+ final ArraySet<JobStatus> jobsToRun = mUnbatchedJobs;
+
+ if (DEBUG) {
+ Slog.d(TAG, "maybeQueueReadyJobsForExecutionLocked: "
+ + mUnbatchedJobs.size() + " unbatched jobs.");
+ }
+
+ int unbatchedCount = 0;
+
+ for (int n = mBatches.size() - 1; n >= 0; --n) {
+ final Network network = mBatches.keyAt(n);
+
+ // Count all of the unbatched jobs, including the ones without a network.
+ final Integer unbatchedJobCountObj = mUnbatchedJobCount.get(network);
+ final int unbatchedJobCount;
+ if (unbatchedJobCountObj != null) {
+ unbatchedJobCount = unbatchedJobCountObj;
+ unbatchedCount += unbatchedJobCount;
+ } else {
+ unbatchedJobCount = 0;
}
- noteJobsPending(runnableJobs);
- mPendingJobQueue.addAll(runnableJobs);
+
+ // Skip the non-networked jobs here. They'll be handled after evaluating
+ // everything else.
+ if (network == null) {
+ continue;
+ }
+
+ final ArraySet<JobStatus> batchedJobs = mBatches.valueAt(n);
+ if (unbatchedJobCount > 0) {
+ // Some job is going to activate the network anyway. Might as well run all
+ // the other jobs that will use this network.
+ if (DEBUG) {
+ Slog.d(TAG, "maybeQueueReadyJobsForExecutionLocked: piggybacking "
+ + batchedJobs.size() + " jobs on " + network
+ + " because of unbatched job");
+ }
+ jobsToRun.addAll(batchedJobs);
+ continue;
+ }
+
+ final NetworkCapabilities networkCapabilities =
+ mConnectivityController.getNetworkCapabilities(network);
+ if (networkCapabilities == null) {
+ Slog.e(TAG, "Couldn't get NetworkCapabilities for network " + network);
+ continue;
+ }
+
+ final int[] transports = networkCapabilities.getTransportTypes();
+ int maxNetworkBatchReq = 1;
+ for (int transport : transports) {
+ maxNetworkBatchReq = Math.max(maxNetworkBatchReq,
+ mConstants.CONN_TRANSPORT_BATCH_THRESHOLD.get(transport));
+ }
+
+ if (batchedJobs.size() >= maxNetworkBatchReq) {
+ if (DEBUG) {
+ Slog.d(TAG, "maybeQueueReadyJobsForExecutionLocked: "
+ + batchedJobs.size()
+ + " batched network jobs meet requirement for " + network);
+ }
+ jobsToRun.addAll(batchedJobs);
+ }
+ }
+
+ final ArraySet<JobStatus> batchedNonNetworkedJobs = mBatches.get(null);
+ if (batchedNonNetworkedJobs != null) {
+ final int minReadyCount = Flags.batchActiveBucketJobs()
+ ? mConstants.MIN_READY_CPU_ONLY_JOBS_COUNT
+ : mConstants.MIN_READY_NON_ACTIVE_JOBS_COUNT;
+ if (jobsToRun.size() > 0) {
+ // Some job is going to use the CPU anyway. Might as well run all the other
+ // CPU-only jobs.
+ if (DEBUG) {
+ Slog.d(TAG, "maybeQueueReadyJobsForExecutionLocked: piggybacking "
+ + batchedNonNetworkedJobs.size() + " non-network jobs");
+ }
+ jobsToRun.addAll(batchedNonNetworkedJobs);
+ } else if (batchedNonNetworkedJobs.size() >= minReadyCount) {
+ if (DEBUG) {
+ Slog.d(TAG, "maybeQueueReadyJobsForExecutionLocked: adding "
+ + batchedNonNetworkedJobs.size() + " batched non-network jobs.");
+ }
+ jobsToRun.addAll(batchedNonNetworkedJobs);
+ }
+ }
+
+ // In order to properly determine an accurate batch count, the running jobs must be
+ // included in the earlier lists and can only be removed after checking if the batch
+ // count requirement is satisfied.
+ jobsToRun.removeIf(JobSchedulerService.this::isCurrentlyRunningLocked);
+
+ if (unbatchedCount > 0 || jobsToRun.size() > 0) {
+ if (DEBUG) {
+ Slog.d(TAG, "maybeQueueReadyJobsForExecutionLocked: Running "
+ + jobsToRun + " jobs.");
+ }
+ noteJobsPending(jobsToRun);
+ mPendingJobQueue.addAll(jobsToRun);
} else {
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(), job.getNamespace());
- if (reasons == null) {
- reasons = new SparseIntArray();
- mPendingJobReasonCache
- .add(job.getUid(), job.getNamespace(), 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);
+ }
+
+ // Update the pending reason for any jobs that aren't going to be run.
+ final int numRunnableJobs = runnableJobs.size();
+ if (numRunnableJobs > 0 && numRunnableJobs != jobsToRun.size()) {
+ synchronized (mPendingJobReasonCache) {
+ for (int i = 0; i < numRunnableJobs; ++i) {
+ final JobStatus job = runnableJobs.get(i);
+ if (jobsToRun.contains(job)) {
+ // We're running this job. Skip updating the pending reason.
+ continue;
}
+ SparseIntArray reasons =
+ mPendingJobReasonCache.get(job.getUid(), job.getNamespace());
+ if (reasons == null) {
+ reasons = new SparseIntArray();
+ mPendingJobReasonCache.add(job.getUid(), job.getNamespace(), 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);
}
}
}
@@ -3638,9 +3923,10 @@
@VisibleForTesting
void reset() {
- forceBatchedCount = 0;
- unbatchedCount = 0;
runnableJobs.clear();
+ mBatches.clear();
+ mUnbatchedJobs.clear();
+ mUnbatchedJobCount.clear();
}
}
@@ -3976,20 +4262,34 @@
.sendToTarget();
}
- private final class BatteryStateTracker extends BroadcastReceiver {
- /**
- * Track whether we're "charging", where charging means that we're ready to commit to
- * doing work.
- */
- private boolean mCharging;
+ @VisibleForTesting
+ final class BatteryStateTracker extends BroadcastReceiver
+ implements BatteryManagerInternal.ChargingPolicyChangeListener {
+ private final BatteryManagerInternal mBatteryManagerInternal;
+
+ /** Last reported battery level. */
+ private int mBatteryLevel;
/** Keep track of whether the battery is charged enough that we want to do work. */
private boolean mBatteryNotLow;
+ /**
+ * Charging status based on {@link BatteryManager#ACTION_CHARGING} and
+ * {@link BatteryManager#ACTION_DISCHARGING}.
+ */
+ private boolean mCharging;
+ /**
+ * The most recently acquired value of
+ * {@link BatteryManager#BATTERY_PROPERTY_CHARGING_POLICY}.
+ */
+ private int mChargingPolicy;
+ /** Track whether there is power connected. It doesn't mean the device is charging. */
+ private boolean mPowerConnected;
/** Sequence number of last broadcast. */
private int mLastBatterySeq = -1;
private BroadcastReceiver mMonitor;
BatteryStateTracker() {
+ mBatteryManagerInternal = LocalServices.getService(BatteryManagerInternal.class);
}
public void startTracking() {
@@ -4001,13 +4301,18 @@
// Charging/not charging.
filter.addAction(BatteryManager.ACTION_CHARGING);
filter.addAction(BatteryManager.ACTION_DISCHARGING);
+ filter.addAction(Intent.ACTION_BATTERY_LEVEL_CHANGED);
+ filter.addAction(Intent.ACTION_POWER_CONNECTED);
+ filter.addAction(Intent.ACTION_POWER_DISCONNECTED);
getTestableContext().registerReceiver(this, filter);
+ mBatteryManagerInternal.registerChargingPolicyChangeListener(this);
+
// Initialise tracker state.
- BatteryManagerInternal batteryManagerInternal =
- LocalServices.getService(BatteryManagerInternal.class);
- mBatteryNotLow = !batteryManagerInternal.getBatteryLevelLow();
- mCharging = batteryManagerInternal.isPowered(BatteryManager.BATTERY_PLUGGED_ANY);
+ mBatteryLevel = mBatteryManagerInternal.getBatteryLevel();
+ mBatteryNotLow = !mBatteryManagerInternal.getBatteryLevelLow();
+ mCharging = mBatteryManagerInternal.isPowered(BatteryManager.BATTERY_PLUGGED_ANY);
+ mChargingPolicy = mBatteryManagerInternal.getChargingPolicy();
}
public void setMonitorBatteryLocked(boolean enabled) {
@@ -4030,7 +4335,7 @@
}
public boolean isCharging() {
- return mCharging;
+ return isConsideredCharging();
}
public boolean isBatteryNotLow() {
@@ -4041,17 +4346,42 @@
return mMonitor != null;
}
+ public boolean isPowerConnected() {
+ return mPowerConnected;
+ }
+
public int getSeq() {
return mLastBatterySeq;
}
@Override
+ public void onChargingPolicyChanged(int newPolicy) {
+ synchronized (mLock) {
+ if (mChargingPolicy == newPolicy) {
+ return;
+ }
+ if (DEBUG) {
+ Slog.i(TAG,
+ "Charging policy changed from " + mChargingPolicy + " to " + newPolicy);
+ }
+
+ final boolean wasConsideredCharging = isConsideredCharging();
+ mChargingPolicy = newPolicy;
+
+ if (isConsideredCharging() != wasConsideredCharging) {
+ for (int c = mControllers.size() - 1; c >= 0; --c) {
+ mControllers.get(c).onBatteryStateChangedLocked();
+ }
+ }
+ }
+ }
+
+ @Override
public void onReceive(Context context, Intent intent) {
onReceiveInternal(intent);
}
- @VisibleForTesting
- public void onReceiveInternal(Intent intent) {
+ private void onReceiveInternal(Intent intent) {
synchronized (mLock) {
final String action = intent.getAction();
boolean changed = false;
@@ -4071,21 +4401,49 @@
mBatteryNotLow = true;
changed = true;
}
+ } else if (Intent.ACTION_BATTERY_LEVEL_CHANGED.equals(action)) {
+ if (DEBUG) {
+ Slog.d(TAG, "Battery level changed @ "
+ + sElapsedRealtimeClock.millis());
+ }
+ final boolean wasConsideredCharging = isConsideredCharging();
+ mBatteryLevel = mBatteryManagerInternal.getBatteryLevel();
+ changed = isConsideredCharging() != wasConsideredCharging;
+ } else if (Intent.ACTION_POWER_CONNECTED.equals(action)) {
+ if (DEBUG) {
+ Slog.d(TAG, "Power connected @ " + sElapsedRealtimeClock.millis());
+ }
+ if (mPowerConnected) {
+ return;
+ }
+ mPowerConnected = true;
+ changed = true;
+ } else if (Intent.ACTION_POWER_DISCONNECTED.equals(action)) {
+ if (DEBUG) {
+ Slog.d(TAG, "Power disconnected @ " + sElapsedRealtimeClock.millis());
+ }
+ if (!mPowerConnected) {
+ return;
+ }
+ mPowerConnected = false;
+ changed = true;
} else if (BatteryManager.ACTION_CHARGING.equals(action)) {
if (DEBUG) {
Slog.d(TAG, "Battery charging @ " + sElapsedRealtimeClock.millis());
}
if (!mCharging) {
+ final boolean wasConsideredCharging = isConsideredCharging();
mCharging = true;
- changed = true;
+ changed = isConsideredCharging() != wasConsideredCharging;
}
} else if (BatteryManager.ACTION_DISCHARGING.equals(action)) {
if (DEBUG) {
Slog.d(TAG, "Battery discharging @ " + sElapsedRealtimeClock.millis());
}
if (mCharging) {
+ final boolean wasConsideredCharging = isConsideredCharging();
mCharging = false;
- changed = true;
+ changed = isConsideredCharging() != wasConsideredCharging;
}
}
mLastBatterySeq =
@@ -4097,6 +4455,30 @@
}
}
}
+
+ private boolean isConsideredCharging() {
+ if (mCharging) {
+ return true;
+ }
+ // BatteryService (or Health HAL or whatever central location makes sense)
+ // should ideally hold this logic so that everyone has a consistent
+ // idea of when the device is charging (or an otherwise stable charging/plugged state).
+ // TODO(304512874): move this determination to BatteryService
+ if (!mPowerConnected) {
+ return false;
+ }
+
+ if (mChargingPolicy == Integer.MIN_VALUE) {
+ // Property not supported on this device.
+ return false;
+ }
+ // Adaptive charging policies don't expose their target battery level, but 80% is a
+ // commonly used threshold for battery health, so assume that's what's being used by
+ // the policies and use 70%+ as the threshold here for charging in case some
+ // implementations choose to discharge the device slightly before recharging back up
+ // to the target level.
+ return mBatteryLevel >= 70 && BatteryManager.isAdaptiveChargingPolicy(mChargingPolicy);
+ }
}
final class LocalService implements JobSchedulerInternal {
@@ -5165,6 +5547,13 @@
}
}
+ /** Return {@code true} if the device is connected to power. */
+ public boolean isPowerConnected() {
+ synchronized (mLock) {
+ return mBatteryStateTracker.isPowerConnected();
+ }
+ }
+
int getStorageSeq() {
synchronized (mLock) {
return mStorageController.getTracker().getSeq();
@@ -5468,8 +5857,14 @@
pw.println("Aconfig flags:");
pw.increaseIndent();
+ pw.print(Flags.FLAG_BATCH_ACTIVE_BUCKET_JOBS, Flags.batchActiveBucketJobs());
+ pw.println();
+ pw.print(Flags.FLAG_BATCH_CONNECTIVITY_JOBS_PER_NETWORK,
+ Flags.batchConnectivityJobsPerNetwork());
+ pw.println();
pw.print(Flags.FLAG_DO_NOT_FORCE_RUSH_EXECUTION_AT_BOOT,
Flags.doNotForceRushExecutionAtBoot());
+ pw.println();
pw.print(Flags.FLAG_THROW_ON_UNSUPPORTED_BIAS_USAGE,
Flags.throwOnUnsupportedBiasUsage());
pw.println();
@@ -5487,8 +5882,14 @@
mQuotaTracker.dump(pw);
pw.println();
+ pw.print("Power connected: ");
+ pw.println(mBatteryStateTracker.isPowerConnected());
pw.print("Battery charging: ");
- pw.println(mBatteryStateTracker.isCharging());
+ pw.println(mBatteryStateTracker.mCharging);
+ pw.print("Considered charging: ");
+ pw.println(mBatteryStateTracker.isConsideredCharging());
+ pw.print("Battery level: ");
+ pw.println(mBatteryStateTracker.mBatteryLevel);
pw.print("Battery not low: ");
pw.println(mBatteryStateTracker.isBatteryNotLow());
if (mBatteryStateTracker.isMonitoring()) {
diff --git a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerShellCommand.java b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerShellCommand.java
index 0cf6a7a..90b4630 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerShellCommand.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerShellCommand.java
@@ -350,6 +350,12 @@
case android.app.job.Flags.FLAG_JOB_DEBUG_INFO_APIS:
pw.println(android.app.job.Flags.jobDebugInfoApis());
break;
+ case com.android.server.job.Flags.FLAG_BATCH_ACTIVE_BUCKET_JOBS:
+ pw.println(com.android.server.job.Flags.batchActiveBucketJobs());
+ break;
+ case com.android.server.job.Flags.FLAG_BATCH_CONNECTIVITY_JOBS_PER_NETWORK:
+ pw.println(com.android.server.job.Flags.batchConnectivityJobsPerNetwork());
+ break;
case com.android.server.job.Flags.FLAG_DO_NOT_FORCE_RUSH_EXECUTION_AT_BOOT:
pw.println(com.android.server.job.Flags.doNotForceRushExecutionAtBoot());
break;
diff --git a/apex/jobscheduler/service/java/com/android/server/job/PendingJobQueue.java b/apex/jobscheduler/service/java/com/android/server/job/PendingJobQueue.java
index 4f4096f..813cf87 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/PendingJobQueue.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/PendingJobQueue.java
@@ -18,6 +18,7 @@
import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.util.ArraySet;
import android.util.Pools;
import android.util.SparseArray;
@@ -96,10 +97,10 @@
}
}
- void addAll(@NonNull List<JobStatus> jobs) {
+ void addAll(@NonNull ArraySet<JobStatus> jobs) {
final SparseArray<List<JobStatus>> jobsByUid = new SparseArray<>();
for (int i = jobs.size() - 1; i >= 0; --i) {
- final JobStatus job = jobs.get(i);
+ final JobStatus job = jobs.valueAt(i);
List<JobStatus> appJobs = jobsByUid.get(job.getSourceUid());
if (appJobs == null) {
appJobs = new ArrayList<>();
diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/BatteryController.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/BatteryController.java
index ddbc2ec..e9f9b14 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/controllers/BatteryController.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/BatteryController.java
@@ -20,12 +20,6 @@
import android.annotation.NonNull;
import android.app.job.JobInfo;
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.os.BatteryManager;
-import android.os.BatteryManagerInternal;
import android.os.UserHandle;
import android.util.ArraySet;
import android.util.IndentingPrintWriter;
@@ -36,7 +30,6 @@
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.server.AppSchedulingModuleThread;
-import com.android.server.LocalServices;
import com.android.server.job.JobSchedulerService;
import com.android.server.job.StateControllerProto;
@@ -60,8 +53,6 @@
@GuardedBy("mLock")
private final ArraySet<JobStatus> mTopStartedJobs = new ArraySet<>();
- private final PowerTracker mPowerTracker;
-
private final FlexibilityController mFlexibilityController;
/**
* Helper set to avoid too much GC churn from frequent calls to
@@ -77,16 +68,10 @@
public BatteryController(JobSchedulerService service,
FlexibilityController flexibilityController) {
super(service);
- mPowerTracker = new PowerTracker();
mFlexibilityController = flexibilityController;
}
@Override
- public void startTrackingLocked() {
- mPowerTracker.startTracking();
- }
-
- @Override
public void maybeStartTrackingJobLocked(JobStatus taskStatus, JobStatus lastJob) {
if (taskStatus.hasPowerConstraint()) {
final long nowElapsed = sElapsedRealtimeClock.millis();
@@ -95,7 +80,7 @@
if (taskStatus.hasChargingConstraint()) {
if (hasTopExemptionLocked(taskStatus)) {
taskStatus.setChargingConstraintSatisfied(nowElapsed,
- mPowerTracker.isPowerConnected());
+ mService.isPowerConnected());
} else {
taskStatus.setChargingConstraintSatisfied(nowElapsed,
mService.isBatteryCharging() && mService.isBatteryNotLow());
@@ -178,7 +163,7 @@
@GuardedBy("mLock")
private void maybeReportNewChargingStateLocked() {
- final boolean powerConnected = mPowerTracker.isPowerConnected();
+ final boolean powerConnected = mService.isPowerConnected();
final boolean stablePower = mService.isBatteryCharging() && mService.isBatteryNotLow();
final boolean batteryNotLow = mService.isBatteryNotLow();
if (DEBUG) {
@@ -239,62 +224,6 @@
mChangedJobs.clear();
}
- private final class PowerTracker extends BroadcastReceiver {
- /**
- * Track whether there is power connected. It doesn't mean the device is charging.
- * Use {@link JobSchedulerService#isBatteryCharging()} to determine if the device is
- * charging.
- */
- private boolean mPowerConnected;
-
- PowerTracker() {
- }
-
- void startTracking() {
- IntentFilter filter = new IntentFilter();
-
- filter.addAction(Intent.ACTION_POWER_CONNECTED);
- filter.addAction(Intent.ACTION_POWER_DISCONNECTED);
- mContext.registerReceiver(this, filter);
-
- // Initialize tracker state.
- BatteryManagerInternal batteryManagerInternal =
- LocalServices.getService(BatteryManagerInternal.class);
- mPowerConnected = batteryManagerInternal.isPowered(BatteryManager.BATTERY_PLUGGED_ANY);
- }
-
- boolean isPowerConnected() {
- return mPowerConnected;
- }
-
- @Override
- public void onReceive(Context context, Intent intent) {
- synchronized (mLock) {
- final String action = intent.getAction();
-
- if (Intent.ACTION_POWER_CONNECTED.equals(action)) {
- if (DEBUG) {
- Slog.d(TAG, "Power connected @ " + sElapsedRealtimeClock.millis());
- }
- if (mPowerConnected) {
- return;
- }
- mPowerConnected = true;
- } else if (Intent.ACTION_POWER_DISCONNECTED.equals(action)) {
- if (DEBUG) {
- Slog.d(TAG, "Power disconnected @ " + sElapsedRealtimeClock.millis());
- }
- if (!mPowerConnected) {
- return;
- }
- mPowerConnected = false;
- }
-
- maybeReportNewChargingStateLocked();
- }
- }
- }
-
@VisibleForTesting
ArraySet<JobStatus> getTrackedJobs() {
return mTrackedTasks;
@@ -308,7 +237,6 @@
@Override
public void dumpControllerStateLocked(IndentingPrintWriter pw,
Predicate<JobStatus> predicate) {
- pw.println("Power connected: " + mPowerTracker.isPowerConnected());
pw.println("Stable power: " + (mService.isBatteryCharging() && mService.isBatteryNotLow()));
pw.println("Not low: " + mService.isBatteryNotLow());
diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/ConnectivityController.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/ConnectivityController.java
index f405083..6ed42ec 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/controllers/ConnectivityController.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/ConnectivityController.java
@@ -22,11 +22,11 @@
import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
import static android.net.NetworkCapabilities.TRANSPORT_ETHERNET;
import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
+import static android.text.format.DateUtils.MINUTE_IN_MILLIS;
import static com.android.server.job.JobSchedulerService.RESTRICTED_INDEX;
import static com.android.server.job.JobSchedulerService.sElapsedRealtimeClock;
import static com.android.server.job.Flags.FLAG_RELAX_PREFETCH_CONNECTIVITY_CONSTRAINT_ONLY_ON_CHARGER;
-import static com.android.server.job.Flags.relaxPrefetchConnectivityConstraintOnlyOnCharger;
import android.annotation.NonNull;
import android.annotation.Nullable;
@@ -66,6 +66,7 @@
import com.android.internal.annotations.VisibleForTesting;
import com.android.server.AppSchedulingModuleThread;
import com.android.server.LocalServices;
+import com.android.server.job.Flags;
import com.android.server.job.JobSchedulerService;
import com.android.server.job.JobSchedulerService.Constants;
import com.android.server.job.StateControllerProto;
@@ -166,6 +167,10 @@
@GuardedBy("mLock")
private final ArrayMap<Network, CachedNetworkMetadata> mAvailableNetworks = new ArrayMap<>();
+ @GuardedBy("mLock")
+ @Nullable
+ private Network mSystemDefaultNetwork;
+
private final SparseArray<UidDefaultNetworkCallback> mCurrentDefaultNetworkCallbacks =
new SparseArray<>();
private final Comparator<UidStats> mUidStatsComparator = new Comparator<UidStats>() {
@@ -286,6 +291,7 @@
private static final int MSG_UPDATE_ALL_TRACKED_JOBS = 1;
private static final int MSG_DATA_SAVER_TOGGLED = 2;
private static final int MSG_UID_POLICIES_CHANGED = 3;
+ private static final int MSG_PROCESS_ACTIVE_NETWORK = 4;
private final Handler mHandler;
@@ -313,6 +319,14 @@
}
}
+ @Override
+ public void startTrackingLocked() {
+ if (Flags.batchConnectivityJobsPerNetwork()) {
+ mConnManager.registerSystemDefaultNetworkCallback(mDefaultNetworkCallback, mHandler);
+ mConnManager.addDefaultNetworkActiveListener(this);
+ }
+ }
+
@GuardedBy("mLock")
@Override
public void maybeStartTrackingJobLocked(JobStatus jobStatus, JobStatus lastJob) {
@@ -911,8 +925,8 @@
return true;
}
if (capabilities.hasTransport(NetworkCapabilities.TRANSPORT_VPN)) {
- // Exclude VPNs because it's currently not possible to determine the VPN's underlying
- // network, and thus the correct signal strength of the VPN's network.
+ // VPNs may have multiple underlying networks and determining the correct strength
+ // may not be straightforward.
// Transmitting data over a VPN is generally more battery-expensive than on the
// underlying network, so:
// TODO: find a good way to reduce job use of VPN when it'll be very expensive
@@ -1007,7 +1021,7 @@
// Need to at least know the estimated download bytes for a prefetch job.
return false;
}
- if (relaxPrefetchConnectivityConstraintOnlyOnCharger()) {
+ if (Flags.relaxPrefetchConnectivityConstraintOnlyOnCharger()) {
// Since the constraint relaxation isn't required by the job, only do it when the
// device is charging and the battery level is above the "low battery" threshold.
if (!mService.isBatteryCharging() || !mService.isBatteryNotLow()) {
@@ -1309,7 +1323,7 @@
}
@Nullable
- private NetworkCapabilities getNetworkCapabilities(@Nullable Network network) {
+ public NetworkCapabilities getNetworkCapabilities(@Nullable Network network) {
final CachedNetworkMetadata metadata = getNetworkMetadata(network);
return metadata == null ? null : metadata.networkCapabilities;
}
@@ -1527,26 +1541,138 @@
}
/**
+ * Returns {@code true} if the job's assigned network is active or otherwise considered to be
+ * in a good state to run the job now.
+ */
+ @GuardedBy("mLock")
+ public boolean isNetworkInStateForJobRunLocked(@NonNull JobStatus jobStatus) {
+ if (jobStatus.network == null) {
+ return false;
+ }
+ if (jobStatus.shouldTreatAsExpeditedJob() || jobStatus.shouldTreatAsUserInitiatedJob()
+ || mService.getUidProcState(jobStatus.getSourceUid())
+ <= ActivityManager.PROCESS_STATE_BOUND_FOREGROUND_SERVICE) {
+ // EJs, UIJs, and BFGS+ jobs should be able to activate the network.
+ return true;
+ }
+ return isNetworkInStateForJobRunLocked(jobStatus.network);
+ }
+
+ @GuardedBy("mLock")
+ @VisibleForTesting
+ boolean isNetworkInStateForJobRunLocked(@NonNull Network network) {
+ if (!Flags.batchConnectivityJobsPerNetwork()) {
+ // Active network batching isn't enabled. We don't care about the network state.
+ return true;
+ }
+
+ CachedNetworkMetadata cachedNetworkMetadata = mAvailableNetworks.get(network);
+ if (cachedNetworkMetadata == null) {
+ return false;
+ }
+
+ final long nowElapsed = sElapsedRealtimeClock.millis();
+ if (cachedNetworkMetadata.defaultNetworkActivationLastConfirmedTimeElapsed
+ + mCcConfig.NETWORK_ACTIVATION_EXPIRATION_MS > nowElapsed) {
+ // Network is still presumed to be active.
+ return true;
+ }
+
+ final boolean inactiveForTooLong =
+ cachedNetworkMetadata.capabilitiesFirstAcquiredTimeElapsed
+ < nowElapsed - mCcConfig.NETWORK_ACTIVATION_MAX_WAIT_TIME_MS
+ && cachedNetworkMetadata.defaultNetworkActivationLastConfirmedTimeElapsed
+ < nowElapsed - mCcConfig.NETWORK_ACTIVATION_MAX_WAIT_TIME_MS;
+ // We can only know the state of the system default network. If that's not available
+ // or the network in question isn't the system default network,
+ // then return true if we haven't gotten an active signal in a long time.
+ if (mSystemDefaultNetwork == null) {
+ return inactiveForTooLong;
+ }
+
+ if (!mSystemDefaultNetwork.equals(network)) {
+ final NetworkCapabilities capabilities = cachedNetworkMetadata.networkCapabilities;
+ if (capabilities != null
+ && capabilities.hasTransport(NetworkCapabilities.TRANSPORT_VPN)) {
+ // VPNs won't have an active signal sent for them. Check their underlying networks
+ // instead, prioritizing the system default if it's one of them.
+ final List<Network> underlyingNetworks = capabilities.getUnderlyingNetworks();
+ if (underlyingNetworks == null) {
+ return inactiveForTooLong;
+ }
+
+ if (underlyingNetworks.contains(mSystemDefaultNetwork)) {
+ if (DEBUG) {
+ Slog.i(TAG, "Substituting system default network "
+ + mSystemDefaultNetwork + " for VPN " + network);
+ }
+ return isNetworkInStateForJobRunLocked(mSystemDefaultNetwork);
+ }
+
+ for (int i = underlyingNetworks.size() - 1; i >= 0; --i) {
+ if (isNetworkInStateForJobRunLocked(underlyingNetworks.get(i))) {
+ return true;
+ }
+ }
+ }
+ return inactiveForTooLong;
+ }
+
+ if (cachedNetworkMetadata.defaultNetworkActivationLastCheckTimeElapsed
+ + mCcConfig.NETWORK_ACTIVATION_EXPIRATION_MS < nowElapsed) {
+ // We haven't checked the state recently enough. Let's check if the network is active.
+ // However, if we checked after the last confirmed active time and it wasn't active,
+ // then the network is still not active (we would be told when it becomes active
+ // via onNetworkActive()).
+ if (cachedNetworkMetadata.defaultNetworkActivationLastCheckTimeElapsed
+ > cachedNetworkMetadata.defaultNetworkActivationLastConfirmedTimeElapsed) {
+ return inactiveForTooLong;
+ }
+ // We need to explicitly check because there's no callback telling us when the network
+ // leaves the high power state.
+ cachedNetworkMetadata.defaultNetworkActivationLastCheckTimeElapsed = nowElapsed;
+ final boolean isActive = mConnManager.isDefaultNetworkActive();
+ if (isActive) {
+ cachedNetworkMetadata.defaultNetworkActivationLastConfirmedTimeElapsed = nowElapsed;
+ return true;
+ }
+ return inactiveForTooLong;
+ }
+
+ // We checked the state recently enough, but the network wasn't active. Assume it still
+ // isn't active.
+ return false;
+ }
+
+ /**
* We know the network has just come up. We want to run any jobs that are ready.
*/
@Override
public void onNetworkActive() {
synchronized (mLock) {
- for (int i = mTrackedJobs.size()-1; i >= 0; i--) {
- final ArraySet<JobStatus> jobs = mTrackedJobs.valueAt(i);
- for (int j = jobs.size() - 1; j >= 0; j--) {
- final JobStatus js = jobs.valueAt(j);
- if (js.isReady()) {
- if (DEBUG) {
- Slog.d(TAG, "Running " + js + " due to network activity.");
- }
- mStateChangedListener.onRunJobNow(js);
- }
- }
+ if (mSystemDefaultNetwork == null) {
+ Slog.wtf(TAG, "System default network is unknown but active");
+ return;
}
+
+ CachedNetworkMetadata cachedNetworkMetadata =
+ mAvailableNetworks.get(mSystemDefaultNetwork);
+ if (cachedNetworkMetadata == null) {
+ Slog.wtf(TAG, "System default network capabilities are unknown but active");
+ return;
+ }
+
+ // This method gets called on the system's main thread (not the
+ // AppSchedulingModuleThread), so shift the processing work to a handler to avoid
+ // blocking important operations on the main thread.
+ cachedNetworkMetadata.defaultNetworkActivationLastConfirmedTimeElapsed =
+ cachedNetworkMetadata.defaultNetworkActivationLastCheckTimeElapsed =
+ sElapsedRealtimeClock.millis();
+ mHandler.sendEmptyMessage(MSG_PROCESS_ACTIVE_NETWORK);
}
}
+ /** NetworkCallback to track all network changes. */
private final NetworkCallback mNetworkCallback = new NetworkCallback() {
@Override
public void onAvailable(Network network) {
@@ -1565,6 +1691,7 @@
CachedNetworkMetadata cnm = mAvailableNetworks.get(network);
if (cnm == null) {
cnm = new CachedNetworkMetadata();
+ cnm.capabilitiesFirstAcquiredTimeElapsed = sElapsedRealtimeClock.millis();
mAvailableNetworks.put(network, cnm);
} else {
final NetworkCapabilities oldCaps = cnm.networkCapabilities;
@@ -1700,6 +1827,29 @@
}
};
+ /** NetworkCallback to track only changes to the default network. */
+ private final NetworkCallback mDefaultNetworkCallback = new NetworkCallback() {
+ @Override
+ public void onAvailable(Network network) {
+ if (DEBUG) Slog.v(TAG, "systemDefault-onAvailable: " + network);
+ synchronized (mLock) {
+ mSystemDefaultNetwork = network;
+ }
+ }
+
+ @Override
+ public void onLost(Network network) {
+ if (DEBUG) {
+ Slog.v(TAG, "systemDefault-onLost: " + network);
+ }
+ synchronized (mLock) {
+ if (network.equals(mSystemDefaultNetwork)) {
+ mSystemDefaultNetwork = null;
+ }
+ }
+ }
+ };
+
private final INetworkPolicyListener mNetPolicyListener = new NetworkPolicyManager.Listener() {
@Override
public void onRestrictBackgroundChanged(boolean restrictBackground) {
@@ -1762,6 +1912,66 @@
}
}
break;
+
+ case MSG_PROCESS_ACTIVE_NETWORK:
+ removeMessages(MSG_PROCESS_ACTIVE_NETWORK);
+ synchronized (mLock) {
+ if (mSystemDefaultNetwork == null) {
+ break;
+ }
+ if (!Flags.batchConnectivityJobsPerNetwork()) {
+ break;
+ }
+ if (!isNetworkInStateForJobRunLocked(mSystemDefaultNetwork)) {
+ break;
+ }
+
+ final ArrayMap<Network, Boolean> includeInProcessing = new ArrayMap<>();
+ // Try to get the jobs to piggyback on the active network.
+ for (int u = mTrackedJobs.size() - 1; u >= 0; --u) {
+ final ArraySet<JobStatus> jobs = mTrackedJobs.valueAt(u);
+ for (int j = jobs.size() - 1; j >= 0; --j) {
+ final JobStatus js = jobs.valueAt(j);
+ if (!mSystemDefaultNetwork.equals(js.network)) {
+ final NetworkCapabilities capabilities =
+ getNetworkCapabilities(js.network);
+ if (capabilities == null
+ || !capabilities.hasTransport(
+ NetworkCapabilities.TRANSPORT_VPN)) {
+ includeInProcessing.put(js.network, Boolean.FALSE);
+ continue;
+ }
+ if (includeInProcessing.containsKey(js.network)) {
+ if (!includeInProcessing.get(js.network)) {
+ continue;
+ }
+ } else {
+ // VPNs most likely use the system default network as
+ // their underlying network. If so, process the job.
+ final List<Network> underlyingNetworks =
+ capabilities.getUnderlyingNetworks();
+ final boolean isSystemDefaultInUnderlying =
+ underlyingNetworks != null
+ && underlyingNetworks.contains(
+ mSystemDefaultNetwork);
+ includeInProcessing.put(js.network,
+ isSystemDefaultInUnderlying);
+ if (!isSystemDefaultInUnderlying) {
+ continue;
+ }
+ }
+ }
+ if (js.isReady()) {
+ if (DEBUG) {
+ Slog.d(TAG, "Potentially running " + js
+ + " due to network activity");
+ }
+ mStateChangedListener.onRunJobNow(js);
+ }
+ }
+ }
+ }
+ break;
}
}
}
@@ -1782,8 +1992,15 @@
@VisibleForTesting
static final String KEY_AVOID_UNDEFINED_TRANSPORT_AFFINITY =
CC_CONFIG_PREFIX + "avoid_undefined_transport_affinity";
+ private static final String KEY_NETWORK_ACTIVATION_EXPIRATION_MS =
+ CC_CONFIG_PREFIX + "network_activation_expiration_ms";
+ private static final String KEY_NETWORK_ACTIVATION_MAX_WAIT_TIME_MS =
+ CC_CONFIG_PREFIX + "network_activation_max_wait_time_ms";
private static final boolean DEFAULT_AVOID_UNDEFINED_TRANSPORT_AFFINITY = false;
+ private static final long DEFAULT_NETWORK_ACTIVATION_EXPIRATION_MS = 10000L;
+ private static final long DEFAULT_NETWORK_ACTIVATION_MAX_WAIT_TIME_MS =
+ 31 * MINUTE_IN_MILLIS;
/**
* If true, will avoid network transports that don't have an explicitly defined affinity.
@@ -1791,6 +2008,19 @@
public boolean AVOID_UNDEFINED_TRANSPORT_AFFINITY =
DEFAULT_AVOID_UNDEFINED_TRANSPORT_AFFINITY;
+ /**
+ * Amount of time that needs to pass before needing to determine if the network is still
+ * active.
+ */
+ public long NETWORK_ACTIVATION_EXPIRATION_MS = DEFAULT_NETWORK_ACTIVATION_EXPIRATION_MS;
+
+ /**
+ * Max time to wait since the network was last activated before deciding to allow jobs to
+ * run even if the network isn't active
+ */
+ public long NETWORK_ACTIVATION_MAX_WAIT_TIME_MS =
+ DEFAULT_NETWORK_ACTIVATION_MAX_WAIT_TIME_MS;
+
@GuardedBy("mLock")
public void processConstantLocked(@NonNull DeviceConfig.Properties properties,
@NonNull String key) {
@@ -1803,6 +2033,22 @@
mShouldReprocessNetworkCapabilities = true;
}
break;
+ case KEY_NETWORK_ACTIVATION_EXPIRATION_MS:
+ final long gracePeriodMs = properties.getLong(key,
+ DEFAULT_NETWORK_ACTIVATION_EXPIRATION_MS);
+ if (NETWORK_ACTIVATION_EXPIRATION_MS != gracePeriodMs) {
+ NETWORK_ACTIVATION_EXPIRATION_MS = gracePeriodMs;
+ // This doesn't need to trigger network capability reprocessing.
+ }
+ break;
+ case KEY_NETWORK_ACTIVATION_MAX_WAIT_TIME_MS:
+ final long maxWaitMs = properties.getLong(key,
+ DEFAULT_NETWORK_ACTIVATION_MAX_WAIT_TIME_MS);
+ if (NETWORK_ACTIVATION_MAX_WAIT_TIME_MS != maxWaitMs) {
+ NETWORK_ACTIVATION_MAX_WAIT_TIME_MS = maxWaitMs;
+ mShouldReprocessNetworkCapabilities = true;
+ }
+ break;
}
}
@@ -1814,6 +2060,10 @@
pw.print(KEY_AVOID_UNDEFINED_TRANSPORT_AFFINITY,
AVOID_UNDEFINED_TRANSPORT_AFFINITY).println();
+ pw.print(KEY_NETWORK_ACTIVATION_EXPIRATION_MS,
+ NETWORK_ACTIVATION_EXPIRATION_MS).println();
+ pw.print(KEY_NETWORK_ACTIVATION_MAX_WAIT_TIME_MS,
+ NETWORK_ACTIVATION_MAX_WAIT_TIME_MS).println();
pw.decreaseIndent();
}
@@ -1925,11 +2175,24 @@
private static class CachedNetworkMetadata {
public NetworkCapabilities networkCapabilities;
public boolean satisfiesTransportAffinities;
+ /**
+ * Track the first time ConnectivityController was informed about the capabilities of the
+ * network after it became available.
+ */
+ public long capabilitiesFirstAcquiredTimeElapsed;
+ public long defaultNetworkActivationLastCheckTimeElapsed;
+ public long defaultNetworkActivationLastConfirmedTimeElapsed;
public String toString() {
return "CNM{"
+ networkCapabilities.toString()
+ ", satisfiesTransportAffinities=" + satisfiesTransportAffinities
+ + ", capabilitiesFirstAcquiredTimeElapsed="
+ + capabilitiesFirstAcquiredTimeElapsed
+ + ", defaultNetworkActivationLastCheckTimeElapsed="
+ + defaultNetworkActivationLastCheckTimeElapsed
+ + ", defaultNetworkActivationLastConfirmedTimeElapsed="
+ + defaultNetworkActivationLastConfirmedTimeElapsed
+ "}";
}
}
@@ -2017,7 +2280,7 @@
pw.println("Aconfig flags:");
pw.increaseIndent();
pw.print(FLAG_RELAX_PREFETCH_CONNECTIVITY_CONSTRAINT_ONLY_ON_CHARGER,
- relaxPrefetchConnectivityConstraintOnlyOnCharger());
+ Flags.relaxPrefetchConnectivityConstraintOnlyOnCharger());
pw.println();
pw.decreaseIndent();
pw.println();
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 2ea980d..a3a686f 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
@@ -2053,6 +2053,11 @@
case CONSTRAINT_WITHIN_QUOTA:
return JobParameters.STOP_REASON_QUOTA;
+ // This can change from true to false, but should never change when a job is already
+ // running, so there's no reason to log a message or create a new stop reason.
+ case CONSTRAINT_FLEXIBLE:
+ return JobParameters.STOP_REASON_UNDEFINED;
+
// These should never be stop reasons since they can never go from true to false.
case CONSTRAINT_CONTENT_TRIGGER:
case CONSTRAINT_DEADLINE:
diff --git a/api/Android.bp b/api/Android.bp
index b3b18b6..9d2147c 100644
--- a/api/Android.bp
+++ b/api/Android.bp
@@ -115,6 +115,7 @@
"framework-pdf",
"framework-permission",
"framework-permission-s",
+ "framework-profiling",
"framework-scheduling",
"framework-sdkextensions",
"framework-statsd",
@@ -199,7 +200,7 @@
out: ["current.srcjar"],
cmd: "$(location merge_zips) $(out) $(in)",
srcs: [
- ":api-stubs-docs-non-updatable",
+ ":api-stubs-docs-non-updatable{.exportable}",
":all-modules-public-stubs-source",
],
visibility: ["//visibility:private"], // Used by make module in //development, mind
@@ -222,7 +223,7 @@
name: "sdk-annotations.zip",
defaults: ["sdk-annotations-defaults"],
srcs: [
- ":android-non-updatable-doc-stubs{.annotations.zip}",
+ ":android-non-updatable-doc-stubs{.exportable.annotations.zip}",
":all-modules-public-annotations",
],
}
@@ -231,7 +232,7 @@
name: "sdk-annotations-system.zip",
defaults: ["sdk-annotations-defaults"],
srcs: [
- ":android-non-updatable-doc-stubs-system{.annotations.zip}",
+ ":android-non-updatable-doc-stubs-system{.exportable.annotations.zip}",
":all-modules-system-annotations",
],
}
@@ -240,7 +241,7 @@
name: "sdk-annotations-module-lib.zip",
defaults: ["sdk-annotations-defaults"],
srcs: [
- ":android-non-updatable-doc-stubs-module-lib{.annotations.zip}",
+ ":android-non-updatable-doc-stubs-module-lib{.exportable.annotations.zip}",
":all-modules-module-lib-annotations",
],
}
@@ -249,7 +250,7 @@
name: "sdk-annotations-system-server.zip",
defaults: ["sdk-annotations-defaults"],
srcs: [
- ":android-non-updatable-doc-stubs-system-server{.annotations.zip}",
+ ":android-non-updatable-doc-stubs-system-server{.exportable.annotations.zip}",
":all-modules-system-server-annotations",
],
}
@@ -360,7 +361,10 @@
previous_api: ":android.api.public.latest",
merge_annotations_dirs: ["metalava-manual"],
defaults_visibility: ["//frameworks/base/api"],
- visibility: ["//frameworks/base/api"],
+ visibility: [
+ "//frameworks/base/api",
+ "//frameworks/base/core/api",
+ ],
}
// We resolve dependencies on APIs in modules by depending on a prebuilt of the whole
diff --git a/api/ApiDocs.bp b/api/ApiDocs.bp
index 7ae3224..e8fcf4b 100644
--- a/api/ApiDocs.bp
+++ b/api/ApiDocs.bp
@@ -67,6 +67,7 @@
":framework-ondevicepersonalization-sources",
":framework-permission-sources",
":framework-permission-s-sources",
+ ":framework-profiling-sources",
":framework-scheduling-sources",
":framework-sdkextensions-sources",
":framework-statsd-sources",
@@ -220,7 +221,7 @@
name: "offline-sdk-docs",
defaults: ["framework-docs-default"],
srcs: [
- ":framework-doc-stubs",
+ ":framework-doc-stubs{.exportable}",
],
hdf: [
"android.whichdoc offline",
@@ -241,7 +242,7 @@
name: "offline-sdk-referenceonly-docs",
defaults: ["framework-docs-default"],
srcs: [
- ":framework-doc-stubs",
+ ":framework-doc-stubs{.exportable}",
],
hdf: [
"android.whichdoc offline",
@@ -285,7 +286,7 @@
name: "ds-docs-java",
defaults: ["framework-docs-default"],
srcs: [
- ":framework-doc-stubs",
+ ":framework-doc-stubs{.exportable}",
],
hdf: [
"android.whichdoc online",
@@ -319,7 +320,7 @@
droiddoc {
name: "ds-docs-kt",
srcs: [
- ":framework-doc-stubs",
+ ":framework-doc-stubs{.exportable}",
],
flags: [
"-noJdkLink",
@@ -373,7 +374,7 @@
name: "ds-static-docs",
defaults: ["framework-docs-default"],
srcs: [
- ":framework-doc-stubs",
+ ":framework-doc-stubs{.exportable}",
],
hdf: [
"android.whichdoc online",
@@ -390,7 +391,7 @@
name: "ds-ref-navtree-docs",
defaults: ["framework-docs-default"],
srcs: [
- ":framework-doc-stubs",
+ ":framework-doc-stubs{.exportable}",
],
hdf: [
"android.whichdoc online",
diff --git a/api/StubLibraries.bp b/api/StubLibraries.bp
index 59c0128..852abdf 100644
--- a/api/StubLibraries.bp
+++ b/api/StubLibraries.bp
@@ -961,7 +961,7 @@
java_library {
name: "android_stubs_current_with_test_libs",
static_libs: [
- "android_stubs_current",
+ "android_stubs_current_exportable",
"android.test.base.stubs",
"android.test.mock.stubs",
"android.test.runner.stubs",
@@ -976,7 +976,7 @@
java_library {
name: "android_system_stubs_current_with_test_libs",
static_libs: [
- "android_system_stubs_current",
+ "android_system_stubs_current_exportable",
"android.test.base.stubs.system",
"android.test.mock.stubs.system",
"android.test.runner.stubs.system",
@@ -991,7 +991,7 @@
java_library {
name: "android_module_stubs_current_with_test_libs",
static_libs: [
- "android_module_lib_stubs_current",
+ "android_module_lib_stubs_current_exportable",
"android.test.base.stubs",
"android.test.mock.stubs",
"android.test.runner.stubs",
@@ -1006,7 +1006,7 @@
java_library {
name: "android_system_server_stubs_current_with_test_libs",
static_libs: [
- "android_system_server_stubs_current",
+ "android_system_server_stubs_current_exportable",
"android.test.base.stubs.system",
"android.test.mock.stubs.system",
"android.test.runner.stubs.system",
diff --git a/api/api.go b/api/api.go
index fa2be21..c733f5b 100644
--- a/api/api.go
+++ b/api/api.go
@@ -130,7 +130,7 @@
Scope string
}
-func createMergedTxt(ctx android.LoadHookContext, txt MergedTxtDefinition) {
+func createMergedTxt(ctx android.LoadHookContext, txt MergedTxtDefinition, stubsTypeSuffix string, doDist bool) {
metalavaCmd := "$(location metalava)"
// Silence reflection warnings. See b/168689341
metalavaCmd += " -J--add-opens=java.base/java.util=ALL-UNNAMED "
@@ -140,7 +140,7 @@
if txt.Scope != "public" {
filename = txt.Scope + "-" + filename
}
- moduleName := ctx.ModuleName() + "-" + filename
+ moduleName := ctx.ModuleName() + stubsTypeSuffix + filename
props := genruleProps{}
props.Name = proptools.StringPtr(moduleName)
@@ -148,17 +148,19 @@
props.Out = []string{filename}
props.Cmd = proptools.StringPtr(metalavaCmd + "$(in) --out $(out)")
props.Srcs = append([]string{txt.BaseTxt}, createSrcs(txt.Modules, txt.ModuleTag)...)
- props.Dists = []android.Dist{
- {
- Targets: []string{"droidcore"},
- Dir: proptools.StringPtr("api"),
- Dest: proptools.StringPtr(filename),
- },
- {
- Targets: []string{"api_txt", "sdk"},
- Dir: proptools.StringPtr("apistubs/android/" + txt.Scope + "/api"),
- Dest: proptools.StringPtr(txt.DistFilename),
- },
+ if doDist {
+ props.Dists = []android.Dist{
+ {
+ Targets: []string{"droidcore"},
+ Dir: proptools.StringPtr("api"),
+ Dest: proptools.StringPtr(filename),
+ },
+ {
+ Targets: []string{"api_txt", "sdk"},
+ Dir: proptools.StringPtr("apistubs/android/" + txt.Scope + "/api"),
+ Dest: proptools.StringPtr(txt.DistFilename),
+ },
+ }
}
props.Visibility = []string{"//visibility:public"}
ctx.CreateModule(genrule.GenRuleFactory, &props)
@@ -343,7 +345,7 @@
ctx.CreateModule(android.FileGroupFactory, &props)
}
-func createMergedTxts(ctx android.LoadHookContext, bootclasspath, system_server_classpath []string) {
+func createMergedTxts(ctx android.LoadHookContext, bootclasspath, system_server_classpath []string, baseTxtModulePrefix, stubsTypeSuffix string, doDist bool) {
var textFiles []MergedTxtDefinition
tagSuffix := []string{".api.txt}", ".removed-api.txt}"}
@@ -352,7 +354,7 @@
textFiles = append(textFiles, MergedTxtDefinition{
TxtFilename: f,
DistFilename: distFilename[i],
- BaseTxt: ":non-updatable-" + f,
+ BaseTxt: ":" + baseTxtModulePrefix + f,
Modules: bootclasspath,
ModuleTag: "{.public" + tagSuffix[i],
Scope: "public",
@@ -360,7 +362,7 @@
textFiles = append(textFiles, MergedTxtDefinition{
TxtFilename: f,
DistFilename: distFilename[i],
- BaseTxt: ":non-updatable-system-" + f,
+ BaseTxt: ":" + baseTxtModulePrefix + "system-" + f,
Modules: bootclasspath,
ModuleTag: "{.system" + tagSuffix[i],
Scope: "system",
@@ -368,7 +370,7 @@
textFiles = append(textFiles, MergedTxtDefinition{
TxtFilename: f,
DistFilename: distFilename[i],
- BaseTxt: ":non-updatable-module-lib-" + f,
+ BaseTxt: ":" + baseTxtModulePrefix + "module-lib-" + f,
Modules: bootclasspath,
ModuleTag: "{.module-lib" + tagSuffix[i],
Scope: "module-lib",
@@ -376,14 +378,14 @@
textFiles = append(textFiles, MergedTxtDefinition{
TxtFilename: f,
DistFilename: distFilename[i],
- BaseTxt: ":non-updatable-system-server-" + f,
+ BaseTxt: ":" + baseTxtModulePrefix + "system-server-" + f,
Modules: system_server_classpath,
ModuleTag: "{.system-server" + tagSuffix[i],
Scope: "system-server",
})
}
for _, txt := range textFiles {
- createMergedTxt(ctx, txt)
+ createMergedTxt(ctx, txt, stubsTypeSuffix, doDist)
}
}
@@ -465,7 +467,8 @@
bootclasspath = append(bootclasspath, a.properties.Conditional_bootclasspath...)
sort.Strings(bootclasspath)
}
- createMergedTxts(ctx, bootclasspath, system_server_classpath)
+ createMergedTxts(ctx, bootclasspath, system_server_classpath, "non-updatable-", "-", false)
+ createMergedTxts(ctx, bootclasspath, system_server_classpath, "non-updatable-exportable-", "-exportable-", true)
createMergedPublicStubs(ctx, bootclasspath)
createMergedSystemStubs(ctx, bootclasspath)
diff --git a/boot/Android.bp b/boot/Android.bp
index 228d060..cdfa7c80 100644
--- a/boot/Android.bp
+++ b/boot/Android.bp
@@ -122,6 +122,10 @@
module: "com.android.permission-bootclasspath-fragment",
},
{
+ apex: "com.android.profiling",
+ module: "com.android.profiling-bootclasspath-fragment",
+ },
+ {
apex: "com.android.scheduling",
module: "com.android.scheduling-bootclasspath-fragment",
},
diff --git a/cmds/uinput/src/com/android/commands/uinput/Device.java b/cmds/uinput/src/com/android/commands/uinput/Device.java
index 787055c..25d3a34 100644
--- a/cmds/uinput/src/com/android/commands/uinput/Device.java
+++ b/cmds/uinput/src/com/android/commands/uinput/Device.java
@@ -172,8 +172,10 @@
RuntimeException ex = new RuntimeException(
"Could not create uinput device \"" + name + "\"");
Log.e(TAG, "Couldn't create uinput device, exiting.", ex);
+ args.recycle();
throw ex;
}
+ args.recycle();
break;
case MSG_INJECT_EVENT:
if (mPtr != 0) {
diff --git a/core/api/Android.bp b/core/api/Android.bp
index 8d8a82b..77594b7 100644
--- a/core/api/Android.bp
+++ b/core/api/Android.bp
@@ -96,3 +96,54 @@
name: "non-updatable-test-lint-baseline.txt",
srcs: ["test-lint-baseline.txt"],
}
+
+// Exportable stub artifacts
+filegroup {
+ name: "non-updatable-exportable-current.txt",
+ srcs: [":api-stubs-docs-non-updatable{.exportable.api.txt}"],
+}
+
+filegroup {
+ name: "non-updatable-exportable-removed.txt",
+ srcs: [":api-stubs-docs-non-updatable{.exportable.removed-api.txt}"],
+}
+
+filegroup {
+ name: "non-updatable-exportable-system-current.txt",
+ srcs: [":system-api-stubs-docs-non-updatable{.exportable.api.txt}"],
+}
+
+filegroup {
+ name: "non-updatable-exportable-system-removed.txt",
+ srcs: [":system-api-stubs-docs-non-updatable{.exportable.removed-api.txt}"],
+}
+
+filegroup {
+ name: "non-updatable-exportable-module-lib-current.txt",
+ srcs: [":module-lib-api-stubs-docs-non-updatable{.exportable.api.txt}"],
+}
+
+filegroup {
+ name: "non-updatable-exportable-module-lib-removed.txt",
+ srcs: [":module-lib-api-stubs-docs-non-updatable{.exportable.removed-api.txt}"],
+}
+
+filegroup {
+ name: "non-updatable-exportable-test-current.txt",
+ srcs: [":test-api-stubs-docs-non-updatable{.exportable.api.txt}"],
+}
+
+filegroup {
+ name: "non-updatable-exportable-test-removed.txt",
+ srcs: [":test-api-stubs-docs-non-updatable{.exportable.removed-api.txt}"],
+}
+
+filegroup {
+ name: "non-updatable-exportable-system-server-current.txt",
+ srcs: [":services-non-updatable-stubs{.exportable.api.txt}"],
+}
+
+filegroup {
+ name: "non-updatable-exportable-system-server-removed.txt",
+ srcs: [":services-non-updatable-stubs{.exportable.removed-api.txt}"],
+}
diff --git a/core/api/current.txt b/core/api/current.txt
index aec2842..bfa486b 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -90,6 +90,7 @@
field public static final String DELETE_PACKAGES = "android.permission.DELETE_PACKAGES";
field public static final String DELIVER_COMPANION_MESSAGES = "android.permission.DELIVER_COMPANION_MESSAGES";
field public static final String DETECT_SCREEN_CAPTURE = "android.permission.DETECT_SCREEN_CAPTURE";
+ field @FlaggedApi("com.android.window.flags.screen_recording_callbacks") public static final String DETECT_SCREEN_RECORDING = "android.permission.DETECT_SCREEN_RECORDING";
field public static final String DIAGNOSTIC = "android.permission.DIAGNOSTIC";
field public static final String DISABLE_KEYGUARD = "android.permission.DISABLE_KEYGUARD";
field public static final String DUMP = "android.permission.DUMP";
@@ -505,6 +506,7 @@
field public static final int autoSizeTextType = 16844085; // 0x1010535
field public static final int autoStart = 16843445; // 0x10102b5
field @Deprecated public static final int autoText = 16843114; // 0x101016a
+ field @FlaggedApi("android.nfc.Flags.FLAG_NFC_READ_POLLING_LOOP") public static final int autoTransact;
field public static final int autoUrlDetect = 16843404; // 0x101028c
field public static final int autoVerify = 16844014; // 0x10104ee
field public static final int autofillHints = 16844118; // 0x1010556
@@ -763,7 +765,7 @@
field public static final int endY = 16844051; // 0x1010513
field @Deprecated public static final int endYear = 16843133; // 0x101017d
field public static final int enforceNavigationBarContrast = 16844293; // 0x1010605
- field public static final int enforceStatusBarContrast = 16844292; // 0x1010604
+ field @Deprecated public static final int enforceStatusBarContrast = 16844292; // 0x1010604
field public static final int enterFadeDuration = 16843532; // 0x101030c
field public static final int entries = 16842930; // 0x10100b2
field public static final int entryValues = 16843256; // 0x10101f8
@@ -1194,8 +1196,8 @@
field public static final int multiprocess = 16842771; // 0x1010013
field public static final int name = 16842755; // 0x1010003
field public static final int nativeHeapZeroInitialized = 16844325; // 0x1010625
- field public static final int navigationBarColor = 16843858; // 0x1010452
- field public static final int navigationBarDividerColor = 16844141; // 0x101056d
+ field @Deprecated public static final int navigationBarColor = 16843858; // 0x1010452
+ field @Deprecated public static final int navigationBarDividerColor = 16844141; // 0x101056d
field public static final int navigationContentDescription = 16843969; // 0x10104c1
field public static final int navigationIcon = 16843968; // 0x10104c0
field public static final int navigationMode = 16843471; // 0x10102cf
@@ -1373,6 +1375,7 @@
field public static final int reqTouchScreen = 16843303; // 0x1010227
field public static final int requestLegacyExternalStorage = 16844291; // 0x1010603
field public static final int requestRawExternalStorageAccess = 16844357; // 0x1010645
+ field @FlaggedApi("android.security.content_uri_permission_apis") public static final int requireContentUriPermissionFromCaller;
field public static final int requireDeviceScreenOn = 16844317; // 0x101061d
field public static final int requireDeviceUnlock = 16843756; // 0x10103ec
field public static final int required = 16843406; // 0x101028e
@@ -1566,7 +1569,7 @@
field public static final int state_single = 16842915; // 0x10100a3
field public static final int state_window_focused = 16842909; // 0x101009d
field public static final int staticWallpaperPreview = 16843569; // 0x1010331
- field public static final int statusBarColor = 16843857; // 0x1010451
+ field @Deprecated public static final int statusBarColor = 16843857; // 0x1010451
field public static final int stepSize = 16843078; // 0x1010146
field public static final int stopWithTask = 16843626; // 0x101036a
field public static final int streamType = 16843273; // 0x1010209
@@ -1795,6 +1798,7 @@
field public static final int updatePeriodMillis = 16843344; // 0x1010250
field public static final int use32bitAbi = 16844053; // 0x1010515
field public static final int useAppZygote = 16844183; // 0x1010597
+ field @FlaggedApi("com.android.text.flags.use_bounds_for_width") public static final int useBoundsForWidth;
field public static final int useDefaultMargins = 16843641; // 0x1010379
field public static final int useEmbeddedDex = 16844190; // 0x101059e
field public static final int useIntrinsicSizeAsMinimum = 16843536; // 0x1010310
@@ -1887,6 +1891,7 @@
field public static final int windowNoDisplay = 16843294; // 0x101021e
field public static final int windowNoMoveAnimation = 16844421; // 0x1010685
field public static final int windowNoTitle = 16842838; // 0x1010056
+ field @FlaggedApi("com.android.window.flags.enforce_edge_to_edge") public static final int windowOptOutEdgeToEdgeEnforcement;
field @Deprecated public static final int windowOverscan = 16843727; // 0x10103cf
field public static final int windowReenterTransition = 16843951; // 0x10104af
field public static final int windowReturnTransition = 16843950; // 0x10104ae
@@ -4377,6 +4382,7 @@
method public android.transition.TransitionManager getContentTransitionManager();
method @Nullable public android.view.View getCurrentFocus();
method @Deprecated public android.app.FragmentManager getFragmentManager();
+ method @FlaggedApi("android.security.content_uri_permission_apis") @NonNull public android.app.ComponentCaller getInitialCaller();
method public android.content.Intent getIntent();
method @Nullable public Object getLastNonConfigurationInstance();
method @Nullable public String getLaunchedFromPackage();
@@ -5417,6 +5423,12 @@
field public static final int DELIVERY_GROUP_POLICY_MOST_RECENT = 1; // 0x1
}
+ @FlaggedApi("android.security.content_uri_permission_apis") public final class ComponentCaller {
+ ctor public ComponentCaller(@NonNull android.os.IBinder, @Nullable android.os.IBinder);
+ method @Nullable public String getPackage();
+ method public int getUid();
+ }
+
public class DatePickerDialog extends android.app.AlertDialog implements android.widget.DatePicker.OnDateChangedListener android.content.DialogInterface.OnClickListener {
ctor public DatePickerDialog(@NonNull android.content.Context);
ctor public DatePickerDialog(@NonNull android.content.Context, @StyleRes int);
@@ -6879,6 +6891,7 @@
method public CharSequence getName();
method @Nullable public String getParentChannelId();
method public android.net.Uri getSound();
+ method @FlaggedApi("android.app.notification_channel_vibration_effect_api") @Nullable public android.os.VibrationEffect getVibrationEffect();
method public long[] getVibrationPattern();
method public boolean hasUserSetImportance();
method public boolean hasUserSetSound();
@@ -6898,6 +6911,7 @@
method public void setName(CharSequence);
method public void setShowBadge(boolean);
method public void setSound(android.net.Uri, android.media.AudioAttributes);
+ method @FlaggedApi("android.app.notification_channel_vibration_effect_api") public void setVibrationEffect(@Nullable android.os.VibrationEffect);
method public void setVibrationPattern(long[]);
method public boolean shouldShowLights();
method public boolean shouldVibrate();
@@ -7882,6 +7896,7 @@
field public static final String AUTO_TIME_POLICY = "autoTime";
field public static final String BACKUP_SERVICE_POLICY = "backupService";
field public static final String CAMERA_DISABLED_POLICY = "cameraDisabled";
+ field @FlaggedApi("android.view.contentprotection.flags.manage_device_policy_enabled") public static final String CONTENT_PROTECTION_POLICY = "contentProtection";
field public static final String KEYGUARD_DISABLED_FEATURES_POLICY = "keyguardDisabledFeatures";
field public static final String LOCK_TASK_POLICY = "lockTask";
field public static final String PACKAGES_SUSPENDED_POLICY = "packagesSuspended";
@@ -7932,6 +7947,7 @@
method public boolean getBluetoothContactSharingDisabled(@NonNull android.content.ComponentName);
method @RequiresPermission(value=android.Manifest.permission.MANAGE_DEVICE_POLICY_CAMERA, conditional=true) public boolean getCameraDisabled(@Nullable android.content.ComponentName);
method @Deprecated @Nullable public String getCertInstallerPackage(@NonNull android.content.ComponentName) throws java.lang.SecurityException;
+ method @FlaggedApi("android.view.contentprotection.flags.manage_device_policy_enabled") @RequiresPermission(value=android.Manifest.permission.MANAGE_DEVICE_POLICY_CONTENT_PROTECTION, conditional=true) public int getContentProtectionPolicy(@Nullable android.content.ComponentName);
method @Nullable public android.app.admin.PackagePolicy getCredentialManagerPolicy();
method @Deprecated @Nullable public java.util.Set<java.lang.String> getCrossProfileCalendarPackages(@NonNull android.content.ComponentName);
method @Deprecated public boolean getCrossProfileCallerIdDisabled(@NonNull android.content.ComponentName);
@@ -8055,8 +8071,8 @@
method public boolean isUsbDataSignalingEnabled();
method public boolean isUsingUnifiedPassword(@NonNull android.content.ComponentName);
method @NonNull public java.util.List<android.os.UserHandle> listForegroundAffiliatedUsers();
- method @RequiresPermission(value=android.Manifest.permission.MANAGE_DEVICE_POLICY_LOCK, conditional=true) public void lockNow();
- method @RequiresPermission(value=android.Manifest.permission.MANAGE_DEVICE_POLICY_LOCK, conditional=true) public void lockNow(int);
+ method @RequiresPermission(value="android.permission.LOCK_DEVICE", conditional=true) public void lockNow();
+ method @RequiresPermission(value="android.permission.LOCK_DEVICE", conditional=true) public void lockNow(int);
method public int logoutUser(@NonNull android.content.ComponentName);
method public void reboot(@NonNull android.content.ComponentName);
method public void removeActiveAdmin(@NonNull android.content.ComponentName);
@@ -8088,6 +8104,7 @@
method @Deprecated public void setCertInstallerPackage(@NonNull android.content.ComponentName, @Nullable String) throws java.lang.SecurityException;
method @RequiresPermission(value=android.Manifest.permission.MANAGE_DEVICE_POLICY_COMMON_CRITERIA_MODE, conditional=true) public void setCommonCriteriaModeEnabled(@Nullable android.content.ComponentName, boolean);
method @RequiresPermission(value=android.Manifest.permission.MANAGE_DEVICE_POLICY_WIFI, conditional=true) public void setConfiguredNetworksLockdownState(@Nullable android.content.ComponentName, boolean);
+ method @FlaggedApi("android.view.contentprotection.flags.manage_device_policy_enabled") @RequiresPermission(value=android.Manifest.permission.MANAGE_DEVICE_POLICY_CONTENT_PROTECTION, conditional=true) public void setContentProtectionPolicy(@Nullable android.content.ComponentName, int);
method public void setCredentialManagerPolicy(@Nullable android.app.admin.PackagePolicy);
method @Deprecated public void setCrossProfileCalendarPackages(@NonNull android.content.ComponentName, @Nullable java.util.Set<java.lang.String>);
method @Deprecated public void setCrossProfileCallerIdDisabled(@NonNull android.content.ComponentName, boolean);
@@ -8513,6 +8530,7 @@
field public static final int TAG_ADB_SHELL_CMD = 210002; // 0x33452
field public static final int TAG_ADB_SHELL_INTERACTIVE = 210001; // 0x33451
field public static final int TAG_APP_PROCESS_START = 210005; // 0x33455
+ field @FlaggedApi("android.app.admin.flags.backup_service_security_log_event_enabled") public static final int TAG_BACKUP_SERVICE_TOGGLED = 210044; // 0x3347c
field public static final int TAG_BLUETOOTH_CONNECTION = 210039; // 0x33477
field public static final int TAG_BLUETOOTH_DISCONNECTION = 210040; // 0x33478
field public static final int TAG_CAMERA_POLICY_SET = 210034; // 0x33472
@@ -9345,9 +9363,12 @@
method public long getDataBytes();
method public long getExternalCacheBytes();
method public void writeToParcel(android.os.Parcel, int);
- field @FlaggedApi("android.app.usage.get_app_bytes_by_data_type_api") public static final int APP_DATA_TYPE_FILE_TYPE_APK = 0; // 0x0
- field @FlaggedApi("android.app.usage.get_app_bytes_by_data_type_api") public static final int APP_DATA_TYPE_FILE_TYPE_DM = 1; // 0x1
- field @FlaggedApi("android.app.usage.get_app_bytes_by_data_type_api") public static final int APP_DATA_TYPE_LIB = 2; // 0x2
+ field @FlaggedApi("android.app.usage.get_app_bytes_by_data_type_api") public static final int APP_DATA_TYPE_FILE_TYPE_APK = 3; // 0x3
+ field @FlaggedApi("android.app.usage.get_app_bytes_by_data_type_api") public static final int APP_DATA_TYPE_FILE_TYPE_CURRENT_PROFILE = 2; // 0x2
+ field @FlaggedApi("android.app.usage.get_app_bytes_by_data_type_api") public static final int APP_DATA_TYPE_FILE_TYPE_DEXOPT_ARTIFACT = 0; // 0x0
+ field @FlaggedApi("android.app.usage.get_app_bytes_by_data_type_api") public static final int APP_DATA_TYPE_FILE_TYPE_DM = 4; // 0x4
+ field @FlaggedApi("android.app.usage.get_app_bytes_by_data_type_api") public static final int APP_DATA_TYPE_FILE_TYPE_REFERENCE_PROFILE = 1; // 0x1
+ field @FlaggedApi("android.app.usage.get_app_bytes_by_data_type_api") public static final int APP_DATA_TYPE_LIB = 5; // 0x5
field @NonNull public static final android.os.Parcelable.Creator<android.app.usage.StorageStats> CREATOR;
}
@@ -9511,8 +9532,8 @@
method public static android.appwidget.AppWidgetManager getInstance(android.content.Context);
method @FlaggedApi("android.appwidget.flags.generated_previews") @Nullable public android.widget.RemoteViews getWidgetPreview(@NonNull android.content.ComponentName, @Nullable android.os.UserHandle, int);
method public boolean isRequestPinAppWidgetSupported();
- method @Deprecated public void notifyAppWidgetViewDataChanged(int[], int);
- method @Deprecated public void notifyAppWidgetViewDataChanged(int, int);
+ method public void notifyAppWidgetViewDataChanged(int[], int);
+ method public void notifyAppWidgetViewDataChanged(int, int);
method public void partiallyUpdateAppWidget(int[], android.widget.RemoteViews);
method public void partiallyUpdateAppWidget(int, android.widget.RemoteViews);
method @FlaggedApi("android.appwidget.flags.generated_previews") public void removeWidgetPreview(@NonNull android.content.ComponentName, int);
@@ -12395,7 +12416,7 @@
method public void registerCallback(android.content.pm.LauncherApps.Callback, android.os.Handler);
method public void registerPackageInstallerSessionCallback(@NonNull java.util.concurrent.Executor, @NonNull android.content.pm.PackageInstaller.SessionCallback);
method public android.content.pm.LauncherActivityInfo resolveActivity(android.content.Intent, android.os.UserHandle);
- method @FlaggedApi("android.content.pm.archiving") public void setArchiveCompatibilityOptions(boolean, boolean);
+ method @FlaggedApi("android.content.pm.archiving") public void setArchiveCompatibility(@NonNull android.content.pm.LauncherApps.ArchiveCompatibilityParams);
method public boolean shouldHideFromSuggestions(@NonNull String, @NonNull android.os.UserHandle);
method public void startAppDetailsActivity(android.content.ComponentName, android.os.UserHandle, android.graphics.Rect, android.os.Bundle);
method public void startMainActivity(android.content.ComponentName, android.os.UserHandle, android.graphics.Rect, android.os.Bundle);
@@ -12409,6 +12430,12 @@
field public static final String EXTRA_PIN_ITEM_REQUEST = "android.content.pm.extra.PIN_ITEM_REQUEST";
}
+ @FlaggedApi("android.content.pm.archiving") public static class LauncherApps.ArchiveCompatibilityParams {
+ ctor public LauncherApps.ArchiveCompatibilityParams();
+ method public void setEnableIconOverlay(boolean);
+ method public void setEnableUnarchivalConfirmation(boolean);
+ }
+
public abstract static class LauncherApps.Callback {
ctor public LauncherApps.Callback();
method public abstract void onPackageAdded(String, android.os.UserHandle);
@@ -13838,6 +13865,7 @@
method public android.content.res.AssetFileDescriptor openRawResourceFd(@RawRes int) throws android.content.res.Resources.NotFoundException;
method public void parseBundleExtra(String, android.util.AttributeSet, android.os.Bundle) throws org.xmlpull.v1.XmlPullParserException;
method public void parseBundleExtras(android.content.res.XmlResourceParser, android.os.Bundle) throws java.io.IOException, org.xmlpull.v1.XmlPullParserException;
+ method @FlaggedApi("android.content.res.register_resource_paths") public static void registerResourcePaths(@NonNull String, @NonNull android.content.pm.ApplicationInfo);
method public void removeLoaders(@NonNull android.content.res.loader.ResourcesLoader...);
method @Deprecated public void updateConfiguration(android.content.res.Configuration, android.util.DisplayMetrics);
field @AnyRes public static final int ID_NULL = 0; // 0x0
@@ -16503,8 +16531,8 @@
field public static final int START_HYPHEN_EDIT_NO_EDIT = 0; // 0x0
field public static final int STRIKE_THRU_TEXT_FLAG = 16; // 0x10
field public static final int SUBPIXEL_TEXT_FLAG = 128; // 0x80
- field @FlaggedApi("com.android.text.flags.inter_character_justification") public static final int TEXT_RUN_FLAG_LEFT_EDGE = 8192; // 0x2000
- field @FlaggedApi("com.android.text.flags.inter_character_justification") public static final int TEXT_RUN_FLAG_RIGHT_EDGE = 16384; // 0x4000
+ field @FlaggedApi("com.android.text.flags.letter_spacing_justification") public static final int TEXT_RUN_FLAG_LEFT_EDGE = 8192; // 0x2000
+ field @FlaggedApi("com.android.text.flags.letter_spacing_justification") public static final int TEXT_RUN_FLAG_RIGHT_EDGE = 16384; // 0x4000
field public static final int UNDERLINE_TEXT_FLAG = 8; // 0x8
}
@@ -17981,7 +18009,7 @@
field public static final int HYPHENATION_FREQUENCY_FULL = 2; // 0x2
field public static final int HYPHENATION_FREQUENCY_NONE = 0; // 0x0
field public static final int HYPHENATION_FREQUENCY_NORMAL = 1; // 0x1
- field @FlaggedApi("com.android.text.flags.inter_character_justification") public static final int JUSTIFICATION_MODE_INTER_CHARACTER = 2; // 0x2
+ field @FlaggedApi("com.android.text.flags.letter_spacing_justification") public static final int JUSTIFICATION_MODE_INTER_CHARACTER = 2; // 0x2
field public static final int JUSTIFICATION_MODE_INTER_WORD = 1; // 0x1
field public static final int JUSTIFICATION_MODE_NONE = 0; // 0x0
}
@@ -19150,11 +19178,13 @@
}
public final class CameraExtensionCharacteristics {
+ method @FlaggedApi("com.android.internal.camera.flags.camera_extensions_characteristics_get") public <T> T get(int, @NonNull android.hardware.camera2.CameraCharacteristics.Key<T>);
method @NonNull public java.util.Set<android.hardware.camera2.CaptureRequest.Key> getAvailableCaptureRequestKeys(int);
method @NonNull public java.util.Set<android.hardware.camera2.CaptureResult.Key> getAvailableCaptureResultKeys(int);
method @Nullable public android.util.Range<java.lang.Long> getEstimatedCaptureLatencyRangeMillis(int, @NonNull android.util.Size, int);
method @NonNull public <T> java.util.List<android.util.Size> getExtensionSupportedSizes(int, @NonNull Class<T>);
method @NonNull public java.util.List<android.util.Size> getExtensionSupportedSizes(int, int);
+ method @FlaggedApi("com.android.internal.camera.flags.camera_extensions_characteristics_get") @NonNull public java.util.Set<android.hardware.camera2.CameraCharacteristics.Key> getKeys(int);
method @NonNull public java.util.List<android.util.Size> getPostviewSupportedSizes(int, @NonNull android.util.Size, int);
method @NonNull public java.util.List<java.lang.Integer> getSupportedExtensions();
method public boolean isCaptureProcessProgressAvailable(int);
@@ -20265,6 +20295,7 @@
method @Nullable public android.hardware.input.HostUsiVersion getHostUsiVersion(@NonNull android.view.Display);
method @Nullable public android.view.InputDevice getInputDevice(int);
method public int[] getInputDeviceIds();
+ method @FlaggedApi("com.android.input.flags.input_device_view_behavior_api") @Nullable public android.view.InputDevice.ViewBehavior getInputDeviceViewBehavior(int);
method @FloatRange(from=0, to=1) public float getMaximumObscuringOpacityForTouch();
method public boolean isStylusPointerIconEnabled();
method public void registerInputDeviceListener(android.hardware.input.InputManager.InputDeviceListener, android.os.Handler);
@@ -24681,6 +24712,7 @@
}
public class Ringtone {
+ method protected void finalize();
method public android.media.AudioAttributes getAudioAttributes();
method @Deprecated public int getStreamType();
method public String getTitle(android.content.Context);
@@ -26607,6 +26639,8 @@
method public void writeToParcel(@NonNull android.os.Parcel, int);
field @NonNull public static final android.os.Parcelable.Creator<android.media.tv.BroadcastInfoRequest> CREATOR;
field public static final int REQUEST_OPTION_AUTO_UPDATE = 1; // 0x1
+ field @FlaggedApi("android.media.tv.flags.tiaf_v_apis") public static final int REQUEST_OPTION_ONESHOT = 3; // 0x3
+ field @FlaggedApi("android.media.tv.flags.tiaf_v_apis") public static final int REQUEST_OPTION_ONEWAY = 2; // 0x2
field public static final int REQUEST_OPTION_REPEAT = 0; // 0x0
}
@@ -27280,6 +27314,24 @@
field public static final int RECORDING_ERROR_INSUFFICIENT_SPACE = 1; // 0x1
field public static final int RECORDING_ERROR_RESOURCE_BUSY = 2; // 0x2
field public static final int RECORDING_ERROR_UNKNOWN = 0; // 0x0
+ field @FlaggedApi("android.media.tv.flags.enable_ad_service_fw") public static final String SESSION_DATA_KEY_AD_BUFFER = "ad_buffer";
+ field @FlaggedApi("android.media.tv.flags.enable_ad_service_fw") public static final String SESSION_DATA_KEY_AD_RESPONSE = "ad_response";
+ field @FlaggedApi("android.media.tv.flags.enable_ad_service_fw") public static final String SESSION_DATA_KEY_BROADCAST_INFO_RESPONSE = "broadcast_info_response";
+ field @FlaggedApi("android.media.tv.flags.enable_ad_service_fw") public static final String SESSION_DATA_KEY_CHANNEL_URI = "channel_uri";
+ field @FlaggedApi("android.media.tv.flags.enable_ad_service_fw") public static final String SESSION_DATA_KEY_TRACKS = "tracks";
+ field @FlaggedApi("android.media.tv.flags.enable_ad_service_fw") public static final String SESSION_DATA_KEY_TRACK_ID = "track_id";
+ field @FlaggedApi("android.media.tv.flags.enable_ad_service_fw") public static final String SESSION_DATA_KEY_TRACK_TYPE = "track_type";
+ field @FlaggedApi("android.media.tv.flags.enable_ad_service_fw") public static final String SESSION_DATA_KEY_TV_MESSAGE_TYPE = "tv_message_type";
+ field @FlaggedApi("android.media.tv.flags.enable_ad_service_fw") public static final String SESSION_DATA_KEY_VIDEO_UNAVAILABLE_REASON = "video_unavailable_reason";
+ field @FlaggedApi("android.media.tv.flags.enable_ad_service_fw") public static final String SESSION_DATA_TYPE_AD_BUFFER_CONSUMED = "ad_buffer_consumed";
+ field @FlaggedApi("android.media.tv.flags.enable_ad_service_fw") public static final String SESSION_DATA_TYPE_AD_RESPONSE = "ad_response";
+ field @FlaggedApi("android.media.tv.flags.enable_ad_service_fw") public static final String SESSION_DATA_TYPE_BROADCAST_INFO_RESPONSE = "broadcast_info_response";
+ field @FlaggedApi("android.media.tv.flags.enable_ad_service_fw") public static final String SESSION_DATA_TYPE_TRACKS_CHANGED = "tracks_changed";
+ field @FlaggedApi("android.media.tv.flags.enable_ad_service_fw") public static final String SESSION_DATA_TYPE_TRACK_SELECTED = "track_selected";
+ field @FlaggedApi("android.media.tv.flags.enable_ad_service_fw") public static final String SESSION_DATA_TYPE_TUNED = "tuned";
+ field @FlaggedApi("android.media.tv.flags.enable_ad_service_fw") public static final String SESSION_DATA_TYPE_TV_MESSAGE = "tv_message";
+ field @FlaggedApi("android.media.tv.flags.enable_ad_service_fw") public static final String SESSION_DATA_TYPE_VIDEO_AVAILABLE = "video_available";
+ field @FlaggedApi("android.media.tv.flags.enable_ad_service_fw") public static final String SESSION_DATA_TYPE_VIDEO_UNAVAILABLE = "video_unavailable";
field public static final int SIGNAL_STRENGTH_LOST = 1; // 0x1
field public static final int SIGNAL_STRENGTH_STRONG = 3; // 0x3
field public static final int SIGNAL_STRENGTH_WEAK = 2; // 0x2
@@ -27317,6 +27369,7 @@
field public static final int VIDEO_UNAVAILABLE_REASON_CAS_UNKNOWN = 18; // 0x12
field public static final int VIDEO_UNAVAILABLE_REASON_INSUFFICIENT_RESOURCE = 6; // 0x6
field public static final int VIDEO_UNAVAILABLE_REASON_NOT_CONNECTED = 5; // 0x5
+ field @FlaggedApi("android.media.tv.flags.tiaf_v_apis") public static final int VIDEO_UNAVAILABLE_REASON_STOPPED = 19; // 0x13
field public static final int VIDEO_UNAVAILABLE_REASON_TUNING = 1; // 0x1
field public static final int VIDEO_UNAVAILABLE_REASON_UNKNOWN = 0; // 0x0
field public static final int VIDEO_UNAVAILABLE_REASON_WEAK_SIGNAL = 2; // 0x2
@@ -27394,6 +27447,7 @@
method public void notifyTuned(@NonNull android.net.Uri);
method public void notifyTvMessage(int, @NonNull android.os.Bundle);
method public void notifyVideoAvailable();
+ method @FlaggedApi("android.media.tv.flags.tiaf_v_apis") public void notifyVideoFreezeUpdated(boolean);
method public void notifyVideoUnavailable(int);
method public void onAdBufferReady(@NonNull android.media.tv.AdBuffer);
method public void onAppPrivateCommand(@NonNull String, android.os.Bundle);
@@ -27408,6 +27462,7 @@
method public void onRemoveBroadcastInfo(int);
method public void onRequestAd(@NonNull android.media.tv.AdRequest);
method public void onRequestBroadcastInfo(@NonNull android.media.tv.BroadcastInfoRequest);
+ method @FlaggedApi("android.media.tv.flags.tiaf_v_apis") public void onResumePlayback();
method public boolean onSelectAudioPresentation(int, int);
method public boolean onSelectTrack(int, @Nullable String);
method public abstract void onSetCaptionEnabled(boolean);
@@ -27415,6 +27470,7 @@
method public abstract void onSetStreamVolume(@FloatRange(from=0.0, to=1.0) float);
method public abstract boolean onSetSurface(@Nullable android.view.Surface);
method public void onSetTvMessageEnabled(int, boolean);
+ method @FlaggedApi("android.media.tv.flags.tiaf_v_apis") public void onStopPlayback(int);
method public void onSurfaceChanged(int, int, int);
method public long onTimeShiftGetCurrentPosition();
method public long onTimeShiftGetStartPosition();
@@ -27428,8 +27484,10 @@
method public boolean onTrackballEvent(android.view.MotionEvent);
method public abstract boolean onTune(android.net.Uri);
method public boolean onTune(android.net.Uri, android.os.Bundle);
+ method @FlaggedApi("android.media.tv.flags.enable_ad_service_fw") public void onTvAdSessionData(@NonNull String, @NonNull android.os.Bundle);
method public void onTvMessage(int, @NonNull android.os.Bundle);
method public void onUnblockContent(android.media.tv.TvContentRating);
+ method @FlaggedApi("android.media.tv.flags.enable_ad_service_fw") public void sendTvInputSessionData(@NonNull String, @NonNull android.os.Bundle);
method public void setOverlayViewEnabled(boolean);
}
@@ -27549,6 +27607,7 @@
method public boolean onUnhandledInputEvent(android.view.InputEvent);
method public void overrideTvAppAttributionSource(@NonNull android.content.AttributionSource);
method public void reset();
+ method @FlaggedApi("android.media.tv.flags.tiaf_v_apis") public void resumePlayback();
method public void selectAudioPresentation(int, int);
method public void selectTrack(int, String);
method public void sendAppPrivateCommand(@NonNull String, android.os.Bundle);
@@ -27559,8 +27618,10 @@
method public void setStreamVolume(@FloatRange(from=0.0, to=1.0) float);
method public void setTimeShiftPositionCallback(@Nullable android.media.tv.TvView.TimeShiftPositionCallback);
method public void setTvMessageEnabled(int, boolean);
+ method @FlaggedApi("android.media.tv.flags.tiaf_v_apis") public void setVideoFrozen(boolean);
method public void setZOrderMediaOverlay(boolean);
method public void setZOrderOnTop(boolean);
+ method @FlaggedApi("android.media.tv.flags.tiaf_v_apis") public void stopPlayback(int);
method public void timeShiftPause();
method public void timeShiftPlay(String, android.net.Uri);
method public void timeShiftResume();
@@ -27601,6 +27662,7 @@
method public void onTuned(@NonNull String, @NonNull android.net.Uri);
method public void onTvMessage(@NonNull String, int, @NonNull android.os.Bundle);
method public void onVideoAvailable(String);
+ method @FlaggedApi("android.media.tv.flags.tiaf_v_apis") public void onVideoFreezeUpdated(@NonNull String, boolean);
method public void onVideoSizeChanged(String, int, int);
method public void onVideoUnavailable(String, int);
}
@@ -27610,6 +27672,104 @@
package android.media.tv.ad {
@FlaggedApi("android.media.tv.flags.enable_ad_service_fw") public class TvAdManager {
+ method @NonNull public java.util.List<android.media.tv.ad.TvAdServiceInfo> getTvAdServiceList();
+ method public void registerCallback(@NonNull java.util.concurrent.Executor, @NonNull android.media.tv.ad.TvAdManager.TvAdServiceCallback);
+ method public void sendAppLinkCommand(@NonNull String, @NonNull android.os.Bundle);
+ method public void unregisterCallback(@NonNull android.media.tv.ad.TvAdManager.TvAdServiceCallback);
+ field public static final String ACTION_APP_LINK_COMMAND = "android.media.tv.ad.action.APP_LINK_COMMAND";
+ field public static final String APP_LINK_KEY_BACK_URI = "back_uri";
+ field public static final String APP_LINK_KEY_CLASS_NAME = "class_name";
+ field public static final String APP_LINK_KEY_COMMAND_TYPE = "command_type";
+ field public static final String APP_LINK_KEY_PACKAGE_NAME = "package_name";
+ field public static final String APP_LINK_KEY_SERVICE_ID = "service_id";
+ field public static final String INTENT_KEY_AD_SERVICE_ID = "ad_service_id";
+ field public static final String INTENT_KEY_CHANNEL_URI = "channel_uri";
+ field public static final String INTENT_KEY_COMMAND_TYPE = "command_type";
+ field public static final String INTENT_KEY_TV_INPUT_ID = "tv_input_id";
+ field public static final String SESSION_DATA_KEY_AD_BUFFER = "ad_buffer";
+ field public static final String SESSION_DATA_KEY_AD_REQUEST = "ad_request";
+ field public static final String SESSION_DATA_KEY_BROADCAST_INFO_REQUEST = "broadcast_info_request";
+ field public static final String SESSION_DATA_KEY_REQUEST_ID = "request_id";
+ field public static final String SESSION_DATA_TYPE_AD_BUFFER_READY = "ad_buffer_ready";
+ field public static final String SESSION_DATA_TYPE_AD_REQUEST = "ad_request";
+ field public static final String SESSION_DATA_TYPE_BROADCAST_INFO_REQUEST = "broadcast_info_request";
+ field public static final String SESSION_DATA_TYPE_REMOVE_BROADCAST_INFO_REQUEST = "remove_broadcast_info_request";
+ }
+
+ public abstract static class TvAdManager.TvAdServiceCallback {
+ ctor public TvAdManager.TvAdServiceCallback();
+ method public void onAdServiceAdded(@NonNull String);
+ method public void onAdServiceRemoved(@NonNull String);
+ method public void onAdServiceUpdated(@NonNull String);
+ }
+
+ @FlaggedApi("android.media.tv.flags.enable_ad_service_fw") public abstract class TvAdService extends android.app.Service {
+ ctor public TvAdService();
+ method public void onAppLinkCommand(@NonNull android.os.Bundle);
+ method @Nullable public final android.os.IBinder onBind(@Nullable android.content.Intent);
+ method @Nullable public abstract android.media.tv.ad.TvAdService.Session onCreateSession(@NonNull String, @NonNull String);
+ field public static final String SERVICE_INTERFACE = "android.media.tv.ad.TvAdService";
+ field public static final String SERVICE_META_DATA = "android.media.tv.ad.service";
+ }
+
+ public abstract static class TvAdService.Session implements android.view.KeyEvent.Callback {
+ ctor public TvAdService.Session(@NonNull android.content.Context);
+ method public boolean isMediaViewEnabled();
+ method @CallSuper public void layoutSurface(int, int, int, int);
+ method @Nullable public android.view.View onCreateMediaView();
+ method public boolean onGenericMotionEvent(@NonNull android.view.MotionEvent);
+ method public boolean onKeyDown(int, @Nullable android.view.KeyEvent);
+ method public boolean onKeyLongPress(int, @Nullable android.view.KeyEvent);
+ method public boolean onKeyMultiple(int, int, @Nullable android.view.KeyEvent);
+ method public boolean onKeyUp(int, @Nullable android.view.KeyEvent);
+ method public void onMediaViewSizeChanged(@Px int, @Px int);
+ method public abstract void onRelease();
+ method public void onResetAdService();
+ method public abstract boolean onSetSurface(@Nullable android.view.Surface);
+ method public void onStartAdService();
+ method public void onStopAdService();
+ method public void onSurfaceChanged(int, int, int);
+ method public boolean onTouchEvent(@NonNull android.view.MotionEvent);
+ method public boolean onTrackballEvent(@NonNull android.view.MotionEvent);
+ method public void onTvInputSessionData(@NonNull String, @NonNull android.os.Bundle);
+ method public void sendTvAdSessionData(@NonNull String, @NonNull android.os.Bundle);
+ method @CallSuper public void setMediaViewEnabled(boolean);
+ }
+
+ @FlaggedApi("android.media.tv.flags.enable_ad_service_fw") public final class TvAdServiceInfo implements android.os.Parcelable {
+ ctor public TvAdServiceInfo(@NonNull android.content.Context, @NonNull android.content.ComponentName);
+ method public int describeContents();
+ method @NonNull public String getId();
+ method @Nullable public android.content.pm.ServiceInfo getServiceInfo();
+ method @NonNull public java.util.List<java.lang.String> getSupportedTypes();
+ method public void writeToParcel(@NonNull android.os.Parcel, int);
+ field @NonNull public static final android.os.Parcelable.Creator<android.media.tv.ad.TvAdServiceInfo> CREATOR;
+ }
+
+ @FlaggedApi("android.media.tv.flags.enable_ad_service_fw") public class TvAdView extends android.view.ViewGroup {
+ ctor public TvAdView(@NonNull android.content.Context);
+ ctor public TvAdView(@NonNull android.content.Context, @Nullable android.util.AttributeSet);
+ ctor public TvAdView(@NonNull android.content.Context, @Nullable android.util.AttributeSet, int);
+ method public void clearOnUnhandledInputEventListener();
+ method public boolean dispatchUnhandledInputEvent(@NonNull android.view.InputEvent);
+ method @Nullable public android.media.tv.ad.TvAdView.OnUnhandledInputEventListener getOnUnhandledInputEventListener();
+ method public void onAttachedToWindow();
+ method public void onDetachedFromWindow();
+ method public void onLayout(boolean, int, int, int, int);
+ method public void onMeasure(int, int);
+ method public boolean onUnhandledInputEvent(@NonNull android.view.InputEvent);
+ method public void onVisibilityChanged(@NonNull android.view.View, int);
+ method public void prepareAdService(@NonNull String, @NonNull String);
+ method public void reset();
+ method public void resetAdService();
+ method public void setOnUnhandledInputEventListener(@NonNull java.util.concurrent.Executor, @NonNull android.media.tv.ad.TvAdView.OnUnhandledInputEventListener);
+ method public boolean setTvView(@Nullable android.media.tv.TvView);
+ method public void startAdService();
+ method public void stopAdService();
+ }
+
+ public static interface TvAdView.OnUnhandledInputEventListener {
+ method public boolean onUnhandledInputEvent(@NonNull android.view.InputEvent);
}
}
@@ -27722,6 +27882,7 @@
method public void onAdResponse(@NonNull android.media.tv.AdResponse);
method public void onAvailableSpeeds(@NonNull float[]);
method public void onBroadcastInfoResponse(@NonNull android.media.tv.BroadcastInfoResponse);
+ method @FlaggedApi("android.media.tv.flags.tiaf_v_apis") public void onCertificate(@NonNull String, int, @NonNull android.net.http.SslCertificate);
method public void onContentAllowed();
method public void onContentBlocked(@NonNull android.media.tv.TvContentRating);
method public void onCreateBiInteractiveAppRequest(@NonNull android.net.Uri, @Nullable android.os.Bundle);
@@ -27747,6 +27908,7 @@
method public void onRecordingTuned(@NonNull String, @NonNull android.net.Uri);
method public abstract void onRelease();
method public void onResetInteractiveApp();
+ method @FlaggedApi("android.media.tv.flags.tiaf_v_apis") public void onSelectedTrackInfo(@NonNull java.util.List<android.media.tv.TvTrackInfo>);
method public abstract boolean onSetSurface(@Nullable android.view.Surface);
method public void onSetTeletextAppEnabled(boolean);
method public void onSignalStrength(int);
@@ -27770,18 +27932,22 @@
method public void onTvRecordingInfo(@Nullable android.media.tv.TvRecordingInfo);
method public void onTvRecordingInfoList(@NonNull java.util.List<android.media.tv.TvRecordingInfo>);
method public void onVideoAvailable();
+ method @FlaggedApi("android.media.tv.flags.tiaf_v_apis") public void onVideoFreezeUpdated(boolean);
method public void onVideoUnavailable(int);
method @CallSuper public void removeBroadcastInfo(int);
method @CallSuper public void requestAd(@NonNull android.media.tv.AdRequest);
method @CallSuper public void requestAvailableSpeeds();
method @CallSuper public void requestBroadcastInfo(@NonNull android.media.tv.BroadcastInfoRequest);
+ method @FlaggedApi("android.media.tv.flags.tiaf_v_apis") @CallSuper public void requestCertificate(@NonNull String, int);
method @CallSuper public void requestCurrentChannelLcn();
method @CallSuper public void requestCurrentChannelUri();
method @CallSuper public void requestCurrentTvInputId();
method @CallSuper public void requestCurrentVideoBounds();
method @CallSuper public void requestScheduleRecording(@NonNull String, @NonNull String, @NonNull android.net.Uri, @NonNull android.net.Uri, @NonNull android.os.Bundle);
method @CallSuper public void requestScheduleRecording(@NonNull String, @NonNull String, @NonNull android.net.Uri, long, long, int, @NonNull android.os.Bundle);
+ method @FlaggedApi("android.media.tv.flags.tiaf_v_apis") @CallSuper public void requestSelectedTrackInfo();
method @CallSuper public void requestSigning(@NonNull String, @NonNull String, @NonNull String, @NonNull byte[]);
+ method @FlaggedApi("android.media.tv.flags.tiaf_v_apis") @CallSuper public void requestSigning(@NonNull String, @NonNull String, @NonNull String, int, @NonNull byte[]);
method @CallSuper public void requestStartRecording(@NonNull String, @Nullable android.net.Uri);
method @CallSuper public void requestStopRecording(@NonNull String);
method @CallSuper public void requestStreamVolume();
@@ -27831,6 +27997,7 @@
method public void notifyTimeShiftStartPositionChanged(@NonNull String, long);
method public void notifyTimeShiftStatusChanged(@NonNull String, int);
method public void notifyTvMessage(@NonNull int, @NonNull android.os.Bundle);
+ method @FlaggedApi("android.media.tv.flags.tiaf_v_apis") public void notifyVideoFreezeUpdated(boolean);
method public void onAttachedToWindow();
method public void onDetachedFromWindow();
method public void onLayout(boolean, int, int, int, int);
@@ -27841,10 +28008,12 @@
method public void reset();
method public void resetInteractiveApp();
method public void sendAvailableSpeeds(@NonNull float[]);
+ method @FlaggedApi("android.media.tv.flags.tiaf_v_apis") public void sendCertificate(@NonNull String, int, @NonNull android.net.http.SslCertificate);
method public void sendCurrentChannelLcn(int);
method public void sendCurrentChannelUri(@Nullable android.net.Uri);
method public void sendCurrentTvInputId(@Nullable String);
method public void sendCurrentVideoBounds(@NonNull android.graphics.Rect);
+ method @FlaggedApi("android.media.tv.flags.tiaf_v_apis") public void sendSelectedTrackInfo(@Nullable java.util.List<android.media.tv.TvTrackInfo>);
method public void sendSigningResult(@NonNull String, @NonNull byte[]);
method public void sendStreamVolume(float);
method public void sendTimeShiftMode(int);
@@ -27874,13 +28043,16 @@
method public void onBiInteractiveAppCreated(@NonNull String, @NonNull android.net.Uri, @Nullable String);
method public void onPlaybackCommandRequest(@NonNull String, @NonNull String, @NonNull android.os.Bundle);
method public void onRequestAvailableSpeeds(@NonNull String);
+ method @FlaggedApi("android.media.tv.flags.tiaf_v_apis") public void onRequestCertificate(@NonNull String, @NonNull String, int);
method public void onRequestCurrentChannelLcn(@NonNull String);
method public void onRequestCurrentChannelUri(@NonNull String);
method public void onRequestCurrentTvInputId(@NonNull String);
method public void onRequestCurrentVideoBounds(@NonNull String);
method public void onRequestScheduleRecording(@NonNull String, @NonNull String, @NonNull String, @NonNull android.net.Uri, @NonNull android.net.Uri, @NonNull android.os.Bundle);
method public void onRequestScheduleRecording(@NonNull String, @NonNull String, @NonNull String, @NonNull android.net.Uri, long, long, int, @NonNull android.os.Bundle);
+ method @FlaggedApi("android.media.tv.flags.tiaf_v_apis") public void onRequestSelectedTrackInfo(@NonNull String);
method public void onRequestSigning(@NonNull String, @NonNull String, @NonNull String, @NonNull String, @NonNull byte[]);
+ method @FlaggedApi("android.media.tv.flags.tiaf_v_apis") public void onRequestSigning(@NonNull String, @NonNull String, @NonNull String, @NonNull String, int, @NonNull byte[]);
method public void onRequestStartRecording(@NonNull String, @NonNull String, @Nullable android.net.Uri);
method public void onRequestStopRecording(@NonNull String, @NonNull String);
method public void onRequestStreamVolume(@NonNull String);
@@ -42300,6 +42472,8 @@
method public void onConference(android.telecom.Connection, android.telecom.Connection);
method public void onConnectionServiceFocusGained();
method public void onConnectionServiceFocusLost();
+ method @FlaggedApi("com.android.server.telecom.flags.telecom_resolve_hidden_dependencies") public void onCreateConferenceComplete(@NonNull android.telecom.Conference);
+ method @FlaggedApi("com.android.server.telecom.flags.telecom_resolve_hidden_dependencies") public void onCreateConnectionComplete(@NonNull android.telecom.Connection);
method @Nullable public android.telecom.Conference onCreateIncomingConference(@NonNull android.telecom.PhoneAccountHandle, @NonNull android.telecom.ConnectionRequest);
method public void onCreateIncomingConferenceFailed(@Nullable android.telecom.PhoneAccountHandle, @Nullable android.telecom.ConnectionRequest);
method public android.telecom.Connection onCreateIncomingConnection(android.telecom.PhoneAccountHandle, android.telecom.ConnectionRequest);
@@ -43467,13 +43641,44 @@
}
public static final class CarrierConfigManager.ImsEmergency {
+ field @FlaggedApi("com.android.internal.telephony.flags.use_oem_domain_selection_service") public static final int DOMAIN_CS = 1; // 0x1
+ field @FlaggedApi("com.android.internal.telephony.flags.use_oem_domain_selection_service") public static final int DOMAIN_PS_3GPP = 2; // 0x2
+ field @FlaggedApi("com.android.internal.telephony.flags.use_oem_domain_selection_service") public static final int DOMAIN_PS_NON_3GPP = 3; // 0x3
+ field @FlaggedApi("com.android.internal.telephony.flags.use_oem_domain_selection_service") public static final String KEY_CROSS_STACK_REDIAL_TIMER_SEC_INT = "imsemergency.cross_stack_redial_timer_sec_int";
field public static final String KEY_EMERGENCY_CALLBACK_MODE_SUPPORTED_BOOL = "imsemergency.emergency_callback_mode_supported_bool";
+ field @FlaggedApi("com.android.internal.telephony.flags.use_oem_domain_selection_service") public static final String KEY_EMERGENCY_CALL_SETUP_TIMER_ON_CURRENT_NETWORK_SEC_INT = "imsemergency.emergency_call_setup_timer_on_current_network_sec_int";
+ field @FlaggedApi("com.android.internal.telephony.flags.use_oem_domain_selection_service") public static final String KEY_EMERGENCY_CDMA_PREFERRED_NUMBERS_STRING_ARRAY = "imsemergency.emergency_cdma_preferred_numbers_string_array";
+ field @FlaggedApi("com.android.internal.telephony.flags.use_oem_domain_selection_service") public static final String KEY_EMERGENCY_DOMAIN_PREFERENCE_INT_ARRAY = "imsemergency.emergency_domain_preference_int_array";
+ field @FlaggedApi("com.android.internal.telephony.flags.use_oem_domain_selection_service") public static final String KEY_EMERGENCY_DOMAIN_PREFERENCE_ROAMING_INT_ARRAY = "imsemergency.emergency_domain_preference_roaming_int_array";
+ field @FlaggedApi("com.android.internal.telephony.flags.use_oem_domain_selection_service") public static final String KEY_EMERGENCY_LTE_PREFERRED_AFTER_NR_FAILED_BOOL = "imsemergency.emergency_lte_preferred_after_nr_failed_bool";
+ field @FlaggedApi("com.android.internal.telephony.flags.use_oem_domain_selection_service") public static final String KEY_EMERGENCY_NETWORK_SCAN_TYPE_INT = "imsemergency.emergency_network_scan_type_int";
+ field @FlaggedApi("com.android.internal.telephony.flags.use_oem_domain_selection_service") public static final String KEY_EMERGENCY_OVER_CS_ROAMING_SUPPORTED_ACCESS_NETWORK_TYPES_INT_ARRAY = "imsemergency.emergency_over_cs_roaming_supported_access_network_types_int_array";
+ field @FlaggedApi("com.android.internal.telephony.flags.use_oem_domain_selection_service") public static final String KEY_EMERGENCY_OVER_CS_SUPPORTED_ACCESS_NETWORK_TYPES_INT_ARRAY = "imsemergency.emergency_over_cs_supported_access_network_types_int_array";
+ field @FlaggedApi("com.android.internal.telephony.flags.use_oem_domain_selection_service") public static final String KEY_EMERGENCY_OVER_IMS_ROAMING_SUPPORTED_3GPP_NETWORK_TYPES_INT_ARRAY = "imsemergency.emergency_over_ims_roaming_supported_3gpp_network_types_int_array";
+ field @FlaggedApi("com.android.internal.telephony.flags.use_oem_domain_selection_service") public static final String KEY_EMERGENCY_OVER_IMS_SUPPORTED_3GPP_NETWORK_TYPES_INT_ARRAY = "imsemergency.emergency_over_ims_supported_3gpp_network_types_int_array";
field public static final String KEY_EMERGENCY_OVER_IMS_SUPPORTED_RATS_INT_ARRAY = "imsemergency.emergency_over_ims_supported_rats_int_array";
field public static final String KEY_EMERGENCY_QOS_PRECONDITION_SUPPORTED_BOOL = "imsemergency.emergency_qos_precondition_supported_bool";
field public static final String KEY_EMERGENCY_REGISTRATION_TIMER_MILLIS_INT = "imsemergency.emergency_registration_timer_millis_int";
+ field @FlaggedApi("com.android.internal.telephony.flags.use_oem_domain_selection_service") public static final String KEY_EMERGENCY_REQUIRES_IMS_REGISTRATION_BOOL = "imsemergency.emergency_requires_ims_registration_bool";
+ field @FlaggedApi("com.android.internal.telephony.flags.use_oem_domain_selection_service") public static final String KEY_EMERGENCY_REQUIRES_VOLTE_ENABLED_BOOL = "imsemergency.emergency_requires_volte_enabled_bool";
+ field @FlaggedApi("com.android.internal.telephony.flags.use_oem_domain_selection_service") public static final String KEY_EMERGENCY_SCAN_TIMER_SEC_INT = "imsemergency.emergency_scan_timer_sec_int";
+ field @FlaggedApi("com.android.internal.telephony.flags.use_oem_domain_selection_service") public static final String KEY_EMERGENCY_VOWIFI_REQUIRES_CONDITION_INT = "imsemergency.emergency_vowifi_requires_condition_int";
+ field @FlaggedApi("com.android.internal.telephony.flags.use_oem_domain_selection_service") public static final String KEY_MAXIMUM_CELLULAR_SEARCH_TIMER_SEC_INT = "imsemergency.maximum_cellular_search_timer_sec_int";
+ field @FlaggedApi("com.android.internal.telephony.flags.use_oem_domain_selection_service") public static final String KEY_MAXIMUM_NUMBER_OF_EMERGENCY_TRIES_OVER_VOWIFI_INT = "imsemergency.maximum_number_of_emergency_tries_over_vowifi_int";
+ field @FlaggedApi("com.android.internal.telephony.flags.use_oem_domain_selection_service") public static final String KEY_PREFER_IMS_EMERGENCY_WHEN_VOICE_CALLS_ON_CS_BOOL = "imsemergency.prefer_ims_emergency_when_voice_calls_on_cs_bool";
field public static final String KEY_PREFIX = "imsemergency.";
+ field @FlaggedApi("com.android.internal.telephony.flags.use_oem_domain_selection_service") public static final String KEY_QUICK_CROSS_STACK_REDIAL_TIMER_SEC_INT = "imsemergency.quick_cross_stack_redial_timer_sec_int";
field public static final String KEY_REFRESH_GEOLOCATION_TIMEOUT_MILLIS_INT = "imsemergency.refresh_geolocation_timeout_millis_int";
field public static final String KEY_RETRY_EMERGENCY_ON_IMS_PDN_BOOL = "imsemergency.retry_emergency_on_ims_pdn_bool";
+ field @FlaggedApi("com.android.internal.telephony.flags.use_oem_domain_selection_service") public static final String KEY_SCAN_LIMITED_SERVICE_AFTER_VOLTE_FAILURE_BOOL = "imsemergency.scan_limited_service_after_volte_failure_bool";
+ field @FlaggedApi("com.android.internal.telephony.flags.use_oem_domain_selection_service") public static final String KEY_START_QUICK_CROSS_STACK_REDIAL_TIMER_WHEN_REGISTERED_BOOL = "imsemergency.start_quick_cross_stack_redial_timer_when_registered_bool";
+ field @FlaggedApi("com.android.internal.telephony.flags.use_oem_domain_selection_service") public static final int REDIAL_TIMER_DISABLED = 0; // 0x0
+ field @FlaggedApi("com.android.internal.telephony.flags.use_oem_domain_selection_service") public static final int SCAN_TYPE_FULL_SERVICE = 1; // 0x1
+ field @FlaggedApi("com.android.internal.telephony.flags.use_oem_domain_selection_service") public static final int SCAN_TYPE_FULL_SERVICE_FOLLOWED_BY_LIMITED_SERVICE = 2; // 0x2
+ field @FlaggedApi("com.android.internal.telephony.flags.use_oem_domain_selection_service") public static final int SCAN_TYPE_NO_PREFERENCE = 0; // 0x0
+ field @FlaggedApi("com.android.internal.telephony.flags.use_oem_domain_selection_service") public static final int VOWIFI_REQUIRES_NONE = 0; // 0x0
+ field @FlaggedApi("com.android.internal.telephony.flags.use_oem_domain_selection_service") public static final int VOWIFI_REQUIRES_SETTING_ENABLED = 1; // 0x1
+ field @FlaggedApi("com.android.internal.telephony.flags.use_oem_domain_selection_service") public static final int VOWIFI_REQUIRES_VALID_EID = 2; // 0x2
}
public static final class CarrierConfigManager.ImsRtt {
@@ -45156,13 +45361,13 @@
method @FlaggedApi("com.android.internal.telephony.flags.enforce_subscription_user_filter") @NonNull @RequiresPermission(android.Manifest.permission.INTERACT_ACROSS_PROFILES) public android.telephony.SubscriptionManager createForAllUserProfiles();
method @NonNull @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public android.os.ParcelUuid createSubscriptionGroup(@NonNull java.util.List<java.lang.Integer>);
method @Deprecated public static android.telephony.SubscriptionManager from(android.content.Context);
- method public java.util.List<android.telephony.SubscriptionInfo> getAccessibleSubscriptionInfoList();
+ method @Nullable public java.util.List<android.telephony.SubscriptionInfo> getAccessibleSubscriptionInfoList();
method public static int getActiveDataSubscriptionId();
method @RequiresPermission(android.Manifest.permission.READ_PHONE_STATE) public android.telephony.SubscriptionInfo getActiveSubscriptionInfo(int);
method @RequiresPermission(android.Manifest.permission.READ_PHONE_STATE) public int getActiveSubscriptionInfoCount();
method public int getActiveSubscriptionInfoCountMax();
method @RequiresPermission(android.Manifest.permission.READ_PHONE_STATE) public android.telephony.SubscriptionInfo getActiveSubscriptionInfoForSimSlotIndex(int);
- method @RequiresPermission(android.Manifest.permission.READ_PHONE_STATE) public java.util.List<android.telephony.SubscriptionInfo> getActiveSubscriptionInfoList();
+ method @Nullable @RequiresPermission(android.Manifest.permission.READ_PHONE_STATE) public java.util.List<android.telephony.SubscriptionInfo> getActiveSubscriptionInfoList();
method @NonNull @RequiresPermission(anyOf={android.Manifest.permission.READ_PHONE_STATE, "carrier privileges"}) public java.util.List<android.telephony.SubscriptionInfo> getAllSubscriptionInfoList();
method @NonNull public java.util.List<android.telephony.SubscriptionInfo> getCompleteActiveSubscriptionInfoList();
method public static int getDefaultDataSubscriptionId();
@@ -46100,6 +46305,7 @@
method @NonNull public android.telephony.euicc.EuiccManager createForCardId(int);
method @RequiresPermission("android.permission.WRITE_EMBEDDED_SUBSCRIPTIONS") public void deleteSubscription(int, android.app.PendingIntent);
method @RequiresPermission("android.permission.WRITE_EMBEDDED_SUBSCRIPTIONS") public void downloadSubscription(android.telephony.euicc.DownloadableSubscription, boolean, android.app.PendingIntent);
+ method @FlaggedApi("com.android.internal.telephony.flags.esim_available_memory") @RequiresPermission(anyOf={android.Manifest.permission.READ_PHONE_STATE, "android.permission.READ_PRIVILEGED_PHONE_STATE", "carrier privileges"}) public long getAvailableMemoryInBytes();
method @Nullable public String getEid();
method @Nullable public android.telephony.euicc.EuiccInfo getEuiccInfo();
method public boolean isEnabled();
@@ -46132,6 +46338,7 @@
field public static final int ERROR_SIM_MISSING = 10008; // 0x2718
field public static final int ERROR_TIME_OUT = 10005; // 0x2715
field public static final int ERROR_UNSUPPORTED_VERSION = 10007; // 0x2717
+ field @FlaggedApi("com.android.internal.telephony.flags.esim_available_memory") public static final long EUICC_MEMORY_FIELD_UNAVAILABLE = -1L; // 0xffffffffffffffffL
field public static final String EXTRA_EMBEDDED_SUBSCRIPTION_DETAILED_CODE = "android.telephony.euicc.extra.EMBEDDED_SUBSCRIPTION_DETAILED_CODE";
field public static final String EXTRA_EMBEDDED_SUBSCRIPTION_DOWNLOADABLE_SUBSCRIPTION = "android.telephony.euicc.extra.EMBEDDED_SUBSCRIPTION_DOWNLOADABLE_SUBSCRIPTION";
field public static final String EXTRA_EMBEDDED_SUBSCRIPTION_ERROR_CODE = "android.telephony.euicc.extra.EMBEDDED_SUBSCRIPTION_ERROR_CODE";
@@ -47105,7 +47312,7 @@
method public int getLineForOffset(int);
method public int getLineForVertical(int);
method public float getLineLeft(int);
- method @FlaggedApi("com.android.text.flags.inter_character_justification") @IntRange(from=0) public int getLineLetterSpacingUnitCount(@IntRange(from=0) int, boolean);
+ method @FlaggedApi("com.android.text.flags.letter_spacing_justification") @IntRange(from=0) public int getLineLetterSpacingUnitCount(@IntRange(from=0) int, boolean);
method public float getLineMax(int);
method public float getLineRight(int);
method @FlaggedApi("com.android.text.flags.use_bounds_for_width") public final float getLineSpacingAmount();
@@ -47156,7 +47363,7 @@
field @NonNull public static final android.text.Layout.TextInclusionStrategy INCLUSION_STRATEGY_ANY_OVERLAP;
field @NonNull public static final android.text.Layout.TextInclusionStrategy INCLUSION_STRATEGY_CONTAINS_ALL;
field @NonNull public static final android.text.Layout.TextInclusionStrategy INCLUSION_STRATEGY_CONTAINS_CENTER;
- field @FlaggedApi("com.android.text.flags.inter_character_justification") public static final int JUSTIFICATION_MODE_INTER_CHARACTER = 2; // 0x2
+ field @FlaggedApi("com.android.text.flags.letter_spacing_justification") public static final int JUSTIFICATION_MODE_INTER_CHARACTER = 2; // 0x2
field public static final int JUSTIFICATION_MODE_INTER_WORD = 1; // 0x1
field public static final int JUSTIFICATION_MODE_NONE = 0; // 0x0
}
@@ -50436,6 +50643,10 @@
method public boolean isFromSource(int);
}
+ @FlaggedApi("com.android.input.flags.input_device_view_behavior_api") public static final class InputDevice.ViewBehavior {
+ method @FlaggedApi("com.android.input.flags.input_device_view_behavior_api") public boolean shouldSmoothScroll(int, int);
+ }
+
public abstract class InputEvent implements android.os.Parcelable {
method public int describeContents();
method public final android.view.InputDevice getDevice();
@@ -53416,8 +53627,8 @@
method @NonNull public abstract android.view.LayoutInflater getLayoutInflater();
method protected final int getLocalFeatures();
method public android.media.session.MediaController getMediaController();
- method @ColorInt public abstract int getNavigationBarColor();
- method @ColorInt public int getNavigationBarDividerColor();
+ method @Deprecated @ColorInt public abstract int getNavigationBarColor();
+ method @Deprecated @ColorInt public int getNavigationBarDividerColor();
method @NonNull public android.window.OnBackInvokedDispatcher getOnBackInvokedDispatcher();
method public android.transition.Transition getReenterTransition();
method public android.transition.Transition getReturnTransition();
@@ -53427,7 +53638,7 @@
method public android.transition.Transition getSharedElementReenterTransition();
method public android.transition.Transition getSharedElementReturnTransition();
method public boolean getSharedElementsUseOverlay();
- method @ColorInt public abstract int getStatusBarColor();
+ method @Deprecated @ColorInt public abstract int getStatusBarColor();
method @NonNull public java.util.List<android.graphics.Rect> getSystemGestureExclusionRects();
method public long getTransitionBackgroundFadeDuration();
method public android.transition.TransitionManager getTransitionManager();
@@ -53443,7 +53654,7 @@
method public abstract boolean isFloating();
method public boolean isNavigationBarContrastEnforced();
method public abstract boolean isShortcutKey(int, android.view.KeyEvent);
- method public boolean isStatusBarContrastEnforced();
+ method @Deprecated public boolean isStatusBarContrastEnforced();
method public boolean isWideColorGamut();
method public final void makeActive();
method protected abstract void onActive();
@@ -53475,7 +53686,7 @@
method public abstract void setContentView(android.view.View);
method public abstract void setContentView(android.view.View, android.view.ViewGroup.LayoutParams);
method public abstract void setDecorCaptionShade(int);
- method public void setDecorFitsSystemWindows(boolean);
+ method @Deprecated public void setDecorFitsSystemWindows(boolean);
method protected void setDefaultWindowFormat(int);
method @FlaggedApi("com.android.graphics.hwui.flags.limited_hdr") public void setDesiredHdrHeadroom(@FloatRange(from=0.0f, to=10000.0) float);
method public void setDimAmount(float);
@@ -53497,9 +53708,9 @@
method public void setLocalFocus(boolean, boolean);
method public void setLogo(@DrawableRes int);
method public void setMediaController(android.media.session.MediaController);
- method public abstract void setNavigationBarColor(@ColorInt int);
+ method @Deprecated public abstract void setNavigationBarColor(@ColorInt int);
method public void setNavigationBarContrastEnforced(boolean);
- method public void setNavigationBarDividerColor(@ColorInt int);
+ method @Deprecated public void setNavigationBarDividerColor(@ColorInt int);
method public void setPreferMinimalPostProcessing(boolean);
method public void setReenterTransition(android.transition.Transition);
method public abstract void setResizingCaptionDrawable(android.graphics.drawable.Drawable);
@@ -53511,8 +53722,8 @@
method public void setSharedElementReturnTransition(android.transition.Transition);
method public void setSharedElementsUseOverlay(boolean);
method public void setSoftInputMode(int);
- method public abstract void setStatusBarColor(@ColorInt int);
- method public void setStatusBarContrastEnforced(boolean);
+ method @Deprecated public abstract void setStatusBarColor(@ColorInt int);
+ method @Deprecated public void setStatusBarContrastEnforced(boolean);
method public void setSustainedPerformanceMode(boolean);
method public void setSystemGestureExclusionRects(@NonNull java.util.List<android.graphics.Rect>);
method public abstract void setTitle(CharSequence);
@@ -53784,6 +53995,7 @@
method public default void addCrossWindowBlurEnabledListener(@NonNull java.util.function.Consumer<java.lang.Boolean>);
method public default void addCrossWindowBlurEnabledListener(@NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<java.lang.Boolean>);
method public default void addProposedRotationListener(@NonNull java.util.concurrent.Executor, @NonNull java.util.function.IntConsumer);
+ method @FlaggedApi("com.android.window.flags.screen_recording_callbacks") @RequiresPermission(android.Manifest.permission.DETECT_SCREEN_RECORDING) public default int addScreenRecordingCallback(@NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<java.lang.Integer>);
method @NonNull public default android.view.WindowMetrics getCurrentWindowMetrics();
method @Deprecated public android.view.Display getDefaultDisplay();
method @NonNull public default android.view.WindowMetrics getMaximumWindowMetrics();
@@ -53793,11 +54005,13 @@
method @FlaggedApi("com.android.window.flags.surface_control_input_receiver") public default void registerUnbatchedSurfaceControlInputReceiver(int, @NonNull android.os.IBinder, @NonNull android.view.SurfaceControl, @NonNull android.os.Looper, @NonNull android.view.SurfaceControlInputReceiver);
method public default void removeCrossWindowBlurEnabledListener(@NonNull java.util.function.Consumer<java.lang.Boolean>);
method public default void removeProposedRotationListener(@NonNull java.util.function.IntConsumer);
+ method @FlaggedApi("com.android.window.flags.screen_recording_callbacks") @RequiresPermission(android.Manifest.permission.DETECT_SCREEN_RECORDING) public default void removeScreenRecordingCallback(@NonNull java.util.function.Consumer<java.lang.Integer>);
method public void removeViewImmediate(android.view.View);
method @FlaggedApi("com.android.window.flags.surface_control_input_receiver") public default void unregisterSurfaceControlInputReceiver(@NonNull android.view.SurfaceControl);
method @FlaggedApi("com.android.window.flags.trusted_presentation_listener_for_window") public default void unregisterTrustedPresentationListener(@NonNull java.util.function.Consumer<java.lang.Boolean>);
field public static final String PROPERTY_ACTIVITY_EMBEDDING_ALLOW_SYSTEM_OVERRIDE = "android.window.PROPERTY_ACTIVITY_EMBEDDING_ALLOW_SYSTEM_OVERRIDE";
field public static final String PROPERTY_ACTIVITY_EMBEDDING_SPLITS_ENABLED = "android.window.PROPERTY_ACTIVITY_EMBEDDING_SPLITS_ENABLED";
+ field @FlaggedApi("com.android.window.flags.untrusted_embedding_state_sharing") public static final String PROPERTY_ALLOW_UNTRUSTED_ACTIVITY_EMBEDDING_STATE_SHARING = "android.window.PROPERTY_ALLOW_UNTRUSTED_ACTIVITY_EMBEDDING_STATE_SHARING";
field public static final String PROPERTY_CAMERA_COMPAT_ALLOW_FORCE_ROTATION = "android.window.PROPERTY_CAMERA_COMPAT_ALLOW_FORCE_ROTATION";
field public static final String PROPERTY_CAMERA_COMPAT_ALLOW_REFRESH = "android.window.PROPERTY_CAMERA_COMPAT_ALLOW_REFRESH";
field public static final String PROPERTY_CAMERA_COMPAT_ENABLE_REFRESH_VIA_PAUSE = "android.window.PROPERTY_CAMERA_COMPAT_ENABLE_REFRESH_VIA_PAUSE";
@@ -53812,6 +54026,8 @@
field public static final String PROPERTY_COMPAT_ENABLE_FAKE_FOCUS = "android.window.PROPERTY_COMPAT_ENABLE_FAKE_FOCUS";
field public static final String PROPERTY_COMPAT_IGNORE_REQUESTED_ORIENTATION = "android.window.PROPERTY_COMPAT_IGNORE_REQUESTED_ORIENTATION";
field @FlaggedApi("com.android.window.flags.supports_multi_instance_system_ui") public static final String PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI = "android.window.PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI";
+ field @FlaggedApi("com.android.window.flags.screen_recording_callbacks") public static final int SCREEN_RECORDING_STATE_NOT_VISIBLE = 0; // 0x0
+ field @FlaggedApi("com.android.window.flags.screen_recording_callbacks") public static final int SCREEN_RECORDING_STATE_VISIBLE = 1; // 0x1
}
public static class WindowManager.BadTokenException extends java.lang.RuntimeException {
@@ -59525,7 +59741,7 @@
method public void setRadioGroupChecked(@IdRes int, @IdRes int);
method public void setRelativeScrollPosition(@IdRes int, int);
method @Deprecated public void setRemoteAdapter(int, @IdRes int, android.content.Intent);
- method @Deprecated public void setRemoteAdapter(@IdRes int, android.content.Intent);
+ method public void setRemoteAdapter(@IdRes int, android.content.Intent);
method public void setRemoteAdapter(@IdRes int, @NonNull android.widget.RemoteViews.RemoteCollectionItems);
method public void setScrollPosition(@IdRes int, int);
method public void setShort(@IdRes int, String, short);
diff --git a/core/api/lint-baseline.txt b/core/api/lint-baseline.txt
index 162f54c..e901f00 100644
--- a/core/api/lint-baseline.txt
+++ b/core/api/lint-baseline.txt
@@ -389,6 +389,12 @@
Invalid nullability on parameter `intent` in method `onBind`. Parameters of overrides cannot be NonNull if the super parameter is unannotated.
+KotlinOperator: android.graphics.Matrix44#get(int, int):
+ Method can be invoked with an indexing operator from Kotlin: `get` (this is usually desirable; just make sure it makes sense for this type of object)
+KotlinOperator: android.graphics.Matrix44#set(int, int, float):
+ Method can be invoked with an indexing operator from Kotlin: `set` (this is usually desirable; just make sure it makes sense for this type of object)
+
+
RequiresPermission: android.accounts.AccountManager#getAccountsByTypeAndFeatures(String, String[], android.accounts.AccountManagerCallback<android.accounts.Account[]>, android.os.Handler):
Method 'getAccountsByTypeAndFeatures' documentation mentions permissions without declaring @RequiresPermission
RequiresPermission: android.accounts.AccountManager#hasFeatures(android.accounts.Account, String[], android.accounts.AccountManagerCallback<java.lang.Boolean>, android.os.Handler):
@@ -477,6 +483,8 @@
Method 'clearResetPasswordToken' documentation mentions permissions already declared by @RequiresPermission
RequiresPermission: android.app.admin.DevicePolicyManager#generateKeyPair(android.content.ComponentName, String, android.security.keystore.KeyGenParameterSpec, int):
Method 'generateKeyPair' documentation mentions permissions already declared by @RequiresPermission
+RequiresPermission: android.app.admin.DevicePolicyManager#getContentProtectionPolicy(android.content.ComponentName):
+ Method 'getContentProtectionPolicy' documentation mentions permissions already declared by @RequiresPermission
RequiresPermission: android.app.admin.DevicePolicyManager#getCrossProfileWidgetProviders(android.content.ComponentName):
Method 'getCrossProfileWidgetProviders' documentation mentions permissions already declared by @RequiresPermission
RequiresPermission: android.app.admin.DevicePolicyManager#getLockTaskFeatures(android.content.ComponentName):
@@ -513,6 +521,8 @@
Method 'removeCrossProfileWidgetProvider' documentation mentions permissions already declared by @RequiresPermission
RequiresPermission: android.app.admin.DevicePolicyManager#setAlwaysOnVpnPackage(android.content.ComponentName, String, boolean):
Method 'setAlwaysOnVpnPackage' documentation mentions permissions without declaring @RequiresPermission
+RequiresPermission: android.app.admin.DevicePolicyManager#setContentProtectionPolicy(android.content.ComponentName, int):
+ Method 'setContentProtectionPolicy' documentation mentions permissions already declared by @RequiresPermission
RequiresPermission: android.app.admin.DevicePolicyManager#setLockTaskFeatures(android.content.ComponentName, int):
Method 'setLockTaskFeatures' documentation mentions permissions already declared by @RequiresPermission
RequiresPermission: android.app.admin.DevicePolicyManager#setLockTaskPackages(android.content.ComponentName, String[]):
diff --git a/core/api/module-lib-current.txt b/core/api/module-lib-current.txt
index 55ed1f5..1273da7 100644
--- a/core/api/module-lib-current.txt
+++ b/core/api/module-lib-current.txt
@@ -132,6 +132,11 @@
field public static final int MATCH_STATIC_SHARED_AND_SDK_LIBRARIES = 67108864; // 0x4000000
}
+ @FlaggedApi("android.permission.flags.enhanced_confirmation_mode_apis_enabled") public class SignedPackage {
+ method @NonNull public byte[] getCertificateDigest();
+ method @NonNull public String getPkgName();
+ }
+
}
package android.hardware.usb {
@@ -187,6 +192,7 @@
method @NonNull public static android.media.BluetoothProfileConnectionInfo createA2dpInfo(boolean, int);
method @NonNull public static android.media.BluetoothProfileConnectionInfo createA2dpSinkInfo(int);
method @NonNull public static android.media.BluetoothProfileConnectionInfo createHearingAidInfo(boolean);
+ method @FlaggedApi("android.media.audio.sco_managed_by_audio") @NonNull public static android.media.BluetoothProfileConnectionInfo createHfpInfo();
method @NonNull public static android.media.BluetoothProfileConnectionInfo createLeAudioInfo(boolean, boolean);
method @NonNull public static android.media.BluetoothProfileConnectionInfo createLeAudioOutputInfo(boolean, int);
method public int describeContents();
@@ -344,7 +350,9 @@
package android.os {
public class ArtModuleServiceManager {
+ method @FlaggedApi("android.content.pm.use_art_service_v2") @NonNull public android.os.ArtModuleServiceManager.ServiceRegisterer getArtdPreRebootServiceRegisterer();
method @NonNull public android.os.ArtModuleServiceManager.ServiceRegisterer getArtdServiceRegisterer();
+ method @FlaggedApi("android.content.pm.use_art_service_v2") @NonNull public android.os.ArtModuleServiceManager.ServiceRegisterer getDexoptChrootSetupServiceRegisterer();
}
public static final class ArtModuleServiceManager.ServiceRegisterer {
@@ -378,6 +386,10 @@
field public static final int DEVICE_INITIAL_SDK_INT;
}
+ public class Environment {
+ method @FlaggedApi("android.crashrecovery.flags.enable_crashrecovery") @NonNull public static java.io.File getDataSystemDeDirectory();
+ }
+
public class IpcDataCache<Query, Result> {
ctor public IpcDataCache(int, @NonNull String, @NonNull String, @NonNull String, @NonNull android.os.IpcDataCache.QueryHandler<Query,Result>);
method public void disableForCurrentProcess();
@@ -427,6 +439,8 @@
public class SystemConfigManager {
method @NonNull public java.util.List<android.content.ComponentName> getEnabledComponentOverrides(@NonNull String);
+ method @FlaggedApi("android.permission.flags.enhanced_confirmation_mode_apis_enabled") @NonNull @RequiresPermission(android.Manifest.permission.MANAGE_ENHANCED_CONFIRMATION_STATES) public java.util.Set<android.content.pm.SignedPackage> getEnhancedConfirmationTrustedInstallers();
+ method @FlaggedApi("android.permission.flags.enhanced_confirmation_mode_apis_enabled") @NonNull @RequiresPermission(android.Manifest.permission.MANAGE_ENHANCED_CONFIRMATION_STATES) public java.util.Set<android.content.pm.SignedPackage> getEnhancedConfirmationTrustedPackages();
}
public final class Trace {
diff --git a/core/api/module-lib-lint-baseline.txt b/core/api/module-lib-lint-baseline.txt
index a2179bc..42c4efc 100644
--- a/core/api/module-lib-lint-baseline.txt
+++ b/core/api/module-lib-lint-baseline.txt
@@ -611,6 +611,8 @@
Method 'generateKeyPair' documentation mentions permissions already declared by @RequiresPermission
RequiresPermission: android.app.admin.DevicePolicyManager#getApplicationExemptions(String):
Method 'getApplicationExemptions' documentation mentions permissions already declared by @RequiresPermission
+RequiresPermission: android.app.admin.DevicePolicyManager#getContentProtectionPolicy(android.content.ComponentName):
+ Method 'getContentProtectionPolicy' documentation mentions permissions already declared by @RequiresPermission
RequiresPermission: android.app.admin.DevicePolicyManager#getCrossProfileWidgetProviders(android.content.ComponentName):
Method 'getCrossProfileWidgetProviders' documentation mentions permissions already declared by @RequiresPermission
RequiresPermission: android.app.admin.DevicePolicyManager#getLockTaskFeatures(android.content.ComponentName):
@@ -657,6 +659,8 @@
Method 'setAlwaysOnVpnPackage' documentation mentions permissions without declaring @RequiresPermission
RequiresPermission: android.app.admin.DevicePolicyManager#setApplicationExemptions(String, java.util.Set<java.lang.Integer>):
Method 'setApplicationExemptions' documentation mentions permissions already declared by @RequiresPermission
+RequiresPermission: android.app.admin.DevicePolicyManager#setContentProtectionPolicy(android.content.ComponentName, int):
+ Method 'setContentProtectionPolicy' documentation mentions permissions already declared by @RequiresPermission
RequiresPermission: android.app.admin.DevicePolicyManager#setDeviceProvisioningConfigApplied():
Method 'setDeviceProvisioningConfigApplied' documentation mentions permissions already declared by @RequiresPermission
RequiresPermission: android.app.admin.DevicePolicyManager#setLockTaskFeatures(android.content.ComponentName, int):
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index e6040f8..a942e0d 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -11,6 +11,7 @@
field public static final String ACCESS_DRM_CERTIFICATES = "android.permission.ACCESS_DRM_CERTIFICATES";
field @Deprecated public static final String ACCESS_FM_RADIO = "android.permission.ACCESS_FM_RADIO";
field public static final String ACCESS_FPS_COUNTER = "android.permission.ACCESS_FPS_COUNTER";
+ field @FlaggedApi("android.multiuser.flags.enable_permission_to_access_hidden_profiles") public static final String ACCESS_HIDDEN_PROFILES_FULL = "android.permission.ACCESS_HIDDEN_PROFILES_FULL";
field public static final String ACCESS_INSTANT_APPS = "android.permission.ACCESS_INSTANT_APPS";
field @FlaggedApi("com.android.server.telecom.flags.telecom_resolve_hidden_dependencies") public static final String ACCESS_LAST_KNOWN_CELL_ID = "android.permission.ACCESS_LAST_KNOWN_CELL_ID";
field public static final String ACCESS_LOCUS_ID_USAGE_STATS = "android.permission.ACCESS_LOCUS_ID_USAGE_STATS";
@@ -287,7 +288,7 @@
field public static final String READ_DEVICE_CONFIG = "android.permission.READ_DEVICE_CONFIG";
field public static final String READ_DREAM_STATE = "android.permission.READ_DREAM_STATE";
field public static final String READ_GLOBAL_APP_SEARCH_DATA = "android.permission.READ_GLOBAL_APP_SEARCH_DATA";
- field public static final String READ_INSTALLED_SESSION_PATHS = "android.permission.READ_INSTALLED_SESSION_PATHS";
+ field @FlaggedApi("android.content.pm.get_resolved_apk_path") public static final String READ_INSTALLED_SESSION_PATHS = "android.permission.READ_INSTALLED_SESSION_PATHS";
field public static final String READ_INSTALL_SESSIONS = "android.permission.READ_INSTALL_SESSIONS";
field public static final String READ_NETWORK_USAGE_HISTORY = "android.permission.READ_NETWORK_USAGE_HISTORY";
field public static final String READ_OEM_UNLOCK_STATE = "android.permission.READ_OEM_UNLOCK_STATE";
@@ -3150,14 +3151,38 @@
package android.app.wearable {
+ @FlaggedApi("android.app.wearable.enable_data_request_observer_api") public final class WearableSensingDataRequest implements android.os.Parcelable {
+ method public int describeContents();
+ method public int getDataSize();
+ method public int getDataType();
+ method public static int getMaxRequestSize();
+ method public static int getRateLimit();
+ method @NonNull public static java.time.Duration getRateLimitWindowSize();
+ method @NonNull public android.os.PersistableBundle getRequestDetails();
+ method public void writeToParcel(@NonNull android.os.Parcel, int);
+ field @NonNull public static final android.os.Parcelable.Creator<android.app.wearable.WearableSensingDataRequest> CREATOR;
+ }
+
+ @FlaggedApi("android.app.wearable.enable_data_request_observer_api") public static final class WearableSensingDataRequest.Builder {
+ ctor public WearableSensingDataRequest.Builder(int);
+ method @NonNull public android.app.wearable.WearableSensingDataRequest build();
+ method @NonNull public android.app.wearable.WearableSensingDataRequest.Builder setRequestDetails(@NonNull android.os.PersistableBundle);
+ }
+
public class WearableSensingManager {
+ method @FlaggedApi("android.app.wearable.enable_data_request_observer_api") @Nullable public static android.app.wearable.WearableSensingDataRequest getDataRequestFromIntent(@NonNull android.content.Intent);
method @RequiresPermission(android.Manifest.permission.MANAGE_WEARABLE_SENSING_SERVICE) public void provideData(@NonNull android.os.PersistableBundle, @Nullable android.os.SharedMemory, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<java.lang.Integer>);
method @RequiresPermission(android.Manifest.permission.MANAGE_WEARABLE_SENSING_SERVICE) public void provideDataStream(@NonNull android.os.ParcelFileDescriptor, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<java.lang.Integer>);
+ method @FlaggedApi("android.app.wearable.enable_provide_wearable_connection_api") @RequiresPermission(android.Manifest.permission.MANAGE_WEARABLE_SENSING_SERVICE) public void provideWearableConnection(@NonNull android.os.ParcelFileDescriptor, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<java.lang.Integer>);
+ method @FlaggedApi("android.app.wearable.enable_data_request_observer_api") @RequiresPermission(android.Manifest.permission.MANAGE_WEARABLE_SENSING_SERVICE) public void registerDataRequestObserver(int, @NonNull android.app.PendingIntent, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<java.lang.Integer>);
+ method @FlaggedApi("android.app.wearable.enable_data_request_observer_api") @RequiresPermission(android.Manifest.permission.MANAGE_WEARABLE_SENSING_SERVICE) public void unregisterDataRequestObserver(int, @NonNull android.app.PendingIntent, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<java.lang.Integer>);
field public static final int STATUS_ACCESS_DENIED = 5; // 0x5
+ field @FlaggedApi("android.app.wearable.enable_provide_wearable_connection_api") public static final int STATUS_CHANNEL_ERROR = 7; // 0x7
field public static final int STATUS_SERVICE_UNAVAILABLE = 3; // 0x3
field public static final int STATUS_SUCCESS = 1; // 0x1
field public static final int STATUS_UNKNOWN = 0; // 0x0
field public static final int STATUS_UNSUPPORTED = 2; // 0x2
+ field @FlaggedApi("android.app.wearable.enable_data_request_observer_api") public static final int STATUS_UNSUPPORTED_DATA_TYPE = 8; // 0x8
field @FlaggedApi("android.app.wearable.enable_unsupported_operation_status_code") public static final int STATUS_UNSUPPORTED_OPERATION = 6; // 0x6
field public static final int STATUS_WEARABLE_UNAVAILABLE = 4; // 0x4
}
@@ -3944,7 +3969,9 @@
method @RequiresPermission("com.android.permission.USE_INSTALLER_V2") public void addFile(int, @NonNull String, long, @NonNull byte[], @Nullable byte[]);
method @RequiresPermission(android.Manifest.permission.INSTALL_PACKAGES) public void commitTransferred(@NonNull android.content.IntentSender);
method @Nullable @RequiresPermission("com.android.permission.USE_INSTALLER_V2") public android.content.pm.DataLoaderParams getDataLoaderParams();
+ method @FlaggedApi("android.content.pm.set_pre_verified_domains") @NonNull @RequiresPermission(android.Manifest.permission.ACCESS_INSTANT_APPS) public java.util.Set<java.lang.String> getPreVerifiedDomains();
method @RequiresPermission("com.android.permission.USE_INSTALLER_V2") public void removeFile(int, @NonNull String);
+ method @FlaggedApi("android.content.pm.set_pre_verified_domains") @RequiresPermission(android.Manifest.permission.ACCESS_INSTANT_APPS) public void setPreVerifiedDomains(@NonNull java.util.Set<java.lang.String>);
}
public static class PackageInstaller.SessionInfo implements android.os.Parcelable {
@@ -3991,6 +4018,7 @@
method @NonNull public boolean canUserUninstall(@NonNull String, @NonNull android.os.UserHandle);
method @NonNull public abstract java.util.List<android.content.IntentFilter> getAllIntentFilters(@NonNull String);
method @NonNull @RequiresPermission(android.Manifest.permission.GET_APP_METADATA) public android.os.PersistableBundle getAppMetadata(@NonNull String) throws android.content.pm.PackageManager.NameNotFoundException;
+ method @FlaggedApi("android.content.pm.asl_in_apk_app_metadata_source") @RequiresPermission(android.Manifest.permission.GET_APP_METADATA) public int getAppMetadataSource(@NonNull String) throws android.content.pm.PackageManager.NameNotFoundException;
method @NonNull @RequiresPermission(android.Manifest.permission.INTERACT_ACROSS_USERS) public android.content.pm.ApplicationInfo getApplicationInfoAsUser(@NonNull String, int, @NonNull android.os.UserHandle) throws android.content.pm.PackageManager.NameNotFoundException;
method @NonNull @RequiresPermission(android.Manifest.permission.INTERACT_ACROSS_USERS) public android.content.pm.ApplicationInfo getApplicationInfoAsUser(@NonNull String, @NonNull android.content.pm.PackageManager.ApplicationInfoFlags, @NonNull android.os.UserHandle) throws android.content.pm.PackageManager.NameNotFoundException;
method @NonNull public android.content.pm.dex.ArtManager getArtManager();
@@ -4043,6 +4071,10 @@
method @Deprecated @RequiresPermission(android.Manifest.permission.INTENT_FILTER_VERIFICATION_AGENT) public abstract void verifyIntentFilter(int, int, @NonNull java.util.List<java.lang.String>);
field public static final String ACTION_REQUEST_PERMISSIONS = "android.content.pm.action.REQUEST_PERMISSIONS";
field public static final String ACTION_REQUEST_PERMISSIONS_FOR_OTHER = "android.content.pm.action.REQUEST_PERMISSIONS_FOR_OTHER";
+ field @FlaggedApi("android.content.pm.asl_in_apk_app_metadata_source") public static final int APP_METADATA_SOURCE_APK = 1; // 0x1
+ field @FlaggedApi("android.content.pm.asl_in_apk_app_metadata_source") public static final int APP_METADATA_SOURCE_INSTALLER = 2; // 0x2
+ field @FlaggedApi("android.content.pm.asl_in_apk_app_metadata_source") public static final int APP_METADATA_SOURCE_SYSTEM_IMAGE = 3; // 0x3
+ field @FlaggedApi("android.content.pm.asl_in_apk_app_metadata_source") public static final int APP_METADATA_SOURCE_UNKNOWN = 0; // 0x0
field public static final int DELETE_ALL_USERS = 2; // 0x2
field public static final int DELETE_FAILED_ABORTED = -5; // 0xfffffffb
field public static final int DELETE_FAILED_DEVICE_POLICY_MANAGER = -2; // 0xfffffffe
@@ -4242,7 +4274,6 @@
public final class UserProperties implements android.os.Parcelable {
method public int describeContents();
method public int getCrossProfileContentSharingStrategy();
- method @FlaggedApi("android.multiuser.support_hiding_profiles") @NonNull public int getProfileApiVisibility();
method public int getShowInQuietMode();
method public int getShowInSharingSurfaces();
method public boolean isCredentialShareableWithParent();
@@ -4252,9 +4283,6 @@
field public static final int CROSS_PROFILE_CONTENT_SHARING_DELEGATE_FROM_PARENT = 1; // 0x1
field public static final int CROSS_PROFILE_CONTENT_SHARING_NO_DELEGATION = 0; // 0x0
field public static final int CROSS_PROFILE_CONTENT_SHARING_UNKNOWN = -1; // 0xffffffff
- field @FlaggedApi("android.multiuser.support_hiding_profiles") public static final int PROFILE_API_VISIBILITY_HIDDEN = 1; // 0x1
- field @FlaggedApi("android.multiuser.support_hiding_profiles") public static final int PROFILE_API_VISIBILITY_UNKNOWN = -1; // 0xffffffff
- field @FlaggedApi("android.multiuser.support_hiding_profiles") public static final int PROFILE_API_VISIBILITY_VISIBLE = 0; // 0x0
field public static final int SHOW_IN_QUIET_MODE_DEFAULT = 2; // 0x2
field public static final int SHOW_IN_QUIET_MODE_HIDDEN = 1; // 0x1
field public static final int SHOW_IN_QUIET_MODE_PAUSED = 0; // 0x0
@@ -4392,6 +4420,60 @@
package android.credentials.selection {
+ @FlaggedApi("android.credentials.flags.configurable_selector_ui_enabled") public final class AuthenticationEntry implements android.os.Parcelable {
+ ctor public AuthenticationEntry(@NonNull String, @NonNull String, @NonNull android.app.slice.Slice, int, @NonNull android.content.Intent);
+ method public int describeContents();
+ method @Nullable public android.content.Intent getFrameworkExtrasIntent();
+ method @NonNull public String getKey();
+ method @NonNull public android.app.slice.Slice getSlice();
+ method @NonNull public int getStatus();
+ method @NonNull public String getSubkey();
+ method public void writeToParcel(@NonNull android.os.Parcel, int);
+ field @NonNull public static final android.os.Parcelable.Creator<android.credentials.selection.AuthenticationEntry> CREATOR;
+ field public static final int STATUS_LOCKED = 0; // 0x0
+ field public static final int STATUS_UNLOCKED_BUT_EMPTY_LESS_RECENT = 1; // 0x1
+ field public static final int STATUS_UNLOCKED_BUT_EMPTY_MOST_RECENT = 2; // 0x2
+ }
+
+ @FlaggedApi("android.credentials.flags.configurable_selector_ui_enabled") public final class CancelSelectionRequest implements android.os.Parcelable {
+ ctor public CancelSelectionRequest(@NonNull android.credentials.selection.RequestToken, boolean, @NonNull String);
+ method public int describeContents();
+ method @NonNull public String getPackageName();
+ method @NonNull public android.credentials.selection.RequestToken getRequestToken();
+ method public boolean shouldShowCancellationExplanation();
+ method public void writeToParcel(@NonNull android.os.Parcel, int);
+ field @NonNull public static final android.os.Parcelable.Creator<android.credentials.selection.CancelSelectionRequest> CREATOR;
+ }
+
+ @FlaggedApi("android.credentials.flags.configurable_selector_ui_enabled") public final class CreateCredentialProviderInfo {
+ method @NonNull public String getProviderName();
+ method @Nullable public android.credentials.selection.Entry getRemoteEntry();
+ method @NonNull public java.util.List<android.credentials.selection.Entry> getSaveEntries();
+ }
+
+ @FlaggedApi("android.credentials.flags.configurable_selector_ui_enabled") public static final class CreateCredentialProviderInfo.Builder {
+ ctor public CreateCredentialProviderInfo.Builder(@NonNull String);
+ method @NonNull public android.credentials.selection.CreateCredentialProviderInfo build();
+ method @NonNull public android.credentials.selection.CreateCredentialProviderInfo.Builder setRemoteEntry(@Nullable android.credentials.selection.Entry);
+ method @NonNull public android.credentials.selection.CreateCredentialProviderInfo.Builder setSaveEntries(@NonNull java.util.List<android.credentials.selection.Entry>);
+ }
+
+ @FlaggedApi("android.credentials.flags.configurable_selector_ui_enabled") public final class DisabledProviderInfo {
+ ctor public DisabledProviderInfo(@NonNull String);
+ method @NonNull public String getProviderName();
+ }
+
+ @FlaggedApi("android.credentials.flags.configurable_selector_ui_enabled") public final class Entry implements android.os.Parcelable {
+ ctor public Entry(@NonNull String, @NonNull String, @NonNull android.app.slice.Slice, @NonNull android.content.Intent);
+ method public int describeContents();
+ method @Nullable public android.content.Intent getFrameworkExtrasIntent();
+ method @NonNull public String getKey();
+ method @NonNull public android.app.slice.Slice getSlice();
+ method @NonNull public String getSubkey();
+ method public void writeToParcel(@NonNull android.os.Parcel, int);
+ field @NonNull public static final android.os.Parcelable.Creator<android.credentials.selection.Entry> CREATOR;
+ }
+
@FlaggedApi("android.credentials.flags.configurable_selector_ui_enabled") public final class FailureResult {
ctor public FailureResult(int, @Nullable String);
method public int getErrorCode();
@@ -4401,6 +4483,32 @@
field public static final int ERROR_CODE_UI_FAILURE = 0; // 0x0
}
+ @FlaggedApi("android.credentials.flags.configurable_selector_ui_enabled") public final class GetCredentialProviderInfo {
+ method @NonNull public java.util.List<android.credentials.selection.Entry> getActionChips();
+ method @NonNull public java.util.List<android.credentials.selection.AuthenticationEntry> getAuthenticationEntries();
+ method @NonNull public java.util.List<android.credentials.selection.Entry> getCredentialEntries();
+ method @NonNull public String getProviderName();
+ method @Nullable public android.credentials.selection.Entry getRemoteEntry();
+ }
+
+ @FlaggedApi("android.credentials.flags.configurable_selector_ui_enabled") public static final class GetCredentialProviderInfo.Builder {
+ ctor public GetCredentialProviderInfo.Builder(@NonNull String);
+ method @NonNull public android.credentials.selection.GetCredentialProviderInfo build();
+ method @NonNull public android.credentials.selection.GetCredentialProviderInfo.Builder setActionChips(@NonNull java.util.List<android.credentials.selection.Entry>);
+ method @NonNull public android.credentials.selection.GetCredentialProviderInfo.Builder setAuthenticationEntries(@NonNull java.util.List<android.credentials.selection.AuthenticationEntry>);
+ method @NonNull public android.credentials.selection.GetCredentialProviderInfo.Builder setCredentialEntries(@NonNull java.util.List<android.credentials.selection.Entry>);
+ method @NonNull public android.credentials.selection.GetCredentialProviderInfo.Builder setRemoteEntry(@Nullable android.credentials.selection.Entry);
+ }
+
+ @FlaggedApi("android.credentials.flags.configurable_selector_ui_enabled") public final class IntentHelper {
+ method @Nullable public static android.credentials.selection.CancelSelectionRequest extractCancelUiRequest(@NonNull android.content.Intent);
+ method @NonNull public static java.util.List<android.credentials.selection.CreateCredentialProviderInfo> extractCreateCredentialProviderInfoList(@NonNull android.content.Intent);
+ method @NonNull public static java.util.List<android.credentials.selection.DisabledProviderInfo> extractDisabledProviderInfoList(@NonNull android.content.Intent);
+ method @NonNull public static java.util.List<android.credentials.selection.GetCredentialProviderInfo> extractGetCredentialProviderInfoList(@NonNull android.content.Intent);
+ method @Nullable public static android.credentials.selection.RequestInfo extractRequestInfo(@NonNull android.content.Intent);
+ method @Nullable public static android.os.ResultReceiver extractResultReceiver(@NonNull android.content.Intent);
+ }
+
@FlaggedApi("android.credentials.flags.configurable_selector_ui_enabled") public final class ProviderPendingIntentResponse implements android.os.Parcelable {
ctor public ProviderPendingIntentResponse(int, @Nullable android.content.Intent);
method public int describeContents();
@@ -4410,6 +4518,27 @@
field @NonNull public static final android.os.Parcelable.Creator<android.credentials.selection.ProviderPendingIntentResponse> CREATOR;
}
+ @FlaggedApi("android.credentials.flags.configurable_selector_ui_enabled") public final class RequestInfo implements android.os.Parcelable {
+ method public int describeContents();
+ method @Nullable public android.credentials.CreateCredentialRequest getCreateCredentialRequest();
+ method @NonNull public java.util.List<java.lang.String> getDefaultProviderIds();
+ method @Nullable public android.credentials.GetCredentialRequest getGetCredentialRequest();
+ method @NonNull public String getPackageName();
+ method @NonNull public java.util.List<java.lang.String> getRegistryProviderIds();
+ method @NonNull public android.credentials.selection.RequestToken getRequestToken();
+ method @NonNull public String getType();
+ method public boolean hasPermissionToOverrideDefault();
+ method public boolean isShowAllOptionsRequested();
+ method public void writeToParcel(@NonNull android.os.Parcel, int);
+ field @NonNull public static final android.os.Parcelable.Creator<android.credentials.selection.RequestInfo> CREATOR;
+ field @NonNull public static final String TYPE_CREATE = "android.credentials.selection.TYPE_CREATE";
+ field @NonNull public static final String TYPE_GET = "android.credentials.selection.TYPE_GET";
+ field @NonNull public static final String TYPE_UNDEFINED = "android.credentials.selection.TYPE_UNDEFINED";
+ }
+
+ @FlaggedApi("android.credentials.flags.configurable_selector_ui_enabled") public final class RequestToken {
+ }
+
@FlaggedApi("android.credentials.flags.configurable_selector_ui_enabled") public final class ResultHelper {
method public static void sendFailureResult(@NonNull android.os.ResultReceiver, @NonNull android.credentials.selection.FailureResult);
method public static void sendUserSelectionResult(@NonNull android.os.ResultReceiver, @NonNull android.credentials.selection.UserSelectionResult);
@@ -4581,6 +4710,7 @@
ctor @FlaggedApi("com.android.internal.camera.flags.concert_mode") protected AdvancedExtender(@NonNull android.hardware.camera2.CameraManager);
method @FlaggedApi("com.android.internal.camera.flags.concert_mode") @NonNull public abstract java.util.List<android.hardware.camera2.CaptureRequest.Key> getAvailableCaptureRequestKeys(@NonNull String);
method @FlaggedApi("com.android.internal.camera.flags.concert_mode") @NonNull public abstract java.util.List<android.hardware.camera2.CaptureResult.Key> getAvailableCaptureResultKeys(@NonNull String);
+ method @FlaggedApi("com.android.internal.camera.flags.camera_extensions_characteristics_get") @NonNull public abstract java.util.List<android.util.Pair<android.hardware.camera2.CameraCharacteristics.Key,java.lang.Object>> getAvailableCharacteristicsKeyValues();
method @FlaggedApi("com.android.internal.camera.flags.concert_mode") public long getMetadataVendorId(@NonNull String);
method @FlaggedApi("com.android.internal.camera.flags.concert_mode") @NonNull public abstract android.hardware.camera2.extension.SessionProcessor getSessionProcessor();
method @FlaggedApi("com.android.internal.camera.flags.concert_mode") @NonNull public abstract java.util.Map<java.lang.Integer,java.util.List<android.util.Size>> getSupportedCaptureOutputResolutions(@NonNull String);
@@ -6846,6 +6976,8 @@
@FlaggedApi("android.media.audiopolicy.enable_fade_manager_configuration") public final class FadeManagerConfiguration implements android.os.Parcelable {
method public int describeContents();
method @NonNull public java.util.List<android.media.AudioAttributes> getAudioAttributesWithVolumeShaperConfigs();
+ method public static long getDefaultFadeInDurationMillis();
+ method public static long getDefaultFadeOutDurationMillis();
method public long getFadeInDelayForOffenders();
method public long getFadeInDurationForAudioAttributes(@NonNull android.media.AudioAttributes);
method public long getFadeInDurationForUsage(int);
@@ -6871,7 +7003,6 @@
field @NonNull public static final android.os.Parcelable.Creator<android.media.FadeManagerConfiguration> CREATOR;
field public static final long DURATION_NOT_SET = 0L; // 0x0L
field public static final int FADE_STATE_DISABLED = 0; // 0x0
- field public static final int FADE_STATE_ENABLED_AUTO = 2; // 0x2
field public static final int FADE_STATE_ENABLED_DEFAULT = 1; // 0x1
field public static final String TAG = "FadeManagerConfiguration";
field public static final int VOLUME_SHAPER_SYSTEM_FADE_ID = 2; // 0x2
@@ -6886,10 +7017,10 @@
method @NonNull public android.media.FadeManagerConfiguration.Builder addUnfadeableContentType(int);
method @NonNull public android.media.FadeManagerConfiguration.Builder addUnfadeableUid(int);
method @NonNull public android.media.FadeManagerConfiguration build();
- method @NonNull public android.media.FadeManagerConfiguration.Builder clearFadeableUsage(int);
- method @NonNull public android.media.FadeManagerConfiguration.Builder clearUnfadeableAudioAttributes(@NonNull android.media.AudioAttributes);
- method @NonNull public android.media.FadeManagerConfiguration.Builder clearUnfadeableContentType(int);
- method @NonNull public android.media.FadeManagerConfiguration.Builder clearUnfadeableUid(int);
+ method @NonNull public android.media.FadeManagerConfiguration.Builder clearFadeableUsages();
+ method @NonNull public android.media.FadeManagerConfiguration.Builder clearUnfadeableAudioAttributes();
+ method @NonNull public android.media.FadeManagerConfiguration.Builder clearUnfadeableContentTypes();
+ method @NonNull public android.media.FadeManagerConfiguration.Builder clearUnfadeableUids();
method @NonNull public android.media.FadeManagerConfiguration.Builder setFadeInDelayForOffenders(long);
method @NonNull public android.media.FadeManagerConfiguration.Builder setFadeInDurationForAudioAttributes(@NonNull android.media.AudioAttributes, long);
method @NonNull public android.media.FadeManagerConfiguration.Builder setFadeInDurationForUsage(int, long);
@@ -10063,6 +10194,7 @@
@FlaggedApi("android.nfc.enable_nfc_mainline") public final class ApduServiceInfo implements android.os.Parcelable {
ctor @FlaggedApi("android.nfc.enable_nfc_mainline") public ApduServiceInfo(@NonNull android.content.pm.PackageManager, @NonNull android.content.pm.ResolveInfo, boolean) throws java.io.IOException, org.xmlpull.v1.XmlPullParserException;
method @FlaggedApi("android.nfc.nfc_read_polling_loop") public void addPollingLoopFilter(@NonNull String);
+ method @FlaggedApi("android.nfc.nfc_read_polling_loop") public void addPollingLoopFilterToAutoTransact(@NonNull String);
method @FlaggedApi("android.nfc.enable_nfc_mainline") public int describeContents();
method @FlaggedApi("android.nfc.enable_nfc_mainline") public void dump(@NonNull android.os.ParcelFileDescriptor, @NonNull java.io.PrintWriter, @NonNull String[]);
method @FlaggedApi("android.nfc.enable_nfc_mainline") public void dumpDebug(@NonNull android.util.proto.ProtoOutputStream);
@@ -10076,6 +10208,7 @@
method @FlaggedApi("android.nfc.nfc_read_polling_loop") @NonNull public java.util.List<java.lang.String> getPollingLoopFilters();
method @FlaggedApi("android.nfc.enable_nfc_mainline") @NonNull public java.util.List<java.lang.String> getPrefixAids();
method @FlaggedApi("android.nfc.enable_nfc_mainline") @NonNull public String getSettingsActivityName();
+ method @FlaggedApi("android.nfc.nfc_read_polling_loop") public boolean getShouldAutoTransact(@NonNull String);
method @FlaggedApi("android.nfc.enable_nfc_mainline") @NonNull public java.util.List<java.lang.String> getSubsetAids();
method @FlaggedApi("android.nfc.enable_nfc_mainline") public int getUid();
method @FlaggedApi("android.nfc.enable_nfc_mainline") public boolean hasCategory(@NonNull String);
@@ -10282,6 +10415,7 @@
public final class ConfigUpdate {
field public static final String ACTION_UPDATE_CARRIER_ID_DB = "android.os.action.UPDATE_CARRIER_ID_DB";
field public static final String ACTION_UPDATE_CARRIER_PROVISIONING_URLS = "android.intent.action.UPDATE_CARRIER_PROVISIONING_URLS";
+ field @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") public static final String ACTION_UPDATE_CONFIG = "android.os.action.UPDATE_CONFIG";
field public static final String ACTION_UPDATE_CONVERSATION_ACTIONS = "android.intent.action.UPDATE_CONVERSATION_ACTIONS";
field public static final String ACTION_UPDATE_CT_LOGS = "android.intent.action.UPDATE_CT_LOGS";
field public static final String ACTION_UPDATE_EMERGENCY_NUMBER_DB = "android.os.action.UPDATE_EMERGENCY_NUMBER_DB";
@@ -10291,6 +10425,7 @@
field public static final String ACTION_UPDATE_PINS = "android.intent.action.UPDATE_PINS";
field public static final String ACTION_UPDATE_SMART_SELECTION = "android.intent.action.UPDATE_SMART_SELECTION";
field public static final String ACTION_UPDATE_SMS_SHORT_CODES = "android.intent.action.UPDATE_SMS_SHORT_CODES";
+ field @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") public static final String EXTRA_DOMAIN = "android.os.extra.DOMAIN";
field public static final String EXTRA_REQUIRED_HASH = "android.os.extra.REQUIRED_HASH";
field public static final String EXTRA_VERSION = "android.os.extra.VERSION";
}
@@ -11061,6 +11196,7 @@
method @WorkerThread public void allocateBytes(java.io.FileDescriptor, long, @RequiresPermission int) throws java.io.IOException;
method @WorkerThread public long getAllocatableBytes(@NonNull java.util.UUID, @RequiresPermission int) throws java.io.IOException;
method @RequiresPermission(android.Manifest.permission.WRITE_MEDIA_STORAGE) public int getExternalStorageMountMode(int, @NonNull String);
+ method @FlaggedApi("android.os.storage_lifetime_api") @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public int getInternalStorageRemainingLifetime();
method public static boolean hasIsolatedStorage();
method public void updateExternalStorageFileQuotaType(@NonNull java.io.File, int) throws java.io.IOException;
field @RequiresPermission(android.Manifest.permission.ALLOCATE_AGGRESSIVE) public static final int FLAG_ALLOCATE_AGGRESSIVE = 1; // 0x1
@@ -12291,6 +12427,7 @@
method @Deprecated public int onDownloadSubscription(int, @NonNull android.telephony.euicc.DownloadableSubscription, boolean, boolean);
method @Deprecated public abstract int onEraseSubscriptions(int);
method public int onEraseSubscriptions(int, int);
+ method @FlaggedApi("com.android.internal.telephony.flags.esim_available_memory") public long onGetAvailableMemoryInBytes(int);
method public abstract android.service.euicc.GetDefaultDownloadableSubscriptionListResult onGetDefaultDownloadableSubscriptionList(int, boolean);
method public abstract android.service.euicc.GetDownloadableSubscriptionMetadataResult onGetDownloadableSubscriptionMetadata(int, android.telephony.euicc.DownloadableSubscription, boolean);
method @NonNull public android.service.euicc.GetDownloadableSubscriptionMetadataResult onGetDownloadableSubscriptionMetadata(int, int, @NonNull android.telephony.euicc.DownloadableSubscription, boolean);
@@ -13314,12 +13451,24 @@
package android.service.wearable {
+ @FlaggedApi("android.app.wearable.enable_data_request_observer_api") public interface WearableSensingDataRequester {
+ method public void requestData(@NonNull android.app.wearable.WearableSensingDataRequest, @NonNull java.util.function.Consumer<java.lang.Integer>);
+ field public static final int STATUS_OBSERVER_CANCELLED = 2; // 0x2
+ field public static final int STATUS_SUCCESS = 1; // 0x1
+ field public static final int STATUS_TOO_FREQUENT = 4; // 0x4
+ field public static final int STATUS_TOO_LARGE = 3; // 0x3
+ field public static final int STATUS_UNKNOWN = 0; // 0x0
+ }
+
public abstract class WearableSensingService extends android.app.Service {
ctor public WearableSensingService();
method @Nullable public final android.os.IBinder onBind(@NonNull android.content.Intent);
method @BinderThread public abstract void onDataProvided(@NonNull android.os.PersistableBundle, @Nullable android.os.SharedMemory, @NonNull java.util.function.Consumer<java.lang.Integer>);
+ method @FlaggedApi("android.app.wearable.enable_data_request_observer_api") @BinderThread public void onDataRequestObserverRegistered(int, @NonNull String, @NonNull android.service.wearable.WearableSensingDataRequester, @NonNull java.util.function.Consumer<java.lang.Integer>);
+ method @FlaggedApi("android.app.wearable.enable_data_request_observer_api") @BinderThread public void onDataRequestObserverUnregistered(int, @NonNull String, @NonNull android.service.wearable.WearableSensingDataRequester, @NonNull java.util.function.Consumer<java.lang.Integer>);
method @BinderThread public abstract void onDataStreamProvided(@NonNull android.os.ParcelFileDescriptor, @NonNull java.util.function.Consumer<java.lang.Integer>);
method @BinderThread public abstract void onQueryServiceStatus(@NonNull java.util.Set<java.lang.Integer>, @NonNull String, @NonNull java.util.function.Consumer<android.service.ambientcontext.AmbientContextDetectionServiceStatus>);
+ method @FlaggedApi("android.app.wearable.enable_provide_wearable_connection_api") @BinderThread public void onSecureWearableConnectionProvided(@NonNull android.os.ParcelFileDescriptor, @NonNull java.util.function.Consumer<java.lang.Integer>);
method @BinderThread public abstract void onStartDetection(@NonNull android.app.ambientcontext.AmbientContextEventRequest, @NonNull String, @NonNull java.util.function.Consumer<android.service.ambientcontext.AmbientContextDetectionServiceStatus>, @NonNull java.util.function.Consumer<android.service.ambientcontext.AmbientContextDetectionResult>);
method public abstract void onStopDetection(@NonNull String);
field public static final String SERVICE_INTERFACE = "android.service.wearable.WearableSensingService";
@@ -13449,6 +13598,7 @@
public abstract class Connection extends android.telecom.Conferenceable {
method @Deprecated public final android.telecom.AudioState getAudioState();
+ method @FlaggedApi("com.android.server.telecom.flags.telecom_resolve_hidden_dependencies") public final int getCallDirection();
method @IntRange(from=0) public final long getConnectTimeMillis();
method public final long getConnectionStartElapsedRealtimeMillis();
method @Nullable public android.telecom.PhoneAccountHandle getPhoneAccountHandle();
@@ -13463,7 +13613,9 @@
method public void setTelecomCallId(@NonNull String);
field public static final int CAPABILITY_CONFERENCE_HAS_NO_CHILDREN = 2097152; // 0x200000
field public static final int CAPABILITY_SPEED_UP_MT_AUDIO = 262144; // 0x40000
+ field @FlaggedApi("com.android.server.telecom.flags.telecom_resolve_hidden_dependencies") public static final String EVENT_CALL_QUALITY_REPORT = "android.telecom.event.CALL_QUALITY_REPORT";
field public static final String EVENT_DEVICE_TO_DEVICE_MESSAGE = "android.telecom.event.DEVICE_TO_DEVICE_MESSAGE";
+ field @FlaggedApi("com.android.server.telecom.flags.telecom_resolve_hidden_dependencies") public static final String EXTRA_CALL_QUALITY_REPORT = "android.telecom.extra.CALL_QUALITY_REPORT";
field public static final String EXTRA_DEVICE_TO_DEVICE_MESSAGE_TYPE = "android.telecom.extra.DEVICE_TO_DEVICE_MESSAGE_TYPE";
field public static final String EXTRA_DEVICE_TO_DEVICE_MESSAGE_VALUE = "android.telecom.extra.DEVICE_TO_DEVICE_MESSAGE_VALUE";
field public static final String EXTRA_DISABLE_ADD_CALL = "android.telecom.extra.DISABLE_ADD_CALL";
@@ -13492,6 +13644,13 @@
method public final void addExistingConnection(@NonNull android.telecom.PhoneAccountHandle, @NonNull android.telecom.Connection, @NonNull android.telecom.Conference);
}
+ public final class DisconnectCause implements android.os.Parcelable {
+ ctor @FlaggedApi("com.android.server.telecom.flags.telecom_resolve_hidden_dependencies") public DisconnectCause(int, @NonNull CharSequence, @NonNull CharSequence, @NonNull String, int, int, int, @Nullable android.telephony.ims.ImsReasonInfo);
+ method @FlaggedApi("com.android.server.telecom.flags.telecom_resolve_hidden_dependencies") @Nullable public android.telephony.ims.ImsReasonInfo getImsReasonInfo();
+ method @FlaggedApi("com.android.server.telecom.flags.telecom_resolve_hidden_dependencies") public int getTelephonyDisconnectCause();
+ method @FlaggedApi("com.android.server.telecom.flags.telecom_resolve_hidden_dependencies") public int getTelephonyPreciseDisconnectCause();
+ }
+
public abstract class InCallService extends android.app.Service {
method @Deprecated public android.telecom.Phone getPhone();
method @Deprecated public void onPhoneCreated(android.telecom.Phone);
@@ -13731,6 +13890,7 @@
method @Deprecated public java.util.List<android.telecom.PhoneAccountHandle> getPhoneAccountsForPackage();
method @RequiresPermission(anyOf={android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE, android.Manifest.permission.READ_PHONE_STATE}) public java.util.List<android.telecom.PhoneAccountHandle> getPhoneAccountsSupportingScheme(String);
method @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public boolean isInEmergencyCall();
+ method @FlaggedApi("com.android.server.telecom.flags.telecom_resolve_hidden_dependencies") @RequiresPermission(allOf={android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE, android.Manifest.permission.INTERACT_ACROSS_USERS}) public boolean isInSelfManagedCall(@NonNull String, @NonNull android.os.UserHandle, boolean);
method @RequiresPermission(anyOf={android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE, android.Manifest.permission.READ_PHONE_STATE}) public boolean isRinging();
method @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public void setUserSelectedOutgoingPhoneAccount(@Nullable android.telecom.PhoneAccountHandle);
field public static final String ACTION_CURRENT_TTY_MODE_CHANGED = "android.telecom.action.CURRENT_TTY_MODE_CHANGED";
@@ -14063,6 +14223,73 @@
method @NonNull public android.telephony.DataThrottlingRequest.Builder setDataThrottlingAction(int);
}
+ @FlaggedApi("com.android.internal.telephony.flags.use_oem_domain_selection_service") public abstract class DomainSelectionService extends android.app.Service {
+ ctor public DomainSelectionService();
+ method public void onBarringInfoUpdated(int, int, @NonNull android.telephony.BarringInfo);
+ method @Nullable public final android.os.IBinder onBind(@Nullable android.content.Intent);
+ method @NonNull public java.util.concurrent.Executor onCreateExecutor();
+ method public abstract void onDomainSelection(@NonNull android.telephony.DomainSelectionService.SelectionAttributes, @NonNull android.telephony.TransportSelectorCallback);
+ method public void onServiceStateUpdated(int, int, @NonNull android.telephony.ServiceState);
+ field public static final int SCAN_TYPE_FULL_SERVICE = 2; // 0x2
+ field public static final int SCAN_TYPE_LIMITED_SERVICE = 1; // 0x1
+ field public static final int SCAN_TYPE_NO_PREFERENCE = 0; // 0x0
+ field public static final int SELECTOR_TYPE_CALLING = 1; // 0x1
+ field public static final int SELECTOR_TYPE_SMS = 2; // 0x2
+ }
+
+ @FlaggedApi("com.android.internal.telephony.flags.use_oem_domain_selection_service") public static final class DomainSelectionService.SelectionAttributes implements android.os.Parcelable {
+ method public int describeContents();
+ method @Nullable public android.net.Uri getAddress();
+ method @Nullable public String getCallId();
+ method public int getCsDisconnectCause();
+ method @Nullable public android.telephony.EmergencyRegistrationResult getEmergencyRegistrationResult();
+ method @Nullable public android.telephony.ims.ImsReasonInfo getPsDisconnectCause();
+ method public int getSelectorType();
+ method public int getSlotIndex();
+ method public int getSubscriptionId();
+ method public boolean isEmergency();
+ method public boolean isExitedFromAirplaneMode();
+ method public boolean isTestEmergencyNumber();
+ method public boolean isVideoCall();
+ method public void writeToParcel(@NonNull android.os.Parcel, int);
+ field @NonNull public static final android.os.Parcelable.Creator<android.telephony.DomainSelectionService.SelectionAttributes> CREATOR;
+ }
+
+ @FlaggedApi("com.android.internal.telephony.flags.use_oem_domain_selection_service") public static final class DomainSelectionService.SelectionAttributes.Builder {
+ ctor public DomainSelectionService.SelectionAttributes.Builder(int, int, int);
+ method @NonNull public android.telephony.DomainSelectionService.SelectionAttributes build();
+ method @NonNull public android.telephony.DomainSelectionService.SelectionAttributes.Builder setAddress(@Nullable android.net.Uri);
+ method @NonNull public android.telephony.DomainSelectionService.SelectionAttributes.Builder setCallId(@Nullable String);
+ method @NonNull public android.telephony.DomainSelectionService.SelectionAttributes.Builder setCsDisconnectCause(int);
+ method @NonNull public android.telephony.DomainSelectionService.SelectionAttributes.Builder setEmergency(boolean);
+ method @NonNull public android.telephony.DomainSelectionService.SelectionAttributes.Builder setEmergencyRegistrationResult(@Nullable android.telephony.EmergencyRegistrationResult);
+ method @NonNull public android.telephony.DomainSelectionService.SelectionAttributes.Builder setExitedFromAirplaneMode(boolean);
+ method @NonNull public android.telephony.DomainSelectionService.SelectionAttributes.Builder setPsDisconnectCause(@Nullable android.telephony.ims.ImsReasonInfo);
+ method @NonNull public android.telephony.DomainSelectionService.SelectionAttributes.Builder setTestEmergencyNumber(boolean);
+ method @NonNull public android.telephony.DomainSelectionService.SelectionAttributes.Builder setVideoCall(boolean);
+ }
+
+ @FlaggedApi("com.android.internal.telephony.flags.use_oem_domain_selection_service") public interface DomainSelector {
+ method public void finishSelection();
+ method public void reselectDomain(@NonNull android.telephony.DomainSelectionService.SelectionAttributes);
+ }
+
+ @FlaggedApi("com.android.internal.telephony.flags.use_oem_domain_selection_service") public final class EmergencyRegistrationResult implements android.os.Parcelable {
+ method public int describeContents();
+ method public int getAccessNetwork();
+ method @NonNull public String getCountryIso();
+ method public int getDomain();
+ method @NonNull public String getMcc();
+ method @NonNull public String getMnc();
+ method public int getNwProvidedEmc();
+ method public int getNwProvidedEmf();
+ method public int getRegState();
+ method public boolean isEmcBearerSupported();
+ method public boolean isVopsSupported();
+ method public void writeToParcel(@NonNull android.os.Parcel, int);
+ field @NonNull public static final android.os.Parcelable.Creator<android.telephony.EmergencyRegistrationResult> CREATOR;
+ }
+
public final class ImsiEncryptionInfo implements android.os.Parcelable {
method public int describeContents();
method @Nullable public String getKeyIdentifier();
@@ -14333,6 +14560,8 @@
field public static final int CHANNEL_UNACCEPTABLE = 6; // 0x6
field public static final int CONDITIONAL_IE_ERROR = 100; // 0x64
field public static final int DESTINATION_OUT_OF_ORDER = 27; // 0x1b
+ field @FlaggedApi("com.android.internal.telephony.flags.use_oem_domain_selection_service") public static final int EMERGENCY_PERM_FAILURE = 326; // 0x146
+ field @FlaggedApi("com.android.internal.telephony.flags.use_oem_domain_selection_service") public static final int EMERGENCY_TEMP_FAILURE = 325; // 0x145
field public static final int ERROR_UNSPECIFIED = 65535; // 0xffff
field public static final int FACILITY_REJECTED = 29; // 0x1d
field public static final int FDN_BLOCKED = 241; // 0xf1
@@ -14571,6 +14800,7 @@
method public boolean areUiccApplicationsEnabled();
method @Nullable public java.util.List<android.telephony.UiccAccessRule> getAccessRules();
method public int getProfileClass();
+ method @FlaggedApi("com.android.internal.telephony.flags.support_psim_to_esim_conversion") public int getTransferStatus();
method public boolean isGroupDisabled();
}
@@ -14580,7 +14810,7 @@
method @NonNull @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public int[] getActiveSubscriptionIdList();
method @Nullable @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public android.telephony.SubscriptionInfo getActiveSubscriptionInfoForIcc(@NonNull String);
method @NonNull @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public byte[] getAllSimSpecificSettingsForBackup();
- method public java.util.List<android.telephony.SubscriptionInfo> getAvailableSubscriptionInfoList();
+ method @Nullable public java.util.List<android.telephony.SubscriptionInfo> getAvailableSubscriptionInfoList();
method @NonNull @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public int[] getCompleteActiveSubscriptionIdList();
method @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public int getEnabledSubscriptionId(int);
method @NonNull public static android.content.res.Resources getResourcesForSubId(@NonNull android.content.Context, int);
@@ -14593,6 +14823,7 @@
method @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public void setDefaultVoiceSubscriptionId(int);
method @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public void setPreferredDataSubscriptionId(int, boolean, @Nullable java.util.concurrent.Executor, @Nullable java.util.function.Consumer<java.lang.Integer>);
method @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public boolean setSubscriptionEnabled(int, boolean);
+ method @FlaggedApi("com.android.internal.telephony.flags.support_psim_to_esim_conversion") @RequiresPermission(android.Manifest.permission.WRITE_EMBEDDED_SUBSCRIPTIONS) public void setTransferStatus(int, int);
method @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public void setUiccApplicationsEnabled(int, boolean);
field @RequiresPermission(android.Manifest.permission.MANAGE_SUBSCRIPTION_PLANS) public static final String ACTION_SUBSCRIPTION_PLANS_CHANGED = "android.telephony.action.SUBSCRIPTION_PLANS_CHANGED";
field @NonNull public static final android.net.Uri ADVANCED_CALLING_ENABLED_CONTENT_URI;
@@ -14602,6 +14833,9 @@
field public static final int PROFILE_CLASS_PROVISIONING = 1; // 0x1
field public static final int PROFILE_CLASS_TESTING = 0; // 0x0
field public static final int PROFILE_CLASS_UNSET = -1; // 0xffffffff
+ field @FlaggedApi("com.android.internal.telephony.flags.support_psim_to_esim_conversion") public static final int TRANSFER_STATUS_CONVERTED = 2; // 0x2
+ field @FlaggedApi("com.android.internal.telephony.flags.support_psim_to_esim_conversion") public static final int TRANSFER_STATUS_NONE = 0; // 0x0
+ field @FlaggedApi("com.android.internal.telephony.flags.support_psim_to_esim_conversion") public static final int TRANSFER_STATUS_TRANSFERRED_OUT = 1; // 0x1
field @NonNull public static final android.net.Uri VT_ENABLED_CONTENT_URI;
field @NonNull public static final android.net.Uri WFC_ENABLED_CONTENT_URI;
field @NonNull public static final android.net.Uri WFC_MODE_CONTENT_URI;
@@ -14815,6 +15049,7 @@
method @FlaggedApi("com.android.internal.telephony.flags.enable_identifier_disclosure_transparency") @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public boolean isCellularIdentifierDisclosureNotificationsEnabled();
method public boolean isDataConnectivityPossible();
method @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public boolean isDataEnabledForApn(int);
+ method @FlaggedApi("com.android.internal.telephony.flags.use_oem_domain_selection_service") @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public boolean isDomainSelectionSupported();
method @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public boolean isEmergencyAssistanceEnabled();
method @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) @WorkerThread public boolean isIccLockEnabled();
method @Deprecated @RequiresPermission(anyOf={android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE, android.Manifest.permission.READ_PHONE_STATE}) public boolean isIdle();
@@ -14834,7 +15069,7 @@
method @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public boolean matchesCurrentSimOperator(@NonNull String, int, @Nullable String);
method public boolean needsOtaServiceProvisioning();
method @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public void notifyOtaEmergencyNumberDbInstalled();
- method @FlaggedApi("com.android.server.telecom.flags.telecom_resolve_hidden_dependencies") @RequiresPermission(android.Manifest.permission.DUMP) public void persistEmergencyCallDiagnosticData(@NonNull String, @NonNull android.telephony.TelephonyManager.EmergencyCallDiagnosticParams);
+ method @FlaggedApi("com.android.server.telecom.flags.telecom_resolve_hidden_dependencies") @RequiresPermission(android.Manifest.permission.DUMP) public void persistEmergencyCallDiagnosticData(@NonNull String, @NonNull android.telephony.TelephonyManager.EmergencyCallDiagnosticData);
method @RequiresPermission(android.Manifest.permission.REBOOT) public int prepareForUnattendedReboot();
method @Deprecated @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public boolean rebootRadio();
method @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public void registerCarrierPrivilegesCallback(int, @NonNull java.util.concurrent.Executor, @NonNull android.telephony.TelephonyManager.CarrierPrivilegesCallback);
@@ -15013,19 +15248,19 @@
method public default void onCarrierServiceChanged(@Nullable String, int);
}
- @FlaggedApi("com.android.server.telecom.flags.telecom_resolve_hidden_dependencies") public static final class TelephonyManager.EmergencyCallDiagnosticParams {
+ @FlaggedApi("com.android.server.telecom.flags.telecom_resolve_hidden_dependencies") public static final class TelephonyManager.EmergencyCallDiagnosticData {
method public long getLogcatCollectionStartTimeMillis();
method public boolean isLogcatCollectionEnabled();
- method public boolean isTelecomDumpSysCollectionEnabled();
- method public boolean isTelephonyDumpSysCollectionEnabled();
+ method public boolean isTelecomDumpsysCollectionEnabled();
+ method public boolean isTelephonyDumpsysCollectionEnabled();
}
- public static final class TelephonyManager.EmergencyCallDiagnosticParams.Builder {
- ctor public TelephonyManager.EmergencyCallDiagnosticParams.Builder();
- method @NonNull public android.telephony.TelephonyManager.EmergencyCallDiagnosticParams build();
- method @NonNull public android.telephony.TelephonyManager.EmergencyCallDiagnosticParams.Builder setLogcatCollectionStartTimeMillis(long);
- method @NonNull public android.telephony.TelephonyManager.EmergencyCallDiagnosticParams.Builder setTelecomDumpSysCollectionEnabled(boolean);
- method @NonNull public android.telephony.TelephonyManager.EmergencyCallDiagnosticParams.Builder setTelephonyDumpSysCollectionEnabled(boolean);
+ public static final class TelephonyManager.EmergencyCallDiagnosticData.Builder {
+ ctor public TelephonyManager.EmergencyCallDiagnosticData.Builder();
+ method @NonNull public android.telephony.TelephonyManager.EmergencyCallDiagnosticData build();
+ method @NonNull public android.telephony.TelephonyManager.EmergencyCallDiagnosticData.Builder setLogcatCollectionStartTimeMillis(long);
+ method @NonNull public android.telephony.TelephonyManager.EmergencyCallDiagnosticData.Builder setTelecomDumpsysCollectionEnabled(boolean);
+ method @NonNull public android.telephony.TelephonyManager.EmergencyCallDiagnosticData.Builder setTelephonyDumpsysCollectionEnabled(boolean);
}
public static class TelephonyManager.ModemActivityInfoException extends java.lang.Exception {
@@ -15039,7 +15274,7 @@
@FlaggedApi("com.android.server.telecom.flags.telecom_resolve_hidden_dependencies") public class TelephonyRegistryManager {
method @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public void notifyCallStateChangedForAllSubscriptions(int, @Nullable String);
- method public void notifyOutgoingEmergencyCall(int, int, @NonNull android.telephony.emergency.EmergencyNumber);
+ method @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public void notifyOutgoingEmergencyCall(int, int, @NonNull android.telephony.emergency.EmergencyNumber);
}
public final class ThermalMitigationRequest implements android.os.Parcelable {
@@ -15060,6 +15295,13 @@
method @NonNull public android.telephony.ThermalMitigationRequest.Builder setThermalMitigationAction(int);
}
+ @FlaggedApi("com.android.internal.telephony.flags.use_oem_domain_selection_service") public interface TransportSelectorCallback {
+ method public void onCreated(@NonNull android.telephony.DomainSelector);
+ method public void onSelectionTerminated(int);
+ method public void onWlanSelected(boolean);
+ method public void onWwanSelected(@NonNull java.util.function.Consumer<android.telephony.WwanSelectorCallback>);
+ }
+
public final class UiccAccessRule implements android.os.Parcelable {
ctor public UiccAccessRule(byte[], @Nullable String, long);
method public int describeContents();
@@ -15115,6 +15357,11 @@
field @NonNull public static final android.os.Parcelable.Creator<android.telephony.VopsSupportInfo> CREATOR;
}
+ @FlaggedApi("com.android.internal.telephony.flags.use_oem_domain_selection_service") public interface WwanSelectorCallback {
+ method public void onDomainSelected(int, boolean);
+ method public void onRequestEmergencyNetworkScan(@NonNull java.util.List<java.lang.Integer>, int, boolean, @NonNull android.os.CancellationSignal, @NonNull java.util.function.Consumer<android.telephony.EmergencyRegistrationResult>);
+ }
+
}
package android.telephony.cdma {
@@ -15454,7 +15701,9 @@
method @RequiresPermission(android.Manifest.permission.WRITE_EMBEDDED_SUBSCRIPTIONS) public int getOtaStatus();
method @NonNull @RequiresPermission(android.Manifest.permission.WRITE_EMBEDDED_SUBSCRIPTIONS) public java.util.List<java.lang.String> getSupportedCountries();
method @NonNull @RequiresPermission(android.Manifest.permission.WRITE_EMBEDDED_SUBSCRIPTIONS) public java.util.List<java.lang.String> getUnsupportedCountries();
+ method @FlaggedApi("com.android.internal.telephony.flags.support_psim_to_esim_conversion") @RequiresPermission(android.Manifest.permission.WRITE_EMBEDDED_SUBSCRIPTIONS) public boolean isPsimConversionSupported(int);
method @RequiresPermission(android.Manifest.permission.WRITE_EMBEDDED_SUBSCRIPTIONS) public boolean isSupportedCountry(@NonNull String);
+ method @FlaggedApi("com.android.internal.telephony.flags.support_psim_to_esim_conversion") @RequiresPermission(android.Manifest.permission.WRITE_EMBEDDED_SUBSCRIPTIONS) public void setPsimConversionSupportedCarriers(@NonNull java.util.Set<java.lang.Integer>);
method @RequiresPermission(android.Manifest.permission.WRITE_EMBEDDED_SUBSCRIPTIONS) public void setSupportedCountries(@NonNull java.util.List<java.lang.String>);
method @RequiresPermission(android.Manifest.permission.WRITE_EMBEDDED_SUBSCRIPTIONS) public void setUnsupportedCountries(@NonNull java.util.List<java.lang.String>);
field @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public static final String ACTION_CONVERT_TO_EMBEDDED_SUBSCRIPTION = "android.telephony.euicc.action.CONVERT_TO_EMBEDDED_SUBSCRIPTION";
@@ -17178,6 +17427,19 @@
field @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") @NonNull public static final android.os.Parcelable.Creator<android.telephony.satellite.AntennaPosition> CREATOR;
}
+ @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") public class EnableRequestAttributes {
+ method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") public boolean isDemoMode();
+ method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") public boolean isEmergencyMode();
+ method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") public boolean isEnabled();
+ }
+
+ @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") public static final class EnableRequestAttributes.Builder {
+ ctor @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") public EnableRequestAttributes.Builder(boolean);
+ method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") @NonNull public android.telephony.satellite.EnableRequestAttributes build();
+ method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") @NonNull public android.telephony.satellite.EnableRequestAttributes.Builder setDemoMode(boolean);
+ method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") @NonNull public android.telephony.satellite.EnableRequestAttributes.Builder setEmergencyMode(boolean);
+ }
+
@FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") public final class NtnSignalStrength implements android.os.Parcelable {
ctor @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") public NtnSignalStrength(@Nullable android.telephony.satellite.NtnSignalStrength);
method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") public int describeContents();
@@ -17243,10 +17505,11 @@
method @FlaggedApi("com.android.internal.telephony.flags.carrier_enabled_satellite_flag") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public void removeAttachRestrictionForCarrier(int, int, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<java.lang.Integer>);
method @FlaggedApi("com.android.internal.telephony.flags.carrier_enabled_satellite_flag") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public void requestAttachEnabledForCarrier(int, boolean, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<java.lang.Integer>);
method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public void requestCapabilities(@NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<android.telephony.satellite.SatelliteCapabilities,android.telephony.satellite.SatelliteManager.SatelliteException>);
- method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public void requestEnabled(boolean, boolean, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<java.lang.Integer>);
+ method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public void requestEnabled(@NonNull android.telephony.satellite.EnableRequestAttributes, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<java.lang.Integer>);
method @FlaggedApi("com.android.internal.telephony.flags.carrier_enabled_satellite_flag") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public void requestIsAttachEnabledForCarrier(int, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Boolean,android.telephony.satellite.SatelliteManager.SatelliteException>);
method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public void requestIsCommunicationAllowedForCurrentLocation(@NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Boolean,android.telephony.satellite.SatelliteManager.SatelliteException>);
method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public void requestIsDemoModeEnabled(@NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Boolean,android.telephony.satellite.SatelliteManager.SatelliteException>);
+ method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public void requestIsEmergencyModeEnabled(@NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Boolean,android.telephony.satellite.SatelliteManager.SatelliteException>);
method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public void requestIsEnabled(@NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Boolean,android.telephony.satellite.SatelliteManager.SatelliteException>);
method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public void requestIsProvisioned(@NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Boolean,android.telephony.satellite.SatelliteManager.SatelliteException>);
method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") public void requestIsSupported(@NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Boolean,android.telephony.satellite.SatelliteManager.SatelliteException>);
@@ -17308,6 +17571,7 @@
field @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") public static final int SATELLITE_RESULT_INVALID_TELEPHONY_STATE = 6; // 0x6
field @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") public static final int SATELLITE_RESULT_MODEM_BUSY = 22; // 0x16
field @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") public static final int SATELLITE_RESULT_MODEM_ERROR = 4; // 0x4
+ field @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") public static final int SATELLITE_RESULT_MODEM_TIMEOUT = 24; // 0x18
field @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") public static final int SATELLITE_RESULT_NETWORK_ERROR = 5; // 0x5
field @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") public static final int SATELLITE_RESULT_NETWORK_TIMEOUT = 17; // 0x11
field @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") public static final int SATELLITE_RESULT_NOT_AUTHORIZED = 19; // 0x13
diff --git a/core/api/system-lint-baseline.txt b/core/api/system-lint-baseline.txt
index 6c83fd0..8a485d2 100644
--- a/core/api/system-lint-baseline.txt
+++ b/core/api/system-lint-baseline.txt
@@ -525,6 +525,10 @@
Avoid field names that are Kotlin hard keywords ("when"); see https://android.github.io/kotlin-guides/interop.html#no-hard-keywords
+KotlinOperator: android.hardware.camera2.extension.CharacteristicsMap#get(String):
+ Method can be invoked with an indexing operator from Kotlin: `get` (this is usually desirable; just make sure it makes sense for this type of object)
+
+
ListenerLast: android.telephony.satellite.SatelliteManager#stopSatelliteTransmissionUpdates(android.telephony.satellite.SatelliteTransmissionUpdateCallback, java.util.concurrent.Executor, java.util.function.Consumer<java.lang.Integer>) parameter #1:
Listeners should always be at end of argument list (method `stopSatelliteTransmissionUpdates`)
ListenerLast: android.telephony.satellite.SatelliteManager#stopSatelliteTransmissionUpdates(android.telephony.satellite.SatelliteTransmissionUpdateCallback, java.util.concurrent.Executor, java.util.function.Consumer<java.lang.Integer>) parameter #2:
@@ -685,6 +689,8 @@
Method 'generateKeyPair' documentation mentions permissions already declared by @RequiresPermission
RequiresPermission: android.app.admin.DevicePolicyManager#getApplicationExemptions(String):
Method 'getApplicationExemptions' documentation mentions permissions already declared by @RequiresPermission
+RequiresPermission: android.app.admin.DevicePolicyManager#getContentProtectionPolicy(android.content.ComponentName):
+ Method 'getContentProtectionPolicy' documentation mentions permissions already declared by @RequiresPermission
RequiresPermission: android.app.admin.DevicePolicyManager#getCrossProfileWidgetProviders(android.content.ComponentName):
Method 'getCrossProfileWidgetProviders' documentation mentions permissions already declared by @RequiresPermission
RequiresPermission: android.app.admin.DevicePolicyManager#getLockTaskFeatures(android.content.ComponentName):
@@ -731,6 +737,8 @@
Method 'setAlwaysOnVpnPackage' documentation mentions permissions without declaring @RequiresPermission
RequiresPermission: android.app.admin.DevicePolicyManager#setApplicationExemptions(String, java.util.Set<java.lang.Integer>):
Method 'setApplicationExemptions' documentation mentions permissions already declared by @RequiresPermission
+RequiresPermission: android.app.admin.DevicePolicyManager#setContentProtectionPolicy(android.content.ComponentName, int):
+ Method 'setContentProtectionPolicy' documentation mentions permissions already declared by @RequiresPermission
RequiresPermission: android.app.admin.DevicePolicyManager#setDeviceProvisioningConfigApplied():
Method 'setDeviceProvisioningConfigApplied' documentation mentions permissions already declared by @RequiresPermission
RequiresPermission: android.app.admin.DevicePolicyManager#setLockTaskFeatures(android.content.ComponentName, int):
@@ -2305,14 +2313,14 @@
New API must be flagged with @FlaggedApi: constructor android.telephony.satellite.SatelliteManager.SatelliteException(int)
UnflaggedApi: android.telephony.satellite.SatelliteManager.SatelliteException#getErrorCode():
New API must be flagged with @FlaggedApi: method android.telephony.satellite.SatelliteManager.SatelliteException.getErrorCode()
-UnflaggedApi: android.telephony.satellite.SatelliteProvisionStateCallback:
- New API must be flagged with @FlaggedApi: class android.telephony.satellite.SatelliteProvisionStateCallback
-UnflaggedApi: android.telephony.satellite.SatelliteProvisionStateCallback#onSatelliteProvisionStateChanged(boolean):
- New API must be flagged with @FlaggedApi: method android.telephony.satellite.SatelliteProvisionStateCallback.onSatelliteProvisionStateChanged(boolean)
UnflaggedApi: android.telephony.satellite.SatelliteModemStateCallback:
New API must be flagged with @FlaggedApi: class android.telephony.satellite.SatelliteModemStateCallback
UnflaggedApi: android.telephony.satellite.SatelliteModemStateCallback#onSatelliteModemStateChanged(int):
New API must be flagged with @FlaggedApi: method android.telephony.satellite.SatelliteModemStateCallback.onSatelliteModemStateChanged(int)
+UnflaggedApi: android.telephony.satellite.SatelliteProvisionStateCallback:
+ New API must be flagged with @FlaggedApi: class android.telephony.satellite.SatelliteProvisionStateCallback
+UnflaggedApi: android.telephony.satellite.SatelliteProvisionStateCallback#onSatelliteProvisionStateChanged(boolean):
+ New API must be flagged with @FlaggedApi: method android.telephony.satellite.SatelliteProvisionStateCallback.onSatelliteProvisionStateChanged(boolean)
UnflaggedApi: android.telephony.satellite.SatelliteTransmissionUpdateCallback:
New API must be flagged with @FlaggedApi: class android.telephony.satellite.SatelliteTransmissionUpdateCallback
UnflaggedApi: android.telephony.satellite.SatelliteTransmissionUpdateCallback#onReceiveDatagramStateChanged(int, int, int):
diff --git a/core/api/test-current.txt b/core/api/test-current.txt
index 79766ed..fc095d4 100644
--- a/core/api/test-current.txt
+++ b/core/api/test-current.txt
@@ -624,6 +624,7 @@
field public static final int OPERATION_SET_APPLICATION_HIDDEN = 15; // 0xf
field public static final int OPERATION_SET_APPLICATION_RESTRICTIONS = 16; // 0x10
field public static final int OPERATION_SET_CAMERA_DISABLED = 31; // 0x1f
+ field @FlaggedApi("android.view.contentprotection.flags.manage_device_policy_enabled") public static final int OPERATION_SET_CONTENT_PROTECTION_POLICY = 41; // 0x29
field public static final int OPERATION_SET_FACTORY_RESET_PROTECTION_POLICY = 32; // 0x20
field public static final int OPERATION_SET_GLOBAL_PRIVATE_DNS = 33; // 0x21
field public static final int OPERATION_SET_KEEP_UNINSTALLED_PACKAGES = 17; // 0x11
@@ -1267,22 +1268,6 @@
package android.credentials.selection {
- @FlaggedApi("android.credentials.flags.configurable_selector_ui_enabled") public final class AuthenticationEntry implements android.os.Parcelable {
- ctor public AuthenticationEntry(@NonNull String, @NonNull String, @NonNull android.app.slice.Slice, int);
- ctor public AuthenticationEntry(@NonNull String, @NonNull String, @NonNull android.app.slice.Slice, int, @NonNull android.content.Intent);
- method public int describeContents();
- method @Nullable public android.content.Intent getFrameworkExtrasIntent();
- method @NonNull public String getKey();
- method @NonNull public android.app.slice.Slice getSlice();
- method @NonNull public int getStatus();
- method @NonNull public String getSubkey();
- method public void writeToParcel(@NonNull android.os.Parcel, int);
- field @NonNull public static final android.os.Parcelable.Creator<android.credentials.selection.AuthenticationEntry> CREATOR;
- field public static final int STATUS_LOCKED = 0; // 0x0
- field public static final int STATUS_UNLOCKED_BUT_EMPTY_LESS_RECENT = 1; // 0x1
- field public static final int STATUS_UNLOCKED_BUT_EMPTY_MOST_RECENT = 2; // 0x2
- }
-
@FlaggedApi("android.credentials.flags.configurable_selector_ui_enabled") public class BaseDialogResult implements android.os.Parcelable {
ctor public BaseDialogResult(@Nullable android.os.IBinder);
ctor protected BaseDialogResult(@NonNull android.os.Parcel);
@@ -1317,19 +1302,6 @@
field @NonNull public static final android.os.Parcelable.Creator<android.credentials.selection.DisabledProviderData> CREATOR;
}
- @FlaggedApi("android.credentials.flags.configurable_selector_ui_enabled") public final class Entry implements android.os.Parcelable {
- ctor public Entry(@NonNull String, @NonNull String, @NonNull android.app.slice.Slice);
- ctor public Entry(@NonNull String, @NonNull String, @NonNull android.app.slice.Slice, @NonNull android.content.Intent);
- method public int describeContents();
- method @Nullable public android.content.Intent getFrameworkExtrasIntent();
- method @NonNull public String getKey();
- method @Nullable public android.app.PendingIntent getPendingIntent();
- method @NonNull public android.app.slice.Slice getSlice();
- method @NonNull public String getSubkey();
- method public void writeToParcel(@NonNull android.os.Parcel, int);
- field @NonNull public static final android.os.Parcelable.Creator<android.credentials.selection.Entry> CREATOR;
- }
-
@FlaggedApi("android.credentials.flags.configurable_selector_ui_enabled") public final class FailureDialogResult extends android.credentials.selection.BaseDialogResult implements android.os.Parcelable {
ctor public FailureDialogResult(@Nullable android.os.IBinder, @Nullable String);
method public static void addToBundle(@NonNull android.credentials.selection.FailureDialogResult, @NonNull android.os.Bundle);
@@ -1357,7 +1329,8 @@
}
@FlaggedApi("android.credentials.flags.configurable_selector_ui_enabled") public class IntentFactory {
- method @NonNull public static android.content.Intent createCredentialSelectorIntent(@NonNull android.credentials.selection.RequestInfo, @NonNull java.util.ArrayList<android.credentials.selection.ProviderData>, @NonNull java.util.ArrayList<android.credentials.selection.DisabledProviderData>, @NonNull android.os.ResultReceiver);
+ method @NonNull public static android.content.Intent createCancelUiIntent(@NonNull android.os.IBinder, boolean, @NonNull String);
+ method @NonNull public static android.content.Intent createCredentialSelectorIntent(@NonNull android.content.Context, @NonNull android.credentials.selection.RequestInfo, @NonNull java.util.ArrayList<android.credentials.selection.ProviderData>, @NonNull java.util.ArrayList<android.credentials.selection.DisabledProviderData>, @NonNull android.os.ResultReceiver);
}
@FlaggedApi("android.credentials.flags.configurable_selector_ui_enabled") public abstract class ProviderData implements android.os.Parcelable {
@@ -1371,25 +1344,12 @@
}
@FlaggedApi("android.credentials.flags.configurable_selector_ui_enabled") public final class RequestInfo implements android.os.Parcelable {
- method public int describeContents();
- method @NonNull public String getAppPackageName();
- method @Nullable public android.credentials.CreateCredentialRequest getCreateCredentialRequest();
- method @NonNull public java.util.List<java.lang.String> getDefaultProviderIds();
- method @Nullable public android.credentials.GetCredentialRequest getGetCredentialRequest();
- method @NonNull public java.util.List<java.lang.String> getRegistryProviderIds();
- method @NonNull public android.os.IBinder getToken();
- method @NonNull public String getType();
- method public boolean hasPermissionToOverrideDefault();
- method @NonNull public static android.credentials.selection.RequestInfo newCreateRequestInfo(@NonNull android.os.IBinder, @NonNull android.credentials.CreateCredentialRequest, @NonNull String);
- method @NonNull public static android.credentials.selection.RequestInfo newCreateRequestInfo(@NonNull android.os.IBinder, @NonNull android.credentials.CreateCredentialRequest, @NonNull String, boolean, @NonNull java.util.List<java.lang.String>);
- method @NonNull public static android.credentials.selection.RequestInfo newGetRequestInfo(@NonNull android.os.IBinder, @NonNull android.credentials.GetCredentialRequest, @NonNull String, boolean);
- method @NonNull public static android.credentials.selection.RequestInfo newGetRequestInfo(@NonNull android.os.IBinder, @NonNull android.credentials.GetCredentialRequest, @NonNull String);
- method public void writeToParcel(@NonNull android.os.Parcel, int);
- field @NonNull public static final android.os.Parcelable.Creator<android.credentials.selection.RequestInfo> CREATOR;
- field @NonNull public static final String EXTRA_REQUEST_INFO = "android.credentials.selection.extra.REQUEST_INFO";
- field @NonNull public static final String TYPE_CREATE = "android.credentials.selection.TYPE_CREATE";
- field @NonNull public static final String TYPE_GET = "android.credentials.selection.TYPE_GET";
- field @NonNull public static final String TYPE_UNDEFINED = "android.credentials.selection.TYPE_UNDEFINED";
+ method @FlaggedApi("android.credentials.flags.configurable_selector_ui_enabled") @NonNull public static android.credentials.selection.RequestInfo newCreateRequestInfo(@NonNull android.os.IBinder, @NonNull android.credentials.CreateCredentialRequest, @NonNull String, boolean, @NonNull java.util.List<java.lang.String>, boolean);
+ method @FlaggedApi("android.credentials.flags.configurable_selector_ui_enabled") @NonNull public static android.credentials.selection.RequestInfo newGetRequestInfo(@NonNull android.os.IBinder, @NonNull android.credentials.GetCredentialRequest, @NonNull String, boolean, boolean);
+ }
+
+ @FlaggedApi("android.credentials.flags.configurable_selector_ui_enabled") public final class RequestToken {
+ ctor @FlaggedApi("android.credentials.flags.configurable_selector_ui_enabled") public RequestToken(@NonNull android.os.IBinder);
}
@FlaggedApi("android.credentials.flags.configurable_selector_ui_enabled") public final class UserSelectionDialogResult extends android.credentials.selection.BaseDialogResult implements android.os.Parcelable {
@@ -1745,6 +1705,7 @@
method @NonNull public java.util.List<java.lang.String> getKeyboardLayoutDescriptorsForInputDevice(@NonNull android.view.InputDevice);
method @NonNull public String getKeyboardLayoutTypeForLayoutDescriptor(@NonNull String);
method @NonNull @RequiresPermission(android.Manifest.permission.REMAP_MODIFIER_KEYS) public java.util.Map<java.lang.Integer,java.lang.Integer> getModifierKeyRemapping();
+ method public int getMousePointerSpeed();
method @RequiresPermission(android.Manifest.permission.REMAP_MODIFIER_KEYS) public void remapModifierKey(int, int);
method @RequiresPermission(android.Manifest.permission.SET_KEYBOARD_LAYOUT) public void removeKeyboardLayoutForInputDevice(@NonNull android.hardware.input.InputDeviceIdentifier, @NonNull String);
method public void removeUniqueIdAssociation(@NonNull String);
@@ -1754,6 +1715,7 @@
public class InputSettings {
method @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public static void setMaximumObscuringOpacityForTouch(@NonNull android.content.Context, @FloatRange(from=0, to=1) float);
+ field public static final int DEFAULT_POINTER_SPEED = 0; // 0x0
}
}
@@ -2887,6 +2849,10 @@
field public static final String VOICE_INTERACTION_SERVICE = "voice_interaction_service";
}
+ public static final class Settings.System extends android.provider.Settings.NameValueTable {
+ field public static final String POINTER_SPEED = "pointer_speed";
+ }
+
public static final class Telephony.Sms.Intents {
field public static final String SMS_CARRIER_PROVISION_ACTION = "android.provider.Telephony.SMS_CARRIER_PROVISION";
}
@@ -3314,7 +3280,6 @@
method @NonNull public android.util.Pair<java.lang.Integer,java.lang.Integer> getHalVersion(int);
method @RequiresPermission(android.Manifest.permission.READ_PHONE_STATE) public String getLine1AlphaTag();
method @Deprecated public android.util.Pair<java.lang.Integer,java.lang.Integer> getRadioHalVersion();
- method @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public boolean isDomainSelectionSupported();
method public boolean modifyDevicePolicyOverrideApn(@NonNull android.content.Context, int, @NonNull android.telephony.data.ApnSetting);
method @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public void refreshUiccProfile();
method @Deprecated public void setCarrierTestOverride(String, String, String, String, String, String, String);
diff --git a/core/api/test-lint-baseline.txt b/core/api/test-lint-baseline.txt
index 5e904ef9..b938f0f 100644
--- a/core/api/test-lint-baseline.txt
+++ b/core/api/test-lint-baseline.txt
@@ -673,6 +673,8 @@
Method 'generateKeyPair' documentation mentions permissions already declared by @RequiresPermission
RequiresPermission: android.app.admin.DevicePolicyManager#getApplicationExemptions(String):
Method 'getApplicationExemptions' documentation mentions permissions already declared by @RequiresPermission
+RequiresPermission: android.app.admin.DevicePolicyManager#getContentProtectionPolicy(android.content.ComponentName):
+ Method 'getContentProtectionPolicy' documentation mentions permissions already declared by @RequiresPermission
RequiresPermission: android.app.admin.DevicePolicyManager#getCrossProfileWidgetProviders(android.content.ComponentName):
Method 'getCrossProfileWidgetProviders' documentation mentions permissions already declared by @RequiresPermission
RequiresPermission: android.app.admin.DevicePolicyManager#getLockTaskFeatures(android.content.ComponentName):
@@ -721,6 +723,8 @@
Method 'setAlwaysOnVpnPackage' documentation mentions permissions without declaring @RequiresPermission
RequiresPermission: android.app.admin.DevicePolicyManager#setApplicationExemptions(String, java.util.Set<java.lang.Integer>):
Method 'setApplicationExemptions' documentation mentions permissions already declared by @RequiresPermission
+RequiresPermission: android.app.admin.DevicePolicyManager#setContentProtectionPolicy(android.content.ComponentName, int):
+ Method 'setContentProtectionPolicy' documentation mentions permissions already declared by @RequiresPermission
RequiresPermission: android.app.admin.DevicePolicyManager#setDeviceOwner(android.content.ComponentName, int):
Method 'setDeviceOwner' documentation mentions permissions already declared by @RequiresPermission
RequiresPermission: android.app.admin.DevicePolicyManager#setDeviceProvisioningConfigApplied():
diff --git a/core/java/android/app/Activity.java b/core/java/android/app/Activity.java
index 2103055..ab9a4ec 100644
--- a/core/java/android/app/Activity.java
+++ b/core/java/android/app/Activity.java
@@ -844,6 +844,7 @@
private IBinder mToken;
private IBinder mAssistToken;
private IBinder mShareableActivityToken;
+ private ComponentCaller mInitialCaller;
@UnsupportedAppUsage
private int mIdent;
@UnsupportedAppUsage
@@ -7031,6 +7032,20 @@
}
/**
+ * Returns the ComponentCaller instance of the app that initially launched this activity.
+ *
+ * <p>Note that calls to {@link #onNewIntent} have no effect on the returned value of this
+ * method.
+ *
+ * @return {@link ComponentCaller} instance
+ * @see ComponentCaller
+ */
+ @FlaggedApi(android.security.Flags.FLAG_CONTENT_URI_PERMISSION_APIS)
+ public @NonNull ComponentCaller getInitialCaller() {
+ return mInitialCaller;
+ }
+
+ /**
* Control whether this activity's main window is visible. This is intended
* only for the special case of an activity that is not going to show a
* UI itself, but can't just finish prior to onResume() because it needs
@@ -8647,6 +8662,19 @@
Configuration config, String referrer, IVoiceInteractor voiceInteractor,
Window window, ActivityConfigCallback activityConfigCallback, IBinder assistToken,
IBinder shareableActivityToken) {
+ attach(context, aThread, instr, token, ident, application, intent, info, title, parent, id,
+ lastNonConfigurationInstances, config, referrer, voiceInteractor, window,
+ activityConfigCallback, assistToken, shareableActivityToken, null);
+ }
+
+ final void attach(Context context, ActivityThread aThread,
+ Instrumentation instr, IBinder token, int ident,
+ Application application, Intent intent, ActivityInfo info,
+ CharSequence title, Activity parent, String id,
+ NonConfigurationInstances lastNonConfigurationInstances,
+ Configuration config, String referrer, IVoiceInteractor voiceInteractor,
+ Window window, ActivityConfigCallback activityConfigCallback, IBinder assistToken,
+ IBinder shareableActivityToken, IBinder initialCallerInfoAccessToken) {
if (sandboxActivitySdkBasedContext()) {
// Sandbox activities extract a token from the intent's extra to identify the related
// SDK as part of overriding attachBaseContext, then it wraps the passed context in an
@@ -8711,6 +8739,10 @@
getAutofillClientController().onActivityAttached(application);
setContentCaptureOptions(application.getContentCaptureOptions());
+
+ if (android.security.Flags.contentUriPermissionApis()) {
+ mInitialCaller = new ComponentCaller(getActivityToken(), initialCallerInfoAccessToken);
+ }
}
/** @hide */
diff --git a/core/java/android/app/ActivityManager.java b/core/java/android/app/ActivityManager.java
index 6285eb3..a8d183a 100644
--- a/core/java/android/app/ActivityManager.java
+++ b/core/java/android/app/ActivityManager.java
@@ -147,6 +147,7 @@
* </p>
*/
@SystemService(Context.ACTIVITY_SERVICE)
+@android.ravenwood.annotation.RavenwoodKeepPartialClass
public class ActivityManager {
private static String TAG = "ActivityManager";
@@ -966,6 +967,7 @@
* Print capability bits in human-readable form.
* @hide
*/
+ @android.ravenwood.annotation.RavenwoodKeep
public static void printCapabilitiesSummary(PrintWriter pw, @ProcessCapability int caps) {
pw.print((caps & PROCESS_CAPABILITY_FOREGROUND_LOCATION) != 0 ? 'L' : '-');
pw.print((caps & PROCESS_CAPABILITY_FOREGROUND_CAMERA) != 0 ? 'C' : '-');
@@ -976,6 +978,7 @@
}
/** @hide */
+ @android.ravenwood.annotation.RavenwoodKeep
public static void printCapabilitiesSummary(StringBuilder sb, @ProcessCapability int caps) {
sb.append((caps & PROCESS_CAPABILITY_FOREGROUND_LOCATION) != 0 ? 'L' : '-');
sb.append((caps & PROCESS_CAPABILITY_FOREGROUND_CAMERA) != 0 ? 'C' : '-');
@@ -989,6 +992,7 @@
* Print capability bits in human-readable form.
* @hide
*/
+ @android.ravenwood.annotation.RavenwoodKeep
public static void printCapabilitiesFull(PrintWriter pw, @ProcessCapability int caps) {
printCapabilitiesSummary(pw, caps);
final int remain = caps & ~PROCESS_CAPABILITY_ALL;
@@ -999,6 +1003,7 @@
}
/** @hide */
+ @android.ravenwood.annotation.RavenwoodKeep
public static String getCapabilitiesSummary(@ProcessCapability int caps) {
final StringBuilder sb = new StringBuilder();
printCapabilitiesSummary(sb, caps);
@@ -1018,6 +1023,7 @@
* @return the value of the corresponding enums.proto ProcessStateEnum value.
* @hide
*/
+ @android.ravenwood.annotation.RavenwoodKeep
public static final int processStateAmToProto(int amInt) {
switch (amInt) {
case PROCESS_STATE_UNKNOWN:
@@ -1078,16 +1084,19 @@
public static final int MAX_PROCESS_STATE = PROCESS_STATE_NONEXISTENT;
/** @hide Should this process state be considered a background state? */
+ @android.ravenwood.annotation.RavenwoodKeep
public static final boolean isProcStateBackground(int procState) {
return procState >= PROCESS_STATE_TRANSIENT_BACKGROUND;
}
/** @hide Should this process state be considered in the cache? */
+ @android.ravenwood.annotation.RavenwoodKeep
public static final boolean isProcStateCached(int procState) {
return procState >= PROCESS_STATE_CACHED_ACTIVITY;
}
/** @hide Is this a foreground service type? */
+ @android.ravenwood.annotation.RavenwoodKeep
public static boolean isForegroundService(int procState) {
return procState == PROCESS_STATE_FOREGROUND_SERVICE;
}
@@ -1161,10 +1170,25 @@
mContext = context;
}
+ private static volatile int sCurrentUser$ravenwood = UserHandle.USER_NULL;
+
+ /** @hide */
+ @android.ravenwood.annotation.RavenwoodKeep
+ public static void init$ravenwood(int currentUser) {
+ sCurrentUser$ravenwood = currentUser;
+ }
+
+ /** @hide */
+ @android.ravenwood.annotation.RavenwoodKeep
+ public static void reset$ravenwood() {
+ sCurrentUser$ravenwood = UserHandle.USER_NULL;
+ }
+
/**
* Returns whether the launch was successful.
* @hide
*/
+ @android.ravenwood.annotation.RavenwoodKeep
public static final boolean isStartResultSuccessful(int result) {
return FIRST_START_SUCCESS_CODE <= result && result <= LAST_START_SUCCESS_CODE;
}
@@ -1173,6 +1197,7 @@
* Returns whether the launch result was a fatal error.
* @hide
*/
+ @android.ravenwood.annotation.RavenwoodKeep
public static final boolean isStartResultFatalError(int result) {
return FIRST_START_FATAL_ERROR_CODE <= result && result <= LAST_START_FATAL_ERROR_CODE;
}
@@ -1343,6 +1368,7 @@
public @interface RestrictionLevel{}
/** @hide */
+ @android.ravenwood.annotation.RavenwoodKeep
public static String restrictionLevelToName(@RestrictionLevel int level) {
switch (level) {
case RESTRICTION_LEVEL_UNKNOWN:
@@ -4779,6 +4805,7 @@
* Returns "true" if the user interface is currently being messed with
* by a monkey.
*/
+ @android.ravenwood.annotation.RavenwoodReplace
public static boolean isUserAMonkey() {
try {
return getService().isUserAMonkey();
@@ -4787,6 +4814,12 @@
}
}
+ /** @hide */
+ public static boolean isUserAMonkey$ravenwood() {
+ // Ravenwood environment is never considered a "monkey"
+ return false;
+ }
+
/**
* Returns "true" if device is running in a test harness.
*
@@ -4973,6 +5006,7 @@
"android.permission.INTERACT_ACROSS_USERS",
"android.permission.INTERACT_ACROSS_USERS_FULL"
})
+ @android.ravenwood.annotation.RavenwoodReplace
public static int getCurrentUser() {
try {
return getService().getCurrentUserId();
@@ -4981,6 +5015,11 @@
}
}
+ /** @hide */
+ public static int getCurrentUser$ravenwood() {
+ return sCurrentUser$ravenwood;
+ }
+
/**
* @param userid the user's id. Zero indicates the default user.
* @hide
@@ -5320,6 +5359,7 @@
/**
* @hide
*/
+ @android.ravenwood.annotation.RavenwoodReplace
public static boolean isSystemReady() {
if (!sSystemReady) {
if (ActivityThread.isSystem()) {
@@ -5334,6 +5374,12 @@
return sSystemReady;
}
+ /** @hide */
+ public static boolean isSystemReady$ravenwood() {
+ // Ravenwood environment is always considered as booted and ready
+ return true;
+ }
+
/**
* @hide
*/
@@ -5661,11 +5707,13 @@
}
/** @hide */
+ @android.ravenwood.annotation.RavenwoodKeep
public static boolean isProcStateConsideredInteraction(@ProcessState int procState) {
return (procState <= PROCESS_STATE_TOP || procState == PROCESS_STATE_BOUND_TOP);
}
/** @hide */
+ @android.ravenwood.annotation.RavenwoodKeep
public static String procStateToString(int procState) {
final String procStateStr;
switch (procState) {
@@ -5906,6 +5954,20 @@
}
/**
+ * Used by {@link com.android.systemui.theme.ThemeOverlayController} to notify of color
+ * palette readiness.
+ * @hide
+ */
+ @RequiresPermission(Manifest.permission.SET_THEME_OVERLAY_CONTROLLER_READY)
+ public void setThemeOverlayReady(boolean readiness) {
+ try {
+ getService().setThemeOverlayReady(readiness);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
* Resets the state of the {@link com.android.server.am.AppErrors} instance.
* This is intended for use with CTS only.
* @hide
diff --git a/core/java/android/app/ActivityManagerInternal.java b/core/java/android/app/ActivityManagerInternal.java
index 232fc92..0ae2e01 100644
--- a/core/java/android/app/ActivityManagerInternal.java
+++ b/core/java/android/app/ActivityManagerInternal.java
@@ -1258,4 +1258,11 @@
*/
public abstract boolean clearApplicationUserData(String packageName, boolean keepState,
boolean isRestore, IPackageDataObserver observer, int userId);
+
+ /**
+ * Returns current state of {@link com.android.systemui.theme.ThemeOverlayController} color
+ * palette readiness.
+ * @hide
+ */
+ public abstract boolean getThemeOverlayReadiness();
}
diff --git a/core/java/android/app/ActivityOptions.java b/core/java/android/app/ActivityOptions.java
index 111895e..1edf4bd 100644
--- a/core/java/android/app/ActivityOptions.java
+++ b/core/java/android/app/ActivityOptions.java
@@ -1974,8 +1974,8 @@
binder = new Binder(descriptor);
}
- private LaunchCookie(Parcel in) {
- this.binder = in.readStrongBinder();
+ private LaunchCookie(IBinder binder) {
+ this.binder = binder;
}
/** @hide */
@@ -1996,7 +1996,11 @@
/** @hide */
public static LaunchCookie readFromParcel(@NonNull Parcel in) {
- return new LaunchCookie(in);
+ IBinder binder = in.readStrongBinder();
+ if (binder == null) {
+ return null;
+ }
+ return new LaunchCookie(binder);
}
/** @hide */
@@ -2017,7 +2021,7 @@
@Override
public LaunchCookie createFromParcel(Parcel source) {
- return new LaunchCookie(source);
+ return LaunchCookie.readFromParcel(source);
}
@Override
diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java
index 4c54b03..2c00c99 100644
--- a/core/java/android/app/ActivityThread.java
+++ b/core/java/android/app/ActivityThread.java
@@ -580,6 +580,7 @@
public IBinder shareableActivityToken;
// The token of the TaskFragment that embedded this activity.
@Nullable public IBinder mTaskFragmentToken;
+ public IBinder initialCallerInfoAccessToken;
int ident;
@UnsupportedAppUsage
Intent intent;
@@ -668,7 +669,7 @@
List<ReferrerIntent> pendingNewIntents, SceneTransitionInfo sceneTransitionInfo,
boolean isForward, ProfilerInfo profilerInfo, ClientTransactionHandler client,
IBinder assistToken, IBinder shareableActivityToken, boolean launchedFromBubble,
- IBinder taskFragmentToken) {
+ IBinder taskFragmentToken, IBinder initialCallerInfoAccessToken) {
this.token = token;
this.assistToken = assistToken;
this.shareableActivityToken = shareableActivityToken;
@@ -685,6 +686,7 @@
this.profilerInfo = profilerInfo;
this.overrideConfig = overrideConfig;
this.packageInfo = client.getPackageInfoNoCheck(activityInfo.applicationInfo);
+ this.initialCallerInfoAccessToken = initialCallerInfoAccessToken;
mSceneTransitionInfo = sceneTransitionInfo;
mLaunchedFromBubble = launchedFromBubble;
mTaskFragmentToken = taskFragmentToken;
@@ -3914,7 +3916,7 @@
r.ident, app, r.intent, r.activityInfo, title, r.parent,
r.embeddedID, r.lastNonConfigurationInstances, config,
r.referrer, r.voiceInteractor, window, r.activityConfigCallback,
- r.assistToken, r.shareableActivityToken);
+ r.assistToken, r.shareableActivityToken, r.initialCallerInfoAccessToken);
if (customIntent != null) {
activity.mIntent = customIntent;
diff --git a/core/java/android/app/AppOpsManager.java b/core/java/android/app/AppOpsManager.java
index 0dbce97..5074ed7 100644
--- a/core/java/android/app/AppOpsManager.java
+++ b/core/java/android/app/AppOpsManager.java
@@ -4588,19 +4588,19 @@
*/
private @Nullable NoteOpEvent getLastRejectEvent(@UidState int fromUidState,
@UidState int toUidState, @OpFlags int flags) {
- NoteOpEvent lastAccessEvent = null;
+ NoteOpEvent lastRejectEvent = null;
for (AttributedOpEntry attributionEntry : mAttributedOpEntries.values()) {
- NoteOpEvent lastAttributionAccessEvent = attributionEntry.getLastRejectEvent(
+ NoteOpEvent lastAttributionRejectEvent = attributionEntry.getLastRejectEvent(
fromUidState, toUidState, flags);
- if (lastAccessEvent == null || (lastAttributionAccessEvent != null
- && lastAttributionAccessEvent.getNoteTime()
- > lastAccessEvent.getNoteTime())) {
- lastAccessEvent = lastAttributionAccessEvent;
+ if (lastRejectEvent == null || (lastAttributionRejectEvent != null
+ && lastAttributionRejectEvent.getNoteTime()
+ > lastRejectEvent.getNoteTime())) {
+ lastRejectEvent = lastAttributionRejectEvent;
}
}
- return lastAccessEvent;
+ return lastRejectEvent;
}
/**
diff --git a/core/java/android/app/ApplicationPackageManager.java b/core/java/android/app/ApplicationPackageManager.java
index 4f1db7d..d8aded40 100644
--- a/core/java/android/app/ApplicationPackageManager.java
+++ b/core/java/android/app/ApplicationPackageManager.java
@@ -1269,6 +1269,22 @@
return appMetadata != null ? appMetadata : new PersistableBundle();
}
+ @Override
+ public @AppMetadataSource int getAppMetadataSource(@NonNull String packageName)
+ throws NameNotFoundException {
+ Objects.requireNonNull(packageName, "packageName cannot be null");
+ int source = PackageManager.APP_METADATA_SOURCE_UNKNOWN;
+ try {
+ source = mPM.getAppMetadataSource(packageName, getUserId());
+ } catch (ParcelableException e) {
+ e.maybeRethrow(NameNotFoundException.class);
+ throw new RuntimeException(e);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ return source;
+ }
+
@SuppressWarnings("unchecked")
@Override
public List<PackageInfo> getPackagesHoldingPermissions(String[] permissions, int flags) {
diff --git a/core/java/android/app/ComponentCaller.java b/core/java/android/app/ComponentCaller.java
new file mode 100644
index 0000000..583408e
--- /dev/null
+++ b/core/java/android/app/ComponentCaller.java
@@ -0,0 +1,137 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app;
+
+import android.annotation.FlaggedApi;
+import android.annotation.Nullable;
+import android.os.IBinder;
+import android.os.Process;
+
+import androidx.annotation.NonNull;
+
+import java.util.Objects;
+
+/**
+ * Represents the app that launched the component. See below for the APIs available on the component
+ * caller.
+ *
+ * <p><b>Note</b>, that in {@link android.os.Build.VERSION_CODES#VANILLA_ICE_CREAM} only
+ * {@link Activity} has access to {@link ComponentCaller} instances.
+ *
+ * @see Activity#getInitialCaller()
+ */
+@FlaggedApi(android.security.Flags.FLAG_CONTENT_URI_PERMISSION_APIS)
+public final class ComponentCaller {
+ private final IBinder mActivityToken;
+ private final IBinder mCallerToken;
+
+ public ComponentCaller(@NonNull IBinder activityToken, @Nullable IBinder callerToken) {
+ mActivityToken = activityToken;
+ mCallerToken = callerToken;
+ }
+
+ /**
+ * Returns the uid of this component caller.
+ *
+ * <p><b>Note</b>, in {@link android.os.Build.VERSION_CODES#VANILLA_ICE_CREAM} only
+ * {@link Activity} has access to {@link ComponentCaller} instances.
+ * <p>
+ * <h3>Requirements for {@link Activity} callers</h3>
+ *
+ * <p>In order to receive the calling app's uid, at least one of the following has to be met:
+ * <ul>
+ * <li>The calling app must call {@link ActivityOptions#setShareIdentityEnabled(boolean)}
+ * with a value of {@code true} and launch this activity with the resulting
+ * {@code ActivityOptions}.
+ * <li>The launched activity has the same uid as the calling app.
+ * <li>The launched activity is running in a package that is signed with the same key used
+ * to sign the platform (typically only system packages such as Settings will meet this
+ * requirement).
+ * </ul>
+ * These are the same requirements for {@link #getPackage()}; if any of these are met, then
+ * these methods can be used to obtain the uid and package name of the calling app. If none are
+ * met, then {@link Process#INVALID_UID} is returned.
+ *
+ * <p>Note, even if the above conditions are not met, the calling app's identity may still be
+ * available from {@link Activity#getCallingPackage()} if this activity was started with
+ * {@code Activity#startActivityForResult} to allow validation of the result's recipient.
+ *
+ * @return the uid of the calling app or {@link Process#INVALID_UID} if the current component
+ * cannot access the identity of the calling app or the caller is invalid
+ *
+ * @see ActivityOptions#setShareIdentityEnabled(boolean)
+ * @see Activity#getLaunchedFromUid()
+ */
+ public int getUid() {
+ return ActivityClient.getInstance().getLaunchedFromUid(mActivityToken);
+ }
+
+ /**
+ * Returns the package name of this component caller.
+ *
+ * <p><b>Note</b>, in {@link android.os.Build.VERSION_CODES#VANILLA_ICE_CREAM} only
+ * {@link Activity} has access to {@link ComponentCaller} instances.
+ * <p>
+ * <h3>Requirements for {@link Activity} callers</h3>
+ *
+ * <p>In order to receive the calling app's package name, at least one of the following has to
+ * be met:
+ * <ul>
+ * <li>The calling app must call {@link ActivityOptions#setShareIdentityEnabled(boolean)}
+ * with a value of {@code true} and launch this activity with the resulting
+ * {@code ActivityOptions}.
+ * <li>The launched activity has the same uid as the calling app.
+ * <li>The launched activity is running in a package that is signed with the same key used
+ * to sign the platform (typically only system packages such as Settings will meet this
+ * meet this requirement).
+ * </ul>
+ * These are the same requirements for {@link #getUid()}; if any of these are met, then these
+ * methods can be used to obtain the uid and package name of the calling app. If none are met,
+ * then {@code null} is returned.
+ *
+ * <p>Note, even if the above conditions are not met, the calling app's identity may still be
+ * available from {@link Activity#getCallingPackage()} if this activity was started with
+ * {@code Activity#startActivityForResult} to allow validation of the result's recipient.
+ *
+ * @return the package name of the calling app or null if the current component cannot access
+ * the identity of the calling app or the caller is invalid
+ *
+ * @see ActivityOptions#setShareIdentityEnabled(boolean)
+ * @see Activity#getLaunchedFromPackage()
+ */
+ @Nullable
+ public String getPackage() {
+ return ActivityClient.getInstance().getLaunchedFromPackage(mActivityToken);
+ }
+
+ @Override
+ public boolean equals(@Nullable Object obj) {
+ if (obj == null || !(obj instanceof ComponentCaller other)) {
+ return false;
+ }
+ return this.mActivityToken == other.mActivityToken
+ && this.mCallerToken == other.mCallerToken;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = 17;
+ result = 31 * result + Objects.hashCode(mActivityToken);
+ result = 31 * result + Objects.hashCode(mCallerToken);
+ return result;
+ }
+}
diff --git a/core/java/android/app/IActivityManager.aidl b/core/java/android/app/IActivityManager.aidl
index b063d04..ceeaf5d 100644
--- a/core/java/android/app/IActivityManager.aidl
+++ b/core/java/android/app/IActivityManager.aidl
@@ -17,7 +17,6 @@
package android.app;
import android.app.ActivityManager;
-import android.app.ActivityManager.PendingIntentInfo;
import android.app.ActivityTaskManager;
import android.app.ApplicationStartInfo;
import android.app.ApplicationErrorReport;
@@ -553,6 +552,14 @@
@UnsupportedAppUsage(maxTargetSdk = 30, trackingBug = 170729553)
boolean isTopOfTask(in IBinder token);
void bootAnimationComplete();
+
+ /**
+ * Used by {@link com.android.systemui.theme.ThemeOverlayController} to notify of color
+ * palette readiness.
+ * @throws RemoteException
+ */
+ void setThemeOverlayReady(boolean readiness);
+
@UnsupportedAppUsage
void registerTaskStackListener(in ITaskStackListener listener);
void unregisterTaskStackListener(in ITaskStackListener listener);
diff --git a/core/java/android/app/IActivityTaskManager.aidl b/core/java/android/app/IActivityTaskManager.aidl
index d540748..e2e2f1d 100644
--- a/core/java/android/app/IActivityTaskManager.aidl
+++ b/core/java/android/app/IActivityTaskManager.aidl
@@ -225,7 +225,7 @@
boolean focused, boolean newSessionId);
boolean requestAutofillData(in IAssistDataReceiver receiver, in Bundle receiverExtras,
in IBinder activityToken, int flags);
- boolean isAssistDataAllowedOnCurrentActivity();
+ boolean isAssistDataAllowed();
boolean requestAssistDataForTask(in IAssistDataReceiver receiver, int taskId,
in String callingPackageName, String callingAttributionTag);
diff --git a/core/java/android/app/Instrumentation.java b/core/java/android/app/Instrumentation.java
index 68512b8..454d605 100644
--- a/core/java/android/app/Instrumentation.java
+++ b/core/java/android/app/Instrumentation.java
@@ -1423,8 +1423,8 @@
info, title, parent, id,
(Activity.NonConfigurationInstances)lastNonConfigurationInstance,
new Configuration(), null /* referrer */, null /* voiceInteractor */,
- null /* window */, null /* activityCallback */, null /*assistToken*/,
- null /*shareableActivityToken*/);
+ null /* window */, null /* activityCallback */, null /* assistToken */,
+ null /* shareableActivityToken */, null /* initialCallerInfoAccessToken */);
return activity;
}
diff --git a/core/java/android/app/KeyguardManager.java b/core/java/android/app/KeyguardManager.java
index 8b8576a..b5e5074 100644
--- a/core/java/android/app/KeyguardManager.java
+++ b/core/java/android/app/KeyguardManager.java
@@ -721,28 +721,28 @@
/**
* Returns whether the device is currently locked for the user.
* <p>
- * This returns the device locked state for the {@link Context}'s user. If this user is the
- * current user, then the device is considered "locked" when the lock screen is showing (i.e.
- * {@link #isKeyguardLocked()} returns {@code true}) and is not trivially dismissible (e.g. with
- * swipe), and the user has a PIN, pattern, or password.
+ * This method returns the device locked state for the {@link Context}'s user. The device is
+ * considered to be locked for a user when the user's apps are currently inaccessible and some
+ * form of lock screen authentication is required to regain access to them. The lock screen
+ * authentication typically uses PIN, pattern, password, or biometric. Some devices may support
+ * additional methods, such as unlock using a paired smartwatch. "Swipe" does not count as
+ * authentication; if the lock screen is dismissible with swipe, for example due to the lock
+ * screen being set to Swipe or due to the device being kept unlocked by being near a trusted
+ * bluetooth device or in a trusted location, the device is considered unlocked.
+ * <div class="note">
* <p>
- * Note: the above definition implies that a user with no PIN, pattern, or password is never
- * considered locked, even if the lock screen is showing and requesting a SIM card PIN. The
- * device PIN and SIM PIN are separate. Also, the user is not considered locked if face
- * authentication has just completed or a trust agent is keeping the device unlocked, since in
- * these cases the lock screen is dismissible with swipe.
+ * <b>Note:</b> In the case of multiple full users, each user can have their own lock screen
+ * authentication configured. The device-locked state may differ between different users. For
+ * example, the device may be unlocked for the current user, but locked for a non-current user
+ * if lock screen authentication would be required to access that user's apps after switching to
+ * that user.
* <p>
- * For a user that is not the current user but can be switched to (usually this means "another
- * full user"), and that has a PIN, pattern, or password, the device is always considered
- * locked.
- * <p>
- * For a profile with a unified challenge, the device locked state is the same as that of the
- * parent user.
- * <p>
- * For a profile with a separate challenge, the device becomes unlocked when the profile's PIN,
- * pattern, password, or biometric is verified. It becomes locked when the parent user becomes
- * locked, the screen turns off, the device reboots, the device policy controller locks the
- * profile, or the timeout set by the device policy controller expires.
+ * In the case of a profile, when the device goes to the main lock screen, up to two layers of
+ * authentication may be required to regain access to the profile's apps: one to unlock the main
+ * lock screen, and one to unlock the profile (when a separate profile challenge is required).
+ * For a profile, the device is considered to be locked as long as any challenge remains, either
+ * the parent user's challenge (when applicable) or the profile's challenge (when applicable).
+ * </div>
*
* @return {@code true} if the device is currently locked for the user
* @see #isKeyguardLocked()
diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java
index d705eeb..aa9de81 100644
--- a/core/java/android/app/Notification.java
+++ b/core/java/android/app/Notification.java
@@ -23,6 +23,7 @@
import static android.app.admin.DevicePolicyResources.UNDEFINED;
import static android.graphics.drawable.Icon.TYPE_URI;
import static android.graphics.drawable.Icon.TYPE_URI_ADAPTIVE_BITMAP;
+import static android.app.Flags.evenlyDividedCallStyleActionLayout;
import static java.util.Objects.requireNonNull;
@@ -3540,15 +3541,12 @@
* Sets the token used for background operations for the pending intents associated with this
* notification.
*
- * This token is automatically set during deserialization for you, you usually won't need to
- * call this unless you want to change the existing token, if any.
- *
* @hide
*/
- public void clearAllowlistToken() {
- mAllowlistToken = null;
+ public void overrideAllowlistToken(IBinder token) {
+ mAllowlistToken = token;
if (publicVersion != null) {
- publicVersion.clearAllowlistToken();
+ publicVersion.overrideAllowlistToken(token);
}
}
@@ -4649,13 +4647,24 @@
* to turn it off and use a normal notification, as this can be extremely
* disruptive.
*
- * <p>
- * The system UI may choose to display a heads-up notification, instead of
- * launching this intent, while the user is using the device.
- * </p>
* <p>Apps targeting {@link Build.VERSION_CODES#Q} and above will have to request
* a permission ({@link android.Manifest.permission#USE_FULL_SCREEN_INTENT}) in order to
- * use full screen intents.</p>
+ * use full screen intents. </p>
+ * <p>
+ * Prior to {@link Build.VERSION_CODES#TIRAMISU}, the system may display a
+ * heads up notification (which may display on screen longer than other heads up
+ * notifications), instead of launching the intent, while the user is using the device.
+ * From {@link Build.VERSION_CODES#TIRAMISU},
+ * the system UI will display a heads up notification, instead of launching this intent,
+ * while the user is using the device. This notification will display with emphasized
+ * action buttons. If the posting app holds
+ * {@link android.Manifest.permission#USE_FULL_SCREEN_INTENT}, then the heads
+ * up notification will appear persistently until the user dismisses or snoozes it, or
+ * the app cancels it. If the posting app does not hold
+ * {@link android.Manifest.permission#USE_FULL_SCREEN_INTENT}, then the notification will
+ * appear as heads up notification even when the screen is locked or turned off, and this
+ * notification will only be persistent for 60 seconds.
+ * </p>
* <p>
* To be launched as a full screen intent, the notification must also be posted to a
* channel with importance level set to IMPORTANCE_HIGH or higher.
@@ -5946,6 +5955,12 @@
// there is enough space to do so (and fall back to the left edge if not).
big.setInt(R.id.actions, "setCollapsibleIndentDimen",
R.dimen.call_notification_collapsible_indent);
+ if (evenlyDividedCallStyleActionLayout()) {
+ if (CallStyle.DEBUG_NEW_ACTION_LAYOUT) {
+ Log.d(TAG, "setting evenly divided mode on action list");
+ }
+ big.setBoolean(R.id.actions, "setEvenlyDividedMode", true);
+ }
}
big.setBoolean(R.id.actions, "setEmphasizedMode", emphasizedMode);
if (numActions > 0 && !p.mHideActions) {
@@ -6421,7 +6436,15 @@
// Remove full-length color spans and ensure text contrast with the button fill.
title = ContrastColorUtil.ensureColorSpanContrast(title, buttonFillColor);
}
- button.setTextViewText(R.id.action0, ensureColorSpanContrast(title, p));
+ final CharSequence label = ensureColorSpanContrast(title, p);
+ if (p.mCallStyleActions && evenlyDividedCallStyleActionLayout()) {
+ if (CallStyle.DEBUG_NEW_ACTION_LAYOUT) {
+ Log.d(TAG, "new action layout enabled, gluing instead of setting text");
+ }
+ button.setCharSequence(R.id.action0, "glueLabel", label);
+ } else {
+ button.setTextViewText(R.id.action0, label);
+ }
int textColor = ContrastColorUtil.resolvePrimaryColor(mContext,
buttonFillColor, mInNightMode);
if (tombstone) {
@@ -6438,7 +6461,14 @@
button.setColorStateList(R.id.action0, "setButtonBackground",
ColorStateList.valueOf(buttonFillColor));
if (p.mCallStyleActions) {
- button.setImageViewIcon(R.id.action0, action.getIcon());
+ if (evenlyDividedCallStyleActionLayout()) {
+ if (CallStyle.DEBUG_NEW_ACTION_LAYOUT) {
+ Log.d(TAG, "new action layout enabled, gluing instead of setting icon");
+ }
+ button.setIcon(R.id.action0, "glueIcon", action.getIcon());
+ } else {
+ button.setImageViewIcon(R.id.action0, action.getIcon());
+ }
boolean priority = action.getExtras().getBoolean(CallStyle.KEY_ACTION_PRIORITY);
button.setBoolean(R.id.action0, "setIsPriority", priority);
int minWidthDimen =
@@ -9565,6 +9595,10 @@
* </pre>
*/
public static class CallStyle extends Style {
+ /**
+ * @hide
+ */
+ public static final boolean DEBUG_NEW_ACTION_LAYOUT = true;
/**
* @hide
diff --git a/core/java/android/app/NotificationChannel.java b/core/java/android/app/NotificationChannel.java
index 15d692a..ab5395e 100644
--- a/core/java/android/app/NotificationChannel.java
+++ b/core/java/android/app/NotificationChannel.java
@@ -15,6 +15,7 @@
*/
package android.app;
+import android.annotation.FlaggedApi;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.SystemApi;
@@ -30,6 +31,9 @@
import android.net.Uri;
import android.os.Parcel;
import android.os.Parcelable;
+import android.os.VibrationEffect;
+import android.os.vibrator.persistence.VibrationXmlParser;
+import android.os.vibrator.persistence.VibrationXmlSerializer;
import android.provider.Settings;
import android.service.notification.NotificationListenerService;
import android.text.TextUtils;
@@ -49,6 +53,8 @@
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.PrintWriter;
+import java.io.StringReader;
+import java.io.StringWriter;
import java.util.Arrays;
import java.util.Objects;
@@ -146,6 +152,7 @@
private static final String ATT_LIGHTS = "lights";
private static final String ATT_LIGHT_COLOR = "light_color";
private static final String ATT_VIBRATION = "vibration";
+ private static final String ATT_VIBRATION_EFFECT = "vibration_effect";
private static final String ATT_VIBRATION_ENABLED = "vibration_enabled";
private static final String ATT_SOUND = "sound";
private static final String ATT_USAGE = "usage";
@@ -253,7 +260,8 @@
private boolean mSoundRestored = false;
private boolean mLights;
private int mLightColor = DEFAULT_LIGHT_COLOR;
- private long[] mVibration;
+ private long[] mVibrationPattern;
+ private VibrationEffect mVibrationEffect;
// Bitwise representation of fields that have been changed by the user, preventing the app from
// making changes to these fields.
private int mUserLockedFields;
@@ -324,9 +332,13 @@
mSound = null;
}
mLights = in.readByte() != 0;
- mVibration = in.createLongArray();
- if (mVibration != null && mVibration.length > MAX_VIBRATION_LENGTH) {
- mVibration = Arrays.copyOf(mVibration, MAX_VIBRATION_LENGTH);
+ mVibrationPattern = in.createLongArray();
+ if (mVibrationPattern != null && mVibrationPattern.length > MAX_VIBRATION_LENGTH) {
+ mVibrationPattern = Arrays.copyOf(mVibrationPattern, MAX_VIBRATION_LENGTH);
+ }
+ if (Flags.notificationChannelVibrationEffectApi()) {
+ mVibrationEffect =
+ in.readInt() != 0 ? VibrationEffect.CREATOR.createFromParcel(in) : null;
}
mUserLockedFields = in.readInt();
mUserVisibleTaskShown = in.readByte() != 0;
@@ -381,7 +393,15 @@
dest.writeByte((byte) 0);
}
dest.writeByte(mLights ? (byte) 1 : (byte) 0);
- dest.writeLongArray(mVibration);
+ dest.writeLongArray(mVibrationPattern);
+ if (Flags.notificationChannelVibrationEffectApi()) {
+ if (mVibrationEffect != null) {
+ dest.writeInt(1);
+ mVibrationEffect.writeToParcel(dest, /* flags= */ 0);
+ } else {
+ dest.writeInt(0);
+ }
+ }
dest.writeInt(mUserLockedFields);
dest.writeByte(mUserVisibleTaskShown ? (byte) 1 : (byte) 0);
dest.writeByte(mVibrationEnabled ? (byte) 1 : (byte) 0);
@@ -585,8 +605,8 @@
/**
* Sets the vibration pattern for notifications posted to this channel. If the provided
- * pattern is valid (non-null, non-empty), will enable vibration on this channel
- * (equivalent to calling {@link #enableVibration(boolean)} with {@code true}).
+ * pattern is valid (non-null, non-empty with at least 1 non-zero value), will enable vibration
+ * on this channel (equivalent to calling {@link #enableVibration(boolean)} with {@code true}).
* Otherwise, vibration will be disabled unless {@link #enableVibration(boolean)} is
* used with {@code true}, in which case the default vibration will be used.
*
@@ -595,7 +615,56 @@
*/
public void setVibrationPattern(long[] vibrationPattern) {
this.mVibrationEnabled = vibrationPattern != null && vibrationPattern.length > 0;
- this.mVibration = vibrationPattern;
+ this.mVibrationPattern = vibrationPattern;
+ if (Flags.notificationChannelVibrationEffectApi()) {
+ try {
+ this.mVibrationEffect =
+ VibrationEffect.createWaveform(vibrationPattern, /* repeat= */ -1);
+ } catch (IllegalArgumentException | NullPointerException e) {
+ this.mVibrationEffect = null;
+ }
+ }
+ }
+
+ /**
+ * Sets a {@link VibrationEffect} for notifications posted to this channel. If the
+ * provided effect is non-null, will enable vibration on this channel (equivalent
+ * to calling {@link #enableVibration(boolean)} with {@code true}). Otherwise
+ * vibration will be disabled unless {@link #enableVibration(boolean)} is used with
+ * {@code true}, in which case the default vibration will be used.
+ *
+ * <p>The effect passed here will be returned from {@link #getVibrationEffect()}.
+ * If the provided {@link VibrationEffect} is an equivalent to a wave-form
+ * vibration pattern, the equivalent wave-form pattern will be returned from
+ * {@link #getVibrationPattern()}.
+ *
+ * <p>Note that some {@link VibrationEffect}s may not be playable on some devices.
+ * In cases where such an effect is passed here, vibration will still be enabled
+ * for the channel, but the default vibration will be used. Nonetheless, the
+ * provided effect will be stored and be returned from {@link #getVibrationEffect}
+ * calls, and could be used by the same channel on a different device, for example,
+ * in cases the user backs up and restores to a device that does have the ability
+ * to play the effect, where that effect will be used instead of the default. To
+ * avoid such issues that could make the vibration behavior of your notification
+ * channel differ among different devices, it's recommended that you avoid
+ * vibration effect primitives, as the support for them differs widely among
+ * devices (read {@link VibrationEffect.Composition} for more on vibration
+ * primitives).
+ *
+ * <p>Only modifiable before the channel is submitted to
+ * {@link NotificationManager#createNotificationChannel(NotificationChannel)}.
+ *
+ * @see #getVibrationEffect()
+ * @see Vibrator#areEffectsSupported(int...)
+ * @see Vibrator#arePrimitivesSupported(int...)
+ */
+ @FlaggedApi(Flags.FLAG_NOTIFICATION_CHANNEL_VIBRATION_EFFECT_API)
+ public void setVibrationEffect(@Nullable VibrationEffect effect) {
+ this.mVibrationEnabled = effect != null;
+ this.mVibrationEffect = effect;
+ this.mVibrationPattern =
+ effect == null
+ ? null : effect.computeCreateWaveformOffOnTimingsOrNull();
}
/**
@@ -768,7 +837,35 @@
* vibration is not enabled ({@link #shouldVibrate()}).
*/
public long[] getVibrationPattern() {
- return mVibration;
+ return mVibrationPattern;
+ }
+
+ /**
+ * Returns the {@link VibrationEffect} for notifications posted to this channel.
+ * The returned effect is derived from either the effect provided in the
+ * {@link #setVibrationEffect(VibrationEffect)} method, or the equivalent vibration effect
+ * of the pattern set via the {@link #setVibrationPattern(long[])} method, based on setter
+ * method that was called last.
+ *
+ * The returned effect will be ignored in one of the following cases:
+ * <ul>
+ * <li> vibration is not enabled for the channel (i.e. {@link #shouldVibrate()}
+ * returns {@code false}).
+ * <li> the effect is not supported/playable by the device. In this case, if
+ * vibration is enabled for the channel, the default channel vibration will
+ * be used instead.
+ * </ul>
+ *
+ * @return the {@link VibrationEffect} set via {@link
+ * #setVibrationEffect(VibrationEffect)}, or the equivalent of the
+ * vibration set via {@link #setVibrationPattern(long[])}.
+ *
+ * @see VibrationEffect#createWaveform(long[], int)
+ */
+ @FlaggedApi(Flags.FLAG_NOTIFICATION_CHANNEL_VIBRATION_EFFECT_API)
+ @Nullable
+ public VibrationEffect getVibrationEffect() {
+ return mVibrationEffect;
}
/**
@@ -991,7 +1088,19 @@
enableLights(safeBool(parser, ATT_LIGHTS, false));
setLightColor(safeInt(parser, ATT_LIGHT_COLOR, DEFAULT_LIGHT_COLOR));
+ // Set the pattern before the effect, so that we can properly handle cases where the pattern
+ // is null, but the effect is not null (i.e. for non-waveform VibrationEffects - the ones
+ // which cannot be represented as a vibration pattern).
setVibrationPattern(safeLongArray(parser, ATT_VIBRATION, null));
+ if (Flags.notificationChannelVibrationEffectApi()) {
+ VibrationEffect vibrationEffect = safeVibrationEffect(parser, ATT_VIBRATION_EFFECT);
+ if (vibrationEffect != null) {
+ // Restore the effect only if it is not null. This allows to avoid undoing a
+ // `setVibrationPattern` call above, if that was done with a non-null pattern
+ // (e.g. back up from a version that did not support `setVibrationEffect`).
+ setVibrationEffect(vibrationEffect);
+ }
+ }
enableVibration(safeBool(parser, ATT_VIBRATION_ENABLED, false));
setShowBadge(safeBool(parser, ATT_SHOW_BADGE, false));
setDeleted(safeBool(parser, ATT_DELETED, false));
@@ -1180,6 +1289,9 @@
if (getVibrationPattern() != null) {
out.attribute(null, ATT_VIBRATION, longArrayToString(getVibrationPattern()));
}
+ if (getVibrationEffect() != null) {
+ out.attribute(null, ATT_VIBRATION_EFFECT, vibrationToString(getVibrationEffect()));
+ }
if (getUserLockedFields() != 0) {
out.attributeInt(null, ATT_USER_LOCKED, getUserLockedFields());
}
@@ -1260,6 +1372,9 @@
record.put(ATT_USER_LOCKED, Integer.toString(getUserLockedFields()));
record.put(ATT_FG_SERVICE_SHOWN, Boolean.toString(isUserVisibleTaskShown()));
record.put(ATT_VIBRATION, longArrayToString(getVibrationPattern()));
+ if (getVibrationEffect() != null) {
+ record.put(ATT_VIBRATION_EFFECT, vibrationToString(getVibrationEffect()));
+ }
record.put(ATT_SHOW_BADGE, Boolean.toString(canShowBadge()));
record.put(ATT_DELETED, Boolean.toString(isDeleted()));
record.put(ATT_DELETED_TIME_MS, Long.toString(getDeletedTimeMs()));
@@ -1287,6 +1402,30 @@
return val == null ? null : Uri.parse(val);
}
+ private static String vibrationToString(VibrationEffect effect) {
+ StringWriter writer = new StringWriter();
+ try {
+ VibrationXmlSerializer.serialize(
+ effect, writer, VibrationXmlSerializer.FLAG_ALLOW_HIDDEN_APIS);
+ } catch (IOException e) {
+ Log.e(TAG, "Unable to serialize vibration: " + effect, e);
+ }
+ return writer.toString();
+ }
+
+ private static VibrationEffect safeVibrationEffect(TypedXmlPullParser parser, String att) {
+ final String val = parser.getAttributeValue(null, att);
+ if (val != null) {
+ try {
+ return VibrationXmlParser.parseVibrationEffect(
+ new StringReader(val), VibrationXmlParser.FLAG_ALLOW_HIDDEN_APIS);
+ } catch (IOException e) {
+ Log.e(TAG, "Unable to read serialized vibration effect", e);
+ }
+ }
+ return null;
+ }
+
private static int safeInt(TypedXmlPullParser parser, String att, int defValue) {
return parser.getAttributeInt(null, att, defValue);
}
@@ -1361,7 +1500,8 @@
&& Objects.equals(getName(), that.getName())
&& Objects.equals(mDesc, that.mDesc)
&& Objects.equals(getSound(), that.getSound())
- && Arrays.equals(mVibration, that.mVibration)
+ && Arrays.equals(mVibrationPattern, that.mVibrationPattern)
+ && Objects.equals(getVibrationEffect(), that.getVibrationEffect())
&& Objects.equals(getGroup(), that.getGroup())
&& Objects.equals(getAudioAttributes(), that.getAudioAttributes())
&& mImportanceLockedDefaultApp == that.mImportanceLockedDefaultApp
@@ -1379,9 +1519,9 @@
getUserLockedFields(), isUserVisibleTaskShown(),
mVibrationEnabled, mShowBadge, isDeleted(), getDeletedTimeMs(),
getGroup(), getAudioAttributes(), isBlockable(), mAllowBubbles,
- mImportanceLockedDefaultApp, mOriginalImportance,
+ mImportanceLockedDefaultApp, mOriginalImportance, getVibrationEffect(),
mParentId, mConversationId, mDemoted, mImportantConvo);
- result = 31 * result + Arrays.hashCode(mVibration);
+ result = 31 * result + Arrays.hashCode(mVibrationPattern);
return result;
}
@@ -1413,7 +1553,9 @@
+ ", mSound=" + mSound
+ ", mLights=" + mLights
+ ", mLightColor=" + mLightColor
- + ", mVibration=" + Arrays.toString(mVibration)
+ + ", mVibrationPattern=" + Arrays.toString(mVibrationPattern)
+ + ", mVibrationEffect="
+ + (mVibrationEffect == null ? "null" : mVibrationEffect.toString())
+ ", mUserLockedFields=" + Integer.toHexString(mUserLockedFields)
+ ", mUserVisibleTaskShown=" + mUserVisibleTaskShown
+ ", mVibrationEnabled=" + mVibrationEnabled
@@ -1448,8 +1590,8 @@
}
proto.write(NotificationChannelProto.USE_LIGHTS, mLights);
proto.write(NotificationChannelProto.LIGHT_COLOR, mLightColor);
- if (mVibration != null) {
- for (long v : mVibration) {
+ if (mVibrationPattern != null) {
+ for (long v : mVibrationPattern) {
proto.write(NotificationChannelProto.VIBRATION, v);
}
}
diff --git a/core/java/android/app/QueuedWork.java b/core/java/android/app/QueuedWork.java
index edf0a46..60b61f3 100644
--- a/core/java/android/app/QueuedWork.java
+++ b/core/java/android/app/QueuedWork.java
@@ -27,6 +27,7 @@
import android.util.Log;
import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.ExponentiallyBucketedHistogram;
import java.util.LinkedList;
@@ -114,6 +115,36 @@
}
/**
+ * Tear down the handler.
+ */
+ @VisibleForTesting
+ public static void resetHandler() {
+ synchronized (sLock) {
+ if (sHandler == null) {
+ return;
+ }
+ sHandler.getLooper().quitSafely();
+ sHandler = null;
+ }
+ }
+
+ /**
+ * Remove all Messages from the Handler with the given code.
+ *
+ * This method intentionally avoids creating the Handler if it doesn't
+ * already exist.
+ */
+ private static void handlerRemoveMessages(int what) {
+ synchronized (sLock) {
+ if (sHandler == null) {
+ // Nothing to remove
+ return;
+ }
+ getHandler().removeMessages(what);
+ }
+ }
+
+ /**
* Add a finisher-runnable to wait for {@link #queue asynchronously processed work}.
*
* Used by SharedPreferences$Editor#startCommit().
@@ -156,17 +187,13 @@
long startTime = System.currentTimeMillis();
boolean hadMessages = false;
- Handler handler = getHandler();
-
synchronized (sLock) {
- if (handler.hasMessages(QueuedWorkHandler.MSG_RUN)) {
- // Delayed work will be processed at processPendingWork() below
- handler.removeMessages(QueuedWorkHandler.MSG_RUN);
-
- if (DEBUG) {
- hadMessages = true;
- Log.d(LOG_TAG, "waiting");
- }
+ if (DEBUG) {
+ hadMessages = getHandler().hasMessages(QueuedWorkHandler.MSG_RUN);
+ }
+ handlerRemoveMessages(QueuedWorkHandler.MSG_RUN);
+ if (DEBUG && hadMessages) {
+ Log.d(LOG_TAG, "waiting");
}
// We should not delay any work as this might delay the finishers
@@ -257,7 +284,7 @@
sWork = new LinkedList<>();
// Remove all msg-s as all work will be processed now
- getHandler().removeMessages(QueuedWorkHandler.MSG_RUN);
+ handlerRemoveMessages(QueuedWorkHandler.MSG_RUN);
}
if (work.size() > 0) {
diff --git a/core/java/android/app/admin/DevicePolicyIdentifiers.java b/core/java/android/app/admin/DevicePolicyIdentifiers.java
index b0bec78..d7aafa0 100644
--- a/core/java/android/app/admin/DevicePolicyIdentifiers.java
+++ b/core/java/android/app/admin/DevicePolicyIdentifiers.java
@@ -22,7 +22,6 @@
import android.app.admin.flags.Flags;
import android.os.UserManager;
-
import java.util.Objects;
/**
@@ -163,6 +162,12 @@
public static final String CROSS_PROFILE_WIDGET_PROVIDER_POLICY = "crossProfileWidgetProvider";
/**
+ * String identifier for {@link DevicePolicyManager#setContentProtectionPolicy}.
+ */
+ @FlaggedApi(android.view.contentprotection.flags.Flags.FLAG_MANAGE_DEVICE_POLICY_ENABLED)
+ public static final String CONTENT_PROTECTION_POLICY = "contentProtection";
+
+ /**
* String identifier for {@link DevicePolicyManager#setUsbDataSignalingEnabled}.
*/
@FlaggedApi(Flags.FLAG_POLICY_ENGINE_MIGRATION_V2_ENABLED)
diff --git a/core/java/android/app/admin/DevicePolicyManager.java b/core/java/android/app/admin/DevicePolicyManager.java
index 86d0125..c8762c6 100644
--- a/core/java/android/app/admin/DevicePolicyManager.java
+++ b/core/java/android/app/admin/DevicePolicyManager.java
@@ -18,12 +18,14 @@
import static android.Manifest.permission.INTERACT_ACROSS_USERS;
import static android.Manifest.permission.INTERACT_ACROSS_USERS_FULL;
+import static android.Manifest.permission.LOCK_DEVICE;
import static android.Manifest.permission.MANAGE_DEVICE_ADMINS;
import static android.Manifest.permission.MANAGE_DEVICE_POLICY_ACCOUNT_MANAGEMENT;
import static android.Manifest.permission.MANAGE_DEVICE_POLICY_APPS_CONTROL;
import static android.Manifest.permission.MANAGE_DEVICE_POLICY_CAMERA;
import static android.Manifest.permission.MANAGE_DEVICE_POLICY_CERTIFICATES;
import static android.Manifest.permission.MANAGE_DEVICE_POLICY_COMMON_CRITERIA_MODE;
+import static android.Manifest.permission.MANAGE_DEVICE_POLICY_CONTENT_PROTECTION;
import static android.Manifest.permission.MANAGE_DEVICE_POLICY_DEFAULT_SMS;
import static android.Manifest.permission.MANAGE_DEVICE_POLICY_FACTORY_RESET;
import static android.Manifest.permission.MANAGE_DEVICE_POLICY_INPUT_METHODS;
@@ -53,7 +55,6 @@
import static android.content.Intent.LOCAL_FLAG_FROM_SYSTEM;
import static android.net.NetworkCapabilities.NET_ENTERPRISE_ID_1;
import static android.os.Build.VERSION_CODES.UPSIDE_DOWN_CAKE;
-import static android.view.contentprotection.flags.Flags.FLAG_MANAGE_DEVICE_POLICY_ENABLED;
import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage;
@@ -3825,6 +3826,10 @@
/** @hide */
@TestApi
public static final int OPERATION_UNINSTALL_CA_CERT = 40;
+ /** @hide */
+ @TestApi
+ @FlaggedApi(android.view.contentprotection.flags.Flags.FLAG_MANAGE_DEVICE_POLICY_ENABLED)
+ public static final int OPERATION_SET_CONTENT_PROTECTION_POLICY = 41;
private static final String PREFIX_OPERATION = "OPERATION_";
@@ -3869,7 +3874,8 @@
OPERATION_SET_PERMISSION_GRANT_STATE,
OPERATION_SET_PERMISSION_POLICY,
OPERATION_SET_RESTRICTIONS_PROVIDER,
- OPERATION_UNINSTALL_CA_CERT
+ OPERATION_UNINSTALL_CA_CERT,
+ OPERATION_SET_CONTENT_PROTECTION_POLICY
})
@Retention(RetentionPolicy.SOURCE)
public static @interface DevicePolicyOperation {
@@ -4095,15 +4101,15 @@
}
/** Indicates that content protection is not controlled by policy, allowing user to choose. */
- @FlaggedApi(FLAG_MANAGE_DEVICE_POLICY_ENABLED)
+ @FlaggedApi(android.view.contentprotection.flags.Flags.FLAG_MANAGE_DEVICE_POLICY_ENABLED)
public static final int CONTENT_PROTECTION_NOT_CONTROLLED_BY_POLICY = 0;
- /** Indicates that content protection is controlled and disabled by a policy. */
- @FlaggedApi(FLAG_MANAGE_DEVICE_POLICY_ENABLED)
+ /** Indicates that content protection is controlled and disabled by a policy (default). */
+ @FlaggedApi(android.view.contentprotection.flags.Flags.FLAG_MANAGE_DEVICE_POLICY_ENABLED)
public static final int CONTENT_PROTECTION_DISABLED = 1;
/** Indicates that content protection is controlled and enabled by a policy. */
- @FlaggedApi(FLAG_MANAGE_DEVICE_POLICY_ENABLED)
+ @FlaggedApi(android.view.contentprotection.flags.Flags.FLAG_MANAGE_DEVICE_POLICY_ENABLED)
public static final int CONTENT_PROTECTION_ENABLED = 2;
/** @hide */
@@ -4118,6 +4124,86 @@
public @interface ContentProtectionPolicy {}
/**
+ * Sets the content protection policy which controls scanning for deceptive apps.
+ * <p>
+ * This function can only be called by the device owner, a profile owner of an affiliated user
+ * or profile, or the profile owner when no device owner is set or holders of the permission
+ * {@link android.Manifest.permission#MANAGE_DEVICE_POLICY_CONTENT_PROTECTION}. See
+ * {@link #isAffiliatedUser}.
+ * Any policy set via this method will be cleared if the user becomes unaffiliated.
+ * <p>
+ * After the content protection policy has been set,
+ * {@link PolicyUpdateReceiver#onPolicySetResult(Context, String, Bundle, TargetUser,
+ * PolicyUpdateResult)} will notify the admin on whether the policy was successfully set or not.
+ * This callback will contain:
+ * <ul>
+ * <li> The policy identifier {@link DevicePolicyIdentifiers#CONTENT_PROTECTION_POLICY}
+ * <li> The {@link TargetUser} that this policy relates to
+ * <li> The {@link PolicyUpdateResult}, which will be
+ * {@link PolicyUpdateResult#RESULT_POLICY_SET} if the policy was successfully set or the
+ * reason the policy failed to be set
+ * (e.g. {@link PolicyUpdateResult#RESULT_FAILURE_CONFLICTING_ADMIN_POLICY})
+ * </ul>
+ * If there has been a change to the policy,
+ * {@link PolicyUpdateReceiver#onPolicyChanged(Context, String, Bundle, TargetUser,
+ * PolicyUpdateResult)} will notify the admin of this change. This callback will contain the
+ * same parameters as PolicyUpdateReceiver#onPolicySetResult and the {@link PolicyUpdateResult}
+ * will contain the reason why the policy changed.
+ *
+ * @param admin Which {@link DeviceAdminReceiver} this request is associated with. Null if the
+ * caller is not a device admin.
+ * @param policy The content protection policy to set. One of {@link
+ * #CONTENT_PROTECTION_NOT_CONTROLLED_BY_POLICY},
+ * {@link #CONTENT_PROTECTION_DISABLED} or {@link #CONTENT_PROTECTION_ENABLED}.
+ * @throws SecurityException if {@code admin} is not the device owner, the profile owner of an
+ * affiliated user or profile, or the profile owner when no device owner is set or holder of the
+ * permission {@link android.Manifest.permission#MANAGE_DEVICE_POLICY_CONTENT_PROTECTION}.
+ * @see #isAffiliatedUser
+ */
+ @RequiresPermission(value = MANAGE_DEVICE_POLICY_CONTENT_PROTECTION, conditional = true)
+ @SupportsCoexistence
+ @FlaggedApi(android.view.contentprotection.flags.Flags.FLAG_MANAGE_DEVICE_POLICY_ENABLED)
+ public void setContentProtectionPolicy(
+ @Nullable ComponentName admin, @ContentProtectionPolicy int policy) {
+ throwIfParentInstance("setContentProtectionPolicy");
+ if (mService != null) {
+ try {
+ mService.setContentProtectionPolicy(admin, mContext.getPackageName(), policy);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ }
+
+ /**
+ * Returns the current content protection policy.
+ * <p>
+ * The returned policy will be the current resolved policy rather than the policy set by the
+ * calling admin.
+ *
+ * @param admin Which {@link DeviceAdminReceiver} this request is associated with. Null if the
+ * caller is not a device admin.
+ * @throws SecurityException if {@code admin} is not the device owner, the profile owner of an
+ * affiliated user or profile, or the profile owner when no device owner is set or holder of the
+ * permission {@link android.Manifest.permission#MANAGE_DEVICE_POLICY_CONTENT_PROTECTION}.
+ * @see #isAffiliatedUser
+ * @see #setContentProtectionPolicy
+ */
+ @RequiresPermission(value = MANAGE_DEVICE_POLICY_CONTENT_PROTECTION, conditional = true)
+ @FlaggedApi(android.view.contentprotection.flags.Flags.FLAG_MANAGE_DEVICE_POLICY_ENABLED)
+ public @ContentProtectionPolicy int getContentProtectionPolicy(@Nullable ComponentName admin) {
+ throwIfParentInstance("getContentProtectionPolicy");
+ if (mService != null) {
+ try {
+ return mService.getContentProtectionPolicy(admin, mContext.getPackageName());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ return CONTENT_PROTECTION_DISABLED;
+ }
+
+ /**
* This object is a single place to tack on invalidation and disable calls. All
* binder caches in this class derive from this Config, so all can be invalidated or
* disabled through this Config.
@@ -6330,10 +6416,10 @@
* (PIN, pattern, or password). This API is intended for use only by device admins.
* <p>
* From version {@link android.os.Build.VERSION_CODES#R} onwards, the caller must either have
- * the LOCK_DEVICE permission or the device must have the device admin feature; if neither is
- * true, then the method will return without completing any action. Before version
- * {@link android.os.Build.VERSION_CODES#R}, the device needed the device admin feature,
- * regardless of the caller's permissions.
+ * the LOCK_DEVICE permission or the device must have the
+ * device admin feature; if neither is true, then the method will return without completing
+ * any action. Before version {@link android.os.Build.VERSION_CODES#R},
+ * the device needed the device admin feature, regardless of the caller's permissions.
* <p>
* The calling device admin must have requested {@link DeviceAdminInfo#USES_POLICY_FORCE_LOCK}
* to be able to call this method; if it has not, a security exception will be thrown.
@@ -6353,7 +6439,8 @@
* @throws SecurityException if the calling application does not own an active administrator
* that uses {@link DeviceAdminInfo#USES_POLICY_FORCE_LOCK}
*/
- @RequiresPermission(value = MANAGE_DEVICE_POLICY_LOCK, conditional = true)
+ @SuppressLint("RequiresPermission")
+ @RequiresPermission(value = LOCK_DEVICE, conditional = true)
public void lockNow() {
lockNow(0);
}
@@ -6364,14 +6451,13 @@
* <p>
* This method secures the device in response to an urgent situation, such as a lost or stolen
* device. After this method is called, the device must be unlocked using strong authentication
- * (PIN, pattern, or password). This API is for use only by device admins and holders of the
- * {@link android.Manifest.permission#MANAGE_DEVICE_POLICY_LOCK} permission.
+ * (PIN, pattern, or password). This API is intended for use only by device admins.
* <p>
* From version {@link android.os.Build.VERSION_CODES#R} onwards, the caller must either have
- * the LOCK_DEVICE permission or the device must have the device admin feature; if neither is
- * true, then the method will return without completing any action. Before version
- * {@link android.os.Build.VERSION_CODES#R}, the device needed the device admin feature,
- * regardless of the caller's permissions.
+ * the LOCK_DEVICE permission or the device must have the
+ * device admin feature; if neither is true, then the method will return without completing any
+ * action. Before version {@link android.os.Build.VERSION_CODES#R}, the device needed the device
+ * admin feature, regardless of the caller's permissions.
* <p>
* A calling device admin must have requested {@link DeviceAdminInfo#USES_POLICY_FORCE_LOCK}
* to be able to call this method; if it has not, a security exception will be thrown.
@@ -6400,7 +6486,7 @@
* @param flags May be 0 or {@link #FLAG_EVICT_CREDENTIAL_ENCRYPTION_KEY}.
* @throws SecurityException if the calling application does not own an active administrator
* that uses {@link DeviceAdminInfo#USES_POLICY_FORCE_LOCK} and the does not hold
- * the {@link android.Manifest.permission#MANAGE_DEVICE_POLICY_LOCK} permission, or
+ * the {@link android.Manifest.permission#LOCK_DEVICE} permission, or
* the {@link #FLAG_EVICT_CREDENTIAL_ENCRYPTION_KEY} flag is passed by an
* application that is not a profile owner of a managed profile.
* @throws IllegalArgumentException if the {@link #FLAG_EVICT_CREDENTIAL_ENCRYPTION_KEY} flag is
@@ -6409,7 +6495,7 @@
* flag is passed when {@link #getStorageEncryptionStatus} does not return
* {@link #ENCRYPTION_STATUS_ACTIVE_PER_USER}.
*/
- @RequiresPermission(value = MANAGE_DEVICE_POLICY_LOCK, conditional = true)
+ @RequiresPermission(value = LOCK_DEVICE, conditional = true)
public void lockNow(@LockNowFlag int flags) {
if (mService != null) {
try {
diff --git a/core/java/android/app/admin/IDevicePolicyManager.aidl b/core/java/android/app/admin/IDevicePolicyManager.aidl
index 575fa4c..efcf563 100644
--- a/core/java/android/app/admin/IDevicePolicyManager.aidl
+++ b/core/java/android/app/admin/IDevicePolicyManager.aidl
@@ -610,4 +610,7 @@
String getFinancedDeviceKioskRoleHolder(String callerPackageName);
void calculateHasIncompatibleAccounts();
+
+ void setContentProtectionPolicy(in ComponentName who, String callerPackageName, int policy);
+ int getContentProtectionPolicy(in ComponentName who, String callerPackageName);
}
diff --git a/core/java/android/app/admin/SecurityLog.java b/core/java/android/app/admin/SecurityLog.java
index ca2e97e..ed1b8ca 100644
--- a/core/java/android/app/admin/SecurityLog.java
+++ b/core/java/android/app/admin/SecurityLog.java
@@ -17,12 +17,14 @@
package android.app.admin;
import android.Manifest;
+import android.annotation.FlaggedApi;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.RequiresPermission;
import android.annotation.SystemApi;
import android.annotation.TestApi;
+import android.app.admin.flags.Flags;
import android.compat.annotation.UnsupportedAppUsage;
import android.content.ComponentName;
import android.os.Build;
@@ -99,6 +101,7 @@
TAG_PACKAGE_INSTALLED,
TAG_PACKAGE_UPDATED,
TAG_PACKAGE_UNINSTALLED,
+ TAG_BACKUP_SERVICE_TOGGLED,
})
public @interface SecurityLogTag {}
@@ -599,6 +602,18 @@
public static final int TAG_PACKAGE_UNINSTALLED = SecurityLogTags.SECURITY_PACKAGE_UNINSTALLED;
/**
+ * Indicates that an admin has enabled or disabled backup service. The log entry contains the
+ * following information about the event encapsulated in an {@link Object} array, accessible
+ * via {@link SecurityEvent#getData()}:
+ * <li> [0] admin package name ({@code String})
+ * <li> [1] admin user ID ({@code Integer})
+ * <li> [2] backup service state ({@code Integer}, 1 for enabled, 0 for disabled)
+ * @see DevicePolicyManager#setBackupServiceEnabled(ComponentName, boolean)
+ */
+ @FlaggedApi(Flags.FLAG_BACKUP_SERVICE_SECURITY_LOG_EVENT_ENABLED)
+ public static final int TAG_BACKUP_SERVICE_TOGGLED =
+ SecurityLogTags.SECURITY_BACKUP_SERVICE_TOGGLED;
+ /**
* Event severity level indicating that the event corresponds to normal workflow.
*/
public static final int LEVEL_INFO = 1;
diff --git a/core/java/android/app/admin/SecurityLogTags.logtags b/core/java/android/app/admin/SecurityLogTags.logtags
index e4af8dd..7b3aa7b 100644
--- a/core/java/android/app/admin/SecurityLogTags.logtags
+++ b/core/java/android/app/admin/SecurityLogTags.logtags
@@ -47,4 +47,5 @@
210040 security_bluetooth_disconnection (addr|3),(reason|3)
210041 security_package_installed (package_name|3),(version_code|1),(user_id|1)
210042 security_package_updated (package_name|3),(version_code|1),(user_id|1)
-210043 security_package_uninstalled (package_name|3),(version_code|1),(user_id|1)
\ No newline at end of file
+210043 security_package_uninstalled (package_name|3),(version_code|1),(user_id|1)
+210044 security_backup_service_toggled (package|3),(admin_user|1),(enabled|1)
\ No newline at end of file
diff --git a/core/java/android/app/admin/flags/flags.aconfig b/core/java/android/app/admin/flags/flags.aconfig
index b3ecd92..561eb00 100644
--- a/core/java/android/app/admin/flags/flags.aconfig
+++ b/core/java/android/app/admin/flags/flags.aconfig
@@ -62,3 +62,10 @@
description: "Exempt the default sms app of the context user for suspension when calling setPersonalAppsSuspended"
bug: "309183330"
}
+
+flag {
+ name: "backup_service_security_log_event_enabled"
+ namespace: "enterprise"
+ description: "Emit a security log event when DPM.setBackupServiceEnabled is called"
+ bug: "304999634"
+}
diff --git a/core/java/android/app/backup/BackupRestoreEventLogger.java b/core/java/android/app/backup/BackupRestoreEventLogger.java
index ea31ef3..112c5fd 100644
--- a/core/java/android/app/backup/BackupRestoreEventLogger.java
+++ b/core/java/android/app/backup/BackupRestoreEventLogger.java
@@ -26,6 +26,8 @@
import android.util.ArrayMap;
import android.util.Slog;
+import com.android.server.backup.Flags;
+
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.nio.charset.StandardCharsets;
@@ -56,7 +58,7 @@
*
* @hide
*/
- public static final int DATA_TYPES_ALLOWED = 15;
+ public static final int DATA_TYPES_ALLOWED = 150;
/**
* Denotes that the annotated element identifies a data type as required by the logging methods
@@ -299,7 +301,7 @@
}
if (!mResults.containsKey(dataType)) {
- if (mResults.keySet().size() == DATA_TYPES_ALLOWED) {
+ if (mResults.keySet().size() == getDataTypesAllowed()) {
// This is a new data type and we're already at capacity.
Slog.d(TAG, "Logger is full, ignoring new data type");
return null;
@@ -315,6 +317,14 @@
return mHashDigest.digest(metaData.getBytes(StandardCharsets.UTF_8));
}
+ private int getDataTypesAllowed(){
+ if (Flags.enableIncreaseDatatypesForAgentLogging()) {
+ return DATA_TYPES_ALLOWED;
+ } else {
+ return 15;
+ }
+ }
+
/**
* Encapsulate logging results for a single data type.
*/
diff --git a/core/java/android/app/contextualsearch/OWNERS b/core/java/android/app/contextualsearch/OWNERS
new file mode 100644
index 0000000..0c2612c
--- /dev/null
+++ b/core/java/android/app/contextualsearch/OWNERS
@@ -0,0 +1 @@
+include /core/java/android/service/contextualsearch/OWNERS
diff --git a/core/java/android/app/notification.aconfig b/core/java/android/app/notification.aconfig
index a5d4a14..274d02a 100644
--- a/core/java/android/app/notification.aconfig
+++ b/core/java/android/app/notification.aconfig
@@ -50,3 +50,20 @@
description: "Adds a new voicemail category for notifications"
bug: "322806700"
}
+
+flag {
+ name: "notification_channel_vibration_effect_api"
+ namespace: "systemui"
+ description: "This flag enables the API to allow setting VibrationEffect for NotificationChannels"
+ bug: "241732519"
+}
+
+flag {
+ name: "evenly_divided_call_style_action_layout"
+ namespace: "systemui"
+ description: "Evenly divides horizontal space for action buttons in CallStyle notifications."
+ bug: "268733030"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
\ No newline at end of file
diff --git a/core/java/android/app/servertransaction/ClientTransaction.java b/core/java/android/app/servertransaction/ClientTransaction.java
index 5e55268..6357a20 100644
--- a/core/java/android/app/servertransaction/ClientTransaction.java
+++ b/core/java/android/app/servertransaction/ClientTransaction.java
@@ -298,6 +298,33 @@
return result;
}
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder();
+ sb.append("ClientTransaction{");
+ if (mTransactionItems != null) {
+ // #addTransactionItem
+ sb.append("\n transactionItems=[");
+ final int size = mTransactionItems.size();
+ for (int i = 0; i < size; i++) {
+ sb.append("\n ").append(mTransactionItems.get(i));
+ }
+ sb.append("\n ]");
+ } else {
+ // #addCallback
+ sb.append("\n callbacks=[");
+ final int size = mActivityCallbacks != null ? mActivityCallbacks.size() : 0;
+ for (int i = 0; i < size; i++) {
+ sb.append("\n ").append(mActivityCallbacks.get(i));
+ }
+ sb.append("\n ]");
+ // #setLifecycleStateRequest
+ sb.append("\n stateRequest=").append(mLifecycleStateRequest);
+ }
+ sb.append("\n}");
+ return sb.toString();
+ }
+
/** Dump transaction items callback items and final lifecycle state request. */
void dump(@NonNull String prefix, @NonNull PrintWriter pw,
@NonNull ClientTransactionHandler transactionHandler) {
diff --git a/core/java/android/app/servertransaction/LaunchActivityItem.java b/core/java/android/app/servertransaction/LaunchActivityItem.java
index 4d53701..95f5ad0 100644
--- a/core/java/android/app/servertransaction/LaunchActivityItem.java
+++ b/core/java/android/app/servertransaction/LaunchActivityItem.java
@@ -80,6 +80,7 @@
private IBinder mShareableActivityToken;
private boolean mLaunchedFromBubble;
private IBinder mTaskFragmentToken;
+ private IBinder mInitialCallerInfoAccessToken;
/**
* It is only non-null if the process is the first time to launch activity. It is only an
* optimization for quick look up of the interface so the field is ignored for comparison.
@@ -106,7 +107,7 @@
mOverrideConfig, mReferrer, mVoiceInteractor, mState, mPersistentState,
mPendingResults, mPendingNewIntents, mSceneTransitionInfo, mIsForward,
mProfilerInfo, client, mAssistToken, mShareableActivityToken, mLaunchedFromBubble,
- mTaskFragmentToken);
+ mTaskFragmentToken, mInitialCallerInfoAccessToken);
client.handleLaunchActivity(r, pendingActions, mDeviceId, null /* customIntent */);
Trace.traceEnd(TRACE_TAG_ACTIVITY_MANAGER);
}
@@ -140,7 +141,7 @@
boolean isForward, @Nullable ProfilerInfo profilerInfo, @NonNull IBinder assistToken,
@Nullable IActivityClientController activityClientController,
@NonNull IBinder shareableActivityToken, boolean launchedFromBubble,
- @Nullable IBinder taskFragmentToken) {
+ @Nullable IBinder taskFragmentToken, @NonNull IBinder initialCallerInfoAccessToken) {
LaunchActivityItem instance = ObjectPool.obtain(LaunchActivityItem.class);
if (instance == null) {
instance = new LaunchActivityItem();
@@ -155,7 +156,7 @@
sceneTransitionInfo, isForward,
profilerInfo != null ? new ProfilerInfo(profilerInfo) : null,
assistToken, activityClientController, shareableActivityToken,
- launchedFromBubble, taskFragmentToken);
+ launchedFromBubble, taskFragmentToken, initialCallerInfoAccessToken);
return instance;
}
@@ -170,7 +171,7 @@
@Override
public void recycle() {
setValues(this, null, null, 0, null, null, null, 0, null, null, 0, null, null, null, null,
- null, false, null, null, null, null, false, null);
+ null, false, null, null, null, null, false, null, null);
ObjectPool.recycle(this);
}
@@ -201,6 +202,7 @@
dest.writeStrongBinder(mShareableActivityToken);
dest.writeBoolean(mLaunchedFromBubble);
dest.writeStrongBinder(mTaskFragmentToken);
+ dest.writeStrongBinder(mInitialCallerInfoAccessToken);
}
/** Read from Parcel. */
@@ -220,6 +222,7 @@
IActivityClientController.Stub.asInterface(in.readStrongBinder()),
in.readStrongBinder(),
in.readBoolean(),
+ in.readStrongBinder(),
in.readStrongBinder());
}
@@ -259,7 +262,9 @@
&& Objects.equals(mProfilerInfo, other.mProfilerInfo)
&& Objects.equals(mAssistToken, other.mAssistToken)
&& Objects.equals(mShareableActivityToken, other.mShareableActivityToken)
- && Objects.equals(mTaskFragmentToken, other.mTaskFragmentToken);
+ && Objects.equals(mTaskFragmentToken, other.mTaskFragmentToken)
+ && Objects.equals(mInitialCallerInfoAccessToken,
+ other.mInitialCallerInfoAccessToken);
}
@Override
@@ -283,6 +288,7 @@
result = 31 * result + Objects.hashCode(mAssistToken);
result = 31 * result + Objects.hashCode(mShareableActivityToken);
result = 31 * result + Objects.hashCode(mTaskFragmentToken);
+ result = 31 * result + Objects.hashCode(mInitialCallerInfoAccessToken);
return result;
}
@@ -345,7 +351,7 @@
@Nullable ProfilerInfo profilerInfo, @Nullable IBinder assistToken,
@Nullable IActivityClientController activityClientController,
@Nullable IBinder shareableActivityToken, boolean launchedFromBubble,
- @Nullable IBinder taskFragmentToken) {
+ @Nullable IBinder taskFragmentToken, @Nullable IBinder initialCallerInfoAccessToken) {
instance.mActivityToken = activityToken;
instance.mIntent = intent;
instance.mIdent = ident;
@@ -368,5 +374,6 @@
instance.mShareableActivityToken = shareableActivityToken;
instance.mLaunchedFromBubble = launchedFromBubble;
instance.mTaskFragmentToken = taskFragmentToken;
+ instance.mInitialCallerInfoAccessToken = initialCallerInfoAccessToken;
}
}
diff --git a/core/java/android/app/servertransaction/TransactionExecutor.java b/core/java/android/app/servertransaction/TransactionExecutor.java
index ba94077..406e00a 100644
--- a/core/java/android/app/servertransaction/TransactionExecutor.java
+++ b/core/java/android/app/servertransaction/TransactionExecutor.java
@@ -97,6 +97,10 @@
executeCallbacks(transaction);
executeLifecycleState(transaction);
}
+ } catch (Exception e) {
+ Slog.e(TAG, "Failed to execute the transaction: "
+ + transactionToString(transaction, mTransactionHandler));
+ throw e;
} finally {
Trace.traceEnd(Trace.TRACE_TAG_WINDOW_MANAGER);
}
diff --git a/core/java/android/app/usage/StorageStats.java b/core/java/android/app/usage/StorageStats.java
index 87d97d5..7bfaef4 100644
--- a/core/java/android/app/usage/StorageStats.java
+++ b/core/java/android/app/usage/StorageStats.java
@@ -40,28 +40,79 @@
/** @hide */ public long apkBytes;
/** @hide */ public long libBytes;
/** @hide */ public long dmBytes;
+ /** @hide */ public long dexoptBytes;
+ /** @hide */ public long curProfBytes;
+ /** @hide */ public long refProfBytes;
/** @hide */ public long externalCacheBytes;
- /** Represents all .apk files in application code path.
+ /**
+ * Represents all nonstale dexopt and runtime artifacts of application.
+ * This includes AOT-compiled code and other data that can speed up app execution.
+ * For more detailed information, read the
+ * <a href="https://source.android.com/docs/core/runtime/jit-compiler#flow">JIT compiler</a>
+ * guide.
+ *
+ * Dexopt artifacts become stale when one of their dependencies
+ * has changed. They may be cleaned up or replaced by ART Services at any time.
+ *
+ * For a preload app, this type includes dexopt artifacts on readonly partitions
+ * if they are up-to-date.
+ *
+ * Can be used as an input to {@link #getAppBytesByDataType(int)}
+ * to get the sum of sizes for files of this type. The sum might include the size of data
+ * that is part of appBytes, dataBytes or cacheBytes.
+ */
+ @FlaggedApi(Flags.FLAG_GET_APP_BYTES_BY_DATA_TYPE_API)
+ public static final int APP_DATA_TYPE_FILE_TYPE_DEXOPT_ARTIFACT = 0;
+
+ /**
+ * Represents reference profile of application.
+ *
+ * Reference profiles are the ones used during the last profile-guided dexopt.
+ * If the last dexopt wasn't profile-guided, then these profiles were not used.
+ *
+ * Can be used as an input to {@link #getAppBytesByDataType(int)}
+ * to get the size of files of this type.
+ */
+ @FlaggedApi(Flags.FLAG_GET_APP_BYTES_BY_DATA_TYPE_API)
+ public static final int APP_DATA_TYPE_FILE_TYPE_REFERENCE_PROFILE = 1;
+
+ /**
+ * Represents current profile of application.
+ *
+ * Current profiles may or may not be used during the next profile-guided dexopt.
+ *
+ * Can be used as an input to {@link #getAppBytesByDataType(int)}
+ * to get the size of files of this type. This size fluctuates regularly,
+ * it goes up when the user uses more and more classes/methods and comes down when
+ * a deamon merges this into the ref profile and does profile-guided dexopt.
+ */
+ @FlaggedApi(Flags.FLAG_GET_APP_BYTES_BY_DATA_TYPE_API)
+ public static final int APP_DATA_TYPE_FILE_TYPE_CURRENT_PROFILE = 2;
+
+ /**
+ * Represents all .apk files in application code path.
* Can be used as an input to {@link #getAppBytesByDataType(int)}
* to get the sum of sizes for files of this type.
*/
@FlaggedApi(Flags.FLAG_GET_APP_BYTES_BY_DATA_TYPE_API)
- public static final int APP_DATA_TYPE_FILE_TYPE_APK = 0;
+ public static final int APP_DATA_TYPE_FILE_TYPE_APK = 3;
- /** Represents all .dm files in application code path.
+ /**
+ * Represents all .dm files in application code path.
* Can be used as an input to {@link #getAppBytesByDataType(int)}
* to get the sum of sizes for files of this type.
*/
@FlaggedApi(Flags.FLAG_GET_APP_BYTES_BY_DATA_TYPE_API)
- public static final int APP_DATA_TYPE_FILE_TYPE_DM = 1;
+ public static final int APP_DATA_TYPE_FILE_TYPE_DM = 4;
- /** Represents lib/ in application code path.
+ /**
+ * Represents lib/ in application code path.
* Can be used as an input to {@link #getAppBytesByDataType(int)}
* to get the size of lib/ directory.
*/
@FlaggedApi(Flags.FLAG_GET_APP_BYTES_BY_DATA_TYPE_API)
- public static final int APP_DATA_TYPE_LIB = 2;
+ public static final int APP_DATA_TYPE_LIB = 5;
/**
* Keep in sync with the file types defined above.
@@ -69,6 +120,9 @@
*/
@FlaggedApi(Flags.FLAG_GET_APP_BYTES_BY_DATA_TYPE_API)
@IntDef(flag = false, value = {
+ APP_DATA_TYPE_FILE_TYPE_DEXOPT_ARTIFACT,
+ APP_DATA_TYPE_FILE_TYPE_REFERENCE_PROFILE,
+ APP_DATA_TYPE_FILE_TYPE_CURRENT_PROFILE,
APP_DATA_TYPE_FILE_TYPE_APK,
APP_DATA_TYPE_FILE_TYPE_DM,
APP_DATA_TYPE_LIB,
@@ -103,6 +157,9 @@
@FlaggedApi(Flags.FLAG_GET_APP_BYTES_BY_DATA_TYPE_API)
public long getAppBytesByDataType(@AppDataType int dataType) {
switch (dataType) {
+ case APP_DATA_TYPE_FILE_TYPE_DEXOPT_ARTIFACT: return dexoptBytes;
+ case APP_DATA_TYPE_FILE_TYPE_REFERENCE_PROFILE: return refProfBytes;
+ case APP_DATA_TYPE_FILE_TYPE_CURRENT_PROFILE: return curProfBytes;
case APP_DATA_TYPE_FILE_TYPE_APK: return apkBytes;
case APP_DATA_TYPE_LIB: return libBytes;
case APP_DATA_TYPE_FILE_TYPE_DM: return dmBytes;
@@ -161,6 +218,9 @@
this.codeBytes = in.readLong();
this.dataBytes = in.readLong();
this.cacheBytes = in.readLong();
+ this.dexoptBytes = in.readLong();
+ this.refProfBytes = in.readLong();
+ this.curProfBytes = in.readLong();
this.apkBytes = in.readLong();
this.libBytes = in.readLong();
this.dmBytes = in.readLong();
@@ -177,6 +237,9 @@
dest.writeLong(codeBytes);
dest.writeLong(dataBytes);
dest.writeLong(cacheBytes);
+ dest.writeLong(dexoptBytes);
+ dest.writeLong(refProfBytes);
+ dest.writeLong(curProfBytes);
dest.writeLong(apkBytes);
dest.writeLong(libBytes);
dest.writeLong(dmBytes);
diff --git a/core/java/android/app/wearable/IWearableSensingManager.aidl b/core/java/android/app/wearable/IWearableSensingManager.aidl
index ff37bd8..3cbc8a2 100644
--- a/core/java/android/app/wearable/IWearableSensingManager.aidl
+++ b/core/java/android/app/wearable/IWearableSensingManager.aidl
@@ -16,6 +16,7 @@
package android.app.wearable;
+import android.app.PendingIntent;
import android.os.ParcelFileDescriptor;
import android.os.PersistableBundle;
import android.os.RemoteCallback;
@@ -28,7 +29,13 @@
*/
interface IWearableSensingManager {
@JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.MANAGE_WEARABLE_SENSING_SERVICE)")
+ void provideWearableConnection(in ParcelFileDescriptor parcelFileDescriptor, in RemoteCallback callback);
+ @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.MANAGE_WEARABLE_SENSING_SERVICE)")
void provideDataStream(in ParcelFileDescriptor parcelFileDescriptor, in RemoteCallback callback);
@JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.MANAGE_WEARABLE_SENSING_SERVICE)")
void provideData(in PersistableBundle data, in SharedMemory sharedMemory, in RemoteCallback callback);
+ @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.MANAGE_WEARABLE_SENSING_SERVICE)")
+ void registerDataRequestObserver(int dataType, in PendingIntent dataRequestPendingIntent, in RemoteCallback statusCallback);
+ @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.MANAGE_WEARABLE_SENSING_SERVICE)")
+ void unregisterDataRequestObserver(int dataType, in PendingIntent dataRequestPendingIntent, in RemoteCallback statusCallback);
}
\ No newline at end of file
diff --git a/core/java/android/app/wearable/WearableSensingDataRequest.java b/core/java/android/app/wearable/WearableSensingDataRequest.java
new file mode 100644
index 0000000..9329b37
--- /dev/null
+++ b/core/java/android/app/wearable/WearableSensingDataRequest.java
@@ -0,0 +1,190 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app.wearable;
+
+import android.annotation.FlaggedApi;
+import android.annotation.NonNull;
+import android.annotation.SystemApi;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.PersistableBundle;
+
+import java.time.Duration;
+
+/**
+ * Data class for a data request for wearable sensing.
+ *
+ * @hide
+ */
+@FlaggedApi(Flags.FLAG_ENABLE_DATA_REQUEST_OBSERVER_API)
+@SystemApi
+public final class WearableSensingDataRequest implements Parcelable {
+ private static final int MAX_REQUEST_SIZE = 200;
+ private static final Duration RATE_LIMIT_WINDOW_SIZE = Duration.ofMinutes(1);
+ private static final int RATE_LIMIT = 30;
+
+ private final int mDataType;
+ @NonNull private final PersistableBundle mRequestDetails;
+
+ private WearableSensingDataRequest(int dataType, @NonNull PersistableBundle requestDetails) {
+ mDataType = dataType;
+ mRequestDetails = requestDetails;
+ }
+
+ /** Returns the data type this request is for. */
+ public int getDataType() {
+ return mDataType;
+ }
+
+ /** Returns the details for this request. */
+ @NonNull
+ public PersistableBundle getRequestDetails() {
+ return mRequestDetails;
+ }
+
+ /** Returns the data size of this object when it is parcelled. */
+ public int getDataSize() {
+ Parcel parcel = Parcel.obtain();
+ try {
+ writeToParcel(parcel, describeContents());
+ return parcel.dataSize();
+ } finally {
+ parcel.recycle();
+ }
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ dest.writeInt(mDataType);
+ dest.writeTypedObject(mRequestDetails, flags);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public String toString() {
+ return "WearableSensingDataRequest { "
+ + "dataType = "
+ + mDataType
+ + ", "
+ + "requestDetails = "
+ + mRequestDetails
+ + " }";
+ }
+
+ /**
+ * Returns a String representation of this data request that shows its contents.
+ *
+ * @hide
+ */
+ public String toExpandedString() {
+ if (mRequestDetails != null) {
+ // Trigger unparcelling so that its individual fields will be listed in toString
+ boolean unused =
+ mRequestDetails.getBoolean(
+ "PlaceholderForWearableSensingDataRequest#toExpandedString()");
+ }
+ return toString();
+ }
+
+ /**
+ * The bundle key for this class of object, used in {@code RemoteCallback#sendResult}.
+ *
+ * @hide
+ */
+ public static final String REQUEST_BUNDLE_KEY =
+ "android.app.wearable.WearableSensingDataRequestBundleKey";
+
+ /**
+ * The bundle key for the status callback for a data request, used in {@code
+ * RemoteCallback#sendResult}.
+ *
+ * @hide
+ */
+ public static final String REQUEST_STATUS_CALLBACK_BUNDLE_KEY =
+ "android.app.wearable.WearableSensingDataRequestStatusCallbackBundleKey";
+
+ public static final @NonNull Parcelable.Creator<WearableSensingDataRequest> CREATOR =
+ new Parcelable.Creator<WearableSensingDataRequest>() {
+ @Override
+ public WearableSensingDataRequest[] newArray(int size) {
+ return new WearableSensingDataRequest[size];
+ }
+
+ @Override
+ public WearableSensingDataRequest createFromParcel(@NonNull Parcel in) {
+ int dataType = in.readInt();
+ PersistableBundle requestDetails =
+ in.readTypedObject(PersistableBundle.CREATOR);
+ return new WearableSensingDataRequest(dataType, requestDetails);
+ }
+ };
+
+ /**
+ * Returns the maximum allowed size of a WearableSensingDataRequest when it is parcelled.
+ * Instances that exceed this size can be constructed, but will be rejected by the system when
+ * they leave the isolated WearableSensingService.
+ */
+ public static int getMaxRequestSize() {
+ return MAX_REQUEST_SIZE;
+ }
+
+ /**
+ * Returns the rolling time window used to perform rate limiting on data requests leaving the
+ * WearableSensingService.
+ */
+ @NonNull
+ public static Duration getRateLimitWindowSize() {
+ return RATE_LIMIT_WINDOW_SIZE;
+ }
+
+ /**
+ * Returns the number of data requests allowed to leave the WearableSensingService in each
+ * {@link #getRateLimitWindowSize()}.
+ */
+ public static int getRateLimit() {
+ return RATE_LIMIT;
+ }
+
+ /** A builder for WearableSensingDataRequest. */
+ @FlaggedApi(Flags.FLAG_ENABLE_DATA_REQUEST_OBSERVER_API)
+ public static final class Builder {
+ private int mDataType;
+ private PersistableBundle mRequestDetails;
+
+ public Builder(int dataType) {
+ mDataType = dataType;
+ }
+
+ /** Sets the request details. */
+ public @NonNull Builder setRequestDetails(@NonNull PersistableBundle requestDetails) {
+ mRequestDetails = requestDetails;
+ return this;
+ }
+
+ /** Builds the WearableSensingDataRequest. */
+ public @NonNull WearableSensingDataRequest build() {
+ if (mRequestDetails == null) {
+ mRequestDetails = PersistableBundle.EMPTY;
+ }
+ return new WearableSensingDataRequest(mDataType, mRequestDetails);
+ }
+ }
+}
diff --git a/core/java/android/app/wearable/WearableSensingManager.java b/core/java/android/app/wearable/WearableSensingManager.java
index eca0039..077f7b5 100644
--- a/core/java/android/app/wearable/WearableSensingManager.java
+++ b/core/java/android/app/wearable/WearableSensingManager.java
@@ -25,8 +25,11 @@
import android.annotation.RequiresPermission;
import android.annotation.SystemApi;
import android.annotation.SystemService;
+import android.app.PendingIntent;
import android.app.ambientcontext.AmbientContextEvent;
+import android.companion.CompanionDeviceManager;
import android.content.Context;
+import android.content.Intent;
import android.os.Binder;
import android.os.ParcelFileDescriptor;
import android.os.PersistableBundle;
@@ -36,6 +39,8 @@
import android.service.wearable.WearableSensingService;
import android.system.OsConstants;
+import java.io.InputStream;
+import java.io.OutputStream;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.concurrent.Executor;
@@ -55,7 +60,6 @@
*
* @hide
*/
-
@SystemApi
@SystemService(Context.WEARABLE_SENSING_SERVICE)
public class WearableSensingManager {
@@ -68,6 +72,14 @@
public static final String STATUS_RESPONSE_BUNDLE_KEY =
"android.app.wearable.WearableSensingStatusBundleKey";
+ /**
+ * The Intent extra key for the data request in the Intent sent to the PendingIntent registered
+ * with {@link #registerDataRequestObserver(int, PendingIntent, Executor, Consumer)}.
+ *
+ * @hide
+ */
+ public static final String EXTRA_WEARABLE_SENSING_DATA_REQUEST =
+ "android.app.wearable.extra.WEARABLE_SENSING_DATA_REQUEST";
/**
* An unknown status.
@@ -104,22 +116,54 @@
* The value of the status code that indicates the method called is not supported by the
* implementation of {@link WearableSensingService}.
*/
+
@FlaggedApi(Flags.FLAG_ENABLE_UNSUPPORTED_OPERATION_STATUS_CODE)
public static final int STATUS_UNSUPPORTED_OPERATION = 6;
+ /**
+ * The value of the status code that indicates an error occurred in the encrypted channel backed
+ * by the provided connection. See {@link #provideWearableConnection(ParcelFileDescriptor,
+ * Executor, Consumer)}.
+ */
+ @FlaggedApi(Flags.FLAG_ENABLE_PROVIDE_WEARABLE_CONNECTION_API)
+ public static final int STATUS_CHANNEL_ERROR = 7;
+
+ /** The value of the status code that indicates the provided data type is not supported. */
+ @FlaggedApi(Flags.FLAG_ENABLE_DATA_REQUEST_OBSERVER_API)
+ public static final int STATUS_UNSUPPORTED_DATA_TYPE = 8;
+
/** @hide */
- @IntDef(prefix = { "STATUS_" }, value = {
- STATUS_UNKNOWN,
- STATUS_SUCCESS,
- STATUS_UNSUPPORTED,
- STATUS_SERVICE_UNAVAILABLE,
- STATUS_WEARABLE_UNAVAILABLE,
- STATUS_ACCESS_DENIED,
- STATUS_UNSUPPORTED_OPERATION
- })
+ @IntDef(
+ prefix = {"STATUS_"},
+ value = {
+ STATUS_UNKNOWN,
+ STATUS_SUCCESS,
+ STATUS_UNSUPPORTED,
+ STATUS_SERVICE_UNAVAILABLE,
+ STATUS_WEARABLE_UNAVAILABLE,
+ STATUS_ACCESS_DENIED,
+ STATUS_UNSUPPORTED_OPERATION,
+ STATUS_CHANNEL_ERROR,
+ STATUS_UNSUPPORTED_DATA_TYPE
+ })
@Retention(RetentionPolicy.SOURCE)
public @interface StatusCode {}
+ /**
+ * Retrieves a {@link WearableSensingDataRequest} from the Intent sent to the PendingIntent
+ * provided to {@link #registerDataRequestObserver(int, PendingIntent, Executor, Consumer)}.
+ *
+ * @param intent The Intent received from the PendingIntent.
+ * @return The WearableSensingDataRequest in the provided Intent, or null if the Intent does not
+ * contain a WearableSensingDataRequest.
+ */
+ @FlaggedApi(Flags.FLAG_ENABLE_DATA_REQUEST_OBSERVER_API)
+ @Nullable
+ public static WearableSensingDataRequest getDataRequestFromIntent(@NonNull Intent intent) {
+ return intent.getParcelableExtra(
+ EXTRA_WEARABLE_SENSING_DATA_REQUEST, WearableSensingDataRequest.class);
+ }
+
private final Context mContext;
private final IWearableSensingManager mService;
@@ -132,6 +176,60 @@
}
/**
+ * Provides a remote wearable device connection to the WearableSensingService and sends the
+ * resulting status to the {@code statusConsumer} after the call.
+ *
+ * <p>This is used by applications that will also provide an implementation of the isolated
+ * WearableSensingService.
+ *
+ * <p>The provided {@code wearableConnection} is expected to be a connection to a remotely
+ * connected wearable device. This {@code wearableConnection} will be attached to
+ * CompanionDeviceManager via {@link CompanionDeviceManager#attachSystemDataTransport(int,
+ * InputStream, OutputStream)}, which will create an encrypted channel using {@code
+ * wearableConnection} as the raw underlying connection. The wearable device is expected to
+ * attach its side of the raw connection to its CompanionDeviceManager via the same method so
+ * that the two CompanionDeviceManagers on the two devices can perform attestation and set up
+ * the encrypted channel. Attestation requirements are listed in
+ * com.android.server.security.AttestationVerificationPeerDeviceVerifier
+ *
+ * <p>A proxy to the encrypted channel will be provided to the WearableSensingService, which is
+ * referred to as the secureWearableConnection in WearableSensingService. Any data written to
+ * secureWearableConnection will be encrypted by CompanionDeviceManager and sent over the raw
+ * {@code wearableConnection} to the remote wearable device, which is expected to use its
+ * CompanionDeviceManager to decrypt the data. Encrypted data arriving at the raw {@code
+ * wearableConnection} will be decrypted by CompanionDeviceManager and be readable as plain text
+ * from secureWearableConnection. The raw {@code wearableConnection} provided to this method
+ * will not be directly available to the WearableSensingService.
+ *
+ * <p>If an error occurred in the encrypted channel (such as the underlying stream closed), the
+ * system will send a status code of {@link STATUS_CHANNEL_ERROR} to the {@code statusConsumer}
+ * and kill the WearableSensingService process.
+ *
+ * <p>Before providing the secureWearableConnection, the system will restart the
+ * WearableSensingService process. Other method calls into WearableSensingService may be dropped
+ * during the restart. The caller is responsible for ensuring other method calls are queued
+ * until a success status is returned from the {@code statusConsumer}.
+ *
+ * @param wearableConnection The connection to provide
+ * @param executor Executor on which to run the consumer callback
+ * @param statusConsumer A consumer that handles the status codes for providing the connection
+ * and errors in the encrypted channel.
+ */
+ @RequiresPermission(Manifest.permission.MANAGE_WEARABLE_SENSING_SERVICE)
+ @FlaggedApi(Flags.FLAG_ENABLE_PROVIDE_WEARABLE_CONNECTION_API)
+ public void provideWearableConnection(
+ @NonNull ParcelFileDescriptor wearableConnection,
+ @NonNull @CallbackExecutor Executor executor,
+ @NonNull @StatusCode Consumer<Integer> statusConsumer) {
+ try {
+ RemoteCallback callback = createStatusCallback(executor, statusConsumer);
+ mService.provideWearableConnection(wearableConnection, callback);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
* Provides a data stream to the WearableSensingService that's backed by the
* parcelFileDescriptor, and sends the result to the {@link Consumer} right after the call.
* This is used by applications that will also provide an implementation of
@@ -149,15 +247,7 @@
@NonNull @CallbackExecutor Executor executor,
@NonNull @StatusCode Consumer<Integer> statusConsumer) {
try {
- RemoteCallback callback = new RemoteCallback(result -> {
- int status = result.getInt(STATUS_RESPONSE_BUNDLE_KEY);
- final long identity = Binder.clearCallingIdentity();
- try {
- executor.execute(() -> statusConsumer.accept(status));
- } finally {
- Binder.restoreCallingIdentity(identity);
- }
- });
+ RemoteCallback callback = createStatusCallback(executor, statusConsumer);
mService.provideDataStream(parcelFileDescriptor, callback);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
@@ -191,19 +281,117 @@
@NonNull @CallbackExecutor Executor executor,
@NonNull @StatusCode Consumer<Integer> statusConsumer) {
try {
- RemoteCallback callback = new RemoteCallback(result -> {
- int status = result.getInt(STATUS_RESPONSE_BUNDLE_KEY);
- final long identity = Binder.clearCallingIdentity();
- try {
- executor.execute(() -> statusConsumer.accept(status));
- } finally {
- Binder.restoreCallingIdentity(identity);
- }
- });
+ RemoteCallback callback = createStatusCallback(executor, statusConsumer);
mService.provideData(data, sharedMemory, callback);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
+ /**
+ * Registers a data request observer for the provided data type.
+ *
+ * <p>When data is requested, the provided {@code dataRequestPendingIntent} will be invoked. A
+ * {@link WearableSensingDataRequest} can be extracted from the Intent sent to {@code
+ * dataRequestPendingIntent} by calling {@link #getDataRequestFromIntent(Intent)}. The observer
+ * can then provide the requested data via {@link #provideData(PersistableBundle, SharedMemory,
+ * Executor, Consumer)}.
+ *
+ * <p>There is no limit to the number of observers registered for a data type. How they are
+ * handled depends on the implementation of WearableSensingService.
+ *
+ * <p>When the observer is no longer needed, {@link #unregisterDataRequestObserver(int,
+ * PendingIntent, Executor, Consumer)} should be called with the same {@code
+ * dataRequestPendingIntent}. It should be done regardless of the status code returned from
+ * {@code statusConsumer} in order to clean up housekeeping data for the {@code
+ * dataRequestPendingIntent} maintained by the system.
+ *
+ * <p>Example:
+ *
+ * <pre>{@code
+ * // Create a PendingIntent for MyDataRequestBroadcastReceiver
+ * Intent intent =
+ * new Intent(actionString).setClass(context, MyDataRequestBroadcastReceiver.class);
+ * PendingIntent pendingIntent = PendingIntent.getBroadcast(
+ * context, 0, intent, PendingIntent.FLAG_MUTABLE);
+ *
+ * // Register the PendingIntent as a data request observer
+ * wearableSensingManager.registerDataRequestObserver(
+ * dataType, pendingIntent, executor, statusConsumer);
+ *
+ * // Within MyDataRequestBroadcastReceiver, receive the broadcast Intent and extract the
+ * // WearableSensingDataRequest
+ * {@literal @}Override
+ * public void onReceive(Context context, Intent intent) {
+ * WearableSensingDataRequest dataRequest =
+ * WearableSensingManager.getDataRequestFromIntent(intent);
+ * // After parsing the dataRequest, provide the data
+ * wearableSensingManager.provideData(data, sharedMemory, executor, statusConsumer);
+ * }
+ * }</pre>
+ *
+ * @param dataType The data type to listen to. Values are defined by the application that
+ * implements {@link WearableSensingService}.
+ * @param dataRequestPendingIntent A mutable {@link PendingIntent} that will be invoked when
+ * data is requested. See {@link #getDataRequestFromIntent(Intent)}. Activities are not
+ * allowed to be launched using this PendingIntent.
+ * @param executor Executor on which to run the consumer callback.
+ * @param statusConsumer A consumer that handles the status code for the observer registration.
+ */
+ @FlaggedApi(Flags.FLAG_ENABLE_DATA_REQUEST_OBSERVER_API)
+ @RequiresPermission(Manifest.permission.MANAGE_WEARABLE_SENSING_SERVICE)
+ public void registerDataRequestObserver(
+ int dataType,
+ @NonNull PendingIntent dataRequestPendingIntent,
+ @NonNull @CallbackExecutor Executor executor,
+ @NonNull @StatusCode Consumer<Integer> statusConsumer) {
+ try {
+ RemoteCallback statusCallback = createStatusCallback(executor, statusConsumer);
+ mService.registerDataRequestObserver(
+ dataType, dataRequestPendingIntent, statusCallback);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Unregisters a previously registered data request observer. If the provided {@link
+ * PendingIntent} was not registered, or is already unregistered, the {@link
+ * WearableSensingService} will not be notified.
+ *
+ * @param dataType The data type the observer is for.
+ * @param dataRequestPendingIntent The observer to unregister.
+ * @param executor Executor on which to run the consumer callback.
+ * @param statusConsumer A consumer that handles the status code for the observer
+ * unregistration.
+ */
+ @FlaggedApi(Flags.FLAG_ENABLE_DATA_REQUEST_OBSERVER_API)
+ @RequiresPermission(Manifest.permission.MANAGE_WEARABLE_SENSING_SERVICE)
+ public void unregisterDataRequestObserver(
+ int dataType,
+ @NonNull PendingIntent dataRequestPendingIntent,
+ @NonNull @CallbackExecutor Executor executor,
+ @NonNull @StatusCode Consumer<Integer> statusConsumer) {
+ try {
+ RemoteCallback statusCallback = createStatusCallback(executor, statusConsumer);
+ mService.unregisterDataRequestObserver(
+ dataType, dataRequestPendingIntent, statusCallback);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ private static RemoteCallback createStatusCallback(
+ Executor executor, Consumer<Integer> statusConsumer) {
+ return new RemoteCallback(
+ result -> {
+ int status = result.getInt(STATUS_RESPONSE_BUNDLE_KEY);
+ final long identity = Binder.clearCallingIdentity();
+ try {
+ executor.execute(() -> statusConsumer.accept(status));
+ } finally {
+ Binder.restoreCallingIdentity(identity);
+ }
+ });
+ }
}
diff --git a/core/java/android/app/wearable/flags.aconfig b/core/java/android/app/wearable/flags.aconfig
index 074ce9b..5e8bdb5 100644
--- a/core/java/android/app/wearable/flags.aconfig
+++ b/core/java/android/app/wearable/flags.aconfig
@@ -5,4 +5,25 @@
namespace: "machine_learning"
description: "This flag enables the WearableSensingManager#STATUS_UNSUPPORTED_OPERATION status code API."
bug: "301427767"
+}
+
+flag {
+ name: "enable_data_request_observer_api"
+ namespace: "machine_learning"
+ description: "This flag enables the API to register a data request observer on WearableSensingManager."
+ bug: "301427767"
+}
+
+flag {
+ name: "enable_provide_wearable_connection_api"
+ namespace: "machine_learning"
+ description: "This flag enables the WearableSensingManager#provideWearableConnection API."
+ bug: "301427767"
+}
+
+flag {
+ name: "enable_hotword_wearable_sensing_api"
+ namespace: "machine_learning"
+ description: "This flag enables the APIs related to hotword in WearableSensingManager and WearableSensingService."
+ bug: "310055381"
}
\ No newline at end of file
diff --git a/core/java/android/appwidget/AppWidgetManager.java b/core/java/android/appwidget/AppWidgetManager.java
index 6204edc..eb82e1f 100644
--- a/core/java/android/appwidget/AppWidgetManager.java
+++ b/core/java/android/appwidget/AppWidgetManager.java
@@ -822,18 +822,7 @@
*
* @param appWidgetIds The AppWidget instances to notify of view data changes.
* @param viewId The collection view id.
- * @deprecated The corresponding API
- * {@link RemoteViews#setRemoteAdapter(int, Intent)} associated with this method has been
- * deprecated. Moving forward please use
- * {@link RemoteViews#setRemoteAdapter(int, android.widget.RemoteViews.RemoteCollectionItems)}
- * instead to set {@link android.widget.RemoteViews.RemoteCollectionItems} for the remote
- * adapter and update the widget views by calling {@link #updateAppWidget(int[], RemoteViews)},
- * {@link #updateAppWidget(int, RemoteViews)},
- * {@link #updateAppWidget(ComponentName, RemoteViews)},
- * {@link #partiallyUpdateAppWidget(int[], RemoteViews)},
- * or {@link #partiallyUpdateAppWidget(int, RemoteViews)}, whichever applicable.
*/
- @Deprecated
public void notifyAppWidgetViewDataChanged(int[] appWidgetIds, int viewId) {
if (mService == null) {
return;
@@ -884,18 +873,7 @@
*
* @param appWidgetId The AppWidget instance to notify of view data changes.
* @param viewId The collection view id.
- * @deprecated The corresponding API
- * {@link RemoteViews#setRemoteAdapter(int, Intent)} associated with this method has been
- * deprecated. Moving forward please use
- * {@link RemoteViews#setRemoteAdapter(int, android.widget.RemoteViews.RemoteCollectionItems)}
- * instead to set {@link android.widget.RemoteViews.RemoteCollectionItems} for the remote
- * adapter and update the widget views by calling {@link #updateAppWidget(int[], RemoteViews)},
- * {@link #updateAppWidget(int, RemoteViews)},
- * {@link #updateAppWidget(ComponentName, RemoteViews)},
- * {@link #partiallyUpdateAppWidget(int[], RemoteViews)},
- * or {@link #partiallyUpdateAppWidget(int, RemoteViews)}, whichever applicable.
*/
- @Deprecated
public void notifyAppWidgetViewDataChanged(int appWidgetId, int viewId) {
if (mService == null) {
return;
diff --git a/core/java/android/appwidget/flags.aconfig b/core/java/android/appwidget/flags.aconfig
index 084cba3..822f02f 100644
--- a/core/java/android/appwidget/flags.aconfig
+++ b/core/java/android/appwidget/flags.aconfig
@@ -19,6 +19,9 @@
namespace: "app_widgets"
description: "Move state file IO to non-critical path"
bug: "312949280"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
}
flag {
diff --git a/core/java/android/companion/virtual/VirtualDeviceInternal.java b/core/java/android/companion/virtual/VirtualDeviceInternal.java
index c1e443d..39f6de7 100644
--- a/core/java/android/companion/virtual/VirtualDeviceInternal.java
+++ b/core/java/android/companion/virtual/VirtualDeviceInternal.java
@@ -26,6 +26,7 @@
import android.companion.virtual.camera.VirtualCamera;
import android.companion.virtual.camera.VirtualCameraConfig;
import android.companion.virtual.sensor.VirtualSensor;
+import android.companion.virtualdevice.flags.Flags;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
@@ -351,7 +352,11 @@
@Nullable Executor executor,
@Nullable VirtualAudioDevice.AudioConfigurationChangeCallback callback) {
if (mVirtualAudioDevice == null) {
- mVirtualAudioDevice = new VirtualAudioDevice(mContext, mVirtualDevice, display,
+ Context context = mContext;
+ if (Flags.deviceAwareRecordAudioPermission()) {
+ context = mContext.createDeviceContext(getDeviceId());
+ }
+ mVirtualAudioDevice = new VirtualAudioDevice(context, mVirtualDevice, display,
executor, callback, () -> mVirtualAudioDevice = null);
}
return mVirtualAudioDevice;
diff --git a/core/java/android/companion/virtual/flags/flags.aconfig b/core/java/android/companion/virtual/flags/flags.aconfig
index d26890f..ab8db6e 100644
--- a/core/java/android/companion/virtual/flags/flags.aconfig
+++ b/core/java/android/companion/virtual/flags/flags.aconfig
@@ -21,3 +21,11 @@
description: "Enable discovery of the Virtual Camera HAL without a VINTF entry"
bug: "305170199"
}
+
+flag {
+ namespace: "virtual_devices"
+ name: "device_aware_record_audio_permission"
+ description: "Enable device-aware RECORD_AUDIO permission"
+ bug: "291737188"
+ is_fixed_read_only: true
+}
diff --git a/core/java/android/content/ContentProvider.java b/core/java/android/content/ContentProvider.java
index e9b94c9..87fb843 100644
--- a/core/java/android/content/ContentProvider.java
+++ b/core/java/android/content/ContentProvider.java
@@ -185,7 +185,7 @@
*
* @param context A Context object which should be some mock instance (like the
* instance of {@link android.test.mock.MockContext}).
- * @param readPermission The read permision you want this instance should have in the
+ * @param readPermission The read permission you want this instance should have in the
* test, which is available via {@link #getReadPermission()}.
* @param writePermission The write permission you want this instance should have
* in the test, which is available via {@link #getWritePermission()}.
diff --git a/core/java/android/content/ContentResolver.java b/core/java/android/content/ContentResolver.java
index 9253998..a126363 100644
--- a/core/java/android/content/ContentResolver.java
+++ b/core/java/android/content/ContentResolver.java
@@ -387,7 +387,7 @@
* {@link Bundle} under {@link #EXTRA_HONORED_ARGS}.
* <li>When querying a provider, where no QUERY_ARG_SQL* otherwise exists in
* the arguments {@link Bundle}, the Content framework will attempt to
- * synthesize an QUERY_ARG_SQL* argument using the corresponding
+ * synthesize a QUERY_ARG_SQL* argument using the corresponding
* QUERY_ARG_SORT* values.
*/
public static final String QUERY_ARG_SORT_COLUMNS = "android:query-arg-sort-columns";
diff --git a/core/java/android/content/pm/ActivityInfo.java b/core/java/android/content/pm/ActivityInfo.java
index 9fe8af5..a64ee5b 100644
--- a/core/java/android/content/pm/ActivityInfo.java
+++ b/core/java/android/content/pm/ActivityInfo.java
@@ -239,6 +239,110 @@
public String requiredDisplayCategory;
/**
+ * Constant corresponding to {@code none} in the
+ * {@link android.R.attr#requireContentUriPermissionFromCaller} attribute.
+ * @hide
+ */
+ public static final int CONTENT_URI_PERMISSION_NONE = 0;
+
+ /**
+ * Constant corresponding to {@code read} in the
+ * {@link android.R.attr#requireContentUriPermissionFromCaller} attribute.
+ * @hide
+ */
+ public static final int CONTENT_URI_PERMISSION_READ = 1;
+
+ /**
+ * Constant corresponding to {@code write} in the
+ * {@link android.R.attr#requireContentUriPermissionFromCaller} attribute.
+ * @hide
+ */
+ public static final int CONTENT_URI_PERMISSION_WRITE = 2;
+
+ /**
+ * Constant corresponding to {@code readOrWrite} in the
+ * {@link android.R.attr#requireContentUriPermissionFromCaller} attribute.
+ * @hide
+ */
+ public static final int CONTENT_URI_PERMISSION_READ_OR_WRITE = 3;
+
+ /**
+ * Constant corresponding to {@code readAndWrite} in the
+ * {@link android.R.attr#requireContentUriPermissionFromCaller} attribute.
+ * @hide
+ */
+ public static final int CONTENT_URI_PERMISSION_READ_AND_WRITE = 4;
+
+ /** @hide */
+ @SuppressWarnings("SwitchIntDef")
+ public static boolean isRequiredContentUriPermissionRead(
+ @RequiredContentUriPermission int permission) {
+ return switch (permission) {
+ case CONTENT_URI_PERMISSION_READ_AND_WRITE, CONTENT_URI_PERMISSION_READ_OR_WRITE,
+ CONTENT_URI_PERMISSION_READ -> true;
+ default -> false;
+ };
+ }
+
+ /** @hide */
+ @SuppressWarnings("SwitchIntDef")
+ public static boolean isRequiredContentUriPermissionWrite(
+ @RequiredContentUriPermission int permission) {
+ return switch (permission) {
+ case CONTENT_URI_PERMISSION_READ_AND_WRITE, CONTENT_URI_PERMISSION_READ_OR_WRITE,
+ CONTENT_URI_PERMISSION_WRITE -> true;
+ default -> false;
+ };
+ }
+
+ /** @hide */
+ @IntDef(prefix = "CONTENT_URI_PERMISSION_", value = {
+ CONTENT_URI_PERMISSION_NONE,
+ CONTENT_URI_PERMISSION_READ,
+ CONTENT_URI_PERMISSION_WRITE,
+ CONTENT_URI_PERMISSION_READ_OR_WRITE,
+ CONTENT_URI_PERMISSION_READ_AND_WRITE
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface RequiredContentUriPermission {
+ }
+
+ private String requiredContentUriPermissionToFullString(
+ @RequiredContentUriPermission int permission) {
+ return switch (permission) {
+ case CONTENT_URI_PERMISSION_NONE -> "CONTENT_URI_PERMISSION_NONE";
+ case CONTENT_URI_PERMISSION_READ -> "CONTENT_URI_PERMISSION_READ";
+ case CONTENT_URI_PERMISSION_WRITE -> "CONTENT_URI_PERMISSION_WRITE";
+ case CONTENT_URI_PERMISSION_READ_OR_WRITE -> "CONTENT_URI_PERMISSION_READ_OR_WRITE";
+ case CONTENT_URI_PERMISSION_READ_AND_WRITE -> "CONTENT_URI_PERMISSION_READ_AND_WRITE";
+ default -> "unknown=" + permission;
+ };
+ }
+
+ /** @hide */
+ public static String requiredContentUriPermissionToShortString(
+ @RequiredContentUriPermission int permission) {
+ return switch (permission) {
+ case CONTENT_URI_PERMISSION_NONE -> "none";
+ case CONTENT_URI_PERMISSION_READ -> "read";
+ case CONTENT_URI_PERMISSION_WRITE -> "write";
+ case CONTENT_URI_PERMISSION_READ_OR_WRITE -> "read or write";
+ case CONTENT_URI_PERMISSION_READ_AND_WRITE -> "read and write";
+ default -> "unknown=" + permission;
+ };
+ }
+
+ /**
+ * Specifies permissions necessary to launch this activity via
+ * {@link android.content.Context#startActivity} when passing content URIs. The default value is
+ * {@code none}, meaning no specific permissions are required. Setting this attribute restricts
+ * activity invocation based on the invoker's permissions.
+ * @hide
+ */
+ @RequiredContentUriPermission
+ public int requireContentUriPermissionFromCaller;
+
+ /**
* Activity can not be resized and always occupies the fullscreen area with all windows fully
* visible.
* @hide
@@ -1590,6 +1694,7 @@
mMinAspectRatio = orig.mMinAspectRatio;
supportsSizeChanges = orig.supportsSizeChanges;
requiredDisplayCategory = orig.requiredDisplayCategory;
+ requireContentUriPermissionFromCaller = orig.requireContentUriPermissionFromCaller;
}
/**
@@ -1946,6 +2051,11 @@
if (requiredDisplayCategory != null) {
pw.println(prefix + "requiredDisplayCategory=" + requiredDisplayCategory);
}
+ if ((dumpFlags & DUMP_FLAG_DETAILS) != 0) {
+ pw.println(prefix + "requireContentUriPermissionFromCaller="
+ + requiredContentUriPermissionToFullString(
+ requireContentUriPermissionFromCaller));
+ }
super.dumpBack(pw, prefix, dumpFlags);
}
@@ -1993,6 +2103,7 @@
dest.writeBoolean(supportsSizeChanges);
sForStringSet.parcel(mKnownActivityEmbeddingCerts, dest, flags);
dest.writeString8(requiredDisplayCategory);
+ dest.writeInt(requireContentUriPermissionFromCaller);
}
/**
@@ -2119,6 +2230,7 @@
mKnownActivityEmbeddingCerts = null;
}
requiredDisplayCategory = source.readString8();
+ requireContentUriPermissionFromCaller = source.readInt();
}
/**
diff --git a/core/java/android/content/pm/IBackgroundInstallControlService.aidl b/core/java/android/content/pm/IBackgroundInstallControlService.aidl
index c8e7cae..2e7f19e 100644
--- a/core/java/android/content/pm/IBackgroundInstallControlService.aidl
+++ b/core/java/android/content/pm/IBackgroundInstallControlService.aidl
@@ -17,10 +17,15 @@
package android.content.pm;
import android.content.pm.ParceledListSlice;
+import android.os.IRemoteCallback;
/**
* {@hide}
*/
interface IBackgroundInstallControlService {
ParceledListSlice getBackgroundInstalledPackages(long flags, int userId);
+
+ void registerBackgroundInstallCallback(IRemoteCallback callback);
+
+ void unregisterBackgroundInstallCallback(IRemoteCallback callback);
}
diff --git a/core/java/android/content/pm/IPackageInstaller.aidl b/core/java/android/content/pm/IPackageInstaller.aidl
index 1f25fd0..451c0e5 100644
--- a/core/java/android/content/pm/IPackageInstaller.aidl
+++ b/core/java/android/content/pm/IPackageInstaller.aidl
@@ -80,7 +80,7 @@
long timeout);
@JavaPassthrough(annotation="@android.annotation.RequiresPermission(anyOf={android.Manifest.permission.DELETE_PACKAGES,android.Manifest.permission.REQUEST_DELETE_PACKAGES})")
- void requestArchive(String packageName, String callerPackageName, in IntentSender statusReceiver, in UserHandle userHandle);
+ void requestArchive(String packageName, String callerPackageName, int flags, in IntentSender statusReceiver, in UserHandle userHandle);
@JavaPassthrough(annotation="@android.annotation.RequiresPermission(anyOf={android.Manifest.permission.INSTALL_PACKAGES,android.Manifest.permission.REQUEST_INSTALL_PACKAGES})")
void requestUnarchive(String packageName, String callerPackageName, in IntentSender statusReceiver, in UserHandle userHandle);
diff --git a/core/java/android/content/pm/IPackageInstallerSession.aidl b/core/java/android/content/pm/IPackageInstallerSession.aidl
index ea69a2b..9f31aec 100644
--- a/core/java/android/content/pm/IPackageInstallerSession.aidl
+++ b/core/java/android/content/pm/IPackageInstallerSession.aidl
@@ -21,6 +21,7 @@
import android.content.pm.IOnChecksumsReadyListener;
import android.content.pm.IPackageInstallObserver2;
import android.content.pm.PackageInstaller;
+import android.content.pm.verify.domain.DomainSet;
import android.content.IntentSender;
import android.os.ParcelFileDescriptor;
@@ -73,4 +74,7 @@
ParcelFileDescriptor getAppMetadataFd();
ParcelFileDescriptor openWriteAppMetadata();
void removeAppMetadata();
+
+ void setPreVerifiedDomains(in DomainSet preVerifiedDomains);
+ DomainSet getPreVerifiedDomains();
}
diff --git a/core/java/android/content/pm/IPackageManager.aidl b/core/java/android/content/pm/IPackageManager.aidl
index 380de96..bff90f1 100644
--- a/core/java/android/content/pm/IPackageManager.aidl
+++ b/core/java/android/content/pm/IPackageManager.aidl
@@ -843,4 +843,9 @@
Bitmap getArchivedAppIcon(String packageName, in UserHandle user, String callingPackageName);
boolean isAppArchivable(String packageName, in UserHandle user);
+
+ @EnforcePermission("GET_APP_METADATA")
+ int getAppMetadataSource(String packageName, int userId);
+
+ ComponentName getDomainVerificationAgent();
}
diff --git a/core/java/android/content/pm/LauncherApps.java b/core/java/android/content/pm/LauncherApps.java
index 50be983..7c264f6 100644
--- a/core/java/android/content/pm/LauncherApps.java
+++ b/core/java/android/content/pm/LauncherApps.java
@@ -1802,25 +1802,16 @@
}
/**
- * Enable or disable different archive compatibility options of the launcher.
+ * Disable different archive compatibility options of the launcher for the caller of this
+ * method.
*
- * @param enableIconOverlay Provides a cloud overlay for archived apps to ensure users are aware
- * that a certain app is archived. True by default.
- * Launchers might want to disable this operation if they want to provide custom user experience
- * to differentiate archived apps.
- * @param enableUnarchivalConfirmation If true, the user is shown a confirmation dialog when
- * they click an archived app, which explains that the app will be downloaded and restored in
- * the background. True by default.
- * Launchers might want to disable this operation if they provide sufficient, alternative user
- * guidance to highlight that an unarchival is starting and ongoing once an archived app is
- * tapped. E.g., this could be achieved by showing the unarchival progress around the icon.
+ * @see ArchiveCompatibilityParams for individual options.
*/
@FlaggedApi(android.content.pm.Flags.FLAG_ARCHIVING)
- public void setArchiveCompatibilityOptions(boolean enableIconOverlay,
- boolean enableUnarchivalConfirmation) {
+ public void setArchiveCompatibility(@NonNull ArchiveCompatibilityParams params) {
try {
- mService.setArchiveCompatibilityOptions(enableIconOverlay,
- enableUnarchivalConfirmation);
+ mService.setArchiveCompatibilityOptions(params.isEnableIconOverlay(),
+ params.isEnableUnarchivalConfirmation());
} catch (RemoteException re) {
throw re.rethrowFromSystemServer();
}
@@ -1982,6 +1973,50 @@
}
};
+ /**
+ * Used to enable Archiving compatibility options with {@link #setArchiveCompatibility}.
+ */
+ @FlaggedApi(android.content.pm.Flags.FLAG_ARCHIVING)
+ public static class ArchiveCompatibilityParams {
+ private boolean mEnableIconOverlay = true;
+
+ private boolean mEnableUnarchivalConfirmation = true;
+
+ /** @hide */
+ public boolean isEnableIconOverlay() {
+ return mEnableIconOverlay;
+ }
+
+ /** @hide */
+ public boolean isEnableUnarchivalConfirmation() {
+ return mEnableUnarchivalConfirmation;
+ }
+
+ /**
+ * If true, provides a cloud overlay for archived apps to ensure users are aware that a
+ * certain app is archived. True by default.
+ *
+ * <p> Launchers might want to disable this operation if they want to provide custom user
+ * experience to differentiate archived apps.
+ */
+ public void setEnableIconOverlay(boolean enableIconOverlay) {
+ this.mEnableIconOverlay = enableIconOverlay;
+ }
+
+ /**
+ * If true, the user is shown a confirmation dialog when they click an archived app, which
+ * explains that the app will be downloaded and restored in the background. True by default.
+ *
+ * <p> Launchers might want to disable this operation if they provide sufficient,
+ * alternative user guidance to highlight that an unarchival is starting and ongoing once an
+ * archived app is tapped. E.g., this could be achieved by showing the unarchival progress
+ * around the icon.
+ */
+ public void setEnableUnarchivalConfirmation(boolean enableUnarchivalConfirmation) {
+ this.mEnableUnarchivalConfirmation = enableUnarchivalConfirmation;
+ }
+ }
+
private static class CallbackMessageHandler extends Handler {
private static final int MSG_ADDED = 1;
private static final int MSG_REMOVED = 2;
diff --git a/core/java/android/content/pm/OWNERS b/core/java/android/content/pm/OWNERS
index 53dd3bf..fb95608895 100644
--- a/core/java/android/content/pm/OWNERS
+++ b/core/java/android/content/pm/OWNERS
@@ -10,3 +10,4 @@
per-file UserInfo* = file:/MULTIUSER_OWNERS
per-file *UserProperties* = file:/MULTIUSER_OWNERS
per-file *multiuser* = file:/MULTIUSER_OWNERS
+per-file IBackgroundInstallControlService.aidl = file:/services/core/java/com/android/server/pm/BACKGROUND_INSTALL_OWNERS
diff --git a/core/java/android/content/pm/PackageInstaller.java b/core/java/android/content/pm/PackageInstaller.java
index c4bf18d..c2ff9f6 100644
--- a/core/java/android/content/pm/PackageInstaller.java
+++ b/core/java/android/content/pm/PackageInstaller.java
@@ -61,6 +61,7 @@
import android.content.pm.parsing.PackageLite;
import android.content.pm.parsing.result.ParseResult;
import android.content.pm.parsing.result.ParseTypeImpl;
+import android.content.pm.verify.domain.DomainSet;
import android.graphics.Bitmap;
import android.icu.util.ULocale;
import android.net.Uri;
@@ -2296,6 +2297,66 @@
throw e.rethrowFromSystemServer();
}
}
+
+ /**
+ * Sets the pre-verified domains for the app to be installed. By setting pre-verified
+ * domains, the installer allows the app to be opened by the app links of these domains
+ * immediately after it is installed.
+ *
+ * <p>The specified pre-verified domains should be a subset of the hostnames declared with
+ * {@code android:host} and {@code android:autoVerify=true} in the intent filters of the
+ * AndroidManifest.xml of the app. If some of the specified domains are not declared in
+ * the manifest, they will be ignored.</p>
+ * <p>If this API is called multiple times on the same {@link #Session}, the last call
+ * overrides the previous ones.</p>
+ * <p>The instant app installer is the only entity that may call this API.
+ * </p>
+ *
+ * @param preVerifiedDomains domains that are already pre-verified by the installer.
+ *
+ * @throws IllegalArgumentException if the number or the total size of the pre-verified
+ * domains exceeds the maximum allowed, or if the domain
+ * names contain invalid characters.
+ * @throws SecurityException if called from an installer that is not the instant app
+ * installer of the device, or if called after the session has
+ * been committed or abandoned.
+ *
+ * @hide
+ */
+ @SystemApi
+ @FlaggedApi(Flags.FLAG_SET_PRE_VERIFIED_DOMAINS)
+ @RequiresPermission(Manifest.permission.ACCESS_INSTANT_APPS)
+ public void setPreVerifiedDomains(@NonNull Set<String> preVerifiedDomains) {
+ Preconditions.checkArgument(preVerifiedDomains != null && !preVerifiedDomains.isEmpty(),
+ "Provided pre-verified domains cannot be null or empty.");
+ try {
+ mSession.setPreVerifiedDomains(new DomainSet(preVerifiedDomains));
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Retrieve the pre-verified domains set in a session.
+ * See {@link #setPreVerifiedDomains(Set)} for the definition of pre-verified domains.
+ *
+ * @throws SecurityException if called from an installer that is not the owner of the
+ * session, or if called after the session has been committed or
+ * abandoned.
+ * @hide
+ */
+ @SystemApi
+ @FlaggedApi(Flags.FLAG_SET_PRE_VERIFIED_DOMAINS)
+ @RequiresPermission(Manifest.permission.ACCESS_INSTANT_APPS)
+ @NonNull
+ public Set<String> getPreVerifiedDomains() {
+ try {
+ DomainSet domainSet = mSession.getPreVerifiedDomains();
+ return domainSet != null ? domainSet.getDomains() : Collections.emptySet();
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
}
/**
@@ -2362,8 +2423,8 @@
public void requestArchive(@NonNull String packageName, @NonNull IntentSender statusReceiver)
throws PackageManager.NameNotFoundException {
try {
- mInstaller.requestArchive(packageName, mInstallerPackageName, statusReceiver,
- new UserHandle(mUserId));
+ mInstaller.requestArchive(packageName, mInstallerPackageName, /*flags=*/ 0,
+ statusReceiver, new UserHandle(mUserId));
} catch (ParcelableException e) {
e.maybeRethrow(PackageManager.NameNotFoundException.class);
} catch (RemoteException e) {
diff --git a/core/java/android/content/pm/PackageManager.java b/core/java/android/content/pm/PackageManager.java
index 8744eae..407ffbb 100644
--- a/core/java/android/content/pm/PackageManager.java
+++ b/core/java/android/content/pm/PackageManager.java
@@ -2992,6 +2992,46 @@
@SystemApi
public static final int INTENT_FILTER_DOMAIN_VERIFICATION_STATUS_ALWAYS_ASK = 4;
+
+ /**
+ * Indicates that the app metadata does not exist or its source is unknown.
+ * @hide
+ */
+ @FlaggedApi(android.content.pm.Flags.FLAG_ASL_IN_APK_APP_METADATA_SOURCE)
+ @SystemApi
+ public static final int APP_METADATA_SOURCE_UNKNOWN = 0;
+ /**
+ * Indicates that the app metadata is provided by the APK itself.
+ * @hide
+ */
+ @FlaggedApi(android.content.pm.Flags.FLAG_ASL_IN_APK_APP_METADATA_SOURCE)
+ @SystemApi
+ public static final int APP_METADATA_SOURCE_APK = 1;
+ /**
+ * Indicates that the app metadata is provided by the installer that installed the app.
+ * @hide
+ */
+ @FlaggedApi(android.content.pm.Flags.FLAG_ASL_IN_APK_APP_METADATA_SOURCE)
+ @SystemApi
+ public static final int APP_METADATA_SOURCE_INSTALLER = 2;
+ /**
+ * Indicates that the app metadata is provided as part of the system image.
+ * @hide
+ */
+ @FlaggedApi(android.content.pm.Flags.FLAG_ASL_IN_APK_APP_METADATA_SOURCE)
+ @SystemApi
+ public static final int APP_METADATA_SOURCE_SYSTEM_IMAGE = 3;
+
+ /** @hide */
+ @IntDef(flag = true, prefix = { "APP_METADATA_SOURCE_" }, value = {
+ APP_METADATA_SOURCE_UNKNOWN,
+ APP_METADATA_SOURCE_APK,
+ APP_METADATA_SOURCE_INSTALLER,
+ APP_METADATA_SOURCE_SYSTEM_IMAGE,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface AppMetadataSource {}
+
/**
* Can be used as the {@code millisecondsToDelay} argument for
* {@link PackageManager#extendVerificationTimeout}. This is the
@@ -4479,6 +4519,10 @@
* the Android Keystore backed by an isolated execution environment. The version indicates
* which features are implemented in the isolated execution environment:
* <ul>
+ * <li>300: Ability to include a second IMEI in the ID attestation record, see
+ * {@link android.app.admin.DevicePolicyManager#ID_TYPE_IMEI}.
+ * <li>200: Hardware support for Curve 25519 (including both Ed25519 signature generation and
+ * X25519 key agreement).
* <li>100: Hardware support for ECDH (see {@link javax.crypto.KeyAgreement}) and support
* for app-generated attestation keys (see {@link
* android.security.keystore.KeyGenParameterSpec.Builder#setAttestKeyAlias(String)}).
@@ -4508,6 +4552,11 @@
* StrongBox</a>. If this feature has a version, the version number indicates which features are
* implemented in StrongBox:
* <ul>
+ * <li>300: Ability to include a second IMEI in the ID attestation record, see
+ * {@link android.app.admin.DevicePolicyManager#ID_TYPE_IMEI}.
+ * <li>200: No new features for StrongBox (the Android Keystore environment backed by an
+ * isolated execution environment has gained support for Curve 25519 in this version, but
+ * the implementation backed by a dedicated secure processor is not expected to implement it).
* <li>100: Hardware support for ECDH (see {@link javax.crypto.KeyAgreement}) and support
* for app-generated attestation keys (see {@link
* android.security.keystore.KeyGenParameterSpec.Builder#setAttestKeyAlias(String)}).
@@ -6300,6 +6349,29 @@
throw new UnsupportedOperationException("getAppMetadata not implemented in subclass");
}
+
+ /**
+ * Returns the source of the app metadata that is currently associated with the given package.
+ * The value can be {@link #APP_METADATA_SOURCE_UNKNOWN}, {@link #APP_METADATA_SOURCE_APK},
+ * {@link #APP_METADATA_SOURCE_INSTALLER} or {@link #APP_METADATA_SOURCE_SYSTEM_IMAGE}.
+ *
+ * Note: an app can have the app metadata included in the APK, but if the installer also
+ * provides an app metadata during the installation, the one provided by the installer will
+ * take precedence.
+ *
+ * @param packageName The package name for which to get the app metadata source.
+ * @throws NameNotFoundException if no such package is available to the caller.
+ * @throws SecurityException if the caller doesn't have the required permission.
+ * @hide
+ */
+ @FlaggedApi(android.content.pm.Flags.FLAG_ASL_IN_APK_APP_METADATA_SOURCE)
+ @SystemApi
+ @RequiresPermission(Manifest.permission.GET_APP_METADATA)
+ public @AppMetadataSource int getAppMetadataSource(@NonNull String packageName)
+ throws NameNotFoundException {
+ throw new UnsupportedOperationException("getAppMetadataSource not implemented in subclass");
+ }
+
/**
* Return a List of all installed packages that are currently holding any of
* the given permissions.
diff --git a/core/java/android/content/pm/PackageStats.java b/core/java/android/content/pm/PackageStats.java
index b919c4b..db3050f 100644
--- a/core/java/android/content/pm/PackageStats.java
+++ b/core/java/android/content/pm/PackageStats.java
@@ -67,6 +67,18 @@
/** @hide */
public long dmSize;
+ /** Size of dexopt artifacts of the application. */
+ /** @hide */
+ public long dexoptSize;
+
+ /** Size of the current profile of the application. */
+ /** @hide */
+ public long curProfSize;
+
+ /** Size of the reference profile of the application. */
+ /** @hide */
+ public long refProfSize;
+
/**
* Size of the secure container on external storage holding the
* application's code.
@@ -132,6 +144,18 @@
sb.append(" dm=");
sb.append(dmSize);
}
+ if (dexoptSize != 0) {
+ sb.append(" dexopt=");
+ sb.append(dexoptSize);
+ }
+ if (curProfSize != 0) {
+ sb.append(" curProf=");
+ sb.append(curProfSize);
+ }
+ if (refProfSize != 0) {
+ sb.append(" refProf=");
+ sb.append(refProfSize);
+ }
if (externalCodeSize != 0) {
sb.append(" extCode=");
sb.append(externalCodeSize);
@@ -176,6 +200,9 @@
apkSize = source.readLong();
libSize = source.readLong();
dmSize = source.readLong();
+ dexoptSize = source.readLong();
+ curProfSize = source.readLong();
+ refProfSize = source.readLong();
externalCodeSize = source.readLong();
externalDataSize = source.readLong();
externalCacheSize = source.readLong();
@@ -192,6 +219,9 @@
apkSize = pStats.apkSize;
libSize = pStats.libSize;
dmSize = pStats.dmSize;
+ dexoptSize = pStats.dexoptSize;
+ curProfSize = pStats.curProfSize;
+ refProfSize = pStats.refProfSize;
externalCodeSize = pStats.externalCodeSize;
externalDataSize = pStats.externalDataSize;
externalCacheSize = pStats.externalCacheSize;
@@ -212,6 +242,9 @@
dest.writeLong(apkSize);
dest.writeLong(libSize);
dest.writeLong(dmSize);
+ dest.writeLong(dexoptSize);
+ dest.writeLong(curProfSize);
+ dest.writeLong(refProfSize);
dest.writeLong(externalCodeSize);
dest.writeLong(externalDataSize);
dest.writeLong(externalCacheSize);
@@ -234,6 +267,9 @@
&& apkSize == otherStats.apkSize
&& libSize == otherStats.libSize
&& dmSize == otherStats.dmSize
+ && dexoptSize == otherStats.dexoptSize
+ && curProfSize == otherStats.curProfSize
+ && refProfSize == otherStats.refProfSize
&& externalCodeSize == otherStats.externalCodeSize
&& externalDataSize == otherStats.externalDataSize
&& externalCacheSize == otherStats.externalCacheSize
@@ -244,7 +280,8 @@
@Override
public int hashCode() {
return Objects.hash(packageName, userHandle, codeSize, dataSize,
- apkSize, libSize, dmSize, cacheSize, externalCodeSize,
+ apkSize, libSize, dmSize, dexoptSize, curProfSize,
+ refProfSize, cacheSize, externalCodeSize,
externalDataSize, externalCacheSize, externalMediaSize,
externalObbSize);
}
diff --git a/core/java/android/content/pm/ProcessInfo.java b/core/java/android/content/pm/ProcessInfo.java
index 632c0f5..f84b46d 100644
--- a/core/java/android/content/pm/ProcessInfo.java
+++ b/core/java/android/content/pm/ProcessInfo.java
@@ -18,10 +18,8 @@
import android.annotation.NonNull;
import android.annotation.Nullable;
-import android.content.pm.ApplicationInfo;
import android.os.Parcel;
import android.os.Parcelable;
-import android.text.TextUtils;
import android.util.ArraySet;
import com.android.internal.util.DataClass;
@@ -64,6 +62,12 @@
*/
public @ApplicationInfo.NativeHeapZeroInitialized int nativeHeapZeroInitialized;
+ /**
+ * Enable use of embedded dex in the APK, rather than extracted or locally compiled variants.
+ * If false (default), the parent app's configuration determines behavior.
+ */
+ public boolean useEmbeddedDex;
+
@Deprecated
public ProcessInfo(@NonNull ProcessInfo orig) {
this.name = orig.name;
@@ -71,11 +75,12 @@
this.gwpAsanMode = orig.gwpAsanMode;
this.memtagMode = orig.memtagMode;
this.nativeHeapZeroInitialized = orig.nativeHeapZeroInitialized;
+ this.useEmbeddedDex = orig.useEmbeddedDex;
}
- // Code below generated by codegen v1.0.22.
+ // Code below generated by codegen v1.0.23.
//
// DO NOT MODIFY!
// CHECKSTYLE:OFF Generated code
@@ -102,6 +107,9 @@
* disabled, or left unspecified.
* @param nativeHeapZeroInitialized
* Enable automatic zero-initialization of native heap memory allocations.
+ * @param useEmbeddedDex
+ * Enable use of embedded dex in the APK, rather than extracted or locally compiled variants.
+ * If false (default), the parent app's configuration determines behavior.
*/
@DataClass.Generated.Member
public ProcessInfo(
@@ -109,7 +117,8 @@
@Nullable ArraySet<String> deniedPermissions,
@ApplicationInfo.GwpAsanMode int gwpAsanMode,
@ApplicationInfo.MemtagMode int memtagMode,
- @ApplicationInfo.NativeHeapZeroInitialized int nativeHeapZeroInitialized) {
+ @ApplicationInfo.NativeHeapZeroInitialized int nativeHeapZeroInitialized,
+ boolean useEmbeddedDex) {
this.name = name;
com.android.internal.util.AnnotationValidations.validate(
NonNull.class, null, name);
@@ -123,6 +132,7 @@
this.nativeHeapZeroInitialized = nativeHeapZeroInitialized;
com.android.internal.util.AnnotationValidations.validate(
ApplicationInfo.NativeHeapZeroInitialized.class, null, nativeHeapZeroInitialized);
+ this.useEmbeddedDex = useEmbeddedDex;
// onConstructed(); // You can define this method to get a callback
}
@@ -145,6 +155,7 @@
// void parcelFieldName(Parcel dest, int flags) { ... }
byte flg = 0;
+ if (useEmbeddedDex) flg |= 0x20;
if (deniedPermissions != null) flg |= 0x2;
dest.writeByte(flg);
dest.writeString(name);
@@ -166,6 +177,7 @@
// static FieldType unparcelFieldName(Parcel in) { ... }
byte flg = in.readByte();
+ boolean _useEmbeddedDex = (flg & 0x20) != 0;
String _name = in.readString();
ArraySet<String> _deniedPermissions = sParcellingForDeniedPermissions.unparcel(in);
int _gwpAsanMode = in.readInt();
@@ -185,6 +197,7 @@
this.nativeHeapZeroInitialized = _nativeHeapZeroInitialized;
com.android.internal.util.AnnotationValidations.validate(
ApplicationInfo.NativeHeapZeroInitialized.class, null, nativeHeapZeroInitialized);
+ this.useEmbeddedDex = _useEmbeddedDex;
// onConstructed(); // You can define this method to get a callback
}
@@ -204,10 +217,10 @@
};
@DataClass.Generated(
- time = 1615850184524L,
- codegenVersion = "1.0.22",
+ time = 1706177470784L,
+ codegenVersion = "1.0.23",
sourceFile = "frameworks/base/core/java/android/content/pm/ProcessInfo.java",
- inputSignatures = "public @android.annotation.NonNull java.lang.String name\npublic @android.annotation.Nullable @com.android.internal.util.DataClass.ParcelWith(com.android.internal.util.Parcelling.BuiltIn.ForInternedStringArraySet.class) android.util.ArraySet<java.lang.String> deniedPermissions\npublic @android.content.pm.ApplicationInfo.GwpAsanMode int gwpAsanMode\npublic @android.content.pm.ApplicationInfo.MemtagMode int memtagMode\npublic @android.content.pm.ApplicationInfo.NativeHeapZeroInitialized int nativeHeapZeroInitialized\nclass ProcessInfo extends java.lang.Object implements [android.os.Parcelable]\n@com.android.internal.util.DataClass(genGetters=true, genSetters=false, genParcelable=true, genAidl=false, genBuilder=false)")
+ inputSignatures = "public @android.annotation.NonNull java.lang.String name\npublic @android.annotation.Nullable @com.android.internal.util.DataClass.ParcelWith(com.android.internal.util.Parcelling.BuiltIn.ForInternedStringArraySet.class) android.util.ArraySet<java.lang.String> deniedPermissions\npublic @android.content.pm.ApplicationInfo.GwpAsanMode int gwpAsanMode\npublic @android.content.pm.ApplicationInfo.MemtagMode int memtagMode\npublic @android.content.pm.ApplicationInfo.NativeHeapZeroInitialized int nativeHeapZeroInitialized\npublic boolean useEmbeddedDex\nclass ProcessInfo extends java.lang.Object implements [android.os.Parcelable]\n@com.android.internal.util.DataClass(genGetters=true, genSetters=false, genParcelable=true, genAidl=false, genBuilder=false)")
@Deprecated
private void __metadata() {}
diff --git a/core/java/android/content/pm/SignedPackage.java b/core/java/android/content/pm/SignedPackage.java
new file mode 100644
index 0000000..4d1b136
--- /dev/null
+++ b/core/java/android/content/pm/SignedPackage.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2024 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.content.pm;
+
+import android.annotation.FlaggedApi;
+import android.annotation.NonNull;
+import android.annotation.SystemApi;
+
+import java.util.Arrays;
+import java.util.Objects;
+
+/**
+ * A data class representing a package and (SHA-256 hash of) a signing certificate.
+ *
+ * @hide
+ */
+@SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
+@FlaggedApi(android.permission.flags.Flags.FLAG_ENHANCED_CONFIRMATION_MODE_APIS_ENABLED)
+public class SignedPackage {
+ @NonNull
+ private final SignedPackageParcel mData;
+
+ /** @hide */
+ public SignedPackage(@NonNull String pkgName, @NonNull byte[] certificateDigest) {
+ SignedPackageParcel data = new SignedPackageParcel();
+ data.pkgName = pkgName;
+ data.certificateDigest = certificateDigest;
+ mData = data;
+ }
+
+ /** @hide */
+ public SignedPackage(@NonNull SignedPackageParcel data) {
+ mData = data;
+ }
+
+ /** @hide */
+ public final @NonNull SignedPackageParcel getData() {
+ return mData;
+ }
+
+ public @NonNull String getPkgName() {
+ return mData.pkgName;
+ }
+
+ public @NonNull byte[] getCertificateDigest() {
+ return mData.certificateDigest;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof SignedPackage that)) return false;
+ return mData.pkgName.equals(that.mData.pkgName) && Arrays.equals(mData.certificateDigest,
+ that.mData.certificateDigest);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mData.pkgName, Arrays.hashCode(mData.certificateDigest));
+ }
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/animation/ActivityLaunchAnimatorKosmos.kt b/core/java/android/content/pm/SignedPackageParcel.aidl
similarity index 71%
copy from packages/SystemUI/tests/utils/src/com/android/systemui/animation/ActivityLaunchAnimatorKosmos.kt
copy to core/java/android/content/pm/SignedPackageParcel.aidl
index 128f58b..7957f7f 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/animation/ActivityLaunchAnimatorKosmos.kt
+++ b/core/java/android/content/pm/SignedPackageParcel.aidl
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2023 The Android Open Source Project
+ * Copyright (C) 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,8 +14,12 @@
* limitations under the License.
*/
-package com.android.systemui.animation
+package android.content.pm;
-import com.android.systemui.kosmos.Kosmos
+import android.content.ComponentName;
-val Kosmos.activityLaunchAnimator by Kosmos.Fixture { ActivityLaunchAnimator() }
+/** @hide */
+parcelable SignedPackageParcel {
+ String pkgName;
+ byte[] certificateDigest;
+}
diff --git a/core/java/android/content/pm/UserProperties.java b/core/java/android/content/pm/UserProperties.java
index 1d0e2db..d347a0e 100644
--- a/core/java/android/content/pm/UserProperties.java
+++ b/core/java/android/content/pm/UserProperties.java
@@ -16,9 +16,6 @@
package android.content.pm;
-import static android.multiuser.Flags.FLAG_SUPPORT_HIDING_PROFILES;
-
-import android.annotation.FlaggedApi;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
@@ -77,6 +74,8 @@
private static final String ATTR_CROSS_PROFILE_CONTENT_SHARING_STRATEGY =
"crossProfileContentSharingStrategy";
private static final String ATTR_PROFILE_API_VISIBILITY = "profileApiVisibility";
+ private static final String ITEMS_RESTRICTED_ON_HOME_SCREEN =
+ "itemsRestrictedOnHomeScreen";
/** Index values of each property (to indicate whether they are present in this object). */
@IntDef(prefix = "INDEX_", value = {
INDEX_SHOW_IN_LAUNCHER,
@@ -96,7 +95,8 @@
INDEX_AUTH_ALWAYS_REQUIRED_TO_DISABLE_QUIET_MODE,
INDEX_CROSS_PROFILE_CONTENT_SHARING_STRATEGY,
INDEX_ALLOW_STOPPING_USER_WITH_DELAYED_LOCKING,
- INDEX_PROFILE_API_VISIBILITY
+ INDEX_PROFILE_API_VISIBILITY,
+ INDEX_ITEMS_RESTRICTED_ON_HOME_SCREEN
})
@Retention(RetentionPolicy.SOURCE)
private @interface PropertyIndex {
@@ -119,6 +119,7 @@
private static final int INDEX_CROSS_PROFILE_CONTENT_SHARING_STRATEGY = 15;
private static final int INDEX_ALLOW_STOPPING_USER_WITH_DELAYED_LOCKING = 16;
private static final int INDEX_PROFILE_API_VISIBILITY = 17;
+ private static final int INDEX_ITEMS_RESTRICTED_ON_HOME_SCREEN = 18;
/** A bit set, mapping each PropertyIndex to whether it is present (1) or absent (0). */
private long mPropertiesPresent = 0;
@@ -472,22 +473,26 @@
)
public @interface ProfileApiVisibility {
}
- /*
- * The api visibility value for this profile user is undefined or unknown.
+
+ /**
+ * The api visibility value for this profile user is undefined or unknown.
+ *
+ * @hide
*/
- @FlaggedApi(FLAG_SUPPORT_HIDING_PROFILES)
public static final int PROFILE_API_VISIBILITY_UNKNOWN = -1;
/**
* Indicates that information about this profile user should be shown in API surfaces.
+ *
+ * @hide
*/
- @FlaggedApi(FLAG_SUPPORT_HIDING_PROFILES)
public static final int PROFILE_API_VISIBILITY_VISIBLE = 0;
/**
* Indicates that information about this profile should be not be visible in API surfaces.
+ *
+ * @hide
*/
- @FlaggedApi(FLAG_SUPPORT_HIDING_PROFILES)
public static final int PROFILE_API_VISIBILITY_HIDDEN = 1;
@@ -532,6 +537,7 @@
setDeleteAppWithParent(orig.getDeleteAppWithParent());
setAlwaysVisible(orig.getAlwaysVisible());
setAllowStoppingUserWithDelayedLocking(orig.getAllowStoppingUserWithDelayedLocking());
+ setItemsRestrictedOnHomeScreen(orig.areItemsRestrictedOnHomeScreen());
}
if (hasManagePermission) {
// Add items that require MANAGE_USERS or stronger.
@@ -550,9 +556,7 @@
setShowInQuietMode(orig.getShowInQuietMode());
setShowInSharingSurfaces(orig.getShowInSharingSurfaces());
setCrossProfileContentSharingStrategy(orig.getCrossProfileContentSharingStrategy());
- if (android.multiuser.Flags.supportHidingProfiles()) {
- setProfileApiVisibility(orig.getProfileApiVisibility());
- }
+ setProfileApiVisibility(orig.getProfileApiVisibility());
}
/**
@@ -997,9 +1001,10 @@
/**
* Returns the visibility of the profile user in API surfaces. Any information linked to the
* profile (userId, package names) should be hidden API surfaces if a user is marked as hidden.
+ *
+ * @hide
*/
@NonNull
- @FlaggedApi(FLAG_SUPPORT_HIDING_PROFILES)
public @ProfileApiVisibility int getProfileApiVisibility() {
if (isPresent(INDEX_PROFILE_API_VISIBILITY)) return mProfileApiVisibility;
if (mDefaultProperties != null) return mDefaultProperties.mProfileApiVisibility;
@@ -1007,18 +1012,46 @@
}
/** @hide */
@NonNull
- @FlaggedApi(FLAG_SUPPORT_HIDING_PROFILES)
public void setProfileApiVisibility(@ProfileApiVisibility int profileApiVisibility) {
this.mProfileApiVisibility = profileApiVisibility;
setPresent(INDEX_PROFILE_API_VISIBILITY);
}
private @ProfileApiVisibility int mProfileApiVisibility;
+ /**
+ * Returns whether a user (usually a profile) is allowed to have items such as Apps Pending
+ * Installation, Widgets, Custom App Shortcuts, etc. on Launcher home screen.
+ *
+ * <p> For a typical user/profile, this property will be false, allowing framework APIs to
+ * provide information about such items to Launcher(s). When set true, framework APIs will
+ * restrict the same.
+ *
+ * <p> This property only restricts information about items that are accessed solely via the
+ * Launcher home screen. Information about items such as App Icons, Deep Links, which can also
+ * be accessed via other launcher components, such as All Apps Drawer is not restricted by this
+ * property.
+ *
+ * @hide
+ */
+ public boolean areItemsRestrictedOnHomeScreen() {
+ if (isPresent(INDEX_ITEMS_RESTRICTED_ON_HOME_SCREEN)) {
+ return mItemsRestrictedOnHomeScreen;
+ }
+ if (mDefaultProperties != null) {
+ return mDefaultProperties.mItemsRestrictedOnHomeScreen;
+ }
+ throw new SecurityException(
+ "You don't have permission to query mItemsRestrictedOnHomeScreen");
+ }
+ /** @hide */
+ public void setItemsRestrictedOnHomeScreen(boolean val) {
+ this.mItemsRestrictedOnHomeScreen = val;
+ setPresent(INDEX_ITEMS_RESTRICTED_ON_HOME_SCREEN);
+ }
+ private boolean mItemsRestrictedOnHomeScreen;
+
@Override
public String toString() {
- String profileApiVisibility =
- android.multiuser.Flags.supportHidingProfiles() ? ", mProfileApiVisibility="
- + getProfileApiVisibility() : "";
// Please print in increasing order of PropertyIndex.
return "UserProperties{"
+ "mPropertiesPresent=" + Long.toBinaryString(mPropertiesPresent)
@@ -1042,7 +1075,8 @@
+ ", mDeleteAppWithParent=" + getDeleteAppWithParent()
+ ", mAlwaysVisible=" + getAlwaysVisible()
+ ", mCrossProfileContentSharingStrategy=" + getCrossProfileContentSharingStrategy()
- + profileApiVisibility
+ + ", mProfileApiVisibility=" + getProfileApiVisibility()
+ + ", mItemsRestrictedOnHomeScreen=" + areItemsRestrictedOnHomeScreen()
+ "}";
}
@@ -1076,9 +1110,8 @@
pw.println(prefix + " mAlwaysVisible=" + getAlwaysVisible());
pw.println(prefix + " mCrossProfileContentSharingStrategy="
+ getCrossProfileContentSharingStrategy());
- if (android.multiuser.Flags.supportHidingProfiles()) {
- pw.println(prefix + " mProfileApiVisibility=" + getProfileApiVisibility());
- }
+ pw.println(prefix + " mProfileApiVisibility=" + getProfileApiVisibility());
+ pw.println(prefix + " mItemsRestrictedOnHomeScreen=" + areItemsRestrictedOnHomeScreen());
}
/**
@@ -1164,9 +1197,10 @@
setCrossProfileContentSharingStrategy(parser.getAttributeInt(i));
break;
case ATTR_PROFILE_API_VISIBILITY:
- if (android.multiuser.Flags.supportHidingProfiles()) {
- setProfileApiVisibility(parser.getAttributeInt(i));
- }
+ setProfileApiVisibility(parser.getAttributeInt(i));
+ break;
+ case ITEMS_RESTRICTED_ON_HOME_SCREEN:
+ setItemsRestrictedOnHomeScreen(parser.getAttributeBoolean(i));
break;
default:
Slog.w(LOG_TAG, "Skipping unknown property " + attributeName);
@@ -1251,10 +1285,12 @@
mCrossProfileContentSharingStrategy);
}
if (isPresent(INDEX_PROFILE_API_VISIBILITY)) {
- if (android.multiuser.Flags.supportHidingProfiles()) {
- serializer.attributeInt(null, ATTR_PROFILE_API_VISIBILITY,
- mProfileApiVisibility);
- }
+ serializer.attributeInt(null, ATTR_PROFILE_API_VISIBILITY,
+ mProfileApiVisibility);
+ }
+ if (isPresent(INDEX_ITEMS_RESTRICTED_ON_HOME_SCREEN)) {
+ serializer.attributeBoolean(null, ITEMS_RESTRICTED_ON_HOME_SCREEN,
+ mItemsRestrictedOnHomeScreen);
}
}
@@ -1280,6 +1316,7 @@
dest.writeBoolean(mAlwaysVisible);
dest.writeInt(mCrossProfileContentSharingStrategy);
dest.writeInt(mProfileApiVisibility);
+ dest.writeBoolean(mItemsRestrictedOnHomeScreen);
}
/**
@@ -1308,6 +1345,7 @@
mAlwaysVisible = source.readBoolean();
mCrossProfileContentSharingStrategy = source.readInt();
mProfileApiVisibility = source.readInt();
+ mItemsRestrictedOnHomeScreen = source.readBoolean();
}
@Override
@@ -1358,6 +1396,7 @@
private @CrossProfileContentSharingStrategy int mCrossProfileContentSharingStrategy =
CROSS_PROFILE_CONTENT_SHARING_NO_DELEGATION;
private @ProfileApiVisibility int mProfileApiVisibility = 0;
+ private boolean mItemsRestrictedOnHomeScreen = false;
/**
* @hide
@@ -1517,12 +1556,20 @@
* @hide
*/
@NonNull
- @FlaggedApi(FLAG_SUPPORT_HIDING_PROFILES)
public Builder setProfileApiVisibility(@ProfileApiVisibility int profileApiVisibility){
mProfileApiVisibility = profileApiVisibility;
return this;
}
+ /** Sets the value for {@link #mItemsRestrictedOnHomeScreen}
+ * @hide
+ */
+ public Builder setItemsRestrictedOnHomeScreen(
+ boolean itemsRestrictedOnHomeScreen) {
+ mItemsRestrictedOnHomeScreen = itemsRestrictedOnHomeScreen;
+ return this;
+ }
+
/** Builds a UserProperties object with *all* values populated.
* @hide
*/
@@ -1548,7 +1595,8 @@
mDeleteAppWithParent,
mAlwaysVisible,
mCrossProfileContentSharingStrategy,
- mProfileApiVisibility);
+ mProfileApiVisibility,
+ mItemsRestrictedOnHomeScreen);
}
} // end Builder
@@ -1570,7 +1618,8 @@
boolean deleteAppWithParent,
boolean alwaysVisible,
@CrossProfileContentSharingStrategy int crossProfileContentSharingStrategy,
- @ProfileApiVisibility int profileApiVisibility) {
+ @ProfileApiVisibility int profileApiVisibility,
+ boolean itemsRestrictedOnHomeScreen) {
mDefaultProperties = null;
setShowInLauncher(showInLauncher);
setStartWithParent(startWithParent);
@@ -1590,8 +1639,7 @@
setDeleteAppWithParent(deleteAppWithParent);
setAlwaysVisible(alwaysVisible);
setCrossProfileContentSharingStrategy(crossProfileContentSharingStrategy);
- if (android.multiuser.Flags.supportHidingProfiles()) {
- setProfileApiVisibility(profileApiVisibility);
- }
+ setProfileApiVisibility(profileApiVisibility);
+ setItemsRestrictedOnHomeScreen(itemsRestrictedOnHomeScreen);
}
}
diff --git a/core/java/android/content/pm/flags.aconfig b/core/java/android/content/pm/flags.aconfig
index 19bce0b..5e9d8f0 100644
--- a/core/java/android/content/pm/flags.aconfig
+++ b/core/java/android/content/pm/flags.aconfig
@@ -199,3 +199,18 @@
bug: "282783453"
is_fixed_read_only: true
}
+
+flag {
+ name: "set_pre_verified_domains"
+ namespace: "package_manager_service"
+ description: "Feature flag to enable pre-verified domains"
+ bug: "307327678"
+}
+
+flag {
+ name: "min_target_sdk_24"
+ namespace: "responsible_apis"
+ description: "Feature flag to bump min target sdk to 24"
+ bug: "297603927"
+ is_fixed_read_only: true
+}
diff --git a/core/java/android/content/pm/multiuser.aconfig b/core/java/android/content/pm/multiuser.aconfig
index d7e64b6..4b890fa 100644
--- a/core/java/android/content/pm/multiuser.aconfig
+++ b/core/java/android/content/pm/multiuser.aconfig
@@ -44,6 +44,20 @@
}
flag {
+ name: "start_user_before_scheduled_alarms"
+ namespace: "multiuser"
+ description: "Persist list of users with alarms scheduled and wakeup stopped users before alarms are due"
+ bug: "314907186"
+}
+
+flag {
+ name: "add_ui_for_sounds_from_background_users"
+ namespace: "multiuser"
+ description: "Allow foreground user to dismiss sounds that are coming from background users"
+ bug: "314907186"
+}
+
+flag {
name: "enable_biometrics_to_unlock_private_space"
namespace: "profile_experiences"
description: "Add support to unlock the private space using biometrics"
@@ -95,18 +109,10 @@
}
flag {
- name: "allow_private_profile_apis"
+ name: "enable_private_space_features"
namespace: "profile_experiences"
- description: "Enable only the API changes to support private space"
- bug: "299069460"
-}
-
-flag {
- name: "support_hiding_profiles"
- namespace: "profile_experiences"
- description: "Allow the use of a hide_profile property to hide some profiles behind a permission"
- bug: "316362775"
- is_fixed_read_only: true
+ description: "Enable the support for private space and all its sub-features"
+ bug: "286418785"
}
flag {
@@ -122,3 +128,10 @@
description: "Handle listing of private space apps in settings pages with interleaved content"
bug: "323212460"
}
+
+flag {
+ name: "enable_hiding_profiles"
+ namespace: "profile_experiences"
+ description: "Allow the use of a profileApiAvailability user property to exclude HIDDEN profiles in API results"
+ bug: "316362775"
+}
diff --git a/core/java/android/content/pm/parsing/ApkLite.java b/core/java/android/content/pm/parsing/ApkLite.java
index 4990a27..74ce62c 100644
--- a/core/java/android/content/pm/parsing/ApkLite.java
+++ b/core/java/android/content/pm/parsing/ApkLite.java
@@ -145,6 +145,11 @@
private final boolean mUpdatableSystem;
/**
+ * Name of the emergency installer for the designated system app.
+ */
+ private final @Nullable String mEmergencyInstaller;
+
+ /**
* Archival install info.
*/
private final @Nullable ArchivedPackageParcel mArchivedPackage;
@@ -159,7 +164,8 @@
String requiredSystemPropertyName, String requiredSystemPropertyValue,
int minSdkVersion, int targetSdkVersion, int rollbackDataPolicy,
Set<String> requiredSplitTypes, Set<String> splitTypes,
- boolean hasDeviceAdminReceiver, boolean isSdkLibrary, boolean updatableSystem) {
+ boolean hasDeviceAdminReceiver, boolean isSdkLibrary, boolean updatableSystem,
+ String emergencyInstaller) {
mPath = path;
mPackageName = packageName;
mSplitName = splitName;
@@ -194,6 +200,7 @@
mHasDeviceAdminReceiver = hasDeviceAdminReceiver;
mIsSdkLibrary = isSdkLibrary;
mUpdatableSystem = updatableSystem;
+ mEmergencyInstaller = emergencyInstaller;
mArchivedPackage = null;
}
@@ -232,6 +239,7 @@
mHasDeviceAdminReceiver = false;
mIsSdkLibrary = false;
mUpdatableSystem = true;
+ mEmergencyInstaller = null;
mArchivedPackage = archivedPackage;
}
@@ -550,6 +558,14 @@
}
/**
+ * Name of the emergency installer for the designated system app.
+ */
+ @DataClass.Generated.Member
+ public @Nullable String getEmergencyInstaller() {
+ return mEmergencyInstaller;
+ }
+
+ /**
* Archival install info.
*/
@DataClass.Generated.Member
@@ -558,10 +574,10 @@
}
@DataClass.Generated(
- time = 1699587291575L,
+ time = 1706896661616L,
codegenVersion = "1.0.23",
sourceFile = "frameworks/base/core/java/android/content/pm/parsing/ApkLite.java",
- inputSignatures = "private final @android.annotation.NonNull java.lang.String mPackageName\nprivate final @android.annotation.NonNull java.lang.String mPath\nprivate final @android.annotation.Nullable java.lang.String mSplitName\nprivate final @android.annotation.Nullable java.lang.String mUsesSplitName\nprivate final @android.annotation.Nullable java.lang.String mConfigForSplit\nprivate final @android.annotation.Nullable java.util.Set<java.lang.String> mRequiredSplitTypes\nprivate final @android.annotation.Nullable java.util.Set<java.lang.String> mSplitTypes\nprivate final int mVersionCodeMajor\nprivate final int mVersionCode\nprivate final int mRevisionCode\nprivate final int mInstallLocation\nprivate final int mMinSdkVersion\nprivate final int mTargetSdkVersion\nprivate final @android.annotation.NonNull android.content.pm.VerifierInfo[] mVerifiers\nprivate final @android.annotation.NonNull android.content.pm.SigningDetails mSigningDetails\nprivate final boolean mFeatureSplit\nprivate final boolean mIsolatedSplits\nprivate final boolean mSplitRequired\nprivate final boolean mCoreApp\nprivate final boolean mDebuggable\nprivate final boolean mProfileableByShell\nprivate final boolean mMultiArch\nprivate final boolean mUse32bitAbi\nprivate final boolean mExtractNativeLibs\nprivate final boolean mUseEmbeddedDex\nprivate final @android.annotation.Nullable java.lang.String mTargetPackageName\nprivate final boolean mOverlayIsStatic\nprivate final int mOverlayPriority\nprivate final @android.annotation.Nullable java.lang.String mRequiredSystemPropertyName\nprivate final @android.annotation.Nullable java.lang.String mRequiredSystemPropertyValue\nprivate final int mRollbackDataPolicy\nprivate final boolean mHasDeviceAdminReceiver\nprivate final boolean mIsSdkLibrary\nprivate final boolean mUpdatableSystem\nprivate final @android.annotation.Nullable android.content.pm.ArchivedPackageParcel mArchivedPackage\npublic long getLongVersionCode()\nprivate boolean hasAnyRequiredSplitTypes()\nclass ApkLite extends java.lang.Object implements []\n@com.android.internal.util.DataClass(genConstructor=false, genConstDefs=false)")
+ inputSignatures = "private final @android.annotation.NonNull java.lang.String mPackageName\nprivate final @android.annotation.NonNull java.lang.String mPath\nprivate final @android.annotation.Nullable java.lang.String mSplitName\nprivate final @android.annotation.Nullable java.lang.String mUsesSplitName\nprivate final @android.annotation.Nullable java.lang.String mConfigForSplit\nprivate final @android.annotation.Nullable java.util.Set<java.lang.String> mRequiredSplitTypes\nprivate final @android.annotation.Nullable java.util.Set<java.lang.String> mSplitTypes\nprivate final int mVersionCodeMajor\nprivate final int mVersionCode\nprivate final int mRevisionCode\nprivate final int mInstallLocation\nprivate final int mMinSdkVersion\nprivate final int mTargetSdkVersion\nprivate final @android.annotation.NonNull android.content.pm.VerifierInfo[] mVerifiers\nprivate final @android.annotation.NonNull android.content.pm.SigningDetails mSigningDetails\nprivate final boolean mFeatureSplit\nprivate final boolean mIsolatedSplits\nprivate final boolean mSplitRequired\nprivate final boolean mCoreApp\nprivate final boolean mDebuggable\nprivate final boolean mProfileableByShell\nprivate final boolean mMultiArch\nprivate final boolean mUse32bitAbi\nprivate final boolean mExtractNativeLibs\nprivate final boolean mUseEmbeddedDex\nprivate final @android.annotation.Nullable java.lang.String mTargetPackageName\nprivate final boolean mOverlayIsStatic\nprivate final int mOverlayPriority\nprivate final @android.annotation.Nullable java.lang.String mRequiredSystemPropertyName\nprivate final @android.annotation.Nullable java.lang.String mRequiredSystemPropertyValue\nprivate final int mRollbackDataPolicy\nprivate final boolean mHasDeviceAdminReceiver\nprivate final boolean mIsSdkLibrary\nprivate final boolean mUpdatableSystem\nprivate final @android.annotation.Nullable java.lang.String mEmergencyInstaller\nprivate final @android.annotation.Nullable android.content.pm.ArchivedPackageParcel mArchivedPackage\npublic long getLongVersionCode()\nprivate boolean hasAnyRequiredSplitTypes()\nclass ApkLite extends java.lang.Object implements []\n@com.android.internal.util.DataClass(genConstructor=false, genConstDefs=false)")
@Deprecated
private void __metadata() {}
diff --git a/core/java/android/content/pm/parsing/ApkLiteParseUtils.java b/core/java/android/content/pm/parsing/ApkLiteParseUtils.java
index 4626679..ffb69c0 100644
--- a/core/java/android/content/pm/parsing/ApkLiteParseUtils.java
+++ b/core/java/android/content/pm/parsing/ApkLiteParseUtils.java
@@ -39,6 +39,7 @@
import android.util.Pair;
import android.util.Slog;
+import com.android.internal.pm.pkg.component.flags.Flags;
import com.android.internal.util.ArrayUtils;
import libcore.io.IoUtils;
@@ -89,6 +90,8 @@
private static final String TAG_SDK_LIBRARY = "sdk-library";
private static final int SDK_VERSION = Build.VERSION.SDK_INT;
private static final String[] SDK_CODENAMES = Build.VERSION.ACTIVE_CODENAMES;
+ private static final String TAG_PROCESSES = "processes";
+ private static final String TAG_PROCESS = "process";
/**
* Parse only lightweight details about the package at the given location.
@@ -432,6 +435,7 @@
boolean isSplitRequired = parser.getAttributeBooleanValue(ANDROID_RES_NAMESPACE,
"isSplitRequired", false);
String configForSplit = parser.getAttributeValue(null, "configForSplit");
+ String emergencyInstaller = parser.getAttributeValue(null, "emergencyInstaller");
int targetSdkVersion = DEFAULT_TARGET_SDK_VERSION;
int minSdkVersion = DEFAULT_MIN_SDK_VERSION;
@@ -518,6 +522,28 @@
case TAG_SDK_LIBRARY:
isSdkLibrary = true;
break;
+ case TAG_PROCESSES:
+ final int processesDepth = parser.getDepth();
+ int processesType;
+ while ((processesType = parser.next()) != XmlPullParser.END_DOCUMENT
+ && (processesType != XmlPullParser.END_TAG
+ || parser.getDepth() > processesDepth)) {
+ if (processesType == XmlPullParser.END_TAG
+ || processesType == XmlPullParser.TEXT) {
+ continue;
+ }
+
+ if (parser.getDepth() != processesDepth + 1) {
+ // Search only under <processes>.
+ continue;
+ }
+
+ if (parser.getName().equals(TAG_PROCESS)
+ && Flags.enablePerProcessUseEmbeddedDexAttr()) {
+ useEmbeddedDex |= parser.getAttributeBooleanValue(
+ ANDROID_RES_NAMESPACE, "useEmbeddedDex", false);
+ }
+ }
}
}
} else if (TAG_OVERLAY.equals(parser.getName())) {
@@ -619,7 +645,7 @@
overlayIsStatic, overlayPriority, requiredSystemPropertyName,
requiredSystemPropertyValue, minSdkVersion, targetSdkVersion,
rollbackDataPolicy, requiredSplitTypes.first, requiredSplitTypes.second,
- hasDeviceAdminReceiver, isSdkLibrary, updatableSystem));
+ hasDeviceAdminReceiver, isSdkLibrary, updatableSystem, emergencyInstaller));
}
private static boolean isDeviceAdminReceiver(
diff --git a/core/java/android/content/pm/verify/domain/DomainVerificationState.java b/core/java/android/content/pm/verify/domain/DomainVerificationState.java
index 8e28042..6c4fe37 100644
--- a/core/java/android/content/pm/verify/domain/DomainVerificationState.java
+++ b/core/java/android/content/pm/verify/domain/DomainVerificationState.java
@@ -33,7 +33,8 @@
STATE_DENIED,
STATE_LEGACY_FAILURE,
STATE_SYS_CONFIG,
- STATE_FIRST_VERIFIER_DEFINED
+ STATE_PRE_VERIFIED,
+ STATE_FIRST_VERIFIER_DEFINED,
})
@interface State {
}
@@ -92,6 +93,13 @@
int STATE_SYS_CONFIG = 7;
/**
+ * The application has temporarily been granted auto verification for a set of domains as
+ * specified by a trusted installer during the installation. This will treat the domain as
+ * verified, but it should be updated by the verification agent.
+ */
+ int STATE_PRE_VERIFIED = 8;
+
+ /**
* @see DomainVerificationInfo#STATE_FIRST_VERIFIER_DEFINED
*/
int STATE_FIRST_VERIFIER_DEFINED = 0b10000000000;
@@ -115,6 +123,8 @@
return "legacy_failure";
case DomainVerificationState.STATE_SYS_CONFIG:
return "system_configured";
+ case DomainVerificationState.STATE_PRE_VERIFIED:
+ return "pre_verified";
default:
return String.valueOf(state);
}
@@ -135,6 +145,7 @@
case STATE_DENIED:
case STATE_LEGACY_FAILURE:
case STATE_SYS_CONFIG:
+ case STATE_PRE_VERIFIED:
default:
return false;
}
@@ -151,6 +162,7 @@
case DomainVerificationState.STATE_MIGRATED:
case DomainVerificationState.STATE_RESTORED:
case DomainVerificationState.STATE_SYS_CONFIG:
+ case DomainVerificationState.STATE_PRE_VERIFIED:
return true;
case DomainVerificationState.STATE_NO_RESPONSE:
case DomainVerificationState.STATE_DENIED:
@@ -173,6 +185,7 @@
case DomainVerificationState.STATE_MIGRATED:
case DomainVerificationState.STATE_RESTORED:
case DomainVerificationState.STATE_LEGACY_FAILURE:
+ case DomainVerificationState.STATE_PRE_VERIFIED:
return true;
case DomainVerificationState.STATE_APPROVED:
case DomainVerificationState.STATE_DENIED:
@@ -194,6 +207,7 @@
case STATE_RESTORED:
case STATE_APPROVED:
case STATE_DENIED:
+ case STATE_PRE_VERIFIED:
return true;
case STATE_NO_RESPONSE:
case STATE_LEGACY_FAILURE:
diff --git a/core/java/android/content/res/Resources.java b/core/java/android/content/res/Resources.java
index 1b37092..3671980 100644
--- a/core/java/android/content/res/Resources.java
+++ b/core/java/android/content/res/Resources.java
@@ -29,6 +29,7 @@
import android.annotation.DimenRes;
import android.annotation.Discouraged;
import android.annotation.DrawableRes;
+import android.annotation.FlaggedApi;
import android.annotation.FontRes;
import android.annotation.FractionRes;
import android.annotation.IntegerRes;
@@ -46,6 +47,7 @@
import android.content.Context;
import android.content.pm.ActivityInfo;
import android.content.pm.ActivityInfo.Config;
+import android.content.pm.ApplicationInfo;
import android.content.res.loader.ResourcesLoader;
import android.graphics.Movie;
import android.graphics.Typeface;
@@ -2825,4 +2827,22 @@
}
}
}
+
+ /**
+ * Register the resources paths of a package (e.g. a shared library). This will collect the
+ * package resources' paths from its ApplicationInfo and add them to all existing and future
+ * contexts while the application is running.
+ * A second call with the same uniqueId is a no-op.
+ * The paths are not persisted during application restarts. The application is responsible for
+ * calling the API again if this happens.
+ *
+ * @param uniqueId The unique id for the ApplicationInfo object, to detect and ignore repeated
+ * API calls.
+ * @param appInfo The ApplicationInfo that contains resources paths of the package.
+ */
+ @FlaggedApi(android.content.res.Flags.FLAG_REGISTER_RESOURCE_PATHS)
+ public static void registerResourcePaths(@NonNull String uniqueId,
+ @NonNull ApplicationInfo appInfo) {
+ throw new UnsupportedOperationException("The implementation has not been done yet.");
+ }
}
diff --git a/core/java/android/content/res/flags.aconfig b/core/java/android/content/res/flags.aconfig
index db81e84..f660770 100644
--- a/core/java/android/content/res/flags.aconfig
+++ b/core/java/android/content/res/flags.aconfig
@@ -40,3 +40,12 @@
description: "Feature flag for creating an frro from a 9-patch"
bug: "309232726"
}
+
+flag {
+ name: "register_resource_paths"
+ namespace: "resource_manager"
+ description: "Feature flag for register resource paths for shared library"
+ bug: "306202569"
+ # This flag is read in ResourcesImpl at boot time.
+ is_fixed_read_only: true
+}
diff --git a/core/java/android/credentials/Constants.java b/core/java/android/credentials/Constants.java
new file mode 100644
index 0000000..ea30c44
--- /dev/null
+++ b/core/java/android/credentials/Constants.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2024 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.credentials;
+
+/**
+ * Constants for credential manager service that doesn't fit into other structures
+ *
+ * @hide
+ */
+public class Constants {
+ /**
+ * The request is success and user selected an entry
+ */
+ public static final int SUCCESS_CREDMAN_SELECTOR = 0;
+ /**
+ * The error code for ui getting cancelled by user
+ */
+ public static final int FAILURE_CREDMAN_SELECTOR = -1;
+}
diff --git a/core/java/android/credentials/GetCandidateCredentialsResponse.java b/core/java/android/credentials/GetCandidateCredentialsResponse.java
index 73361ad..6e53fd9 100644
--- a/core/java/android/credentials/GetCandidateCredentialsResponse.java
+++ b/core/java/android/credentials/GetCandidateCredentialsResponse.java
@@ -41,8 +41,6 @@
private final PendingIntent mPendingIntent;
- private final GetCredentialResponse mGetCredentialResponse;
-
/**
* @hide
*/
@@ -52,7 +50,6 @@
) {
mCandidateProviderDataList = null;
mPendingIntent = null;
- mGetCredentialResponse = getCredentialResponse;
}
/**
@@ -68,7 +65,6 @@
/*valueName=*/ "candidateProviderDataList");
mCandidateProviderDataList = new ArrayList<>(candidateProviderDataList);
mPendingIntent = pendingIntent;
- mGetCredentialResponse = null;
}
/**
@@ -85,15 +81,6 @@
*
* @hide
*/
- public GetCredentialResponse getGetCredentialResponse() {
- return mGetCredentialResponse;
- }
-
- /**
- * Returns candidate provider data list.
- *
- * @hide
- */
public PendingIntent getPendingIntent() {
return mPendingIntent;
}
@@ -106,14 +93,12 @@
AnnotationValidations.validate(NonNull.class, null, mCandidateProviderDataList);
mPendingIntent = in.readTypedObject(PendingIntent.CREATOR);
- mGetCredentialResponse = in.readTypedObject(GetCredentialResponse.CREATOR);
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeTypedList(mCandidateProviderDataList);
dest.writeTypedObject(mPendingIntent, flags);
- dest.writeTypedObject(mGetCredentialResponse, flags);
}
@Override
diff --git a/core/java/android/credentials/flags.aconfig b/core/java/android/credentials/flags.aconfig
index 1ca11e6..ec46d2f 100644
--- a/core/java/android/credentials/flags.aconfig
+++ b/core/java/android/credentials/flags.aconfig
@@ -55,3 +55,10 @@
description: "Enables Credential Manager to work with the Biometric Authenticate API"
bug: "323211850"
}
+
+flag {
+ namespace: "wear_frameworks"
+ name: "wear_credential_manager_enabled"
+ description: "Enables Credential Manager on Wear Platform"
+ bug: "301168341"
+}
diff --git a/core/java/android/credentials/selection/AuthenticationEntry.java b/core/java/android/credentials/selection/AuthenticationEntry.java
index 54589e1..dd6ca9e 100644
--- a/core/java/android/credentials/selection/AuthenticationEntry.java
+++ b/core/java/android/credentials/selection/AuthenticationEntry.java
@@ -23,9 +23,13 @@
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.SuppressLint;
-import android.annotation.TestApi;
+import android.annotation.SystemApi;
import android.app.slice.Slice;
+import android.content.Context;
import android.content.Intent;
+import android.credentials.GetCredentialRequest;
+import android.os.CancellationSignal;
+import android.os.OutcomeReceiver;
import android.os.Parcel;
import android.os.Parcelable;
@@ -33,17 +37,20 @@
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
+import java.util.concurrent.Executor;
/**
* An authentication entry.
*
- * Applicable only for credential retrieval flow, authentication entries are a special type of
- * entries that require the user to unlock the given provider before its credential options can
- * be fully rendered.
+ * Applicable only for
+ * {@link android.credentials.CredentialManager#getCredential(Context, GetCredentialRequest,
+ * CancellationSignal, Executor, OutcomeReceiver)} flow, authentication entries are a special type
+ * of entries that require the user to unlock the given provider before its credential options
+ * can be fully rendered.
*
* @hide
*/
-@TestApi
+@SystemApi
@FlaggedApi(FLAG_CONFIGURABLE_SELECTOR_UI_ENABLED)
public final class AuthenticationEntry implements Parcelable {
@NonNull
@@ -67,17 +74,31 @@
public @interface Status {
}
- /** This entry is still locked, as initially supplied by the provider. */
+ /**
+ * This entry is still locked, as initially supplied by the provider.
+ *
+ * This entry should be rendered in a way to signal that it is still locked, and when chosen
+ * will lead to an unlock challenge (e.g. draw a trailing lock icon on this entry).
+ */
public static final int STATUS_LOCKED = 0;
/**
* This entry was unlocked but didn't contain any credential. Meanwhile, "less recent" means
* there is another such entry that was unlocked more recently.
+ *
+ * This entry should be rendered in a way to signal that it was unlocked but turns out to
+ * contain no credential that can be used, and as a result, it should be unclickable.
*/
public static final int STATUS_UNLOCKED_BUT_EMPTY_LESS_RECENT = 1;
/**
* This is the most recent entry that was unlocked but didn't contain any credential.
*
* There will be at most one authentication entry with this status.
+ *
+ * This entry should be rendered in a way to signal that it was unlocked but turns out to
+ * contain no credential that can be used, and as a result, it should be unclickable.
+ *
+ * If this was the last clickable option prior to unlocking, then the UI should display an
+ * information that all options are exhausted then gracefully finish itself.
*/
public static final int STATUS_UNLOCKED_BUT_EMPTY_MOST_RECENT = 2;
@@ -94,29 +115,36 @@
}
/**
- * Constructor to be used for an entry that does not require further activities
- * to be invoked when selected.
+ * Constructor to be used for an entry that requires a pending intent to be invoked
+ * when clicked.
+ *
+ * @param key the identifier of this entry that's unique within the context of the given
+ * CredentialManager request. This is used when constructing the
+ * {@link android.credentials.selection.UserSelectionResult#UserSelectionResult(
+ * String providerId, String entryKey, String entrySubkey,
+ * ProviderPendingIntentResponse providerPendingIntentResponse)}
+ *
+ * @param subkey the sub-identifier of this entry that's unique within the context of the
+ * {@code key}. This is used when constructing the
+ * {@link android.credentials.selection.UserSelectionResult#UserSelectionResult(
+ * String providerId, String entryKey, String entrySubkey,
+ * ProviderPendingIntentResponse providerPendingIntentResponse)}
+ * @param intent the intent containing extra data that has to be filled in when launching this
+ * entry's provider PendingIntent
+ * @param slice the Slice to be displayed
+ * @param status the entry status, depending on which the entry should be rendered differently
*/
- // TODO(b/322065508): remove this constructor.
public AuthenticationEntry(@NonNull String key, @NonNull String subkey, @NonNull Slice slice,
- @Status int status) {
+ @Status int status, @NonNull Intent intent) {
mKey = key;
mSubkey = subkey;
mSlice = slice;
mStatus = status;
- }
-
- /** Constructor to be used for an entry that requires a pending intent to be invoked
- * when clicked.
- */
- public AuthenticationEntry(@NonNull String key, @NonNull String subkey, @NonNull Slice slice,
- @Status int status, @NonNull Intent intent) {
- this(key, subkey, slice, status);
mFrameworkExtrasIntent = intent;
}
/**
- * Returns the identifier of this entry that's unique within the context of the
+ * Returns the identifier of this entry that's unique within the context of the given
* CredentialManager request.
*/
@NonNull
@@ -138,7 +166,7 @@
return mSlice;
}
- /** Returns the entry status, depending on which the entry will be rendered differently. */
+ /** Returns the entry status, depending on which the entry should be rendered differently. */
@NonNull
@Status
public int getStatus() {
@@ -146,8 +174,10 @@
}
/**
- * Returns the framework intent to be filled in when launching this entry's provider
- * PendingIntent.
+ * Returns the intent containing extra data that has to be filled in when launching this
+ * entry's provider PendingIntent.
+ *
+ * If null, the provider PendingIntent can be launched without any fill in intent.
*/
@Nullable
@SuppressLint("IntentBuilderName") // Not building a new intent.
diff --git a/core/java/android/credentials/selection/CancelSelectionRequest.java b/core/java/android/credentials/selection/CancelSelectionRequest.java
new file mode 100644
index 0000000..55acfdb
--- /dev/null
+++ b/core/java/android/credentials/selection/CancelSelectionRequest.java
@@ -0,0 +1,149 @@
+/*
+ * 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.credentials.selection;
+
+import static android.credentials.flags.Flags.FLAG_CONFIGURABLE_SELECTOR_UI_ENABLED;
+
+import android.annotation.FlaggedApi;
+import android.annotation.NonNull;
+import android.annotation.SystemApi;
+import android.os.IBinder;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import com.android.internal.util.AnnotationValidations;
+
+/**
+ * A request to cancel the ongoing selection UI matching the identifier token in this request.
+ *
+ * Upon receiving this request, the UI should gracefully finish itself if the given request token
+ * {@link CancelSelectionRequest#getToken()} matches that of the selection UI is currently rendered
+ * for. Also, the UI should display some informational cancellation message (e.g. "Request is
+ * cancelled by the app") before closing when the
+ * {@link CancelSelectionRequest#shouldShowCancellationExplanation()} is true.
+ *
+ * @hide
+ */
+@SystemApi
+@FlaggedApi(FLAG_CONFIGURABLE_SELECTOR_UI_ENABLED)
+public final class CancelSelectionRequest implements Parcelable {
+
+ /**
+ * The intent extra key for the {@code CancelUiRequest} object when launching the UX
+ * activities.
+ *
+ * @hide
+ */
+ @NonNull
+ public static final String EXTRA_CANCEL_UI_REQUEST =
+ "android.credentials.selection.extra.CANCEL_UI_REQUEST";
+
+ @NonNull
+ private final IBinder mToken;
+
+ private final boolean mShouldShowCancellationExplanation;
+
+ @NonNull
+ private final String mPackageName;
+
+ /**
+ * Returns the request token matching the user request that should be cancelled.
+ *
+ * The request token for the current UI can be found from the UI launch intent, mapping to
+ * {@link RequestInfo#getToken()}.
+ *
+ * @hide
+ */
+ @NonNull
+ public IBinder getToken() {
+ return mToken;
+ }
+
+ /** Returns the request token matching the app request that should be cancelled. */
+ @NonNull
+ public RequestToken getRequestToken() {
+ return new RequestToken(mToken);
+ }
+
+ /**
+ * Returns the app package name invoking this request, that can be used to derive display
+ * metadata (e.g. "Cancelled by `App Name`").
+ */
+ @NonNull
+ public String getPackageName() {
+ return mPackageName;
+ }
+
+ /**
+ * Returns whether the UI should display some informational cancellation message (e.g.
+ * "Request is cancelled by the app") before closing. If false, the UI should be silently
+ * cancelled.
+ */
+ public boolean shouldShowCancellationExplanation() {
+ return mShouldShowCancellationExplanation;
+ }
+
+
+ /**
+ * Constructs a {@link CancelSelectionRequest}.
+ *
+ * @param requestToken request token matching the app request that should be cancelled
+ * @param shouldShowCancellationExplanation whether the UI should display some informational
+ * cancellation message before closing
+ * @param packageName package that is invoking this request
+ *
+ */
+ public CancelSelectionRequest(@NonNull RequestToken requestToken,
+ boolean shouldShowCancellationExplanation, @NonNull String packageName) {
+ mToken = requestToken.getToken();
+ mShouldShowCancellationExplanation = shouldShowCancellationExplanation;
+ mPackageName = packageName;
+ }
+
+ private CancelSelectionRequest(@NonNull Parcel in) {
+ mToken = in.readStrongBinder();
+ AnnotationValidations.validate(NonNull.class, null, mToken);
+ mShouldShowCancellationExplanation = in.readBoolean();
+ mPackageName = in.readString8();
+ AnnotationValidations.validate(NonNull.class, null, mPackageName);
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ dest.writeStrongBinder(mToken);
+ dest.writeBoolean(mShouldShowCancellationExplanation);
+ dest.writeString8(mPackageName);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @NonNull
+ public static final Creator<CancelSelectionRequest> CREATOR = new Creator<>() {
+ @Override
+ public CancelSelectionRequest createFromParcel(@NonNull Parcel in) {
+ return new CancelSelectionRequest(in);
+ }
+
+ @Override
+ public CancelSelectionRequest[] newArray(int size) {
+ return new CancelSelectionRequest[size];
+ }
+ };
+}
diff --git a/core/java/android/credentials/selection/CancelUiRequest.java b/core/java/android/credentials/selection/CancelUiRequest.java
deleted file mode 100644
index fca0e2a..0000000
--- a/core/java/android/credentials/selection/CancelUiRequest.java
+++ /dev/null
@@ -1,114 +0,0 @@
-/*
- * 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.credentials.selection;
-
-import android.annotation.NonNull;
-import android.os.IBinder;
-import android.os.Parcel;
-import android.os.Parcelable;
-
-import com.android.internal.util.AnnotationValidations;
-
-/**
- * A request to cancel the ongoing UI matching the identifier token in this request.
- *
- * @hide
- */
-public final class CancelUiRequest implements Parcelable {
-
- /**
- * The intent extra key for the {@code CancelUiRequest} object when launching the UX
- * activities.
- *
- * @hide
- */
- @NonNull
- public static final String EXTRA_CANCEL_UI_REQUEST =
- "android.credentials.selection.extra.CANCEL_UI_REQUEST";
-
- @NonNull
- private final IBinder mToken;
-
- private final boolean mShouldShowCancellationUi;
-
- @NonNull
- private final String mAppPackageName;
-
- /** Returns the request token matching the user request that should be cancelled. */
- @NonNull
- public IBinder getToken() {
- return mToken;
- }
-
- /**
- * Returns the app package name invoking this request, that can be used to derive display
- * metadata (e.g. "Cancelled by `App Name`").
- */
- @NonNull
- public String getAppPackageName() {
- return mAppPackageName;
- }
-
- /**
- * Returns whether the UI should render a cancellation UI upon the request. If false, the UI
- * will be silently cancelled.
- */
- public boolean shouldShowCancellationUi() {
- return mShouldShowCancellationUi;
- }
-
- /** Constructs a {@link CancelUiRequest}. */
- public CancelUiRequest(@NonNull IBinder token, boolean shouldShowCancellationUi,
- @NonNull String appPackageName) {
- mToken = token;
- mShouldShowCancellationUi = shouldShowCancellationUi;
- mAppPackageName = appPackageName;
- }
-
- private CancelUiRequest(@NonNull Parcel in) {
- mToken = in.readStrongBinder();
- AnnotationValidations.validate(NonNull.class, null, mToken);
- mShouldShowCancellationUi = in.readBoolean();
- mAppPackageName = in.readString8();
- AnnotationValidations.validate(NonNull.class, null, mAppPackageName);
- }
-
- @Override
- public void writeToParcel(@NonNull Parcel dest, int flags) {
- dest.writeStrongBinder(mToken);
- dest.writeBoolean(mShouldShowCancellationUi);
- dest.writeString8(mAppPackageName);
- }
-
- @Override
- public int describeContents() {
- return 0;
- }
-
- @NonNull
- public static final Creator<CancelUiRequest> CREATOR = new Creator<>() {
- @Override
- public CancelUiRequest createFromParcel(@NonNull Parcel in) {
- return new CancelUiRequest(in);
- }
-
- @Override
- public CancelUiRequest[] newArray(int size) {
- return new CancelUiRequest[size];
- }
- };
-}
diff --git a/core/java/android/credentials/selection/Constants.java b/core/java/android/credentials/selection/Constants.java
index 7e6c781..f7fec23 100644
--- a/core/java/android/credentials/selection/Constants.java
+++ b/core/java/android/credentials/selection/Constants.java
@@ -36,5 +36,11 @@
public static final String EXTRA_REQ_FOR_ALL_OPTIONS =
"android.credentials.selection.extra.REQ_FOR_ALL_OPTIONS";
+ /**
+ * The intent extra key for the final result receiver object
+ */
+ public static final String EXTRA_FINAL_RESPONSE_RECEIVER =
+ "android.credentials.selection.extra.FINAL_RESPONSE_RECEIVER";
+
private Constants() {}
}
diff --git a/core/java/android/credentials/selection/CreateCredentialProviderData.java b/core/java/android/credentials/selection/CreateCredentialProviderData.java
index fc80ea8..ba9b00a 100644
--- a/core/java/android/credentials/selection/CreateCredentialProviderData.java
+++ b/core/java/android/credentials/selection/CreateCredentialProviderData.java
@@ -118,9 +118,12 @@
@TestApi
@FlaggedApi(FLAG_CONFIGURABLE_SELECTOR_UI_ENABLED)
public static final class Builder {
- @NonNull private String mProviderFlattenedComponentName;
- @NonNull private List<Entry> mSaveEntries = new ArrayList<>();
- @Nullable private Entry mRemoteEntry = null;
+ @NonNull
+ private String mProviderFlattenedComponentName;
+ @NonNull
+ private List<Entry> mSaveEntries = new ArrayList<>();
+ @Nullable
+ private Entry mRemoteEntry = null;
/** Constructor with required properties. */
public Builder(@NonNull String providerFlattenedComponentName) {
diff --git a/core/java/android/credentials/selection/CreateCredentialProviderInfo.java b/core/java/android/credentials/selection/CreateCredentialProviderInfo.java
index 78b9fd44..6d02f0d 100644
--- a/core/java/android/credentials/selection/CreateCredentialProviderInfo.java
+++ b/core/java/android/credentials/selection/CreateCredentialProviderInfo.java
@@ -16,21 +16,40 @@
package android.credentials.selection;
+import static android.credentials.flags.Flags.FLAG_CONFIGURABLE_SELECTOR_UI_ENABLED;
+
+import android.annotation.FlaggedApi;
import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.annotation.SystemApi;
+import android.content.Context;
+import android.credentials.CreateCredentialRequest;
+import android.os.CancellationSignal;
+import android.os.OutcomeReceiver;
import com.android.internal.util.Preconditions;
import java.util.ArrayList;
import java.util.List;
+import java.util.concurrent.Executor;
/**
- * Information pertaining to a specific provider during the given create-credential flow.
+ * Per-provider metadata and entries for the
+ * {@link android.credentials.CredentialManager#createCredential(Context, CreateCredentialRequest,
+ * CancellationSignal, Executor, OutcomeReceiver)} flow.
*
* This includes provider metadata and its credential creation options for display purposes.
*
+ * The selection UI should render all options (from
+ * {@link CreateCredentialProviderInfo#getSaveEntries()} and
+ * {@link CreateCredentialProviderInfo#getRemoteEntry()}) offered by this provider while clearly
+ * associating them with the given provider using the provider icon, label, etc. derived from
+ * {@link CreateCredentialProviderInfo#getProviderName()}.
+ *
* @hide
*/
+@SystemApi
+@FlaggedApi(FLAG_CONFIGURABLE_SELECTOR_UI_ENABLED)
public final class CreateCredentialProviderInfo {
@NonNull
@@ -65,7 +84,8 @@
* Returns the remote credential saving option, if any.
*
* Notice that only one system configured provider can set this option, and when set, it means
- * that the system service has already validated the provider's eligibility.
+ * that the system service has already validated the provider's eligibility. A null value means
+ * no remote entry should be displayed for this provider.
*/
@Nullable
public Entry getRemoteEntry() {
@@ -77,6 +97,8 @@
*
* @hide
*/
+ @SystemApi
+ @FlaggedApi(FLAG_CONFIGURABLE_SELECTOR_UI_ENABLED)
public static final class Builder {
@NonNull
private String mProviderName;
@@ -85,7 +107,12 @@
@Nullable
private Entry mRemoteEntry = null;
- /** Constructor with required properties. */
+ /**
+ * Constructs a {@link CreateCredentialProviderInfo.Builder}.
+ *
+ * @param providerName the provider (component or package) name
+ * @throws IllegalArgumentException if {@code providerName} is null or empty
+ */
public Builder(@NonNull String providerName) {
mProviderName = Preconditions.checkStringNotEmpty(providerName);
}
@@ -97,7 +124,13 @@
return this;
}
- /** Sets the remote entry of the provider. */
+ /**
+ * Sets the remote entry to be displayed to the user.
+ *
+ * The system service should only set this entry to non-null if it has validated that
+ * the given provider does have the permission to set this value. Null means there is
+ * no valid remote entry for display.
+ */
@NonNull
public Builder setRemoteEntry(@Nullable Entry remoteEntry) {
mRemoteEntry = remoteEntry;
diff --git a/core/java/android/credentials/selection/DisabledProviderData.java b/core/java/android/credentials/selection/DisabledProviderData.java
index b6f6ad4..4238612 100644
--- a/core/java/android/credentials/selection/DisabledProviderData.java
+++ b/core/java/android/credentials/selection/DisabledProviderData.java
@@ -63,14 +63,14 @@
}
public static final @NonNull Creator<DisabledProviderData> CREATOR = new Creator<>() {
- @Override
- public DisabledProviderData createFromParcel(@NonNull Parcel in) {
- return new DisabledProviderData(in);
- }
+ @Override
+ public DisabledProviderData createFromParcel(@NonNull Parcel in) {
+ return new DisabledProviderData(in);
+ }
- @Override
- public DisabledProviderData[] newArray(int size) {
- return new DisabledProviderData[size];
- }
+ @Override
+ public DisabledProviderData[] newArray(int size) {
+ return new DisabledProviderData[size];
+ }
};
}
diff --git a/core/java/android/credentials/selection/DisabledProviderInfo.java b/core/java/android/credentials/selection/DisabledProviderInfo.java
index 7d7dbc2..7764d2e 100644
--- a/core/java/android/credentials/selection/DisabledProviderInfo.java
+++ b/core/java/android/credentials/selection/DisabledProviderInfo.java
@@ -16,17 +16,36 @@
package android.credentials.selection;
+import static android.credentials.flags.Flags.FLAG_CONFIGURABLE_SELECTOR_UI_ENABLED;
+
+import android.annotation.FlaggedApi;
import android.annotation.NonNull;
+import android.annotation.SystemApi;
+import android.content.Context;
+import android.credentials.CreateCredentialRequest;
+import android.os.CancellationSignal;
+import android.os.OutcomeReceiver;
import com.android.internal.util.Preconditions;
+import java.util.concurrent.Executor;
+
/**
* Information pertaining to a specific provider that is disabled from the user settings.
*
- * Currently, disabled provider data is only propagated in the create-credential flow.
+ * Currently, disabled provider data is only propagated in the
+ * {@link android.credentials.CredentialManager#createCredential(Context, CreateCredentialRequest,
+ * CancellationSignal, Executor, OutcomeReceiver)} flow.
+ *
+ * This should be used to display an option, e.g. "+ Enable `disabled_provider_1`,
+ * `disabled_provider_2`" to navigate the user to Settings
+ * ({@link android.provider.Settings#ACTION_CREDENTIAL_PROVIDER}) to enable these
+ * disabled providers.
*
* @hide
*/
+@SystemApi
+@FlaggedApi(FLAG_CONFIGURABLE_SELECTOR_UI_ENABLED)
public final class DisabledProviderInfo {
@NonNull
@@ -37,8 +56,7 @@
*
* @throws IllegalArgumentException if {@code providerName} is empty
*/
- public DisabledProviderInfo(
- @NonNull String providerName) {
+ public DisabledProviderInfo(@NonNull String providerName) {
mProviderName = Preconditions.checkStringNotEmpty(providerName);
}
diff --git a/core/java/android/credentials/selection/Entry.java b/core/java/android/credentials/selection/Entry.java
index bcf4ee3..a131f3f 100644
--- a/core/java/android/credentials/selection/Entry.java
+++ b/core/java/android/credentials/selection/Entry.java
@@ -22,7 +22,7 @@
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.SuppressLint;
-import android.annotation.TestApi;
+import android.annotation.SystemApi;
import android.app.PendingIntent;
import android.app.slice.Slice;
import android.content.Intent;
@@ -36,7 +36,7 @@
*
* @hide
*/
-@TestApi
+@SystemApi
@FlaggedApi(FLAG_CONFIGURABLE_SELECTOR_UI_ENABLED)
public final class Entry implements Parcelable {
@NonNull
@@ -67,30 +67,34 @@
}
/**
- * Constructor to be used for an entry that does not require further activities
- * to be invoked when selected.
- */
- // TODO(b/322065508): deprecate this constructor.
- public Entry(@NonNull String key, @NonNull String subkey, @NonNull Slice slice) {
- mKey = key;
- mSubkey = subkey;
- mSlice = slice;
- }
-
- /**
* Constructor to be used for an entry that requires a pending intent to be invoked
* when clicked.
+ *
+ * @param key the identifier of this entry that's unique within the context of the given
+ * CredentialManager request. This is used when constructing the
+ * {@link android.credentials.selection.UserSelectionResult#UserSelectionResult(
+ * String providerId, String entryKey, String entrySubkey,
+ * ProviderPendingIntentResponse providerPendingIntentResponse)}
+ * @param subkey the sub-identifier of this entry that's unique within the context of the
+ * {@code key}. This is used when constructing the
+ * {@link android.credentials.selection.UserSelectionResult#UserSelectionResult(
+ * String providerId, String entryKey, String entrySubkey,
+ * ProviderPendingIntentResponse providerPendingIntentResponse)}
+ * @param intent the intent containing extra data that has to be filled in when launching this
+ * entry's provider PendingIntent
+ * @param slice the Slice to be displayed
*/
public Entry(@NonNull String key, @NonNull String subkey, @NonNull Slice slice,
@NonNull Intent intent) {
- this(key, subkey, slice);
+ mKey = key;
+ mSubkey = subkey;
+ mSlice = slice;
mFrameworkExtrasIntent = intent;
}
/**
* Returns the identifier of this entry that's unique within the context of the
- * CredentialManager
- * request.
+ * CredentialManager request.
*
* Generally used when sending the user selection result back to the system service.
*/
@@ -116,17 +120,10 @@
}
/**
- * Returns the provider PendingIntent to launch once this entry is selected.
- */
- // TODO(b/322065508): deprecate this bit.
- @Nullable
- public PendingIntent getPendingIntent() {
- return mPendingIntent;
- }
-
- /**
- * Returns the framework fill in intent to add to the provider PendingIntent to launch, once
- * this entry is selected.
+ * Returns the intent containing extra data that has to be filled in when launching this
+ * entry's provider PendingIntent.
+ *
+ * If null, the provider PendingIntent can be launched without any fill in intent.
*/
@Nullable
@SuppressLint("IntentBuilderName") // Not building a new intent.
diff --git a/core/java/android/credentials/selection/GetCredentialProviderInfo.java b/core/java/android/credentials/selection/GetCredentialProviderInfo.java
index db0fb84..1e2f27c 100644
--- a/core/java/android/credentials/selection/GetCredentialProviderInfo.java
+++ b/core/java/android/credentials/selection/GetCredentialProviderInfo.java
@@ -16,21 +16,45 @@
package android.credentials.selection;
+import static android.credentials.flags.Flags.FLAG_CONFIGURABLE_SELECTOR_UI_ENABLED;
+
+import android.annotation.FlaggedApi;
import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.annotation.SystemApi;
+import android.content.Context;
+import android.credentials.GetCredentialRequest;
+import android.credentials.PrepareGetCredentialResponse;
+import android.os.CancellationSignal;
+import android.os.OutcomeReceiver;
import com.android.internal.util.Preconditions;
import java.util.ArrayList;
import java.util.List;
+import java.util.concurrent.Executor;
/**
- * Information pertaining to a specific provider during the given create-credential flow.
+ * Information pertaining to a specific provider during the given
+ * {@link android.credentials.CredentialManager#getCredential(Context, GetCredentialRequest,
+ * CancellationSignal, Executor, OutcomeReceiver)} or
+ * {@link android.credentials.CredentialManager#getCredential(Context,
+ * PrepareGetCredentialResponse.PendingGetCredentialHandle, CancellationSignal, Executor,
+ * OutcomeReceiver)} flow.
*
* This includes provider metadata and its credential creation options for display purposes.
*
+ * The selection UI should render all options (from
+ * {@link GetCredentialProviderInfo#getRemoteEntry()},
+ * {@link GetCredentialProviderInfo#getCredentialEntries()}, and
+ * {@link GetCredentialProviderInfo#getActionChips()}) offered by this provider while clearly
+ * associated them with the given provider using the provider icon, label, etc. derived from
+ * {@link GetCredentialProviderInfo#getProviderName()}.
+ *
* @hide
*/
+@SystemApi
+@FlaggedApi(FLAG_CONFIGURABLE_SELECTOR_UI_ENABLED)
public final class GetCredentialProviderInfo {
@NonNull
@@ -95,8 +119,9 @@
/**
* Returns the remote credential retrieval option, if any.
*
- * Notice that only one system configured provider can set this option, and when set, it means
- * that the system service has already validated the provider's eligibility.
+ * Notice that only one system configured provider can set this option, and when set to
+ * non-null, it means that the system service has already validated the provider's eligibility.
+ * A null value means no remote entry should be displayed for this provider.
*/
@Nullable
public Entry getRemoteEntry() {
@@ -108,6 +133,8 @@
*
* @hide
*/
+ @SystemApi
+ @FlaggedApi(FLAG_CONFIGURABLE_SELECTOR_UI_ENABLED)
public static final class Builder {
@NonNull
private String mProviderName;
@@ -123,6 +150,7 @@
/**
* Constructs a {@link GetCredentialProviderInfo.Builder}.
*
+ * @param providerName the provider (component or package) name
* @throws IllegalArgumentException if {@code providerName} is null or empty
*/
public Builder(@NonNull String providerName) {
@@ -151,7 +179,13 @@
return this;
}
- /** Sets the remote entry to be displayed to the user. */
+ /**
+ * Sets the remote entry to be displayed to the user.
+ *
+ * The system service should only set this entry to non-null if it has validated that
+ * the given provider does have the permission to set this value. Null means there is
+ * no valid remote entry for display.
+ */
@NonNull
public Builder setRemoteEntry(@Nullable Entry remoteEntry) {
mRemoteEntry = remoteEntry;
diff --git a/core/java/android/credentials/selection/IntentFactory.java b/core/java/android/credentials/selection/IntentFactory.java
index c3a09ae..ac2bae4 100644
--- a/core/java/android/credentials/selection/IntentFactory.java
+++ b/core/java/android/credentials/selection/IntentFactory.java
@@ -17,17 +17,24 @@
package android.credentials.selection;
import static android.credentials.flags.Flags.FLAG_CONFIGURABLE_SELECTOR_UI_ENABLED;
+import static android.credentials.flags.Flags.configurableSelectorUiEnabled;
import android.annotation.FlaggedApi;
import android.annotation.NonNull;
+import android.annotation.Nullable;
import android.annotation.SuppressLint;
import android.annotation.TestApi;
import android.content.ComponentName;
+import android.content.Context;
import android.content.Intent;
+import android.content.pm.ActivityInfo;
+import android.content.pm.PackageManager;
import android.content.res.Resources;
import android.os.IBinder;
import android.os.Parcel;
import android.os.ResultReceiver;
+import android.text.TextUtils;
+import android.util.Slog;
import java.util.ArrayList;
@@ -41,15 +48,17 @@
public class IntentFactory {
/**
- * Generate a new launch intent to the Credential Selector UI.
+ * Generate a new launch intent to the Credential Selector UI for auto-filling.
*
* @hide
*/
@NonNull
+ // TODO(b/323552850) - clean up method overloads
public static Intent createCredentialSelectorIntent(
+ @NonNull Context context,
@NonNull RequestInfo requestInfo,
@SuppressLint("ConcreteCollection") // Concrete collection needed for marshalling.
- @NonNull
+ @Nullable
ArrayList<ProviderData> enabledProviderDataList,
@SuppressLint("ConcreteCollection") // Concrete collection needed for marshalling.
@NonNull
@@ -57,23 +66,31 @@
@NonNull ResultReceiver resultReceiver,
boolean isRequestForAllOptions) {
- Intent intent = createCredentialSelectorIntent(requestInfo, enabledProviderDataList,
- disabledProviderDataList, resultReceiver);
+ Intent intent;
+ if (enabledProviderDataList != null) {
+ intent = createCredentialSelectorIntent(context, requestInfo, enabledProviderDataList,
+ disabledProviderDataList, resultReceiver);
+ } else {
+ intent = createCredentialSelectorIntent(context, requestInfo,
+ disabledProviderDataList, resultReceiver);
+ }
intent.putExtra(Constants.EXTRA_REQ_FOR_ALL_OPTIONS, isRequestForAllOptions);
return intent;
}
- /** Generate a new launch intent to the Credential Selector UI. */
+ /**
+ * Generate a new launch intent to the Credential Selector UI.
+ *
+ * @hide
+ */
@NonNull
- public static Intent createCredentialSelectorIntent(
+ private static Intent createCredentialSelectorIntent(
+ @NonNull Context context,
@NonNull RequestInfo requestInfo,
@SuppressLint("ConcreteCollection") // Concrete collection needed for marshalling.
- @NonNull
- ArrayList<ProviderData> enabledProviderDataList,
- @SuppressLint("ConcreteCollection") // Concrete collection needed for marshalling.
- @NonNull
- ArrayList<DisabledProviderData> disabledProviderDataList,
+ @NonNull
+ ArrayList<DisabledProviderData> disabledProviderDataList,
@NonNull ResultReceiver resultReceiver) {
Intent intent = new Intent();
ComponentName componentName =
@@ -82,10 +99,11 @@
.getString(
com.android.internal.R.string
.config_credentialManagerDialogComponent));
+ ComponentName oemOverrideComponentName = getOemOverrideComponentName(context);
+ if (oemOverrideComponentName != null) {
+ componentName = oemOverrideComponentName;
+ }
intent.setComponent(componentName);
-
- intent.putParcelableArrayListExtra(
- ProviderData.EXTRA_ENABLED_PROVIDER_DATA_LIST, enabledProviderDataList);
intent.putParcelableArrayListExtra(
ProviderData.EXTRA_DISABLED_PROVIDER_DATA_LIST, disabledProviderDataList);
intent.putExtra(RequestInfo.EXTRA_REQUEST_INFO, requestInfo);
@@ -96,9 +114,89 @@
}
/**
- * Creates an Intent that cancels any UI matching the given request token id.
+ * Returns null if there is not an enabled and valid oem override component. It means the
+ * default platform UI component name should be used instead.
+ */
+ @Nullable
+ private static ComponentName getOemOverrideComponentName(@NonNull Context context) {
+ ComponentName result = null;
+ if (configurableSelectorUiEnabled()) {
+ if (Resources.getSystem().getBoolean(
+ com.android.internal.R.bool.config_enableOemCredentialManagerDialogComponent)) {
+ String oemComponentString =
+ Resources.getSystem()
+ .getString(
+ com.android.internal.R.string
+ .config_oemCredentialManagerDialogComponent);
+ if (!TextUtils.isEmpty(oemComponentString)) {
+ ComponentName oemComponentName = ComponentName.unflattenFromString(
+ oemComponentString);
+ if (oemComponentName != null) {
+ try {
+ ActivityInfo info = context.getPackageManager().getActivityInfo(
+ oemComponentName,
+ PackageManager.ComponentInfoFlags.of(
+ PackageManager.MATCH_SYSTEM_ONLY));
+ if (info.enabled && info.exported) {
+ Slog.i(TAG,
+ "Found enabled oem CredMan UI component."
+ + oemComponentString);
+ result = oemComponentName;
+ } else {
+ Slog.i(TAG,
+ "Found enabled oem CredMan UI component but it was not "
+ + "enabled.");
+ }
+ } catch (PackageManager.NameNotFoundException e) {
+ Slog.i(TAG, "Unable to find oem CredMan UI component: "
+ + oemComponentString + ".");
+ }
+ } else {
+ Slog.i(TAG, "Invalid OEM ComponentName format.");
+ }
+ } else {
+ Slog.i(TAG, "Invalid empty OEM component name.");
+ }
+ }
+ }
+ return result;
+ }
+
+ /**
+ * Generate a new launch intent to the Credential Selector UI.
*
- * @hide
+ * @param context the CredentialManager system service (only expected caller)
+ * context that may be used to query existence of the key UI
+ * application
+ * @param disabledProviderDataList the list of disabled provider data that when non-empty the
+ * UI should accordingly generate an entry suggesting the user
+ * to navigate to settings and enable them
+ * @param enabledProviderDataList the list of enabled provider that contain options for this
+ * request; the UI should render each option to the user for
+ * selection
+ * @param requestInfo the display information about the given app request
+ * @param resultReceiver used by the UI to send the UI selection result back
+ */
+ @NonNull
+ public static Intent createCredentialSelectorIntent(
+ @NonNull Context context,
+ @NonNull RequestInfo requestInfo,
+ @SuppressLint("ConcreteCollection") // Concrete collection needed for marshalling.
+ @NonNull
+ ArrayList<ProviderData> enabledProviderDataList,
+ @SuppressLint("ConcreteCollection") // Concrete collection needed for marshalling.
+ @NonNull
+ ArrayList<DisabledProviderData> disabledProviderDataList,
+ @NonNull ResultReceiver resultReceiver) {
+ Intent intent = createCredentialSelectorIntent(context, requestInfo,
+ disabledProviderDataList, resultReceiver);
+ intent.putParcelableArrayListExtra(
+ ProviderData.EXTRA_ENABLED_PROVIDER_DATA_LIST, enabledProviderDataList);
+ return intent;
+ }
+
+ /**
+ * Creates an Intent that cancels any UI matching the given request token id.
*/
@NonNull
public static Intent createCancelUiIntent(@NonNull IBinder requestToken,
@@ -111,8 +209,9 @@
com.android.internal.R.string
.config_credentialManagerDialogComponent));
intent.setComponent(componentName);
- intent.putExtra(CancelUiRequest.EXTRA_CANCEL_UI_REQUEST,
- new CancelUiRequest(requestToken, shouldShowCancellationUi, appPackageName));
+ intent.putExtra(CancelSelectionRequest.EXTRA_CANCEL_UI_REQUEST,
+ new CancelSelectionRequest(new RequestToken(requestToken), shouldShowCancellationUi,
+ appPackageName));
return intent;
}
@@ -132,5 +231,8 @@
return ipcFriendly;
}
- private IntentFactory() {}
+ private IntentFactory() {
+ }
+
+ private static final String TAG = "CredManIntentHelper";
}
diff --git a/core/java/android/credentials/selection/IntentHelper.java b/core/java/android/credentials/selection/IntentHelper.java
index 6bcd05a..2c3a320 100644
--- a/core/java/android/credentials/selection/IntentHelper.java
+++ b/core/java/android/credentials/selection/IntentHelper.java
@@ -16,12 +16,16 @@
package android.credentials.selection;
+import static android.credentials.flags.Flags.FLAG_CONFIGURABLE_SELECTOR_UI_ENABLED;
+
+import android.annotation.FlaggedApi;
import android.annotation.NonNull;
import android.annotation.Nullable;
-import android.annotation.SuppressLint;
+import android.annotation.SystemApi;
import android.content.Intent;
import android.os.ResultReceiver;
+import java.util.Collections;
import java.util.List;
/**
@@ -29,15 +33,17 @@
*
* @hide
*/
+@SystemApi
+@FlaggedApi(FLAG_CONFIGURABLE_SELECTOR_UI_ENABLED)
public final class IntentHelper {
/**
- * Attempts to extract a {@link CancelUiRequest} from the given intent; returns null
+ * Attempts to extract a {@link CancelSelectionRequest} from the given intent; returns null
* if not found.
*/
@Nullable
- public static CancelUiRequest extractCancelUiRequest(@NonNull Intent intent) {
- return intent.getParcelableExtra(CancelUiRequest.EXTRA_CANCEL_UI_REQUEST,
- CancelUiRequest.class);
+ public static CancelSelectionRequest extractCancelUiRequest(@NonNull Intent intent) {
+ return intent.getParcelableExtra(CancelSelectionRequest.EXTRA_CANCEL_UI_REQUEST,
+ CancelSelectionRequest.class);
}
/**
@@ -52,37 +58,44 @@
/**
* Attempts to extract the list of {@link GetCredentialProviderInfo} from the given intent;
- * returns null if not found.
+ * returns an empty list if not found.
*/
- @Nullable
- @SuppressLint("NullableCollection") // To be consistent with the nullable Intent extra APIs
- // and the other APIs in this class.
- public static List<GetCredentialProviderInfo> extractGetCredentialProviderDataList(
+ public static @NonNull List<GetCredentialProviderInfo> extractGetCredentialProviderInfoList(
@NonNull Intent intent) {
List<GetCredentialProviderData> providerList = intent.getParcelableArrayListExtra(
ProviderData.EXTRA_ENABLED_PROVIDER_DATA_LIST,
GetCredentialProviderData.class);
- return providerList == null ? null : providerList.stream().map(
+ return providerList == null ? Collections.emptyList() : providerList.stream().map(
GetCredentialProviderData::toGetCredentialProviderInfo).toList();
}
/**
* Attempts to extract the list of {@link CreateCredentialProviderInfo} from the given intent;
- * returns null if not found.
+ * returns an empty list if not found.
*/
- @Nullable
- @SuppressLint("NullableCollection") // To be consistent with the nullable Intent extra APIs
- // and the other APIs in this class.
- public static List<CreateCredentialProviderInfo> extractCreateCredentialProviderDataList(
- @NonNull Intent intent) {
+ public static @NonNull List<CreateCredentialProviderInfo>
+ extractCreateCredentialProviderInfoList(@NonNull Intent intent) {
List<CreateCredentialProviderData> providerList = intent.getParcelableArrayListExtra(
ProviderData.EXTRA_ENABLED_PROVIDER_DATA_LIST,
CreateCredentialProviderData.class);
- return providerList == null ? null : providerList.stream().map(
+ return providerList == null ? Collections.emptyList() : providerList.stream().map(
CreateCredentialProviderData::toCreateCredentialProviderInfo).toList();
}
/**
+ * Attempts to extract the list of {@link DisabledProviderInfo} from the given intent;
+ * returns an empty list if not found.
+ */
+ public static @NonNull List<DisabledProviderInfo> extractDisabledProviderInfoList(
+ @NonNull Intent intent) {
+ List<DisabledProviderData> providerList = intent.getParcelableArrayListExtra(
+ ProviderData.EXTRA_DISABLED_PROVIDER_DATA_LIST,
+ DisabledProviderData.class);
+ return providerList == null ? Collections.emptyList() : providerList.stream().map(
+ DisabledProviderData::toDisabledProviderInfo).toList();
+ }
+
+ /**
* Attempts to extract a {@link android.os.ResultReceiver} from the given intent, which should
* be used to send back UI results; returns null if not found.
*/
diff --git a/core/java/android/credentials/selection/RequestInfo.java b/core/java/android/credentials/selection/RequestInfo.java
index 7d6ea7e..16d0802 100644
--- a/core/java/android/credentials/selection/RequestInfo.java
+++ b/core/java/android/credentials/selection/RequestInfo.java
@@ -22,6 +22,7 @@
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.StringDef;
+import android.annotation.SystemApi;
import android.annotation.TestApi;
import android.credentials.CreateCredentialRequest;
import android.credentials.GetCredentialRequest;
@@ -41,13 +42,15 @@
*
* @hide
*/
-@TestApi
+@SystemApi
@FlaggedApi(FLAG_CONFIGURABLE_SELECTOR_UI_ENABLED)
public final class RequestInfo implements Parcelable {
/**
* The intent extra key for the {@code RequestInfo} object when launching the UX
* activities.
+ *
+ * @hide
*/
@NonNull
public static final String EXTRA_REQUEST_INFO =
@@ -79,7 +82,7 @@
/** @hide */
@Retention(RetentionPolicy.SOURCE)
- @StringDef(value = {TYPE_GET, TYPE_CREATE})
+ @StringDef(value = {TYPE_GET, TYPE_CREATE, TYPE_UNDEFINED})
public @interface RequestType {
}
@@ -103,52 +106,45 @@
private final String mType;
@NonNull
- private final String mAppPackageName;
+ private final String mPackageName;
private final boolean mHasPermissionToOverrideDefault;
- /** Creates new {@code RequestInfo} for a create-credential flow. */
- @NonNull
- public static RequestInfo newCreateRequestInfo(
- @NonNull IBinder token, @NonNull CreateCredentialRequest createCredentialRequest,
- @NonNull String appPackageName) {
- return new RequestInfo(
- token, TYPE_CREATE, appPackageName, createCredentialRequest, null,
- /*hasPermissionToOverrideDefault=*/ false,
- /*defaultProviderIds=*/ new ArrayList<>());
- }
+ private final boolean mIsShowAllOptionsRequested;
- /** Creates new {@code RequestInfo} for a create-credential flow. */
+ /**
+ * Creates new {@code RequestInfo} for a create-credential flow.
+ *
+ * @hide
+ */
+ @TestApi
+ @FlaggedApi(FLAG_CONFIGURABLE_SELECTOR_UI_ENABLED)
@NonNull
public static RequestInfo newCreateRequestInfo(
@NonNull IBinder token, @NonNull CreateCredentialRequest createCredentialRequest,
@NonNull String appPackageName, boolean hasPermissionToOverrideDefault,
- @NonNull List<String> defaultProviderIds) {
+ @NonNull List<String> defaultProviderIds, boolean isShowAllOptionsRequested) {
return new RequestInfo(
token, TYPE_CREATE, appPackageName, createCredentialRequest, null,
- hasPermissionToOverrideDefault, defaultProviderIds);
+ hasPermissionToOverrideDefault, defaultProviderIds, isShowAllOptionsRequested);
}
- /** Creates new {@code RequestInfo} for a get-credential flow. */
+ /**
+ * Creates new {@code RequestInfo} for a get-credential flow.
+ *
+ * @hide
+ */
+ @TestApi
+ @FlaggedApi(FLAG_CONFIGURABLE_SELECTOR_UI_ENABLED)
@NonNull
public static RequestInfo newGetRequestInfo(
@NonNull IBinder token, @NonNull GetCredentialRequest getCredentialRequest,
- @NonNull String appPackageName, boolean hasPermissionToOverrideDefault) {
+ @NonNull String appPackageName, boolean hasPermissionToOverrideDefault,
+ boolean isShowAllOptionsRequested) {
return new RequestInfo(
token, TYPE_GET, appPackageName, null, getCredentialRequest,
hasPermissionToOverrideDefault,
- /*defaultProviderIds=*/ new ArrayList<>());
- }
-
- /** Creates new {@code RequestInfo} for a get-credential flow. */
- @NonNull
- public static RequestInfo newGetRequestInfo(
- @NonNull IBinder token, @NonNull GetCredentialRequest getCredentialRequest,
- @NonNull String appPackageName) {
- return new RequestInfo(
- token, TYPE_GET, appPackageName, null, getCredentialRequest,
- /*hasPermissionToOverrideDefault=*/ false,
- /*defaultProviderIds=*/ new ArrayList<>());
+ /*defaultProviderIds=*/ new ArrayList<>(), isShowAllOptionsRequested);
}
@@ -157,7 +153,11 @@
return mHasPermissionToOverrideDefault;
}
- /** Returns the request token matching the user request. */
+ /**
+ * Returns the request token matching the user request.
+ *
+ * @hide
+ */
@NonNull
public IBinder getToken() {
return mToken;
@@ -172,8 +172,8 @@
/** Returns the display name of the app that made this request. */
@NonNull
- public String getAppPackageName() {
- return mAppPackageName;
+ public String getPackageName() {
+ return mPackageName;
}
/**
@@ -185,6 +185,12 @@
return mCreateCredentialRequest;
}
+ /** Returns the request token matching the app request that should be cancelled. */
+ @NonNull
+ public RequestToken getRequestToken() {
+ return new RequestToken(mToken);
+ }
+
/**
* Returns default provider identifiers (component or package name) configured from the user
* settings.
@@ -215,20 +221,40 @@
return mGetCredentialRequest;
}
+ /**
+ * Returns true if all options should be immediately displayed in the UI, and false otherwise.
+ *
+ * Normally this bit is set to false, upon which the selection UI should first display a
+ * condensed view of popular, deduplicated options that is determined based on signals like
+ * last-used timestamps, credential type priorities, and preferred providers configured from the
+ * user settings {@link #getDefaultProviderIds()}; at the same time, the UI should offer an
+ * option (button) that navigates the user to viewing all options from this condensed view.
+ *
+ * In some special occasions, e.g. when a request is initiated from the autofill drop-down
+ * suggestion, this bit will be set to true to indicate that the selection UI should immediately
+ * render the all option UI. This means that the request initiator has collected a user signal
+ * to confirm that the user wants to view all the available options at once.
+ */
+ public boolean isShowAllOptionsRequested() {
+ return mIsShowAllOptionsRequested;
+ }
+
private RequestInfo(@NonNull IBinder token, @NonNull @RequestType String type,
@NonNull String appPackageName,
@Nullable CreateCredentialRequest createCredentialRequest,
@Nullable GetCredentialRequest getCredentialRequest,
boolean hasPermissionToOverrideDefault,
- @NonNull List<String> defaultProviderIds) {
+ @NonNull List<String> defaultProviderIds,
+ boolean isShowAllOptionsRequested) {
mToken = token;
mType = type;
- mAppPackageName = appPackageName;
+ mPackageName = appPackageName;
mCreateCredentialRequest = createCredentialRequest;
mGetCredentialRequest = getCredentialRequest;
mHasPermissionToOverrideDefault = hasPermissionToOverrideDefault;
mDefaultProviderIds = defaultProviderIds == null ? new ArrayList<>() : defaultProviderIds;
mRegistryProviderIds = new ArrayList<>();
+ mIsShowAllOptionsRequested = isShowAllOptionsRequested;
}
private RequestInfo(@NonNull Parcel in) {
@@ -244,25 +270,27 @@
AnnotationValidations.validate(NonNull.class, null, mToken);
mType = type;
AnnotationValidations.validate(NonNull.class, null, mType);
- mAppPackageName = appPackageName;
- AnnotationValidations.validate(NonNull.class, null, mAppPackageName);
+ mPackageName = appPackageName;
+ AnnotationValidations.validate(NonNull.class, null, mPackageName);
mCreateCredentialRequest = createCredentialRequest;
mGetCredentialRequest = getCredentialRequest;
mHasPermissionToOverrideDefault = in.readBoolean();
mDefaultProviderIds = in.createStringArrayList();
mRegistryProviderIds = in.createStringArrayList();
+ mIsShowAllOptionsRequested = in.readBoolean();
}
@Override
public void writeToParcel(@NonNull Parcel dest, int flags) {
dest.writeStrongBinder(mToken);
dest.writeString8(mType);
- dest.writeString8(mAppPackageName);
+ dest.writeString8(mPackageName);
dest.writeTypedObject(mCreateCredentialRequest, flags);
dest.writeTypedObject(mGetCredentialRequest, flags);
dest.writeBoolean(mHasPermissionToOverrideDefault);
dest.writeStringList(mDefaultProviderIds);
dest.writeStringList(mRegistryProviderIds);
+ dest.writeBoolean(mIsShowAllOptionsRequested);
}
@Override
diff --git a/core/java/android/credentials/selection/RequestToken.java b/core/java/android/credentials/selection/RequestToken.java
new file mode 100644
index 0000000..f1953ce
--- /dev/null
+++ b/core/java/android/credentials/selection/RequestToken.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2024 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.credentials.selection;
+
+import static android.credentials.flags.Flags.FLAG_CONFIGURABLE_SELECTOR_UI_ENABLED;
+
+import android.annotation.FlaggedApi;
+import android.annotation.NonNull;
+import android.annotation.SystemApi;
+import android.annotation.TestApi;
+import android.os.IBinder;
+
+/**
+ * Unique identifier for a getCredential / createCredential API session.
+ *
+ * To compare if two requests pertain to the same session, compare their RequestTokens using
+ * the {@link RequestToken#equals(Object)} method.
+ *
+ * For example, when receiving a {@link android.credentials.selection.CancelSelectionRequest},
+ * the developer should use {@link RequestToken#getToken()} to retrieve the token from request and
+ * compare whether it is equal with the cached token using {@link RequestToken#equals(Object)}. Only
+ * cancel the request when two tokens are the same.
+ *
+ * @hide
+ */
+@SystemApi
+@FlaggedApi(FLAG_CONFIGURABLE_SELECTOR_UI_ENABLED)
+public final class RequestToken {
+
+ @NonNull
+ private final IBinder mToken;
+
+ /** @hide **/
+ @NonNull
+ public IBinder getToken() {
+ return mToken;
+ }
+
+ /** @hide */
+ @TestApi
+ @FlaggedApi(FLAG_CONFIGURABLE_SELECTOR_UI_ENABLED)
+ public RequestToken(@NonNull IBinder token) {
+ mToken = token;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null || !(obj instanceof RequestToken)) {
+ return false;
+ }
+ final RequestToken other = (RequestToken) obj;
+ return mToken.equals(other.mToken);
+ }
+
+ @Override
+ public int hashCode() {
+ return mToken.hashCode();
+ }
+}
diff --git a/core/java/android/database/Cursor.java b/core/java/android/database/Cursor.java
index cb1d3f5..3b7ade2 100644
--- a/core/java/android/database/Cursor.java
+++ b/core/java/android/database/Cursor.java
@@ -511,7 +511,7 @@
Bundle getExtras();
/**
- * This is an out-of-band way for the the user of a cursor to communicate with the cursor. The
+ * This is an out-of-band way for the user of a cursor to communicate with the cursor. The
* structure of each bundle is entirely defined by the cursor.
*
* <p>One use of this is to tell a cursor that it should retry its network request after it
diff --git a/core/java/android/database/sqlite/SQLiteDatabase.java b/core/java/android/database/sqlite/SQLiteDatabase.java
index 35ae3c9..5dfeac7 100644
--- a/core/java/android/database/sqlite/SQLiteDatabase.java
+++ b/core/java/android/database/sqlite/SQLiteDatabase.java
@@ -693,7 +693,7 @@
* Begins a transaction in DEFERRED mode, with the android-specific constraint that the
* transaction is read-only. The database may not be modified inside a read-only transaction.
* <p>
- * Read-only transactions may run concurrently with other read-only transactions, and if they
+ * Read-only transactions may run concurrently with other read-only transactions, and if the
* database is in WAL mode, they may also run concurrently with IMMEDIATE or EXCLUSIVE
* transactions.
* <p>
diff --git a/core/java/android/ddm/DdmHandleViewDebug.java b/core/java/android/ddm/DdmHandleViewDebug.java
index 0f66fcb..5cbf24f 100644
--- a/core/java/android/ddm/DdmHandleViewDebug.java
+++ b/core/java/android/ddm/DdmHandleViewDebug.java
@@ -16,16 +16,12 @@
package android.ddm;
-import static com.android.internal.util.Preconditions.checkArgument;
-
import android.util.Log;
import android.view.View;
import android.view.ViewDebug;
import android.view.ViewRootImpl;
import android.view.WindowManagerGlobal;
-import com.android.internal.annotations.VisibleForTesting;
-
import org.apache.harmony.dalvik.ddmc.Chunk;
import org.apache.harmony.dalvik.ddmc.ChunkHandler;
import org.apache.harmony.dalvik.ddmc.DdmServer;
@@ -35,10 +31,8 @@
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
-import java.lang.reflect.Method;
import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
-import java.nio.charset.StandardCharsets;
/**
* Handle various requests related to profiling / debugging of the view system.
@@ -352,48 +346,17 @@
*
* The return value is encoded the same way as a single parameter (type + value)
*/
- private Chunk invokeViewMethod(final View rootView, final View targetView, ByteBuffer in) {
+ private Chunk invokeViewMethod(View rootView, final View targetView, ByteBuffer in) {
int l = in.getInt();
String methodName = getString(in, l);
- Class<?>[] argTypes;
- Object[] args;
- if (!in.hasRemaining()) {
- argTypes = new Class<?>[0];
- args = new Object[0];
- } else {
- int nArgs = in.getInt();
- argTypes = new Class<?>[nArgs];
- args = new Object[nArgs];
-
- try {
- deserializeMethodParameters(args, argTypes, in);
- } catch (ViewMethodInvocationSerializationException e) {
- return createFailChunk(ERR_INVALID_PARAM, e.getMessage());
- }
- }
-
- Method method;
try {
- method = targetView.getClass().getMethod(methodName, argTypes);
- } catch (NoSuchMethodException e) {
- Log.e(TAG, "No such method: " + e.getMessage());
- return createFailChunk(ERR_INVALID_PARAM,
- "No such method: " + e.getMessage());
- }
-
- try {
- Object result = ViewDebug.invokeViewMethod(targetView, method, args);
- Class<?> returnType = method.getReturnType();
- byte[] returnValue = serializeReturnValue(returnType, returnType.cast(result));
+ byte[] returnValue = ViewDebug.invokeViewMethod(targetView, methodName, in);
return new Chunk(CHUNK_VUOP, returnValue, 0, returnValue.length);
+ } catch (ViewDebug.ViewMethodInvocationSerializationException e) {
+ return createFailChunk(ERR_INVALID_PARAM, e.getMessage());
} catch (Exception e) {
- Log.e(TAG, "Exception while invoking method: " + e.getCause().getMessage());
- String msg = e.getCause().getMessage();
- if (msg == null) {
- msg = e.getCause().toString();
- }
- return createFailChunk(ERR_EXCEPTION, msg);
+ return createFailChunk(ERR_EXCEPTION, e.getMessage());
}
}
@@ -431,175 +394,4 @@
byte[] data = b.toByteArray();
return new Chunk(CHUNK_VUOP, data, 0, data.length);
}
-
- /**
- * Deserializes parameters according to the VUOP_INVOKE_VIEW_METHOD protocol the {@code in}
- * buffer.
- *
- * The length of {@code args} determines how many arguments are read. The {@code argTypes} must
- * be the same length, and will be set to the argument types of the data read.
- *
- * @hide
- */
- @VisibleForTesting
- public static void deserializeMethodParameters(
- Object[] args, Class<?>[] argTypes, ByteBuffer in) throws
- ViewMethodInvocationSerializationException {
- checkArgument(args.length == argTypes.length);
-
- for (int i = 0; i < args.length; i++) {
- char typeSignature = in.getChar();
- boolean isArray = typeSignature == SIG_ARRAY;
- if (isArray) {
- char arrayType = in.getChar();
- if (arrayType != SIG_BYTE) {
- // This implementation only supports byte-arrays for now.
- throw new ViewMethodInvocationSerializationException(
- "Unsupported array parameter type (" + typeSignature
- + ") to invoke view method @argument " + i);
- }
-
- int arrayLength = in.getInt();
- if (arrayLength > in.remaining()) {
- // The sender did not actually sent the specified amount of bytes. This
- // avoids a malformed packet to trigger an out-of-memory error.
- throw new BufferUnderflowException();
- }
-
- byte[] byteArray = new byte[arrayLength];
- in.get(byteArray);
-
- argTypes[i] = byte[].class;
- args[i] = byteArray;
- } else {
- switch (typeSignature) {
- case SIG_BOOLEAN:
- argTypes[i] = boolean.class;
- args[i] = in.get() != 0;
- break;
- case SIG_BYTE:
- argTypes[i] = byte.class;
- args[i] = in.get();
- break;
- case SIG_CHAR:
- argTypes[i] = char.class;
- args[i] = in.getChar();
- break;
- case SIG_SHORT:
- argTypes[i] = short.class;
- args[i] = in.getShort();
- break;
- case SIG_INT:
- argTypes[i] = int.class;
- args[i] = in.getInt();
- break;
- case SIG_LONG:
- argTypes[i] = long.class;
- args[i] = in.getLong();
- break;
- case SIG_FLOAT:
- argTypes[i] = float.class;
- args[i] = in.getFloat();
- break;
- case SIG_DOUBLE:
- argTypes[i] = double.class;
- args[i] = in.getDouble();
- break;
- case SIG_STRING: {
- argTypes[i] = String.class;
- int stringUtf8ByteCount = Short.toUnsignedInt(in.getShort());
- byte[] rawStringBuffer = new byte[stringUtf8ByteCount];
- in.get(rawStringBuffer);
- args[i] = new String(rawStringBuffer, StandardCharsets.UTF_8);
- break;
- }
- default:
- Log.e(TAG, "arg " + i + ", unrecognized type: " + typeSignature);
- throw new ViewMethodInvocationSerializationException(
- "Unsupported parameter type (" + typeSignature
- + ") to invoke view method.");
- }
- }
-
- }
- }
-
- /**
- * Serializes {@code value} to the wire protocol of VUOP_INVOKE_VIEW_METHOD.
- * @hide
- */
- @VisibleForTesting
- public static byte[] serializeReturnValue(Class<?> type, Object value)
- throws ViewMethodInvocationSerializationException, IOException {
- ByteArrayOutputStream byteOutStream = new ByteArrayOutputStream(1024);
- DataOutputStream dos = new DataOutputStream(byteOutStream);
-
- if (type.isArray()) {
- if (!type.equals(byte[].class)) {
- // Only byte arrays are supported currently.
- throw new ViewMethodInvocationSerializationException(
- "Unsupported array return type (" + type + ")");
- }
- byte[] byteArray = (byte[]) value;
- dos.writeChar(SIG_ARRAY);
- dos.writeChar(SIG_BYTE);
- dos.writeInt(byteArray.length);
- dos.write(byteArray);
- } else if (boolean.class.equals(type)) {
- dos.writeChar(SIG_BOOLEAN);
- dos.write((boolean) value ? 1 : 0);
- } else if (byte.class.equals(type)) {
- dos.writeChar(SIG_BYTE);
- dos.writeByte((byte) value);
- } else if (char.class.equals(type)) {
- dos.writeChar(SIG_CHAR);
- dos.writeChar((char) value);
- } else if (short.class.equals(type)) {
- dos.writeChar(SIG_SHORT);
- dos.writeShort((short) value);
- } else if (int.class.equals(type)) {
- dos.writeChar(SIG_INT);
- dos.writeInt((int) value);
- } else if (long.class.equals(type)) {
- dos.writeChar(SIG_LONG);
- dos.writeLong((long) value);
- } else if (double.class.equals(type)) {
- dos.writeChar(SIG_DOUBLE);
- dos.writeDouble((double) value);
- } else if (float.class.equals(type)) {
- dos.writeChar(SIG_FLOAT);
- dos.writeFloat((float) value);
- } else if (String.class.equals(type)) {
- dos.writeChar(SIG_STRING);
- dos.writeUTF(value != null ? (String) value : "");
- } else {
- dos.writeChar(SIG_VOID);
- }
-
- return byteOutStream.toByteArray();
- }
-
- // Prefixes for simple primitives. These match the JNI definitions.
- private static final char SIG_ARRAY = '[';
- private static final char SIG_BOOLEAN = 'Z';
- private static final char SIG_BYTE = 'B';
- private static final char SIG_SHORT = 'S';
- private static final char SIG_CHAR = 'C';
- private static final char SIG_INT = 'I';
- private static final char SIG_LONG = 'J';
- private static final char SIG_FLOAT = 'F';
- private static final char SIG_DOUBLE = 'D';
- private static final char SIG_VOID = 'V';
- // Prefixes for some commonly used objects
- private static final char SIG_STRING = 'R';
-
- /**
- * @hide
- */
- @VisibleForTesting
- public static class ViewMethodInvocationSerializationException extends Exception {
- ViewMethodInvocationSerializationException(String message) {
- super(message);
- }
- }
}
diff --git a/core/java/android/hardware/biometrics/AuthenticationStateListener.aidl b/core/java/android/hardware/biometrics/AuthenticationStateListener.aidl
index d51e62e..1488cff 100644
--- a/core/java/android/hardware/biometrics/AuthenticationStateListener.aidl
+++ b/core/java/android/hardware/biometrics/AuthenticationStateListener.aidl
@@ -15,6 +15,8 @@
*/
package android.hardware.biometrics;
+import android.hardware.biometrics.BiometricSourceType;
+
/**
* Low-level callback interface between <Biometric>Manager and <Auth>Service. Allows core system
* services (e.g. SystemUI) to register a listener for updates about the current state of biometric
@@ -49,4 +51,15 @@
* @param userId The user Id for the requested authentication
*/
void onAuthenticationFailed(int requestReason, int userId);
+
+ /**
+ * Defines behavior in response to biometric being acquired.
+ * @param biometricSourceType identifies [BiometricSourceType] biometric was acquired for
+ * @param requestReason reason from [BiometricRequestConstants.RequestReason] for authentication
+ * @param acquiredInfo [BiometricFingerprintConstants.FingerprintAcquired] int corresponding to
+ * a known acquired message.
+ */
+ void onAuthenticationAcquired(
+ in BiometricSourceType biometricSourceType, int requestReason, int acquiredInfo
+ );
}
diff --git a/core/java/android/hardware/camera2/CameraCharacteristics.java b/core/java/android/hardware/camera2/CameraCharacteristics.java
index 665d8d2..451d6fb 100644
--- a/core/java/android/hardware/camera2/CameraCharacteristics.java
+++ b/core/java/android/hardware/camera2/CameraCharacteristics.java
@@ -5313,7 +5313,7 @@
* </code></pre>
* <ul>
* <li>VIDEO_STABILIZATION_MODES: {OFF, PREVIEW}</li>
- * <li>AE_TARGET_FPS_RANGE: {{<em>, 30}, {</em>, 60}}</li>
+ * <li>AE_TARGET_FPS_RANGE: { {<em>, 30}, {</em>, 60} }</li>
* <li>DYNAMIC_RANGE_PROFILE: {STANDARD, HLG10}</li>
* </ul>
* <p>This key is available on all devices.</p>
diff --git a/core/java/android/hardware/camera2/CameraExtensionCharacteristics.java b/core/java/android/hardware/camera2/CameraExtensionCharacteristics.java
index 1867a17..7abe821 100644
--- a/core/java/android/hardware/camera2/CameraExtensionCharacteristics.java
+++ b/core/java/android/hardware/camera2/CameraExtensionCharacteristics.java
@@ -624,6 +624,120 @@
}
/**
+ * Gets an extension specific camera characteristics field value.
+ *
+ * <p>An extension can have a reduced set of camera capabilities (such as limited zoom ratio
+ * range, available video stabilization modes, etc). This API enables applications to query for
+ * an extension’s specific camera characteristics. Applications are recommended to prioritize
+ * obtaining camera characteristics using this API when using an extension. A {@code null}
+ * result indicates that the extension specific characteristic is not defined or available.
+ *
+ * @param extension The extension type.
+ * @param key The characteristics field to read.
+ * @return The value of that key, or {@code null} if the field is not set.
+ *
+ * @throws IllegalArgumentException if the key is not valid or extension type is not a supported
+ * device-specific extension.
+ */
+ @FlaggedApi(Flags.FLAG_CAMERA_EXTENSIONS_CHARACTERISTICS_GET)
+ public <T> @Nullable T get(@Extension int extension,
+ @NonNull CameraCharacteristics.Key<T> key) {
+ final IBinder token = new Binder(TAG + "#get:" + mCameraId);
+ boolean success = registerClient(mContext, token);
+ if (!success) {
+ throw new IllegalArgumentException("Unsupported extensions");
+ }
+
+ try {
+ if (!isExtensionSupported(mCameraId, extension, mCharacteristicsMapNative)) {
+ throw new IllegalArgumentException("Unsupported extension");
+ }
+
+ if (areAdvancedExtensionsSupported() && getKeys(extension).contains(key)) {
+ IAdvancedExtenderImpl extender = initializeAdvancedExtension(extension);
+ extender.init(mCameraId, mCharacteristicsMapNative);
+ CameraMetadataNative metadata =
+ extender.getAvailableCharacteristicsKeyValues(mCameraId);
+ CameraCharacteristics fallbackCharacteristics = mCharacteristicsMap.get(mCameraId);
+ if (metadata == null) {
+ return fallbackCharacteristics.get(key);
+ }
+ CameraCharacteristics characteristics = new CameraCharacteristics(metadata);
+ T value = characteristics.get(key);
+ return value == null ? fallbackCharacteristics.get(key) : value;
+ }
+ } catch (RemoteException e) {
+ Log.e(TAG, "Failed to query the extension for the specified key! Extension "
+ + "service does not respond!");
+ } finally {
+ unregisterClient(mContext, token);
+ }
+ return null;
+ }
+
+ /**
+ * Returns the {@link CameraCharacteristics} keys that have extension-specific values.
+ *
+ * <p>An application can query the value from the key using
+ * {@link #get(int, CameraCharacteristics.Key)} API.
+ *
+ * @param extension The extension type.
+ * @return An unmodifiable set of keys that are extension specific.
+ *
+ * @throws IllegalArgumentException in case the extension type is not a
+ * supported device-specific extension
+ */
+ @FlaggedApi(Flags.FLAG_CAMERA_EXTENSIONS_CHARACTERISTICS_GET)
+ public @NonNull Set<CameraCharacteristics.Key> getKeys(@Extension int extension) {
+ final IBinder token =
+ new Binder(TAG + "#getKeys:" + mCameraId);
+ boolean success = registerClient(mContext, token);
+ if (!success) {
+ throw new IllegalArgumentException("Unsupported extensions");
+ }
+
+ HashSet<CameraCharacteristics.Key> ret = new HashSet<>();
+
+ try {
+ if (!isExtensionSupported(mCameraId, extension, mCharacteristicsMapNative)) {
+ throw new IllegalArgumentException("Unsupported extension");
+ }
+
+ if (areAdvancedExtensionsSupported()) {
+ IAdvancedExtenderImpl extender = initializeAdvancedExtension(extension);
+ extender.init(mCameraId, mCharacteristicsMapNative);
+ CameraMetadataNative metadata =
+ extender.getAvailableCharacteristicsKeyValues(mCameraId);
+ if (metadata == null) {
+ return Collections.emptySet();
+ }
+
+ int[] keys = metadata.get(
+ CameraCharacteristics.REQUEST_AVAILABLE_CHARACTERISTICS_KEYS);
+ if (keys == null) {
+ throw new AssertionError(
+ "android.request.availableCharacteristicsKeys must be non-null"
+ + " in the characteristics");
+ }
+ CameraCharacteristics chars = new CameraCharacteristics(metadata);
+
+ Object key = CameraCharacteristics.Key.class;
+ Class<CameraCharacteristics.Key<?>> keyTyped =
+ (Class<CameraCharacteristics.Key<?>>) key;
+
+ ret.addAll(chars.getAvailableKeyList(CameraCharacteristics.class, keyTyped, keys,
+ /*includeSynthetic*/ true));
+ }
+ } catch (RemoteException e) {
+ Log.e(TAG, "Failed to query the extension for all available keys! Extension "
+ + "service does not respond!");
+ } finally {
+ unregisterClient(mContext, token);
+ }
+ return Collections.unmodifiableSet(ret);
+ }
+
+ /**
* Checks for postview support of still capture.
*
* <p>A postview is a preview version of the still capture that is available before the final
diff --git a/core/java/android/hardware/camera2/extension/AdvancedExtender.java b/core/java/android/hardware/camera2/extension/AdvancedExtender.java
index fb2df54..6653577 100644
--- a/core/java/android/hardware/camera2/extension/AdvancedExtender.java
+++ b/core/java/android/hardware/camera2/extension/AdvancedExtender.java
@@ -27,6 +27,7 @@
import android.hardware.camera2.impl.CameraMetadataNative;
import android.hardware.camera2.impl.CaptureCallback;
import android.util.Log;
+import android.util.Pair;
import android.util.Size;
import com.android.internal.camera.flags.Flags;
@@ -56,6 +57,7 @@
private HashMap<String, Long> mMetadataVendorIdMap = new HashMap<>();
private final CameraManager mCameraManager;
+ private CameraUsageTracker mCameraUsageTracker;
private static final String TAG = "AdvancedExtender";
@FlaggedApi(Flags.FLAG_CONCERT_MODE)
@@ -81,6 +83,10 @@
}
}
+ void setCameraUsageTracker(CameraUsageTracker tracker) {
+ mCameraUsageTracker = tracker;
+ }
+
@FlaggedApi(Flags.FLAG_CONCERT_MODE)
public long getMetadataVendorId(@NonNull String cameraId) {
long vendorId = mMetadataVendorIdMap.containsKey(cameraId) ?
@@ -222,6 +228,23 @@
public abstract List<CaptureResult.Key> getAvailableCaptureResultKeys(
@NonNull String cameraId);
+ /**
+ * Returns a list of {@link CameraCharacteristics} key/value pairs for apps to use when
+ * querying the Extensions specific {@link CameraCharacteristics}.
+ *
+ * <p>To ensure the correct {@link CameraCharacteristics} are used when an extension is
+ * enabled, an application should prioritize the value returned from the list if the
+ * {@link CameraCharacteristics} key is present. If the key doesn't exist in the returned list,
+ * then the application should query the value using
+ * {@link CameraCharacteristics#get(CameraCharacteristics.Key)}.
+ *
+ * <p>For example, an extension may limit the zoom ratio range. In this case, an OEM can return
+ * a new zoom ratio range for the key {@link CameraCharacteristics#CONTROL_ZOOM_RATIO_RANGE}.
+ */
+ @FlaggedApi(Flags.FLAG_CAMERA_EXTENSIONS_CHARACTERISTICS_GET)
+ @NonNull
+ public abstract List<Pair<CameraCharacteristics.Key, Object>>
+ getAvailableCharacteristicsKeyValues();
private final class AdvancedExtenderImpl extends IAdvancedExtenderImpl.Stub {
@Override
@@ -264,7 +287,9 @@
@Override
public ISessionProcessorImpl getSessionProcessor() {
- return AdvancedExtender.this.getSessionProcessor().getSessionProcessorBinder();
+ SessionProcessor processor =AdvancedExtender.this.getSessionProcessor();
+ processor.setCameraUsageTracker(mCameraUsageTracker);
+ return processor.getSessionProcessorBinder();
}
@Override
@@ -322,6 +347,33 @@
// Feature is currently unsupported
return false;
}
+
+ @FlaggedApi(Flags.FLAG_CAMERA_EXTENSIONS_CHARACTERISTICS_GET)
+ @Override
+ public CameraMetadataNative getAvailableCharacteristicsKeyValues(String cameraId) {
+ List<Pair<CameraCharacteristics.Key, Object>> entries =
+ AdvancedExtender.this.getAvailableCharacteristicsKeyValues();
+
+ if ((entries != null) && !entries.isEmpty()) {
+ CameraMetadataNative ret = new CameraMetadataNative();
+ long vendorId = mMetadataVendorIdMap.containsKey(cameraId)
+ ? mMetadataVendorIdMap.get(cameraId) : Long.MAX_VALUE;
+ ret.setVendorId(vendorId);
+ int[] characteristicsKeyTags = new int[entries.size()];
+ int i = 0;
+ for (Pair<CameraCharacteristics.Key, Object> entry : entries) {
+ int tag = CameraMetadataNative.getTag(entry.first.getName(), vendorId);
+ characteristicsKeyTags[i++] = tag;
+ ret.set(entry.first, entry.second);
+ }
+ ret.set(CameraCharacteristics.REQUEST_AVAILABLE_CHARACTERISTICS_KEYS,
+ characteristicsKeyTags);
+
+ return ret;
+ }
+
+ return null;
+ }
}
@NonNull IAdvancedExtenderImpl getAdvancedExtenderBinder() {
diff --git a/core/java/android/hardware/camera2/extension/CameraExtensionService.java b/core/java/android/hardware/camera2/extension/CameraExtensionService.java
index 1426d7b..fa0d14a 100644
--- a/core/java/android/hardware/camera2/extension/CameraExtensionService.java
+++ b/core/java/android/hardware/camera2/extension/CameraExtensionService.java
@@ -20,6 +20,7 @@
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.SystemApi;
+import android.app.AppOpsManager;
import android.app.Service;
import android.content.Intent;
import android.os.IBinder;
@@ -29,6 +30,11 @@
import com.android.internal.annotations.GuardedBy;
import com.android.internal.camera.flags.Flags;
+interface CameraUsageTracker {
+ void startCameraOperation();
+ void finishCameraOperation();
+}
+
/**
* Base service class that extension service implementations must extend.
*
@@ -38,8 +44,33 @@
@FlaggedApi(Flags.FLAG_CONCERT_MODE)
public abstract class CameraExtensionService extends Service {
private static final String TAG = "CameraExtensionService";
+ private CameraUsageTracker mCameraUsageTracker;
private static Object mLock = new Object();
+ private final class CameraTracker implements CameraUsageTracker {
+
+ private final AppOpsManager mAppOpsService = getApplicationContext().getSystemService(
+ AppOpsManager.class);
+ private final String mPackageName = getPackageName();
+ private final String mAttributionTag = getAttributionTag();
+ private int mUid = getApplicationInfo().uid;
+
+ @Override
+ public void startCameraOperation() {
+ if (mAppOpsService != null) {
+ mAppOpsService.startOp(AppOpsManager.OPSTR_CAMERA, mUid, mPackageName,
+ mAttributionTag, "Camera extensions");
+ }
+ }
+
+ @Override
+ public void finishCameraOperation() {
+ if (mAppOpsService != null) {
+ mAppOpsService.finishOp(AppOpsManager.OPSTR_CAMERA, mUid, mPackageName,
+ mAttributionTag);
+ }
+ }
+ }
@GuardedBy("mLock")
private static IInitializeSessionCallback mInitializeCb = null;
@@ -49,16 +80,22 @@
synchronized (mLock) {
mInitializeCb = null;
}
+ if (mCameraUsageTracker != null) {
+ mCameraUsageTracker.finishCameraOperation();
+ }
}
};
@FlaggedApi(Flags.FLAG_CONCERT_MODE)
- protected CameraExtensionService() {}
+ protected CameraExtensionService() { }
@FlaggedApi(Flags.FLAG_CONCERT_MODE)
@Override
@NonNull
public IBinder onBind(@Nullable Intent intent) {
+ if (mCameraUsageTracker == null) {
+ mCameraUsageTracker = new CameraTracker();
+ }
return new CameraExtensionServiceImpl();
}
@@ -132,8 +169,10 @@
@Override
public IAdvancedExtenderImpl initializeAdvancedExtension(int extensionType)
throws RemoteException {
- return CameraExtensionService.this.onInitializeAdvancedExtension(
- extensionType).getAdvancedExtenderBinder();
+ AdvancedExtender extender = CameraExtensionService.this.onInitializeAdvancedExtension(
+ extensionType);
+ extender.setCameraUsageTracker(mCameraUsageTracker);
+ return extender.getAdvancedExtenderBinder();
}
}
diff --git a/core/java/android/hardware/camera2/extension/IAdvancedExtenderImpl.aidl b/core/java/android/hardware/camera2/extension/IAdvancedExtenderImpl.aidl
index 101442f..3071f0d 100644
--- a/core/java/android/hardware/camera2/extension/IAdvancedExtenderImpl.aidl
+++ b/core/java/android/hardware/camera2/extension/IAdvancedExtenderImpl.aidl
@@ -38,4 +38,5 @@
CameraMetadataNative getAvailableCaptureResultKeys(in String cameraId);
boolean isCaptureProcessProgressAvailable();
boolean isPostviewAvailable();
+ CameraMetadataNative getAvailableCharacteristicsKeyValues(in String cameraId);
}
diff --git a/core/java/android/hardware/camera2/extension/SessionProcessor.java b/core/java/android/hardware/camera2/extension/SessionProcessor.java
index 6ed0c14..9c5136b 100644
--- a/core/java/android/hardware/camera2/extension/SessionProcessor.java
+++ b/core/java/android/hardware/camera2/extension/SessionProcessor.java
@@ -76,10 +76,15 @@
@FlaggedApi(Flags.FLAG_CONCERT_MODE)
public abstract class SessionProcessor {
private static final String TAG = "SessionProcessor";
+ private CameraUsageTracker mCameraUsageTracker;
@FlaggedApi(Flags.FLAG_CONCERT_MODE)
protected SessionProcessor() {}
+ void setCameraUsageTracker(CameraUsageTracker tracker) {
+ mCameraUsageTracker = tracker;
+ }
+
/**
* Callback for notifying the status of {@link
* #startCapture} and {@link #startRepeating}.
@@ -379,12 +384,18 @@
@Override
public void onCaptureSessionStart(IRequestProcessorImpl requestProcessor, String statsKey)
throws RemoteException {
+ if (mCameraUsageTracker != null) {
+ mCameraUsageTracker.startCameraOperation();
+ }
SessionProcessor.this.onCaptureSessionStart(
new RequestProcessor(requestProcessor, mVendorId), statsKey);
}
@Override
public void onCaptureSessionEnd() throws RemoteException {
+ if (mCameraUsageTracker != null) {
+ mCameraUsageTracker.finishCameraOperation();
+ }
SessionProcessor.this.onCaptureSessionEnd();
}
diff --git a/core/java/android/hardware/input/IInputManager.aidl b/core/java/android/hardware/input/IInputManager.aidl
index 7bea9ae..1f54959 100644
--- a/core/java/android/hardware/input/IInputManager.aidl
+++ b/core/java/android/hardware/input/IInputManager.aidl
@@ -67,6 +67,9 @@
KeyCharacterMap getKeyCharacterMap(String layoutDescriptor);
+ // Returns the mouse pointer speed.
+ int getMousePointerSpeed();
+
// Temporarily changes the pointer speed.
void tryPointerSpeed(int speed);
diff --git a/core/java/android/hardware/input/InputManager.java b/core/java/android/hardware/input/InputManager.java
index 4ebbde7..744dfae9 100644
--- a/core/java/android/hardware/input/InputManager.java
+++ b/core/java/android/hardware/input/InputManager.java
@@ -16,9 +16,11 @@
package android.hardware.input;
+import static com.android.input.flags.Flags.FLAG_INPUT_DEVICE_VIEW_BEHAVIOR_API;
import static com.android.hardware.input.Flags.keyboardLayoutPreviewFlag;
import android.Manifest;
+import android.annotation.FlaggedApi;
import android.annotation.FloatRange;
import android.annotation.IntDef;
import android.annotation.NonNull;
@@ -26,6 +28,7 @@
import android.annotation.RequiresPermission;
import android.annotation.SdkConstant;
import android.annotation.SdkConstant.SdkConstantType;
+import android.annotation.SuppressLint;
import android.annotation.SystemService;
import android.annotation.TestApi;
import android.annotation.UserIdInt;
@@ -294,6 +297,23 @@
}
/**
+ * Gets the {@link InputDevice.ViewBehavior} of the input device with a given {@code id}.
+ *
+ * <p>Use this API to query a fresh view behavior instance whenever the input device
+ * changes.
+ *
+ * @param deviceId the id of the input device whose view behavior is being requested.
+ * @return the view behavior of the input device with the provided id, or {@code null} if there
+ * is not input device with the provided id.
+ */
+ @FlaggedApi(FLAG_INPUT_DEVICE_VIEW_BEHAVIOR_API)
+ @Nullable
+ public InputDevice.ViewBehavior getInputDeviceViewBehavior(int deviceId) {
+ InputDevice device = getInputDevice(deviceId);
+ return device == null ? null : device.getViewBehavior();
+ }
+
+ /**
* Gets information about the input device with the specified descriptor.
* @param descriptor The input device descriptor.
* @return The input device or null if not found.
@@ -838,6 +858,28 @@
}
/**
+ * Returns the mouse pointer speed.
+ *
+ * <p>The pointer speed is a value between {@link InputSettings#MIN_POINTER_SPEED} and
+ * {@link InputSettings#MAX_POINTER_SPEED}, the default value being
+ * {@link InputSettings#DEFAULT_POINTER_SPEED}.
+ *
+ * <p> Note that while setting the mouse pointer speed, it's possible that the input reader has
+ * only received this value and has not yet completed reconfiguring itself with this value.
+ *
+ * @hide
+ */
+ @SuppressLint("UnflaggedApi") // TestApi without associated feature.
+ @TestApi
+ public int getMousePointerSpeed() {
+ try {
+ return mIm.getMousePointerSpeed();
+ } catch (RemoteException ex) {
+ throw ex.rethrowFromSystemServer();
+ }
+ }
+
+ /**
* Changes the mouse pointer speed temporarily, but does not save the setting.
* <p>
* Requires {@link android.Manifest.permission#SET_POINTER_SPEED}.
diff --git a/core/java/android/hardware/input/InputSettings.java b/core/java/android/hardware/input/InputSettings.java
index 89fa5fb..62473c5 100644
--- a/core/java/android/hardware/input/InputSettings.java
+++ b/core/java/android/hardware/input/InputSettings.java
@@ -54,8 +54,8 @@
/**
* Pointer Speed: The default pointer speed (0).
- * @hide
*/
+ @SuppressLint("UnflaggedApi") // TestApi without associated feature.
public static final int DEFAULT_POINTER_SPEED = 0;
/**
@@ -76,6 +76,12 @@
*/
public static final int MAX_ACCESSIBILITY_SLOW_KEYS_THRESHOLD_MILLIS = 5000;
+ /**
+ * Default value for {@link Settings.Secure#STYLUS_POINTER_ICON_ENABLED}.
+ * @hide
+ */
+ public static final int DEFAULT_STYLUS_POINTER_ICON_ENABLED = 1;
+
private InputSettings() {
}
@@ -383,14 +389,19 @@
}
/**
- * Whether a pointer icon will be shown over the location of a
- * stylus pointer.
+ * Whether a pointer icon will be shown over the location of a stylus pointer.
+ *
* @hide
*/
public static boolean isStylusPointerIconEnabled(@NonNull Context context) {
+ if (InputProperties.force_enable_stylus_pointer_icon().orElse(false)) {
+ return true;
+ }
return context.getResources()
- .getBoolean(com.android.internal.R.bool.config_enableStylusPointerIcon)
- || InputProperties.force_enable_stylus_pointer_icon().orElse(false);
+ .getBoolean(com.android.internal.R.bool.config_enableStylusPointerIcon)
+ && Settings.Secure.getIntForUser(context.getContentResolver(),
+ Settings.Secure.STYLUS_POINTER_ICON_ENABLED,
+ DEFAULT_STYLUS_POINTER_ICON_ENABLED, UserHandle.USER_CURRENT_OR_SELF) != 0;
}
/**
diff --git a/core/java/android/hardware/input/KeyboardLayoutPreviewDrawable.java b/core/java/android/hardware/input/KeyboardLayoutPreviewDrawable.java
index d943c37..1cc910c 100644
--- a/core/java/android/hardware/input/KeyboardLayoutPreviewDrawable.java
+++ b/core/java/android/hardware/input/KeyboardLayoutPreviewDrawable.java
@@ -219,26 +219,22 @@
if (!glyphData.hasBaseText()) {
return;
}
- if (glyphData.hasValidShiftText() && glyphData.hasValidAltGrText()) {
- mGlyphDrawables.add(new GlyphDrawable(glyphData.getBaseText(), new RectF(),
- GRAVITY_BOTTOM | GRAVITY_LEFT, mBaseTextPaint));
+ boolean isCenter = !glyphData.hasValidAltGrText() && !glyphData.hasValidAltShiftText();
+ mGlyphDrawables.add(new GlyphDrawable(glyphData.getBaseText(), new RectF(),
+ GRAVITY_BOTTOM | (isCenter ? GRAVITY_CENTER_HORIZONTAL : GRAVITY_LEFT),
+ mBaseTextPaint));
+ if (glyphData.hasValidShiftText()) {
mGlyphDrawables.add(new GlyphDrawable(glyphData.getShiftText(), new RectF(),
- GRAVITY_TOP | GRAVITY_LEFT, mModifierTextPaint));
+ GRAVITY_TOP | (isCenter ? GRAVITY_CENTER_HORIZONTAL : GRAVITY_LEFT),
+ mModifierTextPaint));
+ }
+ if (glyphData.hasValidAltGrText()) {
mGlyphDrawables.add(new GlyphDrawable(glyphData.getAltGrText(), new RectF(),
GRAVITY_BOTTOM | GRAVITY_RIGHT, mModifierTextPaint));
- } else if (glyphData.hasValidShiftText()) {
- mGlyphDrawables.add(new GlyphDrawable(glyphData.getBaseText(), new RectF(),
- GRAVITY_BOTTOM | GRAVITY_CENTER_HORIZONTAL, mBaseTextPaint));
- mGlyphDrawables.add(new GlyphDrawable(glyphData.getShiftText(), new RectF(),
- GRAVITY_TOP | GRAVITY_CENTER_HORIZONTAL, mModifierTextPaint));
- } else if (glyphData.hasValidAltGrText()) {
- mGlyphDrawables.add(new GlyphDrawable(glyphData.getBaseText(), new RectF(),
- GRAVITY_BOTTOM | GRAVITY_LEFT, mBaseTextPaint));
- mGlyphDrawables.add(new GlyphDrawable(glyphData.getAltGrText(), new RectF(),
- GRAVITY_BOTTOM | GRAVITY_RIGHT, mModifierTextPaint));
- } else {
- mGlyphDrawables.add(new GlyphDrawable(glyphData.getBaseText(), new RectF(),
- GRAVITY_CENTER, mBaseTextPaint));
+ }
+ if (glyphData.hasValidAltShiftText()) {
+ mGlyphDrawables.add(new GlyphDrawable(glyphData.getAltGrShiftText(), new RectF(),
+ GRAVITY_TOP | GRAVITY_RIGHT, mModifierTextPaint));
}
}
diff --git a/core/java/android/hardware/input/PhysicalKeyLayout.java b/core/java/android/hardware/input/PhysicalKeyLayout.java
index 241c452..844e02f 100644
--- a/core/java/android/hardware/input/PhysicalKeyLayout.java
+++ b/core/java/android/hardware/input/PhysicalKeyLayout.java
@@ -23,8 +23,6 @@
import android.view.KeyCharacterMap;
import android.view.KeyEvent;
-import java.util.Locale;
-
/**
* A complimentary class to {@link KeyboardLayoutPreviewDrawable} describing the physical key layout
* of a Physical keyboard and provides information regarding the scan codes produced by the physical
@@ -339,9 +337,9 @@
}
int utf8Char = (kcm.get(keyCode, modifierState) & KeyCharacterMap.COMBINING_ACCENT_MASK);
if (Character.isValidCodePoint(utf8Char)) {
- return String.valueOf(Character.toChars(utf8Char)).toUpperCase(Locale.getDefault());
+ return String.valueOf(Character.toChars(utf8Char));
} else {
- return String.valueOf(kcm.getDisplayLabel(keyCode)).toUpperCase(Locale.getDefault());
+ return String.valueOf(kcm.getDisplayLabel(keyCode));
}
}
@@ -398,13 +396,17 @@
private final String mBaseText;
private final String mShiftText;
private final String mAltGrText;
+ private final String mAltGrShiftText;
public KeyGlyph(KeyCharacterMap kcm, int keyCode) {
- mBaseText = getKeyText(kcm, keyCode, 0);
+ mBaseText = getKeyText(kcm, keyCode, KeyEvent.META_CAPS_LOCK_ON);
mShiftText = getKeyText(kcm, keyCode,
KeyEvent.META_SHIFT_ON | KeyEvent.META_SHIFT_LEFT_ON);
mAltGrText = getKeyText(kcm, keyCode,
- KeyEvent.META_ALT_ON | KeyEvent.META_ALT_RIGHT_ON);
+ KeyEvent.META_ALT_ON | KeyEvent.META_ALT_RIGHT_ON | KeyEvent.META_CAPS_LOCK_ON);
+ mAltGrShiftText = getKeyText(kcm, keyCode,
+ KeyEvent.META_ALT_ON | KeyEvent.META_ALT_RIGHT_ON | KeyEvent.META_SHIFT_LEFT_ON
+ | KeyEvent.META_SHIFT_ON);
}
public String getBaseText() {
@@ -419,6 +421,10 @@
return mAltGrText;
}
+ public String getAltGrShiftText() {
+ return mAltGrShiftText;
+ }
+
public boolean hasBaseText() {
return !TextUtils.isEmpty(mBaseText);
}
@@ -430,5 +436,12 @@
public boolean hasValidAltGrText() {
return !TextUtils.isEmpty(mAltGrText) && !TextUtils.equals(mBaseText, mAltGrText);
}
+
+ public boolean hasValidAltShiftText() {
+ return !TextUtils.isEmpty(mAltGrShiftText)
+ && !TextUtils.equals(mBaseText, mAltGrShiftText)
+ && !TextUtils.equals(mAltGrText, mAltGrShiftText)
+ && !TextUtils.equals(mShiftText, mAltGrShiftText);
+ }
}
}
diff --git a/core/java/android/hardware/radio/OWNERS b/core/java/android/hardware/radio/OWNERS
index 302fdd7..51a85e4 100644
--- a/core/java/android/hardware/radio/OWNERS
+++ b/core/java/android/hardware/radio/OWNERS
@@ -1,4 +1,3 @@
xuweilin@google.com
oscarazu@google.com
ericjeong@google.com
-keunyoung@google.com
diff --git a/core/java/android/hardware/radio/ProgramSelector.java b/core/java/android/hardware/radio/ProgramSelector.java
index 4c95e02..a968c6f 100644
--- a/core/java/android/hardware/radio/ProgramSelector.java
+++ b/core/java/android/hardware/radio/ProgramSelector.java
@@ -62,55 +62,55 @@
@SystemApi
public final class ProgramSelector implements Parcelable {
/** Invalid program type.
- * @deprecated use {@link ProgramIdentifier} instead
+ * @deprecated use {@link IdentifierType} instead
*/
@Deprecated
public static final int PROGRAM_TYPE_INVALID = 0;
/** Analog AM radio (with or without RDS).
- * @deprecated use {@link ProgramIdentifier} instead
+ * @deprecated use {@link IdentifierType} instead
*/
@Deprecated
public static final int PROGRAM_TYPE_AM = 1;
/** analog FM radio (with or without RDS).
- * @deprecated use {@link ProgramIdentifier} instead
+ * @deprecated use {@link IdentifierType} instead
*/
@Deprecated
public static final int PROGRAM_TYPE_FM = 2;
/** AM HD Radio.
- * @deprecated use {@link ProgramIdentifier} instead
+ * @deprecated use {@link Identifier} instead
*/
@Deprecated
public static final int PROGRAM_TYPE_AM_HD = 3;
/** FM HD Radio.
- * @deprecated use {@link ProgramIdentifier} instead
+ * @deprecated use {@link Identifier} instead
*/
@Deprecated
public static final int PROGRAM_TYPE_FM_HD = 4;
/** Digital audio broadcasting.
- * @deprecated use {@link ProgramIdentifier} instead
+ * @deprecated use {@link Identifier} instead
*/
@Deprecated
public static final int PROGRAM_TYPE_DAB = 5;
/** Digital Radio Mondiale.
- * @deprecated use {@link ProgramIdentifier} instead
+ * @deprecated use {@link Identifier} instead
*/
@Deprecated
public static final int PROGRAM_TYPE_DRMO = 6;
/** SiriusXM Satellite Radio.
- * @deprecated use {@link ProgramIdentifier} instead
+ * @deprecated use {@link Identifier} instead
*/
@Deprecated
public static final int PROGRAM_TYPE_SXM = 7;
/** Vendor-specific, not synced across devices.
- * @deprecated use {@link ProgramIdentifier} instead
+ * @deprecated use {@link Identifier} instead
*/
@Deprecated
public static final int PROGRAM_TYPE_VENDOR_START = 1000;
- /** @deprecated use {@link ProgramIdentifier} instead */
+ /** @deprecated use {@link Identifier} instead */
@Deprecated
public static final int PROGRAM_TYPE_VENDOR_END = 1999;
/**
- * @deprecated use {@link ProgramIdentifier} instead
+ * @deprecated use {@link Identifier} instead
* @removed mistakenly exposed previously
*/
@Deprecated
@@ -271,7 +271,7 @@
*/
public static final int IDENTIFIER_TYPE_HD_STATION_NAME = 10004;
/**
- * @see {@link IDENTIFIER_TYPE_DAB_SID_EXT}
+ * @see #IDENTIFIER_TYPE_DAB_SID_EXT
*
* @deprecated use {@link #IDENTIFIER_TYPE_DAB_DMB_SID_EXT} instead
*/
@@ -381,7 +381,7 @@
*/
public static final int IDENTIFIER_TYPE_VENDOR_START = PROGRAM_TYPE_VENDOR_START;
/**
- * @see {@link IDENTIFIER_TYPE_VENDOR_START}
+ * @see #IDENTIFIER_TYPE_VENDOR_START
*/
public static final int IDENTIFIER_TYPE_VENDOR_END = PROGRAM_TYPE_VENDOR_END;
/**
@@ -771,7 +771,7 @@
* Returns whether this Identifier's type is considered a category when filtering
* ProgramLists for category entries.
*
- * @see {@link ProgramList.Filter#areCategoriesIncluded()}
+ * @see ProgramList.Filter#areCategoriesIncluded
* @return False if this identifier's type is not tuneable (e.g. DAB ensemble or
* vendor-specified type). True otherwise.
*/
diff --git a/core/java/android/hardware/radio/RadioManager.java b/core/java/android/hardware/radio/RadioManager.java
index 41f21ef..61cf8901 100644
--- a/core/java/android/hardware/radio/RadioManager.java
+++ b/core/java/android/hardware/radio/RadioManager.java
@@ -311,7 +311,7 @@
}
/** Unique module identifier provided by the native service.
- * For use with {@link #openTuner(int, BandConfig, boolean, Callback, Handler)}.
+ * For use with {@link #openTuner(int, BandConfig, boolean, RadioTuner.Callback, Handler)}.
* @return the radio module unique identifier.
*/
public int getId() {
@@ -1561,7 +1561,7 @@
/** Main channel expressed in units according to band type.
* Currently all defined band types express channels as frequency in kHz
* @return the program channel
- * @deprecated Use {@link getSelector()} instead.
+ * @deprecated Use {@link ProgramInfo#getSelector} instead.
*/
@Deprecated
public int getChannel() {
@@ -1575,7 +1575,7 @@
/** Sub channel ID. E.g 1 for HD radio HD1
* @return the program sub channel
- * @deprecated Use {@link getSelector()} instead.
+ * @deprecated Use {@link ProgramInfo#getSelector} instead.
*/
@Deprecated
public int getSubChannel() {
@@ -1604,7 +1604,7 @@
/** {@code true} if the received program is digital (e.g HD radio)
* @return {@code true} if digital, {@code false} otherwise.
- * @deprecated Use {@link getLogicallyTunedTo()} instead.
+ * @deprecated Use {@link ProgramInfo#getLogicallyTunedTo()} instead.
*/
@Deprecated
public boolean isDigital() {
@@ -1913,7 +1913,8 @@
* Removes previously registered announcement listener.
*
* @param listener announcement listener, previously registered with
- * {@link addAnnouncementListener}
+ * {@link #addAnnouncementListener(Executor, Set, Announcement.OnListUpdatedListener)}
+ * or {@link #addAnnouncementListener(Set, Announcement.OnListUpdatedListener)}
*/
@RequiresPermission(Manifest.permission.ACCESS_BROADCAST_RADIO)
public void removeAnnouncementListener(@NonNull Announcement.OnListUpdatedListener listener) {
diff --git a/core/java/android/hardware/radio/RadioMetadata.java b/core/java/android/hardware/radio/RadioMetadata.java
index da6b9c2..67381ec 100644
--- a/core/java/android/hardware/radio/RadioMetadata.java
+++ b/core/java/android/hardware/radio/RadioMetadata.java
@@ -593,7 +593,7 @@
* Helper for getting the String key used by {@link RadioMetadata} from the
* corrsponding native integer key.
*
- * @param editorKey The key used by the editor
+ * @param nativeKey The key used by the editor
* @return the key used by this class or null if no mapping exists
* @hide
*/
@@ -743,11 +743,11 @@
* Put a {@link RadioMetadata.Clock} into the meta data. Custom keys may be used, but if the
* METADATA_KEYs defined in this class are used they may only be one of the following:
* <ul>
- * <li>{@link #MEADATA_KEY_CLOCK}</li>
+ * <li>{@link #METADATA_KEY_CLOCK}</li>
* </ul>
*
* @param utcSecondsSinceEpoch Number of seconds since epoch for UTC + 0 timezone.
- * @param timezoneOffsetInMinutes Offset of timezone from UTC + 0 in minutes.
+ * @param timezoneOffsetMinutes Offset of timezone from UTC + 0 in minutes.
* @return the same Builder instance.
*/
public Builder putClock(String key, long utcSecondsSinceEpoch, int timezoneOffsetMinutes) {
diff --git a/core/java/android/inputmethodservice/InputMethodService.java b/core/java/android/inputmethodservice/InputMethodService.java
index 71698e4..281ee50 100644
--- a/core/java/android/inputmethodservice/InputMethodService.java
+++ b/core/java/android/inputmethodservice/InputMethodService.java
@@ -1256,7 +1256,9 @@
private void updateEditorToolTypeInternal(int toolType) {
if (Flags.useHandwritingListenerForTooltype()) {
mLastUsedToolType = toolType;
- mInputEditorInfo.setInitialToolType(toolType);
+ if (mInputEditorInfo != null) {
+ mInputEditorInfo.setInitialToolType(toolType);
+ }
}
onUpdateEditorToolType(toolType);
}
diff --git a/core/java/android/os/ArtModuleServiceManager.java b/core/java/android/os/ArtModuleServiceManager.java
index 0009e61..e0b631d 100644
--- a/core/java/android/os/ArtModuleServiceManager.java
+++ b/core/java/android/os/ArtModuleServiceManager.java
@@ -15,9 +15,11 @@
*/
package android.os;
+import android.annotation.FlaggedApi;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.SystemApi;
+import android.content.pm.Flags;
/**
* Provides a way to register and obtain the system service binder objects managed by the ART
@@ -60,4 +62,18 @@
public ServiceRegisterer getArtdServiceRegisterer() {
return new ServiceRegisterer("artd");
}
+
+ /** Returns {@link ServiceRegisterer} for the "artd_pre_reboot" service. */
+ @NonNull
+ @FlaggedApi(Flags.FLAG_USE_ART_SERVICE_V2)
+ public ServiceRegisterer getArtdPreRebootServiceRegisterer() {
+ return new ServiceRegisterer("artd_pre_reboot");
+ }
+
+ /** Returns {@link ServiceRegisterer} for the "dexopt_chroot_setup" service. */
+ @NonNull
+ @FlaggedApi(Flags.FLAG_USE_ART_SERVICE_V2)
+ public ServiceRegisterer getDexoptChrootSetupServiceRegisterer() {
+ return new ServiceRegisterer("dexopt_chroot_setup");
+ }
}
diff --git a/core/java/android/os/BatteryManager.java b/core/java/android/os/BatteryManager.java
index b9bb059..f3efd89 100644
--- a/core/java/android/os/BatteryManager.java
+++ b/core/java/android/os/BatteryManager.java
@@ -238,6 +238,16 @@
public static final int CHARGING_POLICY_ADAPTIVE_LONGLIFE =
OsProtoEnums.CHARGING_POLICY_ADAPTIVE_LONGLIFE; // = 4
+ /**
+ * Returns true if the policy is some type of adaptive charging policy.
+ * @hide
+ */
+ public static boolean isAdaptiveChargingPolicy(int policy) {
+ return policy == CHARGING_POLICY_ADAPTIVE_AC
+ || policy == CHARGING_POLICY_ADAPTIVE_AON
+ || policy == CHARGING_POLICY_ADAPTIVE_LONGLIFE;
+ }
+
// values for "battery part status" property
/**
* Battery part status is not supported.
diff --git a/core/java/android/os/BatteryManagerInternal.java b/core/java/android/os/BatteryManagerInternal.java
index 9bad0de..0ec8729 100644
--- a/core/java/android/os/BatteryManagerInternal.java
+++ b/core/java/android/os/BatteryManagerInternal.java
@@ -16,6 +16,8 @@
package android.os;
+import android.annotation.NonNull;
+
/**
* Battery manager local system service interface.
*
@@ -84,6 +86,26 @@
*/
public abstract boolean getBatteryLevelLow();
+ public interface ChargingPolicyChangeListener {
+ void onChargingPolicyChanged(int newPolicy);
+ }
+
+ /**
+ * Register a listener for changes to {@link BatteryManager#BATTERY_PROPERTY_CHARGING_POLICY}.
+ * The charging policy can't be added to the BATTERY_CHANGED intent because it requires
+ * the BATTERY_STATS permission.
+ */
+ public abstract void registerChargingPolicyChangeListener(
+ @NonNull ChargingPolicyChangeListener chargingPolicyChangeListener);
+
+ /**
+ * Returns the value of {@link BatteryManager#BATTERY_PROPERTY_CHARGING_POLICY}.
+ * This will return {@link Integer#MIN_VALUE} if the device does not support the property.
+ *
+ * @see BatteryManager#getIntProperty(int)
+ */
+ public abstract int getChargingPolicy();
+
/**
* Returns a non-zero value if an unsupported charger is attached.
*
diff --git a/core/java/android/os/ConfigUpdate.java b/core/java/android/os/ConfigUpdate.java
index 4908919..87cd4ee 100644
--- a/core/java/android/os/ConfigUpdate.java
+++ b/core/java/android/os/ConfigUpdate.java
@@ -16,6 +16,7 @@
package android.os;
+import android.annotation.FlaggedApi;
import android.annotation.SdkConstant;
import android.annotation.SdkConstant.SdkConstantType;
import android.annotation.SystemApi;
@@ -131,6 +132,23 @@
"android.os.action.UPDATE_EMERGENCY_NUMBER_DB";
/**
+ * Broadcast intent action indicating that the updated config data is available.
+ * This broadcast intent action is to be sent by the config updater app, and will be received
+ * and handled by the platform.
+ * <p>Extra: {@link #EXTRA_VERSION} the numeric version of the database.
+ * <p>Extra: {@link #EXTRA_REQUIRED_HASH} hash of the database, which is encoded by base-16
+ * SHA512
+ * <p>Extra: {@link #EXTRA_DOMAIN} the string identifying the affected module
+ * <p>Input: {@link android.content.Intent#getData} the URI to download config data file
+ *
+ * @hide
+ */
+ @SystemApi
+ @FlaggedApi(com.android.internal.telephony.flags.Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_UPDATE_CONFIG = "android.os.action.UPDATE_CONFIG";
+
+ /**
* An integer to indicate the numeric version of the new data. Devices should only install
* if the update version is newer than the current one.
*
@@ -147,6 +165,16 @@
@SystemApi
public static final String EXTRA_REQUIRED_HASH = "android.os.extra.REQUIRED_HASH";
+ /**
+ * String identifying the affected module.
+ * Devices apply the updated config data to the module specified in the string.
+ *
+ * @hide
+ */
+ @SystemApi
+ @FlaggedApi(com.android.internal.telephony.flags.Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
+ public static final String EXTRA_DOMAIN = "android.os.extra.DOMAIN";
+
private ConfigUpdate() {
}
}
diff --git a/core/java/android/os/Environment.java b/core/java/android/os/Environment.java
index 536ef31..a459aaa 100644
--- a/core/java/android/os/Environment.java
+++ b/core/java/android/os/Environment.java
@@ -17,6 +17,7 @@
package android.os;
import android.Manifest;
+import android.annotation.FlaggedApi;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.SuppressLint;
@@ -412,7 +413,9 @@
* Returns the base directory for per-user system directory, device encrypted.
* {@hide}
*/
- public static File getDataSystemDeDirectory() {
+ @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
+ @FlaggedApi(android.crashrecovery.flags.Flags.FLAG_ENABLE_CRASHRECOVERY)
+ public static @NonNull File getDataSystemDeDirectory() {
return buildPath(getDataDirectory(), "system_de");
}
diff --git a/core/java/android/os/ISystemConfig.aidl b/core/java/android/os/ISystemConfig.aidl
index b7649ba..650aead 100644
--- a/core/java/android/os/ISystemConfig.aidl
+++ b/core/java/android/os/ISystemConfig.aidl
@@ -17,6 +17,8 @@
package android.os;
import android.content.ComponentName;
+import android.os.Bundle;
+import android.content.pm.SignedPackageParcel;
/**
* Binder interface to query SystemConfig in the system server.
@@ -57,4 +59,14 @@
* @see SystemConfigManager#getPreventUserDisablePackages
*/
List<String> getPreventUserDisablePackages();
+
+ /**
+ * @see SystemConfigManager#getEnhancedConfirmationTrustedPackages
+ */
+ List<SignedPackageParcel> getEnhancedConfirmationTrustedPackages();
+
+ /**
+ * @see SystemConfigManager#getEnhancedConfirmationTrustedInstallers
+ */
+ List<SignedPackageParcel> getEnhancedConfirmationTrustedInstallers();
}
diff --git a/core/java/android/os/IUserManager.aidl b/core/java/android/os/IUserManager.aidl
index c0d1fb9..800ba6d 100644
--- a/core/java/android/os/IUserManager.aidl
+++ b/core/java/android/os/IUserManager.aidl
@@ -150,4 +150,5 @@
void setBootUser(int userId);
@JavaPassthrough(annotation="@android.annotation.RequiresPermission(anyOf = {android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.CREATE_USERS})")
int getBootUser();
+ int[] getProfileIdsExcludingHidden(int userId, boolean enabledOnly);
}
diff --git a/core/java/android/os/PerformanceHintManager.java b/core/java/android/os/PerformanceHintManager.java
index e6bfcd7..224b10d 100644
--- a/core/java/android/os/PerformanceHintManager.java
+++ b/core/java/android/os/PerformanceHintManager.java
@@ -313,25 +313,33 @@
* close to the target duration.
*
* @param workDuration the work duration of each component.
- * @throws IllegalArgumentException if work period start timestamp is not positive, or
- * actual total duration is not positive, or actual CPU duration is not positive,
- * or actual GPU duration is negative.
+ * @throws IllegalArgumentException if
+ * the work period start timestamp or the total duration are less than or equal to zero,
+ * if either the actual CPU duration or actual GPU duration is less than zero,
+ * or if both the CPU and GPU durations are zero.
*/
@FlaggedApi(Flags.FLAG_ADPF_GPU_REPORT_ACTUAL_WORK_DURATION)
public void reportActualWorkDuration(@NonNull WorkDuration workDuration) {
if (workDuration.mWorkPeriodStartTimestampNanos <= 0) {
throw new IllegalArgumentException(
- "the work period start timestamp should be positive.");
+ "the work period start timestamp should be greater than zero.");
}
if (workDuration.mActualTotalDurationNanos <= 0) {
- throw new IllegalArgumentException("the actual total duration should be positive.");
+ throw new IllegalArgumentException(
+ "the actual total duration should be greater than zero.");
}
- if (workDuration.mActualCpuDurationNanos <= 0) {
- throw new IllegalArgumentException("the actual CPU duration should be positive.");
+ if (workDuration.mActualCpuDurationNanos < 0) {
+ throw new IllegalArgumentException(
+ "the actual CPU duration should be greater than or equal to zero.");
}
if (workDuration.mActualGpuDurationNanos < 0) {
throw new IllegalArgumentException(
- "the actual GPU duration should be non negative.");
+ "the actual GPU duration should be greater than or equal to zero.");
+ }
+ if (workDuration.mActualCpuDurationNanos + workDuration.mActualGpuDurationNanos <= 0) {
+ throw new IllegalArgumentException(
+ "either the actual CPU duration or the actual GPU duration should be greater"
+ + "than zero.");
}
nativeReportActualWorkDuration(mNativeSessionPtr,
workDuration.mWorkPeriodStartTimestampNanos,
diff --git a/core/java/android/os/SystemConfigManager.java b/core/java/android/os/SystemConfigManager.java
index 21ffbf1..13bc398 100644
--- a/core/java/android/os/SystemConfigManager.java
+++ b/core/java/android/os/SystemConfigManager.java
@@ -16,12 +16,15 @@
package android.os;
import android.Manifest;
+import android.annotation.FlaggedApi;
import android.annotation.NonNull;
import android.annotation.RequiresPermission;
import android.annotation.SystemApi;
import android.annotation.SystemService;
import android.content.ComponentName;
import android.content.Context;
+import android.content.pm.SignedPackage;
+import android.content.pm.SignedPackageParcel;
import android.util.ArraySet;
import android.util.Log;
@@ -29,6 +32,7 @@
import java.util.List;
import java.util.Map;
import java.util.Set;
+import java.util.stream.Collectors;
/**
@@ -175,4 +179,69 @@
throw e.rethrowFromSystemServer();
}
}
+
+
+ /**
+ * Returns a set of signed packages, represented as (packageName, certificateDigest) pairs, that
+ * should be considered "trusted packages" by ECM (Enhanced Confirmation Mode).
+ *
+ * <p>"Trusted packages" are exempt from ECM (i.e., they will never be considered "restricted").
+ *
+ * <p>A package will be considered "trusted package" if and only if it *matches* least one of
+ * the (*packageName*, *certificateDigest*) pairs in this set, where *matches* means satisfying
+ * both of the following:
+ *
+ * <ol>
+ * <li>The package's name equals *packageName*
+ * <li>The package is, or was ever, signed by *certificateDigest*, according to the package's
+ * {@link android.content.pm.SigningDetails}
+ * </ol>
+ *
+ * @hide
+ */
+ @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
+ @FlaggedApi(android.permission.flags.Flags.FLAG_ENHANCED_CONFIRMATION_MODE_APIS_ENABLED)
+ @RequiresPermission(Manifest.permission.MANAGE_ENHANCED_CONFIRMATION_STATES)
+ @NonNull
+ public Set<SignedPackage> getEnhancedConfirmationTrustedPackages() {
+ try {
+ List<SignedPackageParcel> parcels = mInterface.getEnhancedConfirmationTrustedPackages();
+ return parcels.stream().map(SignedPackage::new).collect(Collectors.toSet());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Returns a set of signed packages, represented as (packageName, certificateDigest) pairs, that
+ * should be considered "trusted installers" by ECM (Enhanced Confirmation Mode).
+ *
+ * <p>"Trusted installers", and all apps installed by a trusted installer, are exempt from ECM
+ * (i.e., they will never be considered "restricted").
+ *
+ * <p>A package will be considered a "trusted installer" if and only if it *matches* least one
+ * of the (*packageName*, *certificateDigest*) pairs in this set, where *matches* means
+ * satisfying both of the following:
+ *
+ * <ol>
+ * <li>The package's name equals *packageName*
+ * <li>The package is, or was ever, signed by *certificateDigest*, according to the package's
+ * {@link android.content.pm.SigningDetails}
+ * </ol>
+ *
+ * @hide
+ */
+ @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
+ @FlaggedApi(android.permission.flags.Flags.FLAG_ENHANCED_CONFIRMATION_MODE_APIS_ENABLED)
+ @RequiresPermission(Manifest.permission.MANAGE_ENHANCED_CONFIRMATION_STATES)
+ @NonNull
+ public Set<SignedPackage> getEnhancedConfirmationTrustedInstallers() {
+ try {
+ List<SignedPackageParcel> parcels =
+ mInterface.getEnhancedConfirmationTrustedInstallers();
+ return parcels.stream().map(SignedPackage::new).collect(Collectors.toSet());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
}
diff --git a/core/java/android/os/UserManager.java b/core/java/android/os/UserManager.java
index d6df8d9..0da19df 100644
--- a/core/java/android/os/UserManager.java
+++ b/core/java/android/os/UserManager.java
@@ -3438,6 +3438,7 @@
}
/**
+ * @see #isVisibleBackgroundUsersSupported()
* @hide
*/
public static boolean isVisibleBackgroundUsersEnabled() {
@@ -3447,14 +3448,21 @@
}
/**
- * Returns whether the device allows (full) users to be started in background visible in a given
+ * Returns whether the device allows full users to be started in background visible in a given
* display (which would allow them to launch activities in that display).
*
- * @return {@code false} for most devices, except on automotive builds for vehiches with
+ * Note that this is specifically about allowing <b>full</b> users to be background visible.
+ * Even if it is false, there can still be background visible users.
+ *
+ * In particular, the Communal Profile is a background visible user, and it can be supported
+ * unrelated to the value of this method.
+ *
+ * @return {@code false} for most devices, except on automotive builds for vehicles with
* passenger displays.
*
* @hide
*/
+ // TODO(b/310249114): Rename to isVisibleBackgroundFullUsersSupported
@TestApi
public boolean isVisibleBackgroundUsersSupported() {
return isVisibleBackgroundUsersEnabled();
@@ -3470,12 +3478,13 @@
}
/**
- * Returns whether the device allows (full) users to be started in background visible in the
+ * Returns whether the device allows full users to be started in background visible in the
* {@link android.view.Display#DEFAULT_DISPLAY default display}.
*
* @return {@code false} for most devices, except passenger-only automotive build (i.e., when
* Android runs in a separate system in the back seat to manage the passenger displays).
*
+ * @see #isVisibleBackgroundUsersSupported()
* @hide
*/
@TestApi
@@ -5348,6 +5357,25 @@
}
/**
+ * @return A list of ids of profiles associated with the specified user excluding those with
+ * {@link UserProperties#getProfileApiVisibility()} set to hidden. The returned list includes
+ * the user itself.
+ * @hide
+ * @see #getProfileIds(int, boolean)
+ */
+ @RequiresPermission(anyOf = {
+ Manifest.permission.MANAGE_USERS,
+ Manifest.permission.CREATE_USERS,
+ Manifest.permission.QUERY_USERS}, conditional = true)
+ public int[] getProfileIdsExcludingHidden(@UserIdInt int userId, boolean enabled) {
+ try {
+ return mService.getProfileIdsExcludingHidden(userId, enabled);
+ } catch (RemoteException re) {
+ throw re.rethrowFromSystemServer();
+ }
+ }
+
+ /**
* Returns the device credential owner id of the profile from
* which this method is called, or userId if called from a user that
* is not a profile.
diff --git a/core/java/android/os/VibrationAttributes.java b/core/java/android/os/VibrationAttributes.java
index 5078dc35..46705a3 100644
--- a/core/java/android/os/VibrationAttributes.java
+++ b/core/java/android/os/VibrationAttributes.java
@@ -29,6 +29,7 @@
/**
* Encapsulates a collection of attributes describing information about a vibration.
*/
+@android.ravenwood.annotation.RavenwoodKeepWholeClass
public final class VibrationAttributes implements Parcelable {
private static final String TAG = "VibrationAttributes";
@@ -463,6 +464,7 @@
* Builder class for {@link VibrationAttributes} objects.
* By default, all information is set to UNKNOWN.
*/
+ @android.ravenwood.annotation.RavenwoodKeepWholeClass
public static final class Builder {
private int mUsage = USAGE_UNKNOWN;
private int mOriginalAudioUsage = AudioAttributes.USAGE_UNKNOWN;
diff --git a/core/java/android/os/VibrationEffect.java b/core/java/android/os/VibrationEffect.java
index 08b32bf..c9c91fc 100644
--- a/core/java/android/os/VibrationEffect.java
+++ b/core/java/android/os/VibrationEffect.java
@@ -33,7 +33,6 @@
import android.os.vibrator.RampSegment;
import android.os.vibrator.StepSegment;
import android.os.vibrator.VibrationEffectSegment;
-import android.util.Log;
import android.util.MathUtils;
import com.android.internal.util.Preconditions;
@@ -54,7 +53,6 @@
* <p>These effects may be any number of things, from single shot vibrations to complex waveforms.
*/
public abstract class VibrationEffect implements Parcelable {
- private static final String TAG = "VibrationEffect";
// Stevens' coefficient to scale the perceived vibration intensity.
private static final float SCALE_GAMMA = 0.65f;
// If a vibration is playing for longer than 1s, it's probably not haptic feedback
@@ -397,32 +395,26 @@
return null;
}
- try {
- final ContentResolver cr = context.getContentResolver();
- Uri uncanonicalUri = cr.uncanonicalize(uri);
- if (uncanonicalUri == null) {
- // If we already had an uncanonical URI, it's possible we'll get null back here. In
- // this case, just use the URI as passed in since it wasn't canonicalized in the
- // first place.
- uncanonicalUri = uri;
- }
+ final ContentResolver cr = context.getContentResolver();
+ Uri uncanonicalUri = cr.uncanonicalize(uri);
+ if (uncanonicalUri == null) {
+ // If we already had an uncanonical URI, it's possible we'll get null back here. In
+ // this case, just use the URI as passed in since it wasn't canonicalized in the first
+ // place.
+ uncanonicalUri = uri;
+ }
- for (int i = 0; i < uris.length && i < RINGTONES.length; i++) {
- if (uris[i] == null) {
- continue;
- }
- Uri mappedUri = cr.uncanonicalize(Uri.parse(uris[i]));
- if (mappedUri == null) {
- continue;
- }
- if (mappedUri.equals(uncanonicalUri)) {
- return get(RINGTONES[i]);
- }
+ for (int i = 0; i < uris.length && i < RINGTONES.length; i++) {
+ if (uris[i] == null) {
+ continue;
}
- } catch (Exception e) {
- // Don't give unexpected exceptions to callers if the Uri's ContentProvider is
- // misbehaving - it's very unlikely to be mapped in that case anyway.
- Log.e(TAG, "Exception getting default vibration for Uri " + uri, e);
+ Uri mappedUri = cr.uncanonicalize(Uri.parse(uris[i]));
+ if (mappedUri == null) {
+ continue;
+ }
+ if (mappedUri.equals(uncanonicalUri)) {
+ return get(RINGTONES[i]);
+ }
}
return null;
}
diff --git a/core/java/android/os/WorkDuration.java b/core/java/android/os/WorkDuration.java
index 2ebcd83..5a54e90 100644
--- a/core/java/android/os/WorkDuration.java
+++ b/core/java/android/os/WorkDuration.java
@@ -83,7 +83,7 @@
public void setWorkPeriodStartTimestampNanos(long workPeriodStartTimestampNanos) {
if (workPeriodStartTimestampNanos <= 0) {
throw new IllegalArgumentException(
- "the work period start timestamp should be positive.");
+ "the work period start timestamp should be greater than zero.");
}
mWorkPeriodStartTimestampNanos = workPeriodStartTimestampNanos;
}
@@ -95,7 +95,8 @@
*/
public void setActualTotalDurationNanos(long actualTotalDurationNanos) {
if (actualTotalDurationNanos <= 0) {
- throw new IllegalArgumentException("the actual total duration should be positive.");
+ throw new IllegalArgumentException(
+ "the actual total duration should be greater than zero.");
}
mActualTotalDurationNanos = actualTotalDurationNanos;
}
@@ -106,8 +107,9 @@
* All timings should be in {@link SystemClock#uptimeNanos()}.
*/
public void setActualCpuDurationNanos(long actualCpuDurationNanos) {
- if (actualCpuDurationNanos <= 0) {
- throw new IllegalArgumentException("the actual CPU duration should be positive.");
+ if (actualCpuDurationNanos < 0) {
+ throw new IllegalArgumentException(
+ "the actual CPU duration should be greater than or equal to zero.");
}
mActualCpuDurationNanos = actualCpuDurationNanos;
}
@@ -119,7 +121,8 @@
*/
public void setActualGpuDurationNanos(long actualGpuDurationNanos) {
if (actualGpuDurationNanos < 0) {
- throw new IllegalArgumentException("the actual GPU duration should be non negative.");
+ throw new IllegalArgumentException(
+ "the actual GPU duration should be greater than or equal to zero.");
}
mActualGpuDurationNanos = actualGpuDurationNanos;
}
diff --git a/core/java/android/os/ZygoteProcess.java b/core/java/android/os/ZygoteProcess.java
index f3496e7..b1ef05a 100644
--- a/core/java/android/os/ZygoteProcess.java
+++ b/core/java/android/os/ZygoteProcess.java
@@ -71,7 +71,7 @@
*/
public class ZygoteProcess {
- private static final int ZYGOTE_CONNECT_TIMEOUT_MS = 20000;
+ private static final int ZYGOTE_CONNECT_TIMEOUT_MS = 60000;
/**
* Use a relatively short delay, because for app zygote, this is in the critical path of
diff --git a/core/java/android/os/flags.aconfig b/core/java/android/os/flags.aconfig
index 82518bf..abfa4e3 100644
--- a/core/java/android/os/flags.aconfig
+++ b/core/java/android/os/flags.aconfig
@@ -114,3 +114,19 @@
is_fixed_read_only: true
bug: "309792384"
}
+
+flag {
+ name: "storage_lifetime_api"
+ namespace: "phoenix"
+ description: "Feature flag for adding storage component health APIs."
+ is_fixed_read_only: true
+ bug: "309792384"
+}
+
+flag {
+ namespace: "system_performance"
+ name: "telemetry_apis_framework_initialization"
+ description: "Control framework initialization APIs of telemetry APIs feature."
+ is_fixed_read_only: true
+ bug: "324241334"
+}
diff --git a/core/java/android/os/storage/IStorageManager.aidl b/core/java/android/os/storage/IStorageManager.aidl
index 54ed73c..1ab48a2 100644
--- a/core/java/android/os/storage/IStorageManager.aidl
+++ b/core/java/android/os/storage/IStorageManager.aidl
@@ -175,4 +175,12 @@
void setCloudMediaProvider(in String authority) = 96;
String getCloudMediaProvider() = 97;
long getInternalStorageBlockDeviceSize() = 98;
-}
\ No newline at end of file
+ /**
+ * Returns the remaining lifetime of the internal storage device, as an
+ * integer percentage. For example, 90 indicates that 90% of the storage
+ * device's useful lifetime remains. If no information is available, -1
+ * is returned.
+ */
+ @EnforcePermission("READ_PRIVILEGED_PHONE_STATE")
+ int getInternalStorageRemainingLifetime() = 99;
+}
diff --git a/core/java/android/os/storage/StorageManager.java b/core/java/android/os/storage/StorageManager.java
index 3a57e84..5a09541 100644
--- a/core/java/android/os/storage/StorageManager.java
+++ b/core/java/android/os/storage/StorageManager.java
@@ -28,6 +28,7 @@
import android.annotation.BytesLong;
import android.annotation.CallbackExecutor;
+import android.annotation.FlaggedApi;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
@@ -58,6 +59,7 @@
import android.os.Build;
import android.os.Environment;
import android.os.FileUtils;
+import android.os.Flags;
import android.os.Handler;
import android.os.IInstalld;
import android.os.IVold;
@@ -2939,4 +2941,24 @@
/** @hide */
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
public static final int CRYPT_TYPE_DEFAULT = 1;
+
+ /**
+ * Returns the remaining lifetime of the internal storage device, as an integer percentage. For
+ * example, 90 indicates that 90% of the storage device's useful lifetime remains. If no
+ * information is available, -1 is returned.
+ *
+ * @return Percentage of the remaining useful lifetime of the internal storage device.
+ *
+ * @hide
+ */
+ @FlaggedApi(Flags.FLAG_STORAGE_LIFETIME_API)
+ @SystemApi
+ @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE)
+ public int getInternalStorageRemainingLifetime() {
+ try {
+ return mStorageManager.getInternalStorageRemainingLifetime();
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
}
diff --git a/core/java/android/os/vibrator/flags.aconfig b/core/java/android/os/vibrator/flags.aconfig
index 437668c..ea9375e 100644
--- a/core/java/android/os/vibrator/flags.aconfig
+++ b/core/java/android/os/vibrator/flags.aconfig
@@ -16,13 +16,6 @@
flag {
namespace: "haptics"
- name: "haptics_customization_ringtone_v2_enabled"
- description: "Enables the usage of the new RingtoneV2 class"
- bug: "241918098"
-}
-
-flag {
- namespace: "haptics"
name: "enable_vibration_serialization_apis"
description: "Enables the APIs for vibration serialization/deserialization."
bug: "245129509"
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index 524b733..ce7a026 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -6015,8 +6015,10 @@
* +7 = fastest
* @hide
*/
+ @SuppressLint({"NoSettingsProvider", "UnflaggedApi"}) // TestApi without associated feature.
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
@Readable
+ @TestApi
public static final String POINTER_SPEED = "pointer_speed";
/**
@@ -12310,6 +12312,16 @@
public static final String PRIVATE_SPACE_AUTO_LOCK = "private_space_auto_lock";
/**
+ * Toggle for enabling stylus pointer icon. Pointer icons for styluses will only be be shown
+ * when this is enabled. Enabling this alone won't enable the stylus pointer;
+ * config_enableStylusPointerIcon needs to be true as well.
+ *
+ * @hide
+ */
+ @Readable
+ public static final String STYLUS_POINTER_ICON_ENABLED = "stylus_pointer_icon_enabled";
+
+ /**
* These entries are considered common between the personal and the managed profile,
* since the managed profile doesn't get to change them.
*/
@@ -18159,6 +18171,16 @@
"show_notification_channel_warnings";
/**
+ * Whether to disable app and notification screen share protections.
+ *
+ * The value 1 - enable, 0 - disable
+ * @hide
+ */
+ @Readable
+ public static final String DISABLE_SCREEN_SHARE_PROTECTIONS_FOR_APPS_AND_NOTIFICATIONS =
+ "disable_screen_share_protections_for_apps_and_notifications";
+
+ /**
* Whether cell is enabled/disabled
* @hide
*/
diff --git a/core/java/android/provider/Telephony.java b/core/java/android/provider/Telephony.java
index 2841dc0..658cec8 100644
--- a/core/java/android/provider/Telephony.java
+++ b/core/java/android/provider/Telephony.java
@@ -4943,6 +4943,13 @@
public static final String COLUMN_IS_NTN = "is_ntn";
/**
+ * TelephonyProvider column name for transferred status
+ *
+ * @hide
+ */
+ public static final String COLUMN_TRANSFER_STATUS = "transfer_status";
+
+ /**
* TelephonyProvider column name to indicate the service capability bitmasks.
*
* @hide
@@ -5021,7 +5028,8 @@
COLUMN_SATELLITE_ENABLED,
COLUMN_SATELLITE_ATTACH_ENABLED_FOR_CARRIER,
COLUMN_IS_NTN,
- COLUMN_SERVICE_CAPABILITIES
+ COLUMN_SERVICE_CAPABILITIES,
+ COLUMN_TRANSFER_STATUS
);
/**
diff --git a/core/java/android/service/autofill/AutofillService.java b/core/java/android/service/autofill/AutofillService.java
index 298bdb8..e6a84df 100644
--- a/core/java/android/service/autofill/AutofillService.java
+++ b/core/java/android/service/autofill/AutofillService.java
@@ -600,6 +600,14 @@
*/
public static final String EXTRA_ERROR = "error";
+ /**
+ * Name of the key used to mark whether the fill response is for a webview.
+ *
+ * @hide
+ */
+ public static final String WEBVIEW_REQUESTED_CREDENTIAL_KEY = "webview_requested_credential";
+
+
private final IAutoFillService mInterface = new IAutoFillService.Stub() {
@Override
public void onConnectedStateChanged(boolean connected) {
diff --git a/core/java/android/service/autofill/Dataset.java b/core/java/android/service/autofill/Dataset.java
index 1afe8d9..da8817a 100644
--- a/core/java/android/service/autofill/Dataset.java
+++ b/core/java/android/service/autofill/Dataset.java
@@ -26,8 +26,8 @@
import android.annotation.SystemApi;
import android.annotation.TestApi;
import android.content.ClipData;
+import android.content.Intent;
import android.content.IntentSender;
-import android.os.Bundle;
import android.os.Parcel;
import android.os.Parcelable;
import android.util.ArrayMap;
@@ -190,7 +190,7 @@
@Nullable private final InlinePresentation mInlineTooltipPresentation;
private final IntentSender mAuthentication;
- @Nullable private final Bundle mAuthenticationExtras;
+ @Nullable private Intent mCredentialFillInIntent;
@Nullable String mId;
@@ -229,7 +229,7 @@
mInlinePresentation = inlinePresentation;
mInlineTooltipPresentation = inlineTooltipPresentation;
mAuthentication = authentication;
- mAuthenticationExtras = null;
+ mCredentialFillInIntent = null;
mId = id;
}
@@ -252,7 +252,7 @@
mInlinePresentation = dataset.mInlinePresentation;
mInlineTooltipPresentation = dataset.mInlineTooltipPresentation;
mAuthentication = dataset.mAuthentication;
- mAuthenticationExtras = dataset.mAuthenticationExtras;
+ mCredentialFillInIntent = dataset.mCredentialFillInIntent;
mId = dataset.mId;
mAutofillDatatypes = dataset.mAutofillDatatypes;
}
@@ -271,7 +271,7 @@
mInlinePresentation = builder.mInlinePresentation;
mInlineTooltipPresentation = builder.mInlineTooltipPresentation;
mAuthentication = builder.mAuthentication;
- mAuthenticationExtras = builder.mAuthenticationExtras;
+ mCredentialFillInIntent = builder.mCredentialFillInIntent;
mId = builder.mId;
mAutofillDatatypes = builder.mAutofillDatatypes;
}
@@ -354,8 +354,14 @@
/** @hide */
@Hide
- public @Nullable Bundle getAuthenticationExtras() {
- return mAuthenticationExtras;
+ public @Nullable Intent getCredentialFillInIntent() {
+ return mCredentialFillInIntent;
+ }
+
+ /** @hide */
+ @Hide
+ public void setCredentialFillInIntent(Intent intent) {
+ mCredentialFillInIntent = intent;
}
/** @hide */
@@ -415,7 +421,7 @@
if (mAuthentication != null) {
builder.append(", hasAuthentication");
}
- if (mAuthenticationExtras != null) {
+ if (mCredentialFillInIntent != null) {
builder.append(", hasAuthenticationExtras");
}
if (mAutofillDatatypes != null) {
@@ -472,7 +478,7 @@
@Nullable private InlinePresentation mInlineTooltipPresentation;
private IntentSender mAuthentication;
- private Bundle mAuthenticationExtras;
+ private Intent mCredentialFillInIntent;
private boolean mDestroyed;
@Nullable private String mId;
@@ -655,9 +661,9 @@
* @hide
*/
@Hide
- public @NonNull Builder setAuthenticationExtras(@Nullable Bundle authenticationExtra) {
+ public @NonNull Builder setCredentialFillInIntent(@Nullable Intent credentialFillInIntent) {
throwIfDestroyed();
- mAuthenticationExtras = authenticationExtra;
+ mCredentialFillInIntent = credentialFillInIntent;
return this;
}
@@ -1401,7 +1407,7 @@
parcel.writeParcelable(mAuthentication, flags);
parcel.writeString(mId);
parcel.writeInt(mEligibleReason);
- parcel.writeTypedObject(mAuthenticationExtras, flags);
+ parcel.writeTypedObject(mCredentialFillInIntent, flags);
}
public static final @NonNull Creator<Dataset> CREATOR = new Creator<Dataset>() {
@@ -1437,7 +1443,7 @@
android.content.IntentSender.class);
final String datasetId = parcel.readString();
final int eligibleReason = parcel.readInt();
- final Bundle authenticationExtras = parcel.readTypedObject(Bundle.CREATOR);
+ final Intent credentialFillInIntent = parcel.readTypedObject(Intent.CREATOR);
// Always go through the builder to ensure the data ingested by
// the system obeys the contract of the builder to avoid attacks
@@ -1482,7 +1488,7 @@
fieldDialogPresentation);
}
builder.setAuthentication(authentication);
- builder.setAuthenticationExtras(authenticationExtras);
+ builder.setCredentialFillInIntent(credentialFillInIntent);
builder.setId(datasetId);
Dataset dataset = builder.build();
dataset.mEligibleReason = eligibleReason;
diff --git a/core/java/android/service/autofill/FillResponse.java b/core/java/android/service/autofill/FillResponse.java
index 09ec933..c43ba6c 100644
--- a/core/java/android/service/autofill/FillResponse.java
+++ b/core/java/android/service/autofill/FillResponse.java
@@ -56,6 +56,7 @@
* <p>See the main {@link AutofillService} documentation for more details and examples.
*/
public final class FillResponse implements Parcelable {
+ // common_typos_disable
/**
* Flag used to generate {@link FillEventHistory.Event events} of type
@@ -82,11 +83,17 @@
*/
public static final int FLAG_DELAY_FILL = 0x4;
+ /**
+ * @hide
+ */
+ public static final int FLAG_CREDENTIAL_MANAGER_RESPONSE = 0x8;
+
/** @hide */
@IntDef(flag = true, prefix = { "FLAG_" }, value = {
FLAG_TRACK_CONTEXT_COMMITED,
FLAG_DISABLE_ACTIVITY_ONLY,
- FLAG_DELAY_FILL
+ FLAG_DELAY_FILL,
+ FLAG_CREDENTIAL_MANAGER_RESPONSE
})
@Retention(RetentionPolicy.SOURCE)
@interface FillResponseFlags {}
@@ -834,7 +841,9 @@
public Builder setFlags(@FillResponseFlags int flags) {
throwIfDestroyed();
mFlags = Preconditions.checkFlagsArgument(flags,
- FLAG_TRACK_CONTEXT_COMMITED | FLAG_DISABLE_ACTIVITY_ONLY | FLAG_DELAY_FILL);
+ FLAG_TRACK_CONTEXT_COMMITED
+ | FLAG_DISABLE_ACTIVITY_ONLY | FLAG_DELAY_FILL
+ | FLAG_CREDENTIAL_MANAGER_RESPONSE);
return this;
}
diff --git a/core/java/android/service/chooser/flags.aconfig b/core/java/android/service/chooser/flags.aconfig
index 5978383..8aeaacf 100644
--- a/core/java/android/service/chooser/flags.aconfig
+++ b/core/java/android/service/chooser/flags.aconfig
@@ -7,3 +7,9 @@
bug: "268089816"
}
+flag {
+ name: "chooser_payload_toggling"
+ namespace: "intentresolver"
+ description: "This flag controls content toggling in Chooser"
+ bug: "302691505"
+}
diff --git a/core/java/android/service/contextualsearch/OWNERS b/core/java/android/service/contextualsearch/OWNERS
new file mode 100644
index 0000000..463adf4
--- /dev/null
+++ b/core/java/android/service/contextualsearch/OWNERS
@@ -0,0 +1,3 @@
+srazdan@google.com
+volnov@google.com
+hackz@google.com
diff --git a/core/java/android/service/voice/OWNERS b/core/java/android/service/voice/OWNERS
index ec44100..763c79e 100644
--- a/core/java/android/service/voice/OWNERS
+++ b/core/java/android/service/voice/OWNERS
@@ -4,4 +4,4 @@
# The owner here should not be assist owner
liangyuchen@google.com
-tuanng@google.com
+adudani@google.com
diff --git a/core/java/android/service/wearable/IWearableSensingService.aidl b/core/java/android/service/wearable/IWearableSensingService.aidl
index 44a13c4..0556188 100644
--- a/core/java/android/service/wearable/IWearableSensingService.aidl
+++ b/core/java/android/service/wearable/IWearableSensingService.aidl
@@ -28,8 +28,11 @@
* @hide
*/
oneway interface IWearableSensingService {
+ void provideSecureWearableConnection(in ParcelFileDescriptor parcelFileDescriptor, in RemoteCallback callback);
void provideDataStream(in ParcelFileDescriptor parcelFileDescriptor, in RemoteCallback callback);
void provideData(in PersistableBundle data, in SharedMemory sharedMemory, in RemoteCallback callback);
+ void registerDataRequestObserver(int dataType, in RemoteCallback dataRequestCallback, int dataRequestObserverId, in String packageName, in RemoteCallback statusCallback);
+ void unregisterDataRequestObserver(int dataType, int dataRequestObserverId, in String packageName, in RemoteCallback statusCallback);
void startDetection(in AmbientContextEventRequest request, in String packageName,
in RemoteCallback detectionResultCallback, in RemoteCallback statusCallback);
void stopDetection(in String packageName);
diff --git a/core/java/android/service/wearable/WearableSensingDataRequester.java b/core/java/android/service/wearable/WearableSensingDataRequester.java
new file mode 100644
index 0000000..5a8104f
--- /dev/null
+++ b/core/java/android/service/wearable/WearableSensingDataRequester.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 2024 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.service.wearable;
+
+import android.annotation.FlaggedApi;
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.SystemApi;
+import android.app.wearable.Flags;
+import android.app.wearable.WearableSensingDataRequest;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.function.Consumer;
+
+/**
+ * An interface to request wearable sensing data.
+ *
+ * @hide
+ */
+@FlaggedApi(Flags.FLAG_ENABLE_DATA_REQUEST_OBSERVER_API)
+@SystemApi
+public interface WearableSensingDataRequester {
+
+ /** An unknown status. */
+ int STATUS_UNKNOWN = 0;
+
+ /** The value of the status code that indicates success. */
+ int STATUS_SUCCESS = 1;
+
+ /**
+ * The value of the status code that indicates the request is rejected because the data request
+ * observer PendingIntent has been cancelled.
+ */
+ int STATUS_OBSERVER_CANCELLED = 2;
+
+ /**
+ * The value of the status code that indicates the request is rejected because it is larger than
+ * {@link WearableSensingDataRequest#getMaxRequestSize()}.
+ */
+ int STATUS_TOO_LARGE = 3;
+
+ /**
+ * The value of the status code that indicates the request is rejected because it exceeds the
+ * rate limit. See {@link WearableSensingDataRequest#getRateLimit()}.
+ */
+ int STATUS_TOO_FREQUENT = 4;
+
+ /** @hide */
+ @IntDef(
+ prefix = {"STATUS_"},
+ value = {
+ STATUS_UNKNOWN,
+ STATUS_SUCCESS,
+ STATUS_OBSERVER_CANCELLED,
+ STATUS_TOO_LARGE,
+ STATUS_TOO_FREQUENT
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ @interface StatusCode {}
+
+ /**
+ * Sends a data request. See {@link WearableSensingService#onDataRequestObserverRegistered(int,
+ * String, WearableSensingDataRequester, Consumer)} for size and rate restrictions on data
+ * requests.
+ *
+ * @param dataRequest The data request to send.
+ * @param statusConsumer A consumer that handles the status code for the data request.
+ */
+ void requestData(
+ @NonNull WearableSensingDataRequest dataRequest,
+ @NonNull @StatusCode Consumer<Integer> statusConsumer);
+}
diff --git a/core/java/android/service/wearable/WearableSensingService.java b/core/java/android/service/wearable/WearableSensingService.java
index e7e44a5..d25cff7 100644
--- a/core/java/android/service/wearable/WearableSensingService.java
+++ b/core/java/android/service/wearable/WearableSensingService.java
@@ -17,12 +17,15 @@
package android.service.wearable;
import android.annotation.BinderThread;
+import android.annotation.FlaggedApi;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.SystemApi;
import android.app.Service;
import android.app.ambientcontext.AmbientContextEvent;
import android.app.ambientcontext.AmbientContextEventRequest;
+import android.app.wearable.Flags;
+import android.app.wearable.WearableSensingDataRequest;
import android.app.wearable.WearableSensingManager;
import android.content.Intent;
import android.os.Bundle;
@@ -34,11 +37,13 @@
import android.service.ambientcontext.AmbientContextDetectionResult;
import android.service.ambientcontext.AmbientContextDetectionServiceStatus;
import android.util.Slog;
+import android.util.SparseArray;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;
+import java.util.concurrent.Executor;
import java.util.function.Consumer;
/**
@@ -87,6 +92,9 @@
public static final String SERVICE_INTERFACE =
"android.service.wearable.WearableSensingService";
+ private final SparseArray<WearableSensingDataRequester> mDataRequestObserverIdToRequesterMap =
+ new SparseArray<>();
+
@Nullable
@Override
public final IBinder onBind(@NonNull Intent intent) {
@@ -94,17 +102,20 @@
return new IWearableSensingService.Stub() {
/** {@inheritDoc} */
@Override
+ public void provideSecureWearableConnection(
+ ParcelFileDescriptor secureWearableConnection, RemoteCallback callback) {
+ Objects.requireNonNull(secureWearableConnection);
+ Consumer<Integer> consumer = createWearableStatusConsumer(callback);
+ WearableSensingService.this.onSecureWearableConnectionProvided(
+ secureWearableConnection, consumer);
+ }
+
+ /** {@inheritDoc} */
+ @Override
public void provideDataStream(
- ParcelFileDescriptor parcelFileDescriptor,
- RemoteCallback callback) {
+ ParcelFileDescriptor parcelFileDescriptor, RemoteCallback callback) {
Objects.requireNonNull(parcelFileDescriptor);
- Consumer<Integer> consumer = response -> {
- Bundle bundle = new Bundle();
- bundle.putInt(
- STATUS_RESPONSE_BUNDLE_KEY,
- response);
- callback.sendResult(bundle);
- };
+ Consumer<Integer> consumer = createWearableStatusConsumer(callback);
WearableSensingService.this.onDataStreamProvided(
parcelFileDescriptor, consumer);
}
@@ -116,38 +127,87 @@
SharedMemory sharedMemory,
RemoteCallback callback) {
Objects.requireNonNull(data);
- Consumer<Integer> consumer = response -> {
- Bundle bundle = new Bundle();
- bundle.putInt(
- STATUS_RESPONSE_BUNDLE_KEY,
- response);
- callback.sendResult(bundle);
- };
+ Consumer<Integer> consumer = createWearableStatusConsumer(callback);
WearableSensingService.this.onDataProvided(data, sharedMemory, consumer);
}
/** {@inheritDoc} */
@Override
- public void startDetection(@NonNull AmbientContextEventRequest request,
- String packageName, RemoteCallback detectionResultCallback,
+ public void registerDataRequestObserver(
+ int dataType,
+ RemoteCallback dataRequestCallback,
+ int dataRequestObserverId,
+ String packageName,
+ RemoteCallback statusCallback) {
+ Objects.requireNonNull(dataRequestCallback);
+ Objects.requireNonNull(statusCallback);
+ WearableSensingDataRequester dataRequester;
+ synchronized (mDataRequestObserverIdToRequesterMap) {
+ dataRequester =
+ mDataRequestObserverIdToRequesterMap.get(dataRequestObserverId);
+ if (dataRequester == null) {
+ dataRequester = createDataRequester(dataRequestCallback);
+ mDataRequestObserverIdToRequesterMap.put(
+ dataRequestObserverId, dataRequester);
+ }
+ }
+ Consumer<Integer> statusConsumer = createWearableStatusConsumer(statusCallback);
+ WearableSensingService.this.onDataRequestObserverRegistered(
+ dataType, packageName, dataRequester, statusConsumer);
+ }
+
+ @Override
+ public void unregisterDataRequestObserver(
+ int dataType,
+ int dataRequestObserverId,
+ String packageName,
+ RemoteCallback statusCallback) {
+ WearableSensingDataRequester dataRequester;
+ synchronized (mDataRequestObserverIdToRequesterMap) {
+ dataRequester =
+ mDataRequestObserverIdToRequesterMap.get(dataRequestObserverId);
+ if (dataRequester == null) {
+ Slog.w(
+ TAG,
+ "dataRequestObserverId not found, cannot unregister data"
+ + " request observer.");
+ return;
+ }
+ mDataRequestObserverIdToRequesterMap.remove(dataRequestObserverId);
+ }
+ Consumer<Integer> statusConsumer = createWearableStatusConsumer(statusCallback);
+ WearableSensingService.this.onDataRequestObserverUnregistered(
+ dataType, packageName, dataRequester, statusConsumer);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void startDetection(
+ @NonNull AmbientContextEventRequest request,
+ String packageName,
+ RemoteCallback detectionResultCallback,
RemoteCallback statusCallback) {
Objects.requireNonNull(request);
Objects.requireNonNull(packageName);
Objects.requireNonNull(detectionResultCallback);
Objects.requireNonNull(statusCallback);
- Consumer<AmbientContextDetectionResult> detectionResultConsumer = result -> {
- Bundle bundle = new Bundle();
- bundle.putParcelable(
- AmbientContextDetectionResult.RESULT_RESPONSE_BUNDLE_KEY, result);
- detectionResultCallback.sendResult(bundle);
- };
- Consumer<AmbientContextDetectionServiceStatus> statusConsumer = status -> {
- Bundle bundle = new Bundle();
- bundle.putParcelable(
- AmbientContextDetectionServiceStatus.STATUS_RESPONSE_BUNDLE_KEY,
- status);
- statusCallback.sendResult(bundle);
- };
+ Consumer<AmbientContextDetectionResult> detectionResultConsumer =
+ result -> {
+ Bundle bundle = new Bundle();
+ bundle.putParcelable(
+ AmbientContextDetectionResult.RESULT_RESPONSE_BUNDLE_KEY,
+ result);
+ detectionResultCallback.sendResult(bundle);
+ };
+ Consumer<AmbientContextDetectionServiceStatus> statusConsumer =
+ status -> {
+ Bundle bundle = new Bundle();
+ bundle.putParcelable(
+ AmbientContextDetectionServiceStatus
+ .STATUS_RESPONSE_BUNDLE_KEY,
+ status);
+ statusCallback.sendResult(bundle);
+ };
WearableSensingService.this.onStartDetection(
request, packageName, statusConsumer, detectionResultConsumer);
Slog.d(TAG, "startDetection " + request);
@@ -162,23 +222,26 @@
/** {@inheritDoc} */
@Override
- public void queryServiceStatus(@AmbientContextEvent.EventCode int[] eventTypes,
- String packageName, RemoteCallback callback) {
+ public void queryServiceStatus(
+ @AmbientContextEvent.EventCode int[] eventTypes,
+ String packageName,
+ RemoteCallback callback) {
Objects.requireNonNull(eventTypes);
Objects.requireNonNull(packageName);
Objects.requireNonNull(callback);
- Consumer<AmbientContextDetectionServiceStatus> consumer = response -> {
- Bundle bundle = new Bundle();
- bundle.putParcelable(
- AmbientContextDetectionServiceStatus.STATUS_RESPONSE_BUNDLE_KEY,
- response);
- callback.sendResult(bundle);
- };
+ Consumer<AmbientContextDetectionServiceStatus> consumer =
+ response -> {
+ Bundle bundle = new Bundle();
+ bundle.putParcelable(
+ AmbientContextDetectionServiceStatus
+ .STATUS_RESPONSE_BUNDLE_KEY,
+ response);
+ callback.sendResult(bundle);
+ };
Integer[] events = intArrayToIntegerArray(eventTypes);
WearableSensingService.this.onQueryServiceStatus(
new HashSet<>(Arrays.asList(events)), packageName, consumer);
}
-
};
}
Slog.w(TAG, "Incorrect service interface, returning null.");
@@ -186,6 +249,30 @@
}
/**
+ * Called when a secure connection to the wearable is available. See {@link
+ * WearableSensingManager#provideWearableConnection(ParcelFileDescriptor, Executor, Consumer)}
+ * for details about the secure connection.
+ *
+ * <p>When the {@code secureWearableConnection} is closed, the system will send a {@link
+ * WearableSensingManager#STATUS_CHANNEL_ERROR} status code to the status consumer provided by
+ * the caller of {@link WearableSensingManager#provideWearableConnection(ParcelFileDescriptor,
+ * Executor, Consumer)}.
+ *
+ * <p>The implementing class should override this method. It should return an appropriate status
+ * code via {@code statusConsumer} after receiving the {@code secureWearableConnection}.
+ *
+ * @param secureWearableConnection The secure connection to the wearable.
+ * @param statusConsumer The consumer for the service status.
+ */
+ @FlaggedApi(Flags.FLAG_ENABLE_PROVIDE_WEARABLE_CONNECTION_API)
+ @BinderThread
+ public void onSecureWearableConnectionProvided(
+ @NonNull ParcelFileDescriptor secureWearableConnection,
+ @NonNull Consumer<Integer> statusConsumer) {
+ statusConsumer.accept(WearableSensingManager.STATUS_UNSUPPORTED_OPERATION);
+ }
+
+ /**
* Called when a data stream to the wearable is provided. This data stream can be used to obtain
* data from a wearable device. It is up to the implementation to maintain the data stream and
* close the data stream when it is finished.
@@ -198,19 +285,19 @@
@NonNull Consumer<Integer> statusConsumer);
/**
- * Called when configurations and read-only data in a {@link PersistableBundle}
- * can be used by the WearableSensingService and sends the result to the {@link Consumer}
- * right after the call. It is dependent on the application to define the type of data to
- * provide. This is used by applications that will also provide an implementation of an isolated
- * WearableSensingService. If the data was provided successfully
- * {@link WearableSensingManager#STATUS_SUCCESS} will be provided.
+ * Called when configurations and read-only data in a {@link PersistableBundle} can be used by
+ * the WearableSensingService and sends the result to the {@link Consumer} right after the call.
+ * It is dependent on the application to define the type of data to provide. This is used by
+ * applications that will also provide an implementation of an isolated WearableSensingService.
+ * If the data was provided successfully {@link WearableSensingManager#STATUS_SUCCESS} will be
+ * provided.
*
* @param data Application configuration data to provide to the {@link WearableSensingService}.
- * PersistableBundle does not allow any remotable objects or other contents
- * that can be used to communicate with other processes.
- * @param sharedMemory The unrestricted data blob to
- * provide to the {@link WearableSensingService}. Use this to provide the
- * sensing models data or other such data to the trusted process.
+ * PersistableBundle does not allow any remotable objects or other contents that can be used
+ * to communicate with other processes.
+ * @param sharedMemory The unrestricted data blob to provide to the {@link
+ * WearableSensingService}. Use this to provide the sensing models data or other such data
+ * to the trusted process.
* @param statusConsumer the consumer for the service status.
*/
@BinderThread
@@ -220,6 +307,68 @@
@NonNull Consumer<Integer> statusConsumer);
/**
+ * Called when a data request observer is registered. Each request must not be larger than
+ * {@link WearableSensingDataRequest#getMaxRequestSize()}. In addition, at most {@link
+ * WearableSensingDataRequester#getRateLimit()} requests can be sent every rolling {@link
+ * WearableSensingDataRequester#getRateLimitWindowSize()}. Requests that are too large or too
+ * frequent will be dropped by the system. See {@link
+ * WearableSensingDataRequester#requestData(WearableSensingDataRequest, Consumer)} for details
+ * about the status code returned for each request.
+ *
+ * <p>The implementing class should override this method. After the data requester is received,
+ * it should send a {@link WearableSensingManager#STATUS_SUCCESS} status code to the {@code
+ * statusConsumer} unless it encounters an error condition described by a status code listed in
+ * {@link WearableSensingManager}, such as {@link
+ * WearableSensingManager#STATUS_WEARABLE_UNAVAILABLE}, in which case it should return the
+ * corresponding status code.
+ *
+ * @param dataType The data type the observer is registered for. Values are defined by the
+ * application that implements this class.
+ * @param packageName The package name of the app that will receive the requests.
+ * @param dataRequester A handle to the observer registered. It can be used to request data of
+ * the specified data type.
+ * @param statusConsumer the consumer for the status of the data request observer registration.
+ * This is different from the status for each data request.
+ */
+ @FlaggedApi(Flags.FLAG_ENABLE_DATA_REQUEST_OBSERVER_API)
+ @BinderThread
+ public void onDataRequestObserverRegistered(
+ int dataType,
+ @NonNull String packageName,
+ @NonNull WearableSensingDataRequester dataRequester,
+ @NonNull Consumer<Integer> statusConsumer) {
+ statusConsumer.accept(WearableSensingManager.STATUS_UNSUPPORTED_OPERATION);
+ }
+
+ /**
+ * Called when a data request observer is unregistered.
+ *
+ * <p>The implementing class should override this method. It should send a {@link
+ * WearableSensingManager#STATUS_SUCCESS} status code to the {@code statusConsumer} unless it
+ * encounters an error condition described by a status code listed in {@link
+ * WearableSensingManager}, such as {@link WearableSensingManager#STATUS_WEARABLE_UNAVAILABLE},
+ * in which case it should return the corresponding status code.
+ *
+ * @param dataType The data type the observer is for.
+ * @param packageName The package name of the app that will receive the requests sent to the
+ * dataRequester.
+ * @param dataRequester A handle to the observer to be unregistered. It is the exact same
+ * instance provided in a previous {@link #onDataRequestConsumerRegistered(int, String,
+ * WearableSensingDataRequester, Consumer)} invocation.
+ * @param statusConsumer the consumer for the status of the data request observer
+ * unregistration. This is different from the status for each data request.
+ */
+ @FlaggedApi(Flags.FLAG_ENABLE_DATA_REQUEST_OBSERVER_API)
+ @BinderThread
+ public void onDataRequestObserverUnregistered(
+ int dataType,
+ @NonNull String packageName,
+ @NonNull WearableSensingDataRequester dataRequester,
+ @NonNull Consumer<Integer> statusConsumer) {
+ statusConsumer.accept(WearableSensingManager.STATUS_UNSUPPORTED_OPERATION);
+ }
+
+ /**
* Called when a client app requests starting detection of the events in the request. The
* implementation should keep track of whether the user has explicitly consented to detecting
* the events using on-going ambient sensor (e.g. microphone), and agreed to share the
@@ -275,4 +424,32 @@
}
return intArray;
}
+
+ private static WearableSensingDataRequester createDataRequester(
+ RemoteCallback dataRequestCallback) {
+ return (request, requestStatusConsumer) -> {
+ Bundle bundle = new Bundle();
+ bundle.putParcelable(WearableSensingDataRequest.REQUEST_BUNDLE_KEY, request);
+ RemoteCallback requestStatusCallback =
+ new RemoteCallback(
+ requestStatusBundle -> {
+ requestStatusConsumer.accept(
+ requestStatusBundle.getInt(
+ WearableSensingManager.STATUS_RESPONSE_BUNDLE_KEY));
+ });
+ bundle.putParcelable(
+ WearableSensingDataRequest.REQUEST_STATUS_CALLBACK_BUNDLE_KEY,
+ requestStatusCallback);
+ dataRequestCallback.sendResult(bundle);
+ };
+ }
+
+ @NonNull
+ private static Consumer<Integer> createWearableStatusConsumer(RemoteCallback statusCallback) {
+ return response -> {
+ Bundle bundle = new Bundle();
+ bundle.putInt(STATUS_RESPONSE_BUNDLE_KEY, response);
+ statusCallback.sendResult(bundle);
+ };
+ }
}
diff --git a/core/java/android/telephony/TelephonyRegistryManager.java b/core/java/android/telephony/TelephonyRegistryManager.java
index 0a813a3..d39c4ce 100644
--- a/core/java/android/telephony/TelephonyRegistryManager.java
+++ b/core/java/android/telephony/TelephonyRegistryManager.java
@@ -19,12 +19,15 @@
import android.annotation.FlaggedApi;
import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.annotation.RequiresFeature;
import android.annotation.RequiresPermission;
import android.annotation.SystemApi;
+import android.annotation.SystemService;
import android.compat.Compatibility;
import android.compat.annotation.ChangeId;
import android.compat.annotation.EnabledAfter;
import android.content.Context;
+import android.content.pm.PackageManager;
import android.os.Binder;
import android.os.Build;
import android.os.RemoteException;
@@ -73,9 +76,15 @@
* Limit API access to only carrier apps with certain permissions or apps running on
* privileged UID.
*
+ * TelephonyRegistryManager is intended for use on devices that implement
+ * {@link android.content.pm.PackageManager#FEATURE_TELEPHONY FEATURE_TELEPHONY}. On devices
+ * that do not implement this feature, the behavior is not reliable.
+ *
* @hide
*/
@SystemApi
+@SystemService(Context.TELEPHONY_REGISTRY_SERVICE)
+@RequiresFeature(PackageManager.FEATURE_TELEPHONY)
@FlaggedApi(Flags.FLAG_TELECOM_RESOLVE_HIDDEN_DEPENDENCIES)
public class TelephonyRegistryManager {
@@ -389,10 +398,11 @@
}
/**
- * Notify call state changed on all subscriptions.
+ * Notify call state changed on all subscriptions, excluding over-the-top VOIP calls (otherwise
+ * known as self-managed calls in the Android Platform).
*
* @param state latest call state. e.g, offhook, ringing
- * @param incomingNumber incoming phone number.
+ * @param incomingNumber incoming phone number or null in the case for OTT VOIP calls
* @hide
*/
@SystemApi
@@ -627,17 +637,20 @@
}
/**
- * Notify outgoing emergency call.
+ * Notify outgoing emergency call to all applications that have registered a listener
+ * ({@link PhoneStateListener}) or a callback ({@link TelephonyCallback}) to monitor changes in
+ * telephony states.
* @param simSlotIndex Sender phone ID.
- * @param subId Sender subscription ID.
+ * @param subscriptionId Sender subscription ID.
* @param emergencyNumber Emergency number.
* @hide
*/
@SystemApi
- public void notifyOutgoingEmergencyCall(int simSlotIndex, int subId,
+ @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE)
+ public void notifyOutgoingEmergencyCall(int simSlotIndex, int subscriptionId,
@NonNull EmergencyNumber emergencyNumber) {
try {
- sRegistry.notifyOutgoingEmergencyCall(simSlotIndex, subId, emergencyNumber);
+ sRegistry.notifyOutgoingEmergencyCall(simSlotIndex, subscriptionId, emergencyNumber);
} catch (RemoteException ex) {
// system process is dead
throw ex.rethrowFromSystemServer();
diff --git a/core/java/android/text/ClientFlags.java b/core/java/android/text/ClientFlags.java
index 0421d5a..32f05be 100644
--- a/core/java/android/text/ClientFlags.java
+++ b/core/java/android/text/ClientFlags.java
@@ -54,4 +54,11 @@
public static boolean fixLineHeightForLocale() {
return TextFlags.isFeatureEnabled(Flags.FLAG_FIX_LINE_HEIGHT_FOR_LOCALE);
}
+
+ /**
+ * @see Flags#icuBidiMigration()
+ */
+ public static boolean icuBidiMigration() {
+ return TextFlags.isFeatureEnabled(Flags.FLAG_ICU_BIDI_MIGRATION);
+ }
}
diff --git a/core/java/android/text/Layout.java b/core/java/android/text/Layout.java
index 1ea80f1..8ddb42d 100644
--- a/core/java/android/text/Layout.java
+++ b/core/java/android/text/Layout.java
@@ -18,7 +18,7 @@
import static com.android.text.flags.Flags.FLAG_FIX_LINE_HEIGHT_FOR_LOCALE;
import static com.android.text.flags.Flags.FLAG_USE_BOUNDS_FOR_WIDTH;
-import static com.android.text.flags.Flags.FLAG_INTER_CHARACTER_JUSTIFICATION;
+import static com.android.text.flags.Flags.FLAG_LETTER_SPACING_JUSTIFICATION;
import android.annotation.FlaggedApi;
import android.annotation.FloatRange;
@@ -172,7 +172,7 @@
/**
* Value for justification mode indicating the text is justified by stretching letter spacing.
*/
- @FlaggedApi(FLAG_INTER_CHARACTER_JUSTIFICATION)
+ @FlaggedApi(FLAG_LETTER_SPACING_JUSTIFICATION)
public static final int JUSTIFICATION_MODE_INTER_CHARACTER =
LineBreaker.JUSTIFICATION_MODE_INTER_CHARACTER;
@@ -1831,7 +1831,7 @@
* @return the number of cluster count in the line.
*/
@IntRange(from = 0)
- @FlaggedApi(FLAG_INTER_CHARACTER_JUSTIFICATION)
+ @FlaggedApi(FLAG_LETTER_SPACING_JUSTIFICATION)
public int getLineLetterSpacingUnitCount(@IntRange(from = 0) int line,
boolean includeTrailingWhitespace) {
final int start = getLineStart(line);
diff --git a/core/java/android/text/MeasuredParagraph.java b/core/java/android/text/MeasuredParagraph.java
index b268c2e..09f15c3 100644
--- a/core/java/android/text/MeasuredParagraph.java
+++ b/core/java/android/text/MeasuredParagraph.java
@@ -30,6 +30,7 @@
import android.graphics.Rect;
import android.graphics.text.LineBreakConfig;
import android.graphics.text.MeasuredText;
+import android.icu.text.Bidi;
import android.text.AutoGrowArray.ByteArray;
import android.text.AutoGrowArray.FloatArray;
import android.text.AutoGrowArray.IntArray;
@@ -115,6 +116,11 @@
// This is empty if mLtrWithoutBidi is true.
private @NonNull ByteArray mLevels = new ByteArray();
+ // The bidi level for runs.
+ private @NonNull ByteArray mRunLevels = new ByteArray();
+
+ private Bidi mBidi;
+
// The whole width of the text.
// See getWholeWidth comments.
private @FloatRange(from = 0.0f) float mWholeWidth;
@@ -148,6 +154,7 @@
reset();
mLevels.clearWithReleasingLargeArray();
mWidths.clearWithReleasingLargeArray();
+ mRunLevels.clearWithReleasingLargeArray();
mFontMetrics.clearWithReleasingLargeArray();
mSpanEndCache.clearWithReleasingLargeArray();
}
@@ -160,10 +167,12 @@
mCopiedBuffer = null;
mWholeWidth = 0;
mLevels.clear();
+ mRunLevels.clear();
mWidths.clear();
mFontMetrics.clear();
mSpanEndCache.clear();
mMeasuredText = null;
+ mBidi = null;
}
/**
@@ -193,6 +202,13 @@
* @hide
*/
public @Layout.Direction int getParagraphDir() {
+ if (ClientFlags.icuBidiMigration()) {
+ if (mBidi == null) {
+ return Layout.DIR_LEFT_TO_RIGHT;
+ }
+ return (mBidi.getParaLevel() & 0x01) == 0
+ ? Layout.DIR_LEFT_TO_RIGHT : Layout.DIR_RIGHT_TO_LEFT;
+ }
return mParaDir;
}
@@ -204,6 +220,62 @@
*/
public Directions getDirections(@IntRange(from = 0) int start, // inclusive
@IntRange(from = 0) int end) { // exclusive
+ if (ClientFlags.icuBidiMigration()) {
+ // Easy case: mBidi == null means the text is all LTR and no bidi suppot is needed.
+ if (mBidi == null) {
+ return Layout.DIRS_ALL_LEFT_TO_RIGHT;
+ }
+
+ // Easy case: If the original text only contains single directionality run, the
+ // substring is only single run.
+ if (start == end) {
+ if ((mBidi.getParaLevel() & 0x01) == 0) {
+ return Layout.DIRS_ALL_LEFT_TO_RIGHT;
+ } else {
+ return Layout.DIRS_ALL_RIGHT_TO_LEFT;
+ }
+ }
+
+ // Okay, now we need to generate the line instance.
+ Bidi bidi = mBidi.createLineBidi(start, end);
+
+ // Easy case: If the line instance only contains single directionality run, no need
+ // to reorder visually.
+ if (bidi.getRunCount() == 1) {
+ if ((bidi.getParaLevel() & 0x01) == 1) {
+ return Layout.DIRS_ALL_RIGHT_TO_LEFT;
+ } else {
+ return Layout.DIRS_ALL_LEFT_TO_RIGHT;
+ }
+ }
+
+ // Reorder directionality run visually.
+ mRunLevels.resize(bidi.getRunCount());
+ byte[] levels = mRunLevels.getRawArray();
+ for (int i = 0; i < bidi.getRunCount(); ++i) {
+ levels[i] = (byte) bidi.getRunLevel(i);
+ }
+ int[] visualOrders = Bidi.reorderVisual(levels);
+
+ int[] dirs = new int[bidi.getRunCount() * 2];
+ for (int i = 0; i < bidi.getRunCount(); ++i) {
+ int vIndex;
+ if ((mBidi.getBaseLevel() & 0x01) == 1) {
+ // For the historical reasons, if the base directionality is RTL, the Android
+ // draws from the right, i.e. the visually reordered run needs to be reversed.
+ vIndex = visualOrders[bidi.getRunCount() - i - 1];
+ } else {
+ vIndex = visualOrders[i];
+ }
+
+ // Special packing of dire
+ dirs[i * 2] = bidi.getRunStart(vIndex);
+ dirs[i * 2 + 1] = bidi.getRunLevel(vIndex) << Layout.RUN_LEVEL_SHIFT
+ | (bidi.getRunLimit(vIndex) - dirs[i * 2]);
+ }
+
+ return new Directions(dirs);
+ }
if (mLtrWithoutBidi) {
return Layout.DIRS_ALL_LEFT_TO_RIGHT;
}
@@ -608,6 +680,37 @@
}
}
+ if (ClientFlags.icuBidiMigration()) {
+ if ((textDir == TextDirectionHeuristics.LTR
+ || textDir == TextDirectionHeuristics.FIRSTSTRONG_LTR
+ || textDir == TextDirectionHeuristics.ANYRTL_LTR)
+ && TextUtils.doesNotNeedBidi(mCopiedBuffer, 0, mTextLength)) {
+ mLevels.clear();
+ mLtrWithoutBidi = true;
+ return;
+ }
+ final int bidiRequest;
+ if (textDir == TextDirectionHeuristics.LTR) {
+ bidiRequest = Bidi.LTR;
+ } else if (textDir == TextDirectionHeuristics.RTL) {
+ bidiRequest = Bidi.RTL;
+ } else if (textDir == TextDirectionHeuristics.FIRSTSTRONG_LTR) {
+ bidiRequest = Bidi.LEVEL_DEFAULT_LTR;
+ } else if (textDir == TextDirectionHeuristics.FIRSTSTRONG_RTL) {
+ bidiRequest = Bidi.LEVEL_DEFAULT_RTL;
+ } else {
+ final boolean isRtl = textDir.isRtl(mCopiedBuffer, 0, mTextLength);
+ bidiRequest = isRtl ? Bidi.RTL : Bidi.LTR;
+ }
+ mBidi = new Bidi(mCopiedBuffer, 0, null, 0, mCopiedBuffer.length, bidiRequest);
+ mLevels.resize(mTextLength);
+ byte[] rawArray = mLevels.getRawArray();
+ for (int i = 0; i < mTextLength; ++i) {
+ rawArray[i] = mBidi.getLevelAt(i);
+ }
+ mLtrWithoutBidi = false;
+ return;
+ }
if ((textDir == TextDirectionHeuristics.LTR
|| textDir == TextDirectionHeuristics.FIRSTSTRONG_LTR
|| textDir == TextDirectionHeuristics.ANYRTL_LTR)
diff --git a/core/java/android/text/TextFlags.java b/core/java/android/text/TextFlags.java
index 2466386..770e5c9 100644
--- a/core/java/android/text/TextFlags.java
+++ b/core/java/android/text/TextFlags.java
@@ -59,6 +59,7 @@
Flags.FLAG_PHRASE_STRICT_FALLBACK,
Flags.FLAG_USE_BOUNDS_FOR_WIDTH,
Flags.FLAG_FIX_LINE_HEIGHT_FOR_LOCALE,
+ Flags.FLAG_ICU_BIDI_MIGRATION,
};
/**
@@ -71,6 +72,7 @@
Flags.phraseStrictFallback(),
Flags.useBoundsForWidth(),
Flags.fixLineHeightForLocale(),
+ Flags.icuBidiMigration(),
};
/**
diff --git a/core/java/android/text/TextLine.java b/core/java/android/text/TextLine.java
index 224e5d8..bde9c77 100644
--- a/core/java/android/text/TextLine.java
+++ b/core/java/android/text/TextLine.java
@@ -305,7 +305,7 @@
}
mAddedWordSpacingInPx = (justifyWidth - width) / spaces;
mAddedLetterSpacingInPx = 0;
- } else { // justificationMode == Layout.JUSTIFICATION_MODE_INTER_CHARACTER
+ } else { // justificationMode == Layout.JUSTIFICATION_MODE_LETTER_SPACING
LineInfo lineInfo = new LineInfo();
float width = Math.abs(measure(end, false, null, null, lineInfo));
diff --git a/core/java/android/text/flags/flags.aconfig b/core/java/android/text/flags/flags.aconfig
index f3e0ea7..f37c4c2a 100644
--- a/core/java/android/text/flags/flags.aconfig
+++ b/core/java/android/text/flags/flags.aconfig
@@ -77,7 +77,7 @@
}
flag {
- name: "inter_character_justification"
+ name: "letter_spacing_justification"
namespace: "text"
description: "A feature flag that implement inter character justification."
bug: "283193133"
@@ -103,3 +103,10 @@
description: "Fix that InputService#onUpdateSelection is not called when insert mode gesture is performed."
bug: "300850862"
}
+
+flag {
+ name: "icu_bidi_migration"
+ namespace: "text"
+ description: "A flag for replacing AndroidBidi with android.icu.text.Bidi."
+ bug: "317144801"
+}
diff --git a/core/java/android/tracing/perfetto/DataSourceInstance.java b/core/java/android/tracing/perfetto/DataSourceInstance.java
index 4994501..3710b4d 100644
--- a/core/java/android/tracing/perfetto/DataSourceInstance.java
+++ b/core/java/android/tracing/perfetto/DataSourceInstance.java
@@ -69,4 +69,8 @@
public final void release() {
mDataSource.releaseDataSourceInstance(mInstanceIndex);
}
+
+ public final int getInstanceIndex() {
+ return mInstanceIndex;
+ }
}
diff --git a/core/java/android/tracing/transition/TransitionDataSource.java b/core/java/android/tracing/transition/TransitionDataSource.java
index baece75..744f446 100644
--- a/core/java/android/tracing/transition/TransitionDataSource.java
+++ b/core/java/android/tracing/transition/TransitionDataSource.java
@@ -16,7 +16,6 @@
package android.tracing.transition;
-import android.tracing.perfetto.CreateTlsStateArgs;
import android.tracing.perfetto.DataSource;
import android.tracing.perfetto.DataSourceInstance;
import android.tracing.perfetto.FlushCallbackArguments;
@@ -24,14 +23,11 @@
import android.tracing.perfetto.StopCallbackArguments;
import android.util.proto.ProtoInputStream;
-import java.util.HashMap;
-import java.util.Map;
-
/**
* @hide
*/
public class TransitionDataSource
- extends DataSource<DataSourceInstance, TransitionDataSource.TlsState, Void> {
+ extends DataSource<DataSourceInstance, Void, Void> {
public static String DATA_SOURCE_NAME = "com.android.wm.shell.transition";
private final Runnable mOnStartStaticCallback;
@@ -46,15 +42,6 @@
}
@Override
- protected TlsState createTlsState(CreateTlsStateArgs<DataSourceInstance> args) {
- return new TlsState();
- }
-
- public class TlsState {
- public final Map<String, Integer> handlerMapping = new HashMap<>();
- }
-
- @Override
public DataSourceInstance createInstance(ProtoInputStream configStream, int instanceIndex) {
return new DataSourceInstance(this, instanceIndex) {
@Override
diff --git a/core/java/android/view/DisplayCutout.java b/core/java/android/view/DisplayCutout.java
index f819c9b..db665a9 100644
--- a/core/java/android/view/DisplayCutout.java
+++ b/core/java/android/view/DisplayCutout.java
@@ -27,9 +27,7 @@
import static android.view.DisplayCutoutProto.SIDE_OVERRIDES;
import static android.view.DisplayCutoutProto.WATERFALL_INSETS;
import static android.view.Surface.ROTATION_0;
-import static android.view.Surface.ROTATION_180;
import static android.view.Surface.ROTATION_270;
-import static android.view.Surface.ROTATION_90;
import static com.android.internal.annotations.VisibleForTesting.Visibility.PRIVATE;
@@ -168,6 +166,9 @@
// The side index is always under the natural rotation of the device.
private int[] mSideOverrides;
+ static final int[] INVALID_OVERRIDES = new int[]{INVALID_SIDE_OVERRIDE, INVALID_SIDE_OVERRIDE,
+ INVALID_SIDE_OVERRIDE, INVALID_SIDE_OVERRIDE};
+
/** @hide */
@IntDef(prefix = { "BOUNDS_POSITION_" }, value = {
BOUNDS_POSITION_LEFT,
@@ -1157,35 +1158,25 @@
final int resourceId = index >= 0 && index < array.length()
? array.getResourceId(index, ID_NULL)
: ID_NULL;
- final String[] rawOverrides = resourceId != ID_NULL
- ? array.getResources().getStringArray(resourceId)
- : res.getStringArray(R.array.config_mainBuiltInDisplayCutoutSideOverride);
+ final int[] rawOverrides = resourceId != ID_NULL
+ ? array.getResources().getIntArray(resourceId)
+ : res.getIntArray(R.array.config_mainBuiltInDisplayCutoutSideOverride);
array.recycle();
- final int[] override = new int[]{INVALID_SIDE_OVERRIDE, INVALID_SIDE_OVERRIDE,
- INVALID_SIDE_OVERRIDE, INVALID_SIDE_OVERRIDE};
- for (String rawOverride : rawOverrides) {
- int rotation;
- String[] split = rawOverride.split(" *, *");
- switch (split[0]) {
- case "0" -> rotation = ROTATION_0;
- case "90" -> rotation = ROTATION_90;
- case "180" -> rotation = ROTATION_180;
- case "270" -> rotation = ROTATION_270;
- default -> throw new IllegalArgumentException("Invalid side override definition: "
- + rawOverride);
- }
- int side;
- switch (split[1]) {
- case SIDE_STRING_LEFT -> side = BOUNDS_POSITION_LEFT;
- case SIDE_STRING_TOP -> side = BOUNDS_POSITION_TOP;
- case SIDE_STRING_RIGHT -> side = BOUNDS_POSITION_RIGHT;
- case SIDE_STRING_BOTTOM -> side = BOUNDS_POSITION_BOTTOM;
- default -> throw new IllegalArgumentException("Invalid side override definition: "
- + rawOverride);
- }
- override[rotation] = side;
+ if (rawOverrides.length == 0) {
+ return INVALID_OVERRIDES;
+ } else if (rawOverrides.length != 4) {
+ throw new IllegalArgumentException(
+ "Invalid side override definition, exact 4 overrides required: "
+ + Arrays.toString(rawOverrides));
}
- return override;
+ for (int rotation = ROTATION_0; rotation <= ROTATION_270; rotation++) {
+ if (rawOverrides[rotation] < BOUNDS_POSITION_LEFT
+ || rawOverrides[rotation] >= BOUNDS_POSITION_LENGTH) {
+ throw new IllegalArgumentException("Invalid side override definition: "
+ + Arrays.toString(rawOverrides));
+ }
+ }
+ return rawOverrides;
}
/**
diff --git a/core/java/android/view/IWindowManager.aidl b/core/java/android/view/IWindowManager.aidl
index 7903050..99863d0 100644
--- a/core/java/android/view/IWindowManager.aidl
+++ b/core/java/android/view/IWindowManager.aidl
@@ -1085,7 +1085,9 @@
void unregisterTrustedPresentationListener(in ITrustedPresentationListener listener, int id);
+ @EnforcePermission("DETECT_SCREEN_RECORDING")
boolean registerScreenRecordingCallback(IScreenRecordingCallback callback);
+ @EnforcePermission("DETECT_SCREEN_RECORDING")
void unregisterScreenRecordingCallback(IScreenRecordingCallback callback);
}
diff --git a/core/java/android/view/InputDevice.java b/core/java/android/view/InputDevice.java
index f2c3abc..d22d2a5 100644
--- a/core/java/android/view/InputDevice.java
+++ b/core/java/android/view/InputDevice.java
@@ -16,7 +16,10 @@
package android.view;
+import static com.android.input.flags.Flags.FLAG_INPUT_DEVICE_VIEW_BEHAVIOR_API;
+
import android.Manifest;
+import android.annotation.FlaggedApi;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
@@ -28,6 +31,7 @@
import android.hardware.SensorManager;
import android.hardware.input.HostUsiVersion;
import android.hardware.input.InputDeviceIdentifier;
+import android.hardware.input.InputManager;
import android.hardware.input.InputManagerGlobal;
import android.hardware.lights.LightsManager;
import android.icu.util.ULocale;
@@ -90,6 +94,8 @@
private final int mAssociatedDisplayId;
private final ArrayList<MotionRange> mMotionRanges = new ArrayList<MotionRange>();
+ private final ViewBehavior mViewBehavior = new ViewBehavior(this);
+
@GuardedBy("mMotionRanges")
private Vibrator mVibrator; // guarded by mMotionRanges during initialization
@@ -539,6 +545,8 @@
addMotionRange(in.readInt(), in.readInt(), in.readFloat(), in.readFloat(),
in.readFloat(), in.readFloat(), in.readFloat());
}
+
+ mViewBehavior.mShouldSmoothScroll = in.readBoolean();
}
/**
@@ -571,6 +579,7 @@
private int mUsiVersionMinor = -1;
private int mAssociatedDisplayId = Display.INVALID_DISPLAY;
private List<MotionRange> mMotionRanges = new ArrayList<>();
+ private boolean mShouldSmoothScroll;
/** @see InputDevice#getId() */
public Builder setId(int id) {
@@ -706,6 +715,16 @@
return this;
}
+ /**
+ * Sets the view behavior for smooth scrolling ({@code false} by default).
+ *
+ * @see ViewBehavior#shouldSmoothScroll(int, int)
+ */
+ public Builder setShouldSmoothScroll(boolean shouldSmoothScroll) {
+ mShouldSmoothScroll = shouldSmoothScroll;
+ return this;
+ }
+
/** Build {@link InputDevice}. */
public InputDevice build() {
InputDevice device = new InputDevice(
@@ -745,6 +764,8 @@
range.getResolution());
}
+ device.setShouldSmoothScroll(mShouldSmoothScroll);
+
return device;
}
}
@@ -1123,6 +1144,22 @@
return mMotionRanges;
}
+ /**
+ * Provides the {@link ViewBehavior} for the device.
+ *
+ * <p>This behavior is designed to be obtained using the
+ * {@link InputManager#getInputDeviceViewBehavior(int)} API, to allow associating the behavior
+ * with a {@link Context} (since input device is not associated with a context).
+ * The ability to associate the behavior with a context opens capabilities like linking the
+ * behavior to user settings, for example.
+ *
+ * @hide
+ */
+ @NonNull
+ public ViewBehavior getViewBehavior() {
+ return mViewBehavior;
+ }
+
// Called from native code.
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
private void addMotionRange(int axis, int source,
@@ -1130,6 +1167,11 @@
mMotionRanges.add(new MotionRange(axis, source, min, max, flat, fuzz, resolution));
}
+ // Called from native code.
+ private void setShouldSmoothScroll(boolean shouldSmoothScroll) {
+ mViewBehavior.mShouldSmoothScroll = shouldSmoothScroll;
+ }
+
/**
* Returns the Bluetooth address of this input device, if known.
*
@@ -1447,6 +1489,82 @@
}
}
+ /**
+ * Provides information on how views processing {@link MotionEvent}s generated by this input
+ * device should respond to the events. Use {@link InputManager#getInputDeviceViewBehavior(int)}
+ * to get an instance of the view behavior for an input device.
+ *
+ * <p>See an example below how a {@link View} can use this class to determine and apply the
+ * scrolling behavior for a generic {@link MotionEvent}.
+ *
+ * <pre>{@code
+ * public boolean onGenericMotionEvent(MotionEvent event) {
+ * InputManager manager = context.getSystemService(InputManager.class);
+ * ViewBehavior viewBehavior = manager.getInputDeviceViewBehavior(event.getDeviceId());
+ * // Assume a helper function that tells us which axis to use for scrolling purpose.
+ * int axis = getScrollAxisForGenericMotionEvent(event);
+ * int source = event.getSource();
+ *
+ * boolean shouldSmoothScroll =
+ * viewBehavior != null && viewBehavior.shouldSmoothScroll(axis, source);
+ * // Proceed to running the scrolling logic...
+ * }
+ * }</pre>
+ *
+ * @see InputManager#getInputDeviceViewBehavior(int)
+ */
+ @FlaggedApi(FLAG_INPUT_DEVICE_VIEW_BEHAVIOR_API)
+ public static final class ViewBehavior {
+ private static final boolean DEFAULT_SHOULD_SMOOTH_SCROLL = false;
+
+ private final InputDevice mInputDevice;
+
+ // TODO(b/246946631): implement support for InputDevices to adjust this configuration
+ // by axis and source. When implemented, the axis/source specific config will take
+ // precedence over this global config.
+ /** A global smooth scroll configuration applying to all motion axis and input source. */
+ private boolean mShouldSmoothScroll = DEFAULT_SHOULD_SMOOTH_SCROLL;
+
+ /** @hide */
+ public ViewBehavior(@NonNull InputDevice inputDevice) {
+ mInputDevice = inputDevice;
+ }
+
+ /**
+ * Returns whether a view should smooth scroll when scrolling due to a {@link MotionEvent}
+ * generated by the input device.
+ *
+ * <p>Smooth scroll in this case refers to a scroll that animates the transition between
+ * the starting and ending positions of the scroll. When this method returns {@code true},
+ * views should try to animate a scroll generated by this device at the given axis and with
+ * the given source to produce a good scroll user experience. If this method returns
+ * {@code false}, animating scrolls is not necessary.
+ *
+ * <p>If the input device does not have a {@link MotionRange} with the provided axis and
+ * source, this method returns {@code false}.
+ *
+ * @param axis the {@link MotionEvent} axis whose value is used to get the scroll extent.
+ * @param source the {@link InputDevice} source from which the {@link MotionEvent} that
+ * triggers the scroll came.
+ * @return {@code true} if smooth scrolling should be used for the scroll, or {@code false}
+ * if smooth scrolling is not necessary, or if the provided axis and source combination
+ * is not available for the input device.
+ */
+ @FlaggedApi(FLAG_INPUT_DEVICE_VIEW_BEHAVIOR_API)
+ public boolean shouldSmoothScroll(int axis, int source) {
+ // Note: although we currently do not use axis and source in computing the return value,
+ // we will keep the API params to avoid further public API changes when we start
+ // supporting axis/source configuration. Also, having these params lets OEMs provide
+ // their custom implementation of the API that depends on axis and source.
+
+ // TODO(b/246946631): speed up computation using caching of results.
+ if (mInputDevice.getMotionRange(axis, source) == null) {
+ return false;
+ }
+ return mShouldSmoothScroll;
+ }
+ }
+
@Override
public void writeToParcel(Parcel out, int flags) {
mKeyCharacterMap.writeToParcel(out, flags);
@@ -1484,6 +1602,8 @@
out.writeFloat(range.mFuzz);
out.writeFloat(range.mResolution);
}
+
+ out.writeBoolean(mViewBehavior.mShouldSmoothScroll);
}
@Override
diff --git a/core/java/android/view/InputWindowHandle.java b/core/java/android/view/InputWindowHandle.java
index 59ec605..9db1060 100644
--- a/core/java/android/view/InputWindowHandle.java
+++ b/core/java/android/view/InputWindowHandle.java
@@ -162,6 +162,12 @@
public float alpha;
/**
+ * Sets a property on this window indicating that its visible region should be considered when
+ * computing TrustedPresentation Thresholds.
+ */
+ public boolean canOccludePresentation;
+
+ /**
* The input token for the window to which focus should be transferred when this input window
* can be successfully focused. If null, this input window will not transfer its focus to
* any other window.
@@ -205,6 +211,7 @@
focusTransferTarget = other.focusTransferTarget;
contentSize = new Size(other.contentSize.getWidth(), other.contentSize.getHeight());
alpha = other.alpha;
+ canOccludePresentation = other.canOccludePresentation;
}
@Override
@@ -219,6 +226,7 @@
.append(", isClone=").append((inputConfig & InputConfig.CLONE) != 0)
.append(", contentSize=").append(contentSize)
.append(", alpha=").append(alpha)
+ .append(", canOccludePresentation=").append(canOccludePresentation)
.toString();
}
diff --git a/core/java/android/view/ScreenRecordingCallbacks.java b/core/java/android/view/ScreenRecordingCallbacks.java
new file mode 100644
index 0000000..ee55737
--- /dev/null
+++ b/core/java/android/view/ScreenRecordingCallbacks.java
@@ -0,0 +1,146 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import static android.Manifest.permission.DETECT_SCREEN_RECORDING;
+import static android.view.WindowManager.SCREEN_RECORDING_STATE_NOT_VISIBLE;
+import static android.view.WindowManager.SCREEN_RECORDING_STATE_VISIBLE;
+
+import android.annotation.CallbackExecutor;
+import android.annotation.NonNull;
+import android.annotation.RequiresPermission;
+import android.os.Binder;
+import android.os.RemoteException;
+import android.util.ArrayMap;
+import android.view.WindowManager.ScreenRecordingState;
+import android.window.IScreenRecordingCallback;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+import java.util.concurrent.Executor;
+import java.util.function.Consumer;
+
+/**
+ * This class is responsible for calling app-registered screen recording callbacks. This class
+ * registers a single screen recording callback with WindowManagerService and calls the
+ * app-registered callbacks whenever that WindowManagerService callback is called.
+ *
+ * @hide
+ */
+public final class ScreenRecordingCallbacks {
+
+ private static ScreenRecordingCallbacks sInstance;
+ private static final Object sLock = new Object();
+
+ private final ArrayMap<Consumer<@ScreenRecordingState Integer>, Executor> mCallbacks =
+ new ArrayMap<>();
+
+ private IScreenRecordingCallback mCallbackNotifier;
+ private @ScreenRecordingState int mState = SCREEN_RECORDING_STATE_NOT_VISIBLE;
+
+ private ScreenRecordingCallbacks() {}
+
+ private static @NonNull IWindowManager getWindowManagerService() {
+ return Objects.requireNonNull(WindowManagerGlobal.getWindowManagerService());
+ }
+
+ static ScreenRecordingCallbacks getInstance() {
+ synchronized (sLock) {
+ if (sInstance == null) {
+ sInstance = new ScreenRecordingCallbacks();
+ }
+ return sInstance;
+ }
+ }
+
+ @RequiresPermission(DETECT_SCREEN_RECORDING)
+ @ScreenRecordingState
+ int addCallback(
+ @NonNull @CallbackExecutor Executor executor,
+ @NonNull Consumer<@ScreenRecordingState Integer> callback) {
+ synchronized (sLock) {
+ if (mCallbackNotifier == null) {
+ mCallbackNotifier =
+ new IScreenRecordingCallback.Stub() {
+ @Override
+ public void onScreenRecordingStateChanged(
+ boolean visibleInScreenRecording) {
+ int state =
+ visibleInScreenRecording
+ ? SCREEN_RECORDING_STATE_VISIBLE
+ : SCREEN_RECORDING_STATE_NOT_VISIBLE;
+ notifyCallbacks(state);
+ }
+ };
+ try {
+ boolean visibleInScreenRecording =
+ getWindowManagerService()
+ .registerScreenRecordingCallback(mCallbackNotifier);
+ mState =
+ visibleInScreenRecording
+ ? SCREEN_RECORDING_STATE_VISIBLE
+ : SCREEN_RECORDING_STATE_NOT_VISIBLE;
+ } catch (RemoteException e) {
+ e.rethrowFromSystemServer();
+ }
+ }
+ mCallbacks.put(callback, executor);
+ return mState;
+ }
+ }
+
+ @RequiresPermission(DETECT_SCREEN_RECORDING)
+ void removeCallback(@NonNull Consumer<@ScreenRecordingState Integer> callback) {
+ synchronized (sLock) {
+ mCallbacks.remove(callback);
+ if (mCallbacks.isEmpty()) {
+ try {
+ getWindowManagerService().unregisterScreenRecordingCallback(mCallbackNotifier);
+ } catch (RemoteException e) {
+ e.rethrowFromSystemServer();
+ }
+ mCallbackNotifier = null;
+ }
+ }
+ }
+
+ private void notifyCallbacks(@ScreenRecordingState int state) {
+ List<Runnable> callbacks;
+ synchronized (sLock) {
+ mState = state;
+ if (mCallbacks.isEmpty()) {
+ return;
+ }
+
+ callbacks = new ArrayList<>();
+ for (int i = 0; i < mCallbacks.size(); i++) {
+ Consumer<Integer> callback = mCallbacks.keyAt(i);
+ Executor executor = mCallbacks.valueAt(i);
+ callbacks.add(() -> executor.execute(() -> callback.accept(state)));
+ }
+ }
+ final long token = Binder.clearCallingIdentity();
+ try {
+ for (int i = 0; i < callbacks.size(); i++) {
+ callbacks.get(i).run();
+ }
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ }
+}
diff --git a/core/java/android/view/SurfaceControl.java b/core/java/android/view/SurfaceControl.java
index 3ed0385..3c0ac06 100644
--- a/core/java/android/view/SurfaceControl.java
+++ b/core/java/android/view/SurfaceControl.java
@@ -168,6 +168,8 @@
boolean isTrustedOverlay);
private static native void nativeSetDropInputMode(
long transactionObj, long nativeObject, int flags);
+ private static native void nativeSetCanOccludePresentation(long transactionObj,
+ long nativeObject, boolean canOccludePresentation);
private static native void nativeSurfaceFlushJankData(long nativeSurfaceObject);
private static native boolean nativeClearContentFrameStats(long nativeObject);
private static native boolean nativeGetContentFrameStats(long nativeObject, WindowContentFrameStats outStats);
@@ -589,6 +591,28 @@
public static final int DISPLAY_DECORATION = 0x00000200;
/**
+ * Ignore any destination frame set on the layer. This is used when the buffer scaling mode
+ * is freeze and the destination frame is applied asynchronously with the buffer submission.
+ * This is needed to maintain compatibility for SurfaceView scaling behavior.
+ * See SurfaceView scaling behavior for more details.
+ * @hide
+ */
+ public static final int IGNORE_DESTINATION_FRAME = 0x00000400;
+
+ /**
+ * Special casing for layer that is a refresh rate indicator
+ * @hide
+ */
+ public static final int LAYER_IS_REFRESH_RATE_INDICATOR = 0x00000800;
+
+ /**
+ * Sets a property on this layer indicating that its visible region should be considered when
+ * computing TrustedPresentation Thresholds
+ * @hide
+ */
+ public static final int CAN_OCCLUDE_PRESENTATION = 0x00001000;
+
+ /**
* Surface creation flag: Creates a surface where color components are interpreted
* as "non pre-multiplied" by their alpha channel. Of course this flag is
* meaningless for surfaces without an alpha channel. By default
@@ -4163,6 +4187,29 @@
}
/**
+ * Sets a property on this SurfaceControl and all its children indicating that the visible
+ * region of this SurfaceControl should be considered when computing TrustedPresentation
+ * Thresholds.
+ * <p>
+ * API Guidance:
+ * The goal of this API is to identify windows that can be used to occlude content on
+ * another window. This includes windows controlled by the user or the system. If the window
+ * is transient, like Toast or notification shade, the window should not set this flag since
+ * the user or the app cannot use the window to occlude content in a persistent manner. All
+ * apps should have this flag set.
+ * <p>
+ * The caller must hold the ACCESS_SURFACE_FLINGER permission.
+ * @hide
+ */
+ public Transaction setCanOccludePresentation(SurfaceControl sc,
+ boolean canOccludePresentation) {
+ checkPreconditions(sc);
+ final int value = (canOccludePresentation) ? CAN_OCCLUDE_PRESENTATION : 0;
+ nativeSetFlags(mNativeObject, sc.mNativeObject, value, CAN_OCCLUDE_PRESENTATION);
+ return this;
+ }
+
+ /**
* Sends a flush jank data transaction for the given surface.
* @hide
*/
diff --git a/core/java/android/view/TextureView.java b/core/java/android/view/TextureView.java
index 163dfa2..021bbf7 100644
--- a/core/java/android/view/TextureView.java
+++ b/core/java/android/view/TextureView.java
@@ -16,7 +16,6 @@
package android.view;
-import android.annotation.FloatRange;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.compat.annotation.UnsupportedAppUsage;
@@ -197,7 +196,6 @@
private Canvas mCanvas;
private int mSaveCount;
- @FloatRange(from = 0.0) float mFrameRate;
@Surface.FrameRateCompatibility int mFrameRateCompatibility;
private final Object[] mNativeWindowLock = new Object[0];
@@ -473,13 +471,13 @@
mLayer.setSurfaceTexture(mSurface);
mSurface.setDefaultBufferSize(getWidth(), getHeight());
mSurface.setOnFrameAvailableListener(mUpdateListener, mAttachInfo.mHandler);
- if (Flags.toolkitSetFrameRate()) {
+ if (Flags.toolkitSetFrameRateReadOnly()) {
mSurface.setOnSetFrameRateListener(
(surfaceTexture, frameRate, compatibility, strategy) -> {
if (Trace.isTagEnabled(Trace.TRACE_TAG_VIEW)) {
Trace.instant(Trace.TRACE_TAG_VIEW, "setFrameRate: " + frameRate);
}
- mFrameRate = frameRate;
+ setRequestedFrameRate(frameRate);
mFrameRateCompatibility = compatibility;
}, mAttachInfo.mHandler);
}
diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java
index 0d2c2cc..5c5817f 100644
--- a/core/java/android/view/View.java
+++ b/core/java/android/view/View.java
@@ -5537,10 +5537,20 @@
*/
private static final float FRAME_RATE_SIZE_PERCENTAGE_THRESHOLD = 0.07f;
+
+ private static final long INFREQUENT_UPDATE_INTERVAL_MILLIS = 100;
+ private static final int INFREQUENT_UPDATE_COUNTS = 2;
+
// The preferred frame rate of the view that is mainly used for
// touch boosting, view velocity handling, and TextureView.
private float mPreferredFrameRate = REQUESTED_FRAME_RATE_CATEGORY_DEFAULT;
+ private int mInfrequentUpdateCount = 0;
+ private long mLastUpdateTimeMillis = 0;
+ private long mMinusOneFrameIntervalMillis = 0;
+ private long mMinusTwoFrameIntervalMillis = 0;
+ private int mLastFrameRateCategory = FRAME_RATE_CATEGORY_HIGH;
+
@FlaggedApi(FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY)
public static final float REQUESTED_FRAME_RATE_CATEGORY_DEFAULT = 0;
@FlaggedApi(FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY)
@@ -15858,20 +15868,7 @@
}
if (onFilterTouchEventForSecurity(event)) {
- if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
- result = true;
- }
- //noinspection SimplifiableIfStatement
- ListenerInfo li = mListenerInfo;
- if (li != null && li.mOnTouchListener != null
- && (mViewFlags & ENABLED_MASK) == ENABLED
- && li.mOnTouchListener.onTouch(this, event)) {
- result = true;
- }
-
- if (!result && onTouchEvent(event)) {
- result = true;
- }
+ result = performOnTouchCallback(event);
}
if (!result && mInputEventConsistencyVerifier != null) {
@@ -15890,6 +15887,36 @@
return result;
}
+ /**
+ * Returns {@code true} if the {@link MotionEvent} from {@link #dispatchTouchEvent} was
+ * handled by this view.
+ */
+ private boolean performOnTouchCallback(MotionEvent event) {
+ boolean handled = false;
+ if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
+ handled = true;
+ }
+ //noinspection SimplifiableIfStatement
+ ListenerInfo li = mListenerInfo;
+ if (li != null && li.mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED) {
+ try {
+ Trace.traceBegin(Trace.TRACE_TAG_VIEW, "View.onTouchListener#onTouch");
+ handled = li.mOnTouchListener.onTouch(this, event);
+ } finally {
+ Trace.traceEnd(Trace.TRACE_TAG_VIEW);
+ }
+ }
+ if (handled) {
+ return true;
+ }
+ try {
+ Trace.traceBegin(Trace.TRACE_TAG_VIEW, "View#onTouchEvent");
+ return onTouchEvent(event);
+ } finally {
+ Trace.traceEnd(Trace.TRACE_TAG_VIEW);
+ }
+ }
+
boolean isAccessibilityFocusedViewOrHost() {
return isAccessibilityFocused() || (getViewRootImpl() != null && getViewRootImpl()
.getAccessibilityFocusedHost() == this);
@@ -20253,7 +20280,10 @@
}
// For VRR to vote the preferred frame rate
- votePreferredFrameRate();
+ if (sToolkitSetFrameRateReadOnlyFlagValue) {
+ updateInfrequentCount();
+ votePreferredFrameRate();
+ }
// Reset content capture caches
mPrivateFlags4 &= ~PFLAG4_CONTENT_CAPTURE_IMPORTANCE_MASK;
@@ -20358,7 +20388,10 @@
protected void damageInParent() {
if (mParent != null && mAttachInfo != null) {
// For VRR to vote the preferred frame rate
- votePreferredFrameRate();
+ if (sToolkitSetFrameRateReadOnlyFlagValue) {
+ updateInfrequentCount();
+ votePreferredFrameRate();
+ }
mParent.onDescendantInvalidated(this, this);
}
}
@@ -33131,11 +33164,20 @@
}
private int calculateFrameRateCategory(float sizePercentage) {
- if (sizePercentage <= FRAME_RATE_SIZE_PERCENTAGE_THRESHOLD) {
- return FRAME_RATE_CATEGORY_LOW;
- } else {
+ if (mMinusTwoFrameIntervalMillis + mMinusOneFrameIntervalMillis
+ < INFREQUENT_UPDATE_INTERVAL_MILLIS) {
+ if (sizePercentage <= FRAME_RATE_SIZE_PERCENTAGE_THRESHOLD) {
+ return FRAME_RATE_CATEGORY_NORMAL;
+ } else {
+ return FRAME_RATE_CATEGORY_HIGH;
+ }
+ }
+
+ if (mInfrequentUpdateCount == INFREQUENT_UPDATE_COUNTS) {
return FRAME_RATE_CATEGORY_NORMAL;
}
+
+ return mLastFrameRateCategory;
}
private void votePreferredFrameRate() {
@@ -33144,22 +33186,22 @@
float sizePercentage = getSizePercentage();
int frameRateCateogry = calculateFrameRateCategory(sizePercentage);
if (viewRootImpl != null && sizePercentage > 0) {
- if (sToolkitSetFrameRateReadOnlyFlagValue) {
- if (mPreferredFrameRate < 0) {
- if (mPreferredFrameRate == REQUESTED_FRAME_RATE_CATEGORY_NO_PREFERENCE) {
- frameRateCateogry = FRAME_RATE_CATEGORY_NO_PREFERENCE;
- } else if (mPreferredFrameRate == REQUESTED_FRAME_RATE_CATEGORY_LOW) {
- frameRateCateogry = FRAME_RATE_CATEGORY_LOW;
- } else if (mPreferredFrameRate == REQUESTED_FRAME_RATE_CATEGORY_NORMAL) {
- frameRateCateogry = FRAME_RATE_CATEGORY_NORMAL;
- } else if (mPreferredFrameRate == REQUESTED_FRAME_RATE_CATEGORY_HIGH) {
- frameRateCateogry = FRAME_RATE_CATEGORY_HIGH;
- }
- } else {
- viewRootImpl.votePreferredFrameRate(mPreferredFrameRate);
+ if (mPreferredFrameRate < 0) {
+ if (mPreferredFrameRate == REQUESTED_FRAME_RATE_CATEGORY_NO_PREFERENCE) {
+ frameRateCateogry = FRAME_RATE_CATEGORY_NO_PREFERENCE;
+ } else if (mPreferredFrameRate == REQUESTED_FRAME_RATE_CATEGORY_LOW) {
+ frameRateCateogry = FRAME_RATE_CATEGORY_LOW;
+ } else if (mPreferredFrameRate == REQUESTED_FRAME_RATE_CATEGORY_NORMAL) {
+ frameRateCateogry = FRAME_RATE_CATEGORY_NORMAL;
+ } else if (mPreferredFrameRate == REQUESTED_FRAME_RATE_CATEGORY_HIGH) {
+ frameRateCateogry = FRAME_RATE_CATEGORY_HIGH;
}
- viewRootImpl.votePreferredFrameRateCategory(frameRateCateogry);
+ } else {
+ viewRootImpl.votePreferredFrameRate(mPreferredFrameRate);
}
+ viewRootImpl.votePreferredFrameRateCategory(frameRateCateogry);
+ mLastFrameRateCategory = frameRateCateogry;
+
if (sToolkitMetricsForFrameRateDecisionFlagValue) {
viewRootImpl.recordViewPercentage(sizePercentage);
}
@@ -33238,4 +33280,27 @@
}
return 0;
}
+
+ /**
+ * This function is mainly used for migrating infrequent layer lagic
+ * from SurfaceFlinger to Toolkit.
+ * The infrequent layter logic includes:
+ * - NORMAL for infrequent update: FT2-FT1 > 100 && FT3-FT2 > 100.
+ * - HIGH/NORMAL based on size for frequent update: (FT3-FT2) + (FT2 - FT1) < 100.
+ * - otherwise, use the previous category value.
+ */
+ private void updateInfrequentCount() {
+ long currentTimeMillis = AnimationUtils.currentAnimationTimeMillis();
+ long timeIntervalMillis = currentTimeMillis - mLastUpdateTimeMillis;
+ mMinusTwoFrameIntervalMillis = mMinusOneFrameIntervalMillis;
+ mMinusOneFrameIntervalMillis = timeIntervalMillis;
+
+ mLastUpdateTimeMillis = currentTimeMillis;
+ if (timeIntervalMillis >= INFREQUENT_UPDATE_INTERVAL_MILLIS) {
+ mInfrequentUpdateCount = mInfrequentUpdateCount == INFREQUENT_UPDATE_COUNTS
+ ? mInfrequentUpdateCount : mInfrequentUpdateCount + 1;
+ } else {
+ mInfrequentUpdateCount = 0;
+ }
+ }
}
diff --git a/core/java/android/view/ViewDebug.java b/core/java/android/view/ViewDebug.java
index 25e0eca..4f1fb40 100644
--- a/core/java/android/view/ViewDebug.java
+++ b/core/java/android/view/ViewDebug.java
@@ -7,7 +7,7 @@
*
* http://www.apache.org/licenses/LICENSE-2.0
*
- * Unless required by applicable law or agreed to in writing, software
+ * Unless required by applicable law or agreed to in writing, softwareViewDebug
* 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
@@ -16,6 +16,8 @@
package android.view;
+import static com.android.internal.util.Preconditions.checkArgument;
+
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.TestApi;
@@ -34,10 +36,13 @@
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
+import android.util.Base64;
import android.util.DisplayMetrics;
import android.util.Log;
import android.util.TypedValue;
+import com.android.internal.annotations.VisibleForTesting;
+
import libcore.util.HexEncoding;
import java.io.BufferedOutputStream;
@@ -54,9 +59,11 @@
import java.lang.annotation.Target;
import java.lang.reflect.AccessibleObject;
import java.lang.reflect.Field;
-import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Member;
import java.lang.reflect.Method;
+import java.nio.BufferUnderflowException;
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
import java.util.ArrayDeque;
import java.util.Arrays;
import java.util.HashMap;
@@ -67,7 +74,6 @@
import java.util.concurrent.FutureTask;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
-import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.Function;
import java.util.stream.Stream;
@@ -76,6 +82,9 @@
* Various debugging/tracing tools related to {@link View} and the view hierarchy.
*/
public class ViewDebug {
+
+ private static final String TAG = "ViewDebug";
+
/**
* @deprecated This flag is now unused
*/
@@ -425,6 +434,7 @@
private static final String REMOTE_PROFILE = "PROFILE";
private static final String REMOTE_COMMAND_CAPTURE_LAYERS = "CAPTURE_LAYERS";
private static final String REMOTE_COMMAND_OUTPUT_DISPLAYLIST = "OUTPUT_DISPLAYLIST";
+ private static final String REMOTE_COMMAND_INVOKE_METHOD = "INVOKE_METHOD";
private static HashMap<Class<?>, PropertyInfo<ExportedProperty, ?>[]> sExportProperties;
private static HashMap<Class<?>, PropertyInfo<CapturedViewProperty, ?>[]>
@@ -555,6 +565,8 @@
requestLayout(view, params[0]);
} else if (REMOTE_PROFILE.equalsIgnoreCase(command)) {
profile(view, clientStream, params[0]);
+ } else if (REMOTE_COMMAND_INVOKE_METHOD.equals(command)) {
+ invokeViewMethod(view, clientStream, params);
}
}
}
@@ -1825,46 +1837,84 @@
Log.d(tag, sb.toString());
}
+ private static void invokeViewMethod(View root, OutputStream clientStream, String[] params)
+ throws IOException {
+ BufferedWriter out = new BufferedWriter(new OutputStreamWriter(clientStream), 32 * 1024);
+ try {
+ if (params.length < 2) {
+ throw new IllegalArgumentException("Missing parameter");
+ }
+ View targetView = findView(root, params[0]);
+ if (targetView == null) {
+ throw new IllegalArgumentException("View not found: " + params[0]);
+ }
+ String method = params[1];
+ ByteBuffer args = ByteBuffer.wrap(params.length < 2
+ ? new byte[0]
+ : Base64.decode(params[2], Base64.NO_WRAP));
+ byte[] result = invokeViewMethod(targetView, method, args);
+ out.write("1");
+ out.newLine();
+ out.write(Base64.encodeToString(result, Base64.NO_WRAP));
+ out.newLine();
+ } catch (Exception e) {
+ out.write("-1");
+ out.newLine();
+ out.write(e.getMessage());
+ out.newLine();
+ } finally {
+ out.close();
+ }
+ }
+
/**
* Invoke a particular method on given view.
* The given method is always invoked on the UI thread. The caller thread will stall until the
* method invocation is complete. Returns an object equal to the result of the method
* invocation, null if the method is declared to return void
+ * @param params all the method parameters encoded in a byteArray
* @throws Exception if the method invocation caused any exception
* @hide
*/
- public static Object invokeViewMethod(final View view, final Method method,
- final Object[] args) {
- final CountDownLatch latch = new CountDownLatch(1);
- final AtomicReference<Object> result = new AtomicReference<Object>();
- final AtomicReference<Throwable> exception = new AtomicReference<Throwable>();
+ public static byte[] invokeViewMethod(View targetView, String methodName, ByteBuffer params)
+ throws ViewMethodInvocationSerializationException {
+ Class<?>[] argTypes;
+ Object[] args;
+ if (!params.hasRemaining()) {
+ argTypes = new Class<?>[0];
+ args = new Object[0];
+ } else {
+ int nArgs = params.getInt();
+ argTypes = new Class<?>[nArgs];
+ args = new Object[nArgs];
- view.post(new Runnable() {
- @Override
- public void run() {
- try {
- result.set(method.invoke(view, args));
- } catch (InvocationTargetException e) {
- exception.set(e.getCause());
- } catch (Exception e) {
- exception.set(e);
- }
+ deserializeMethodParameters(args, argTypes, params);
+ }
- latch.countDown();
- }
- });
+ Method method;
+ try {
+ method = targetView.getClass().getMethod(methodName, argTypes);
+ } catch (NoSuchMethodException e) {
+ Log.e(TAG, "No such method: " + e.getMessage());
+ throw new ViewMethodInvocationSerializationException(
+ "No such method: " + e.getMessage());
+ }
try {
- latch.await();
- } catch (InterruptedException e) {
- throw new RuntimeException(e);
+ // Invoke the method on Views handler
+ FutureTask<Object> task = new FutureTask<>(() -> method.invoke(targetView, args));
+ targetView.post(task);
+ Object result = task.get();
+ Class<?> returnType = method.getReturnType();
+ return serializeReturnValue(returnType, returnType.cast(result));
+ } catch (Exception e) {
+ Log.e(TAG, "Exception while invoking method: " + e.getCause().getMessage());
+ String msg = e.getCause().getMessage();
+ if (msg == null) {
+ msg = e.getCause().toString();
+ }
+ throw new RuntimeException(msg);
}
-
- if (exception.get() != null) {
- throw new RuntimeException(exception.get());
- }
-
- return result.get();
}
/**
@@ -1961,4 +2011,175 @@
*/
Bitmap createBitmap();
}
+
+ /**
+ * Deserializes parameters according to the VUOP_INVOKE_VIEW_METHOD protocol the {@code in}
+ * buffer.
+ *
+ * The length of {@code args} determines how many arguments are read. The {@code argTypes} must
+ * be the same length, and will be set to the argument types of the data read.
+ *
+ * @hide
+ */
+ @VisibleForTesting
+ public static void deserializeMethodParameters(
+ Object[] args, Class<?>[] argTypes, ByteBuffer in) throws
+ ViewMethodInvocationSerializationException {
+ checkArgument(args.length == argTypes.length);
+
+ for (int i = 0; i < args.length; i++) {
+ char typeSignature = in.getChar();
+ boolean isArray = typeSignature == SIG_ARRAY;
+ if (isArray) {
+ char arrayType = in.getChar();
+ if (arrayType != SIG_BYTE) {
+ // This implementation only supports byte-arrays for now.
+ throw new ViewMethodInvocationSerializationException(
+ "Unsupported array parameter type (" + typeSignature
+ + ") to invoke view method @argument " + i);
+ }
+
+ int arrayLength = in.getInt();
+ if (arrayLength > in.remaining()) {
+ // The sender did not actually sent the specified amount of bytes. This
+ // avoids a malformed packet to trigger an out-of-memory error.
+ throw new BufferUnderflowException();
+ }
+
+ byte[] byteArray = new byte[arrayLength];
+ in.get(byteArray);
+
+ argTypes[i] = byte[].class;
+ args[i] = byteArray;
+ } else {
+ switch (typeSignature) {
+ case SIG_BOOLEAN:
+ argTypes[i] = boolean.class;
+ args[i] = in.get() != 0;
+ break;
+ case SIG_BYTE:
+ argTypes[i] = byte.class;
+ args[i] = in.get();
+ break;
+ case SIG_CHAR:
+ argTypes[i] = char.class;
+ args[i] = in.getChar();
+ break;
+ case SIG_SHORT:
+ argTypes[i] = short.class;
+ args[i] = in.getShort();
+ break;
+ case SIG_INT:
+ argTypes[i] = int.class;
+ args[i] = in.getInt();
+ break;
+ case SIG_LONG:
+ argTypes[i] = long.class;
+ args[i] = in.getLong();
+ break;
+ case SIG_FLOAT:
+ argTypes[i] = float.class;
+ args[i] = in.getFloat();
+ break;
+ case SIG_DOUBLE:
+ argTypes[i] = double.class;
+ args[i] = in.getDouble();
+ break;
+ case SIG_STRING: {
+ argTypes[i] = String.class;
+ int stringUtf8ByteCount = Short.toUnsignedInt(in.getShort());
+ byte[] rawStringBuffer = new byte[stringUtf8ByteCount];
+ in.get(rawStringBuffer);
+ args[i] = new String(rawStringBuffer, StandardCharsets.UTF_8);
+ break;
+ }
+ default:
+ Log.e(TAG, "arg " + i + ", unrecognized type: " + typeSignature);
+ throw new ViewMethodInvocationSerializationException(
+ "Unsupported parameter type (" + typeSignature
+ + ") to invoke view method.");
+ }
+ }
+
+ }
+ }
+
+ /**
+ * Serializes {@code value} to the wire protocol of VUOP_INVOKE_VIEW_METHOD.
+ * @hide
+ */
+ @VisibleForTesting
+ public static byte[] serializeReturnValue(Class<?> type, Object value)
+ throws ViewMethodInvocationSerializationException, IOException {
+ ByteArrayOutputStream byteOutStream = new ByteArrayOutputStream(1024);
+ DataOutputStream dos = new DataOutputStream(byteOutStream);
+
+ if (type.isArray()) {
+ if (!type.equals(byte[].class)) {
+ // Only byte arrays are supported currently.
+ throw new ViewMethodInvocationSerializationException(
+ "Unsupported array return type (" + type + ")");
+ }
+ byte[] byteArray = (byte[]) value;
+ dos.writeChar(SIG_ARRAY);
+ dos.writeChar(SIG_BYTE);
+ dos.writeInt(byteArray.length);
+ dos.write(byteArray);
+ } else if (boolean.class.equals(type)) {
+ dos.writeChar(SIG_BOOLEAN);
+ dos.write((boolean) value ? 1 : 0);
+ } else if (byte.class.equals(type)) {
+ dos.writeChar(SIG_BYTE);
+ dos.writeByte((byte) value);
+ } else if (char.class.equals(type)) {
+ dos.writeChar(SIG_CHAR);
+ dos.writeChar((char) value);
+ } else if (short.class.equals(type)) {
+ dos.writeChar(SIG_SHORT);
+ dos.writeShort((short) value);
+ } else if (int.class.equals(type)) {
+ dos.writeChar(SIG_INT);
+ dos.writeInt((int) value);
+ } else if (long.class.equals(type)) {
+ dos.writeChar(SIG_LONG);
+ dos.writeLong((long) value);
+ } else if (double.class.equals(type)) {
+ dos.writeChar(SIG_DOUBLE);
+ dos.writeDouble((double) value);
+ } else if (float.class.equals(type)) {
+ dos.writeChar(SIG_FLOAT);
+ dos.writeFloat((float) value);
+ } else if (String.class.equals(type)) {
+ dos.writeChar(SIG_STRING);
+ dos.writeUTF(value != null ? (String) value : "");
+ } else {
+ dos.writeChar(SIG_VOID);
+ }
+
+ return byteOutStream.toByteArray();
+ }
+
+ // Prefixes for simple primitives. These match the JNI definitions.
+ private static final char SIG_ARRAY = '[';
+ private static final char SIG_BOOLEAN = 'Z';
+ private static final char SIG_BYTE = 'B';
+ private static final char SIG_SHORT = 'S';
+ private static final char SIG_CHAR = 'C';
+ private static final char SIG_INT = 'I';
+ private static final char SIG_LONG = 'J';
+ private static final char SIG_FLOAT = 'F';
+ private static final char SIG_DOUBLE = 'D';
+ private static final char SIG_VOID = 'V';
+ // Prefixes for some commonly used objects
+ private static final char SIG_STRING = 'R';
+
+ /**
+ * @hide
+ */
+ @VisibleForTesting
+ public static class ViewMethodInvocationSerializationException extends Exception {
+ ViewMethodInvocationSerializationException(String message) {
+ super(message);
+ }
+ }
}
diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java
index ae9c109..e03f857 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -26,6 +26,7 @@
import static android.view.InputDevice.SOURCE_CLASS_NONE;
import static android.view.InsetsSource.ID_IME;
import static android.view.Surface.FRAME_RATE_CATEGORY_HIGH;
+import static android.view.Surface.FRAME_RATE_CATEGORY_HIGH_HINT;
import static android.view.Surface.FRAME_RATE_CATEGORY_NO_PREFERENCE;
import static android.view.View.PFLAG_DRAW_ANIMATION;
import static android.view.View.SYSTEM_UI_FLAG_FULLSCREEN;
@@ -1013,8 +1014,10 @@
// Used to check if there were any view invalidations in
// the previous time frame (FRAME_RATE_IDLENESS_REEVALUATE_TIME).
private boolean mHasInvalidation = false;
- // Used to check if it is in the touch boosting period.
+ // Used to check if it is in the frame rate boosting period.
private boolean mIsFrameRateBoosting = false;
+ // Used to check if it is in touch boosting period.
+ private boolean mIsTouchBoosting = false;
// Used to check if there is a message in the message queue
// for idleness handling.
private boolean mHasIdledMessage = false;
@@ -1024,6 +1027,16 @@
private static final int FRAME_RATE_IDLENESS_CHECK_TIME_MILLIS = 500;
// time for revaluating the idle status before lowering the frame rate.
private static final int FRAME_RATE_IDLENESS_REEVALUATE_TIME = 500;
+ // time for evaluating the interval between current time and
+ // the time when frame rate was set previously.
+ private static final int FRAME_RATE_SETTING_REEVALUATE_TIME = 100;
+
+ /*
+ * the variables below are used to determine whther a dVRR feature should be enabled
+ */
+
+ // Used to determine whether to suppress boost on typing
+ private boolean mShouldSuppressBoostOnTyping = false;
/**
* A temporary object used so relayoutWindow can return the latest SyncSeqId
@@ -4073,7 +4086,6 @@
setPreferredFrameRate(mPreferredFrameRate);
setPreferredFrameRateCategory(mPreferredFrameRateCategory);
mPreferredFrameRateCategory = FRAME_RATE_CATEGORY_NO_PREFERENCE;
- mPreferredFrameRate = 0;
}
private void createSyncIfNeeded() {
@@ -6127,6 +6139,7 @@
private static final int MSG_TOUCH_BOOST_TIMEOUT = 39;
private static final int MSG_CHECK_INVALIDATION_IDLE = 40;
private static final int MSG_REFRESH_POINTER_ICON = 41;
+ private static final int MSG_FRAME_RATE_SETTING = 42;
final class ViewRootHandler extends Handler {
@Override
@@ -6438,11 +6451,12 @@
* Lower the frame rate after the boosting period (FRAME_RATE_TOUCH_BOOST_TIME).
*/
mIsFrameRateBoosting = false;
+ mIsTouchBoosting = false;
setPreferredFrameRateCategory(Math.max(mPreferredFrameRateCategory,
mLastPreferredFrameRateCategory));
break;
case MSG_CHECK_INVALIDATION_IDLE:
- if (!mHasInvalidation && !mIsFrameRateBoosting) {
+ if (!mHasInvalidation && !mIsFrameRateBoosting && !mIsTouchBoosting) {
mPreferredFrameRateCategory = FRAME_RATE_CATEGORY_NO_PREFERENCE;
setPreferredFrameRateCategory(mPreferredFrameRateCategory);
mHasIdledMessage = false;
@@ -6465,6 +6479,10 @@
}
updatePointerIcon(mPointerIconEvent);
break;
+ case MSG_FRAME_RATE_SETTING:
+ mPreferredFrameRate = 0;
+ setPreferredFrameRate(mPreferredFrameRate);
+ break;
}
}
}
@@ -7475,7 +7493,7 @@
// For the variable refresh rate project
if (handled && shouldTouchBoost(action, mWindowAttributes.type)) {
// set the frame rate to the maximum value.
- mIsFrameRateBoosting = true;
+ mIsTouchBoosting = true;
setPreferredFrameRateCategory(mPreferredFrameRateCategory);
}
/**
@@ -7483,7 +7501,7 @@
* MotionEvent.ACTION_CANCEL is detected.
* Not using ACTION_MOVE to avoid checking and sending messages too frequently.
*/
- if (mIsFrameRateBoosting && (action == MotionEvent.ACTION_UP
+ if (mIsTouchBoosting && (action == MotionEvent.ACTION_UP
|| action == MotionEvent.ACTION_CANCEL)) {
mHandler.removeMessages(MSG_TOUCH_BOOST_TIMEOUT);
mHandler.sendEmptyMessageDelayed(MSG_TOUCH_BOOST_TIMEOUT,
@@ -11532,15 +11550,6 @@
event.setContentChangeTypes(mChangeTypes);
if (mAction.isPresent()) event.setAction(mAction.getAsInt());
if (AccessibilityEvent.DEBUG_ORIGIN) event.originStackTrace = mOrigin;
-
- if (fixMergedContentChangeEvent()) {
- if ((mChangeTypes & AccessibilityEvent.CONTENT_CHANGE_TYPE_SUBTREE) != 0) {
- final View importantParent = source.getSelfOrParentImportantForA11y();
- if (importantParent != null) {
- source = importantParent;
- }
- }
- }
source.sendAccessibilityEventUnchecked(event);
} else {
mLastEventTimeMillis = 0;
@@ -11577,6 +11586,9 @@
if (mSource != null) {
if (fixMergedContentChangeEvent()) {
View newSource = getCommonPredecessor(mSource, source);
+ if (newSource != null) {
+ newSource = newSource.getSelfOrParentImportantForA11y();
+ }
if (newSource == null) {
// If there is no common predecessor, then mSource points to
// a removed view, hence in this case always prefer the source.
@@ -12227,17 +12239,32 @@
return;
}
- int frameRateCategory = mIsFrameRateBoosting || mInsetsAnimationRunning
- ? FRAME_RATE_CATEGORY_HIGH : preferredFrameRateCategory;
+ int frameRateCategory = mIsTouchBoosting
+ ? FRAME_RATE_CATEGORY_HIGH_HINT : preferredFrameRateCategory;
+
+ // FRAME_RATE_CATEGORY_HIGH has a higher precedence than FRAME_RATE_CATEGORY_HIGH_HINT
+ // For now, FRAME_RATE_CATEGORY_HIGH_HINT is used for boosting with user interaction.
+ // FRAME_RATE_CATEGORY_HIGH is for boosting without user interaction
+ // (e.g., Window Initialization).
+ if (mIsFrameRateBoosting || mInsetsAnimationRunning) {
+ frameRateCategory = FRAME_RATE_CATEGORY_HIGH;
+ }
try {
if (mLastPreferredFrameRateCategory != frameRateCategory) {
+ if (Trace.isTagEnabled(Trace.TRACE_TAG_VIEW)) {
+ Trace.traceBegin(
+ Trace.TRACE_TAG_VIEW, "ViewRootImpl#setFrameRateCategory "
+ + frameRateCategory);
+ }
mFrameRateTransaction.setFrameRateCategory(mSurfaceControl,
frameRateCategory, false).applyAsyncUnsafe();
mLastPreferredFrameRateCategory = frameRateCategory;
}
} catch (Exception e) {
Log.e(mTag, "Unable to set frame rate category", e);
+ } finally {
+ Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
if (mPreferredFrameRateCategory != FRAME_RATE_CATEGORY_NO_PREFERENCE && !mHasIdledMessage) {
@@ -12256,12 +12283,19 @@
try {
if (mLastPreferredFrameRate != preferredFrameRate) {
+ if (Trace.isTagEnabled(Trace.TRACE_TAG_VIEW)) {
+ Trace.traceBegin(
+ Trace.TRACE_TAG_VIEW, "ViewRootImpl#setFrameRate "
+ + preferredFrameRate);
+ }
mFrameRateTransaction.setFrameRate(mSurfaceControl, preferredFrameRate,
Surface.FRAME_RATE_COMPATIBILITY_DEFAULT).applyAsyncUnsafe();
mLastPreferredFrameRate = preferredFrameRate;
}
} catch (Exception e) {
Log.e(mTag, "Unable to set frame rate", e);
+ } finally {
+ Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
}
@@ -12278,14 +12312,14 @@
private boolean shouldSetFrameRate() {
// use toolkitSetFrameRate flag to gate the change
- return mPreferredFrameRate > 0 && sToolkitSetFrameRateReadOnlyFlagValue;
+ return sToolkitSetFrameRateReadOnlyFlagValue;
}
private boolean shouldTouchBoost(int motionEventAction, int windowType) {
boolean desiredAction = motionEventAction == MotionEvent.ACTION_DOWN
|| motionEventAction == MotionEvent.ACTION_MOVE
|| motionEventAction == MotionEvent.ACTION_UP;
- boolean undesiredType = windowType == TYPE_INPUT_METHOD;
+ boolean undesiredType = windowType == TYPE_INPUT_METHOD && mShouldSuppressBoostOnTyping;
// use toolkitSetFrameRate flag to gate the change
return desiredAction && !undesiredType && sToolkitSetFrameRateReadOnlyFlagValue
&& getFrameRateBoostOnTouchEnabled();
@@ -12329,6 +12363,9 @@
}
mHasInvalidation = true;
+ mHandler.removeMessages(MSG_FRAME_RATE_SETTING);
+ mHandler.sendEmptyMessageDelayed(MSG_FRAME_RATE_SETTING,
+ FRAME_RATE_SETTING_REEVALUATE_TIME);
}
/**
diff --git a/core/java/android/view/Window.java b/core/java/android/view/Window.java
index 7bae7ec..4ba4ee3 100644
--- a/core/java/android/view/Window.java
+++ b/core/java/android/view/Window.java
@@ -1497,13 +1497,21 @@
* {@link View#SYSTEM_UI_LAYOUT_FLAGS} as well the
* {@link WindowManager.LayoutParams#SOFT_INPUT_ADJUST_RESIZE} flag and fits content according
* to these flags.
- * </p>
+ *
* <p>
* If set to {@code false}, the framework will not fit the content view to the insets and will
* just pass through the {@link WindowInsets} to the content view.
- * </p>
+ *
+ * <p>
+ * If the app targets
+ * {@link android.os.Build.VERSION_CODES#VANILLA_ICE_CREAM VANILLA_ICE_CREAM} or above,
+ * the behavior will be like setting this to {@code false}, and cannot be changed.
+ *
* @param decorFitsSystemWindows Whether the decor view should fit root-level content views for
* insets.
+ * @deprecated Make space in the container views to prevent the critical elements from getting
+ * obscured by {@link WindowInsets.Type#systemBars()} or
+ * {@link WindowInsets.Type#displayCutout()} instead.
*/
public void setDecorFitsSystemWindows(boolean decorFitsSystemWindows) {
}
@@ -2597,7 +2605,9 @@
/**
* @return the color of the status bar.
+ * @deprecated This is no longer needed since the setter is deprecated.
*/
+ @Deprecated
@ColorInt
public abstract int getStatusBarColor();
@@ -2614,13 +2624,29 @@
* {@link android.view.View#SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN}.
* <p>
* The transitionName for the view background will be "android:status:background".
- * </p>
+ *
+ * <p>
+ * If the color is transparent and the window enforces the status bar contrast, the system
+ * will determine whether a scrim is necessary and draw one on behalf of the app to ensure
+ * that the status bar has enough contrast with the contents of this app, and set an appropriate
+ * effective bar background accordingly.
+ *
+ * <p>
+ * If the app targets
+ * {@link android.os.Build.VERSION_CODES#VANILLA_ICE_CREAM VANILLA_ICE_CREAM} or above,
+ * the color will be transparent and cannot be changed.
+ *
+ * @see #setNavigationBarContrastEnforced
+ * @deprecated Draw proper background behind {@link WindowInsets.Type#statusBars()}} instead.
*/
+ @Deprecated
public abstract void setStatusBarColor(@ColorInt int color);
/**
* @return the color of the navigation bar.
+ * @deprecated This is no longer needed since the setter is deprecated.
*/
+ @Deprecated
@ColorInt
public abstract int getNavigationBarColor();
@@ -2637,9 +2663,24 @@
* {@link android.view.View#SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION}.
* <p>
* The transitionName for the view background will be "android:navigation:background".
- * </p>
+ *
+ * <p>
+ * If the color is transparent and the window enforces the navigation bar contrast, the system
+ * will determine whether a scrim is necessary and draw one on behalf of the app to ensure that
+ * the navigation bar has enough contrast with the contents of this app, and set an appropriate
+ * effective bar background accordingly.
+ *
+ * <p>
+ * If the app targets
+ * {@link android.os.Build.VERSION_CODES#VANILLA_ICE_CREAM VANILLA_ICE_CREAM} or above,
+ * the color will be transparent and cannot be changed.
+ *
* @attr ref android.R.styleable#Window_navigationBarColor
+ * @see #setNavigationBarContrastEnforced
+ * @deprecated Draw proper background behind {@link WindowInsets.Type#navigationBars()} or
+ * {@link WindowInsets.Type#tappableElement()} instead.
*/
+ @Deprecated
public abstract void setNavigationBarColor(@ColorInt int color);
/**
@@ -2651,9 +2692,17 @@
* {@link android.view.WindowManager.LayoutParams#FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS} and
* {@link android.view.WindowManager.LayoutParams#FLAG_TRANSLUCENT_NAVIGATION} must not be set.
*
+ * <p>
+ * If the app targets
+ * {@link android.os.Build.VERSION_CODES#VANILLA_ICE_CREAM VANILLA_ICE_CREAM} or above,
+ * the color will be transparent and cannot be changed.
+ *
* @param dividerColor The color of the thin line.
* @attr ref android.R.styleable#Window_navigationBarDividerColor
+ * @deprecated Draw proper background behind {@link WindowInsets.Type#navigationBars()} or
+ * {@link WindowInsets.Type#tappableElement()} instead.
*/
+ @Deprecated
public void setNavigationBarDividerColor(@ColorInt int dividerColor) {
}
@@ -2663,7 +2712,9 @@
* @return The color of the navigation bar divider color.
* @see #setNavigationBarColor(int)
* @attr ref android.R.styleable#Window_navigationBarDividerColor
+ * @deprecated This is no longer needed since the setter is deprecated.
*/
+ @Deprecated
public @ColorInt int getNavigationBarDividerColor() {
return 0;
}
@@ -2682,7 +2733,9 @@
* @see android.R.attr#enforceStatusBarContrast
* @see #isStatusBarContrastEnforced
* @see #setStatusBarColor
+ * @deprecated Draw proper background behind {@link WindowInsets.Type#statusBars()}} instead.
*/
+ @Deprecated
public void setStatusBarContrastEnforced(boolean ensureContrast) {
}
@@ -2697,7 +2750,9 @@
* @see android.R.attr#enforceStatusBarContrast
* @see #setStatusBarContrastEnforced
* @see #setStatusBarColor
+ * @deprecated This is not needed since the setter is deprecated.
*/
+ @Deprecated
public boolean isStatusBarContrastEnforced() {
return false;
}
diff --git a/core/java/android/view/WindowManager.java b/core/java/android/view/WindowManager.java
index c788261..ac2a66e 100644
--- a/core/java/android/view/WindowManager.java
+++ b/core/java/android/view/WindowManager.java
@@ -128,8 +128,10 @@
import com.android.window.flags.Flags;
+import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
@@ -1470,6 +1472,34 @@
"android.window.PROPERTY_ACTIVITY_EMBEDDING_ALLOW_SYSTEM_OVERRIDE";
/**
+ * Activity-level {@link android.content.pm.PackageManager.Property PackageManager.Property}
+ * that declares whether this (embedded) activity allows the system to share its state with the
+ * host app when it is embedded in a different process in
+ * {@link android.R.attr#allowUntrustedActivityEmbedding untrusted mode}.
+ *
+ * <p>If this property is "true", the host app may receive event callbacks for the activity
+ * state change, including the reparent event and the component name of the activity, which are
+ * required to restore the embedding state after the embedded activity exits picture-in-picture
+ * mode. This property does not share any of the activity content with the host. Note that, for
+ * {@link android.R.attr#knownActivityEmbeddingCerts trusted embedding}, the reparent event and
+ * the component name are always shared with the host regardless of the value of this property.
+ *
+ * <p>The default value is {@code false}.
+ *
+ * <p><b>Syntax:</b>
+ * <pre>
+ * <activity>
+ * <property
+ * android:name="android.window.PROPERTY_ALLOW_UNTRUSTED_ACTIVITY_EMBEDDING_STATE_SHARING"
+ * android:value="true|false"/>
+ * </activity>
+ * </pre>
+ */
+ @FlaggedApi(Flags.FLAG_UNTRUSTED_EMBEDDING_STATE_SHARING)
+ String PROPERTY_ALLOW_UNTRUSTED_ACTIVITY_EMBEDDING_STATE_SHARING =
+ "android.window.PROPERTY_ALLOW_UNTRUSTED_ACTIVITY_EMBEDDING_STATE_SHARING";
+
+ /**
* Application level {@link android.content.pm.PackageManager.Property PackageManager.Property}
* that an app can specify to inform the system that the app is activity embedding split feature
* enabled.
@@ -6117,4 +6147,65 @@
throw new UnsupportedOperationException(
"getDefaultToken is not implemented");
}
+
+ /** @hide */
+ @Target(ElementType.TYPE_USE)
+ @IntDef(
+ prefix = {"SCREEN_RECORDING_STATE"},
+ value = {SCREEN_RECORDING_STATE_NOT_VISIBLE, SCREEN_RECORDING_STATE_VISIBLE})
+ @Retention(RetentionPolicy.SOURCE)
+ @interface ScreenRecordingState {}
+
+ /** Indicates the app that registered the callback is not visible in screen recording. */
+ @FlaggedApi(com.android.window.flags.Flags.FLAG_SCREEN_RECORDING_CALLBACKS)
+ int SCREEN_RECORDING_STATE_NOT_VISIBLE = 0;
+
+ /** Indicates the app that registered the callback is visible in screen recording. */
+ @FlaggedApi(com.android.window.flags.Flags.FLAG_SCREEN_RECORDING_CALLBACKS)
+ int SCREEN_RECORDING_STATE_VISIBLE = 1;
+
+ /**
+ * Adds a screen recording callback. The callback will be invoked whenever the app becomes
+ * visible in screen recording or was visible in screen recording and becomes invisible in
+ * screen recording.
+ *
+ * <p>An app is considered visible in screen recording if any activities owned by the
+ * registering process's UID are being recorded.
+ *
+ * <p>Example:
+ *
+ * <pre>
+ * windowManager.addScreenRecordingCallback(state -> {
+ * // handle change in screen recording state
+ * });
+ * </pre>
+ *
+ * @param executor The executor on which callback method will be invoked.
+ * @param callback The callback that will be invoked when screen recording visibility changes.
+ * @return the current screen recording state.
+ * @see #SCREEN_RECORDING_STATE_NOT_VISIBLE
+ * @see #SCREEN_RECORDING_STATE_VISIBLE
+ */
+ @SuppressLint("AndroidFrameworkRequiresPermission")
+ @RequiresPermission(permission.DETECT_SCREEN_RECORDING)
+ @FlaggedApi(com.android.window.flags.Flags.FLAG_SCREEN_RECORDING_CALLBACKS)
+ default @ScreenRecordingState int addScreenRecordingCallback(
+ @NonNull @CallbackExecutor Executor executor,
+ @NonNull Consumer<@ScreenRecordingState Integer> callback) {
+ throw new UnsupportedOperationException();
+ }
+
+ /**
+ * Removes a screen recording callback.
+ *
+ * @param callback The callback to remove.
+ * @see #addScreenRecordingCallback(Executor, Consumer)
+ */
+ @SuppressLint("AndroidFrameworkRequiresPermission")
+ @RequiresPermission(permission.DETECT_SCREEN_RECORDING)
+ @FlaggedApi(com.android.window.flags.Flags.FLAG_SCREEN_RECORDING_CALLBACKS)
+ default void removeScreenRecordingCallback(
+ @NonNull Consumer<@ScreenRecordingState Integer> callback) {
+ throw new UnsupportedOperationException();
+ }
}
diff --git a/core/java/android/view/WindowManagerGlobal.java b/core/java/android/view/WindowManagerGlobal.java
index c49fce5..848261d 100644
--- a/core/java/android/view/WindowManagerGlobal.java
+++ b/core/java/android/view/WindowManagerGlobal.java
@@ -179,9 +179,29 @@
}
}
+ /**
+ * Sets {@link com.android.server.wm.WindowManagerService} for the system process.
+ * <p>
+ * It is needed to prevent possible deadlock. A possible scenario is:
+ * In system process, WMS holds {@link com.android.server.wm.WindowManagerGlobalLock} to call
+ * {@code WindowManagerGlobal} APIs and wait to lock {@code WindowManagerGlobal} itself
+ * (i.e. call {@link #getWindowManagerService()} in the global lock), while
+ * another component may lock {@code WindowManagerGlobal} and wait to lock
+ * {@link com.android.server.wm.WindowManagerGlobalLock}(i.e call {@link #addView} in the
+ * system process, which calls to {@link com.android.server.wm.WindowManagerService} API
+ * directly).
+ */
+ public static void setWindowManagerServiceForSystemProcess(@NonNull IWindowManager wms) {
+ sWindowManagerService = wms;
+ }
+
@Nullable
@UnsupportedAppUsage
public static IWindowManager getWindowManagerService() {
+ if (sWindowManagerService != null) {
+ // Use WMS directly without locking WMGlobal to prevent deadlock.
+ return sWindowManagerService;
+ }
synchronized (WindowManagerGlobal.class) {
if (sWindowManagerService == null) {
sWindowManagerService = IWindowManager.Stub.asInterface(
diff --git a/core/java/android/view/WindowManagerImpl.java b/core/java/android/view/WindowManagerImpl.java
index 5072ad7..eaf45c4 100644
--- a/core/java/android/view/WindowManagerImpl.java
+++ b/core/java/android/view/WindowManagerImpl.java
@@ -20,6 +20,8 @@
import static android.view.WindowManager.LayoutParams.LAST_SUB_WINDOW;
import static android.window.WindowProviderService.isWindowProviderService;
+import static com.android.window.flags.Flags.screenRecordingCallbacks;
+
import android.annotation.CallbackExecutor;
import android.annotation.IntRange;
import android.annotation.NonNull;
@@ -551,4 +553,25 @@
public IBinder getSurfaceControlInputClientToken(@NonNull SurfaceControl surfaceControl) {
return mGlobal.getSurfaceControlInputClientToken(surfaceControl);
}
+
+ @Override
+ public @ScreenRecordingState int addScreenRecordingCallback(
+ @NonNull @CallbackExecutor Executor executor,
+ @NonNull Consumer<@ScreenRecordingState Integer> callback) {
+ if (screenRecordingCallbacks()) {
+ Objects.requireNonNull(executor, "executor must not be null");
+ Objects.requireNonNull(callback, "callback must not be null");
+ return ScreenRecordingCallbacks.getInstance().addCallback(executor, callback);
+ }
+ return SCREEN_RECORDING_STATE_NOT_VISIBLE;
+ }
+
+ @Override
+ public void removeScreenRecordingCallback(
+ @NonNull Consumer<@ScreenRecordingState Integer> callback) {
+ if (screenRecordingCallbacks()) {
+ Objects.requireNonNull(callback, "callback must not be null");
+ ScreenRecordingCallbacks.getInstance().removeCallback(callback);
+ }
+ }
}
diff --git a/core/java/android/view/accessibility/AccessibilityManager.java b/core/java/android/view/accessibility/AccessibilityManager.java
index 146b576..0deaca1 100644
--- a/core/java/android/view/accessibility/AccessibilityManager.java
+++ b/core/java/android/view/accessibility/AccessibilityManager.java
@@ -67,7 +67,6 @@
import android.view.accessibility.AccessibilityEvent.EventType;
import com.android.internal.R;
-import com.android.internal.accessibility.common.ShortcutConstants;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.IntPair;
@@ -157,6 +156,22 @@
public static final String ACTION_CHOOSE_ACCESSIBILITY_BUTTON =
"com.android.internal.intent.action.CHOOSE_ACCESSIBILITY_BUTTON";
+ /**
+ * Used as an int value for accessibility chooser activity to represent the accessibility button
+ * shortcut type.
+ *
+ * @hide
+ */
+ public static final int ACCESSIBILITY_BUTTON = 0;
+
+ /**
+ * Used as an int value for accessibility chooser activity to represent hardware key shortcut,
+ * such as volume key button.
+ *
+ * @hide
+ */
+ public static final int ACCESSIBILITY_SHORTCUT_KEY = 1;
+
/** @hide */
public static final int FLASH_REASON_CALL = 1;
@@ -170,6 +185,32 @@
public static final int FLASH_REASON_PREVIEW = 4;
/**
+ * Annotations for the shortcut type.
+ * <p>Note: Keep in sync with {@link #SHORTCUT_TYPES}.</p>
+ * @hide
+ */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(value = {
+ // LINT.IfChange(shortcut_type_intdef)
+ ACCESSIBILITY_BUTTON,
+ ACCESSIBILITY_SHORTCUT_KEY
+ // LINT.ThenChange(:shortcut_type_array)
+ })
+ public @interface ShortcutType {}
+
+ /**
+ * Used for iterating through {@link ShortcutType}.
+ * <p>Note: Keep in sync with {@link ShortcutType}.</p>
+ * @hide
+ */
+ public static final int[] SHORTCUT_TYPES = {
+ // LINT.IfChange(shortcut_type_array)
+ ACCESSIBILITY_BUTTON,
+ ACCESSIBILITY_SHORTCUT_KEY,
+ // LINT.ThenChange(:shortcut_type_intdef)
+ };
+
+ /**
* Annotations for content flag of UI.
* @hide
*/
@@ -1745,8 +1786,7 @@
@TestApi
@RequiresPermission(Manifest.permission.MANAGE_ACCESSIBILITY)
@NonNull
- public List<String> getAccessibilityShortcutTargets(
- @ShortcutConstants.UserShortcutType int shortcutType) {
+ public List<String> getAccessibilityShortcutTargets(@ShortcutType int shortcutType) {
final IAccessibilityManager service;
synchronized (mLock) {
service = getServiceLocked();
diff --git a/core/java/android/view/accessibility/MagnificationAnimationCallback.java b/core/java/android/view/accessibility/MagnificationAnimationCallback.java
index 72518db..1755497 100644
--- a/core/java/android/view/accessibility/MagnificationAnimationCallback.java
+++ b/core/java/android/view/accessibility/MagnificationAnimationCallback.java
@@ -16,6 +16,8 @@
package android.view.accessibility;
+import android.view.MagnificationSpec;
+
/**
* A callback for magnification animation result.
* @hide
@@ -31,4 +33,16 @@
* change. Otherwise {@code false}
*/
void onResult(boolean success);
+
+ /**
+ * Called when the animation is finished or interrupted during animating.
+ *
+ * @param success {@code true} if animating successfully with given spec or the spec did not
+ * change. Otherwise {@code false}
+ * @param lastSpecSent the last spec that was sent to WindowManager for animation, in case you
+ * need to update the local copy
+ */
+ default void onResult(boolean success, MagnificationSpec lastSpecSent) {
+ onResult(success);
+ }
}
\ No newline at end of file
diff --git a/core/java/android/view/autofill/AutofillManager.java b/core/java/android/view/autofill/AutofillManager.java
index 559ccfea7..7ebabee 100644
--- a/core/java/android/view/autofill/AutofillManager.java
+++ b/core/java/android/view/autofill/AutofillManager.java
@@ -64,6 +64,7 @@
import android.service.autofill.FillEventHistory;
import android.service.autofill.Flags;
import android.service.autofill.UserData;
+import android.service.credentials.CredentialProviderService;
import android.text.TextUtils;
import android.util.ArrayMap;
import android.util.ArraySet;
@@ -2382,7 +2383,18 @@
return;
}
- final Parcelable result = data.getParcelableExtra(EXTRA_AUTHENTICATION_RESULT);
+ final Parcelable result;
+ if (data.getParcelableExtra(EXTRA_AUTHENTICATION_RESULT) != null) {
+ result = data.getParcelableExtra(EXTRA_AUTHENTICATION_RESULT);
+ } else if (data.getParcelableExtra(
+ CredentialProviderService.EXTRA_GET_CREDENTIAL_RESPONSE) != null
+ && Flags.autofillCredmanIntegration()) {
+ result = data.getParcelableExtra(
+ CredentialProviderService.EXTRA_GET_CREDENTIAL_RESPONSE);
+ } else {
+ result = null;
+ }
+
final Bundle responseData = new Bundle();
responseData.putParcelable(EXTRA_AUTHENTICATION_RESULT, result);
final Bundle newClientState = data.getBundleExtra(EXTRA_CLIENT_STATE);
diff --git a/core/java/android/view/flags/refresh_rate_flags.aconfig b/core/java/android/view/flags/refresh_rate_flags.aconfig
index 0aa516e..9d613bc 100644
--- a/core/java/android/view/flags/refresh_rate_flags.aconfig
+++ b/core/java/android/view/flags/refresh_rate_flags.aconfig
@@ -50,4 +50,28 @@
description: "Feature flag for toolkit metrics collecting for frame rate decision"
bug: "301343249"
is_fixed_read_only: true
+}
+
+flag {
+ name: "toolkit_frame_rate_default_normal_read_only"
+ namespace: "toolkit"
+ description: "Feature flag for setting frame rate category as NORMAL for default"
+ bug: "239979904"
+ is_fixed_read_only: true
+}
+
+flag {
+ name: "toolkit_frame_rate_by_size_read_only"
+ namespace: "toolkit"
+ description: "Feature flag for setting frame rate category based on size"
+ bug: "239979904"
+ is_fixed_read_only: true
+}
+
+flag {
+ name: "toolkit_frame_rate_velocity_mapping_read_only"
+ namespace: "toolkit"
+ description: "Feature flag for setting frame rate based on velocity"
+ bug: "239979904"
+ is_fixed_read_only: true
}
\ No newline at end of file
diff --git a/core/java/android/view/flags/view_flags.aconfig b/core/java/android/view/flags/view_flags.aconfig
index 1dd99ba..6a4408b 100644
--- a/core/java/android/view/flags/view_flags.aconfig
+++ b/core/java/android/view/flags/view_flags.aconfig
@@ -1,13 +1,6 @@
package: "android.view.flags"
flag {
- name: "enable_surface_native_alloc_registration"
- namespace: "toolkit"
- description: "Feature flag for registering surfaces with the VM for faster cleanup"
- bug: "306193257"
-}
-
-flag {
name: "enable_surface_native_alloc_registration_ro"
namespace: "toolkit"
description: "Feature flag for registering surfaces with the VM for faster"
diff --git a/core/java/android/view/flags/window_insets.aconfig b/core/java/android/view/flags/window_insets.aconfig
new file mode 100644
index 0000000..201b7ad
--- /dev/null
+++ b/core/java/android/view/flags/window_insets.aconfig
@@ -0,0 +1,9 @@
+package: "android.view.flags"
+
+flag {
+ name: "customizable_window_headers"
+ namespace: "lse_desktop_experience"
+ description: "Flag to control the caption bar appearance and to fit app content in its empty space"
+ bug: "316387515"
+ is_fixed_read_only: true
+}
diff --git a/core/java/android/widget/AbsListView.java b/core/java/android/widget/AbsListView.java
index 1fdd1a5..f54ef38 100644
--- a/core/java/android/widget/AbsListView.java
+++ b/core/java/android/widget/AbsListView.java
@@ -425,12 +425,12 @@
int mMotionViewNewTop;
/**
- * The X value associated with the the down motion event
+ * The X value associated with the down motion event
*/
int mMotionX;
/**
- * The Y value associated with the the down motion event
+ * The Y value associated with the down motion event
*/
@UnsupportedAppUsage
int mMotionY;
@@ -7381,7 +7381,7 @@
scrap.dispatchStartTemporaryDetach();
- // The the accessibility state of the view may change while temporary
+ // the accessibility state of the view may change while temporary
// detached and we do not allow detached views to fire accessibility
// events. So we are announcing that the subtree changed giving a chance
// to clients holding on to a view in this subtree to refresh it.
@@ -7750,7 +7750,7 @@
}
/**
- * Abstract positon scroller used to handle smooth scrolling.
+ * Abstract position scroller used to handle smooth scrolling.
*/
static abstract class AbsPositionScroller {
public abstract void start(int position);
diff --git a/core/java/android/widget/AbsSpinner.java b/core/java/android/widget/AbsSpinner.java
index 76e97ad..3b7e1e9 100644
--- a/core/java/android/widget/AbsSpinner.java
+++ b/core/java/android/widget/AbsSpinner.java
@@ -170,7 +170,7 @@
* @see android.view.View#measure(int, int)
*
* Figure out the dimensions of this Spinner. The width comes from
- * the widthMeasureSpec as Spinnners can't have their width set to
+ * the widthMeasureSpec as Spinners can't have their width set to
* UNSPECIFIED. The height is based on the height of the selected item
* plus padding.
*/
diff --git a/core/java/android/widget/OWNERS b/core/java/android/widget/OWNERS
index e20357fa..1dc90ed 100644
--- a/core/java/android/widget/OWNERS
+++ b/core/java/android/widget/OWNERS
@@ -15,3 +15,5 @@
per-file Remote* = file:../appwidget/OWNERS
per-file Toast.java = juliacr@google.com, jeffdq@google.com
+
+per-file flags/notification_widget_flags.aconfig = juliacr@google.com, jeffdq@google.com
diff --git a/core/java/android/widget/RemoteCanvas.java b/core/java/android/widget/RemoteCanvas.java
deleted file mode 100644
index 9a0898c..0000000
--- a/core/java/android/widget/RemoteCanvas.java
+++ /dev/null
@@ -1,117 +0,0 @@
-/*
- * Copyright (C) 2024 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.widget;
-
-import static android.appwidget.flags.Flags.FLAG_DRAW_DATA_PARCEL;
-
-import android.annotation.AttrRes;
-import android.annotation.FlaggedApi;
-import android.annotation.StyleRes;
-import android.content.Context;
-import android.util.AttributeSet;
-import android.util.Log;
-import android.util.SparseArray;
-import android.view.View;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.annotation.VisibleForTesting;
-
-import java.util.function.IntConsumer;
-
-/**
- * {@link RemoteCanvas} is designed to support arbitrary protocols between two processes using
- * {@link RemoteViews.DrawInstructions}. Upon instantiation in the host process,
- * {@link RemoteCanvas#setDrawInstructions(RemoteViews.DrawInstructions)} is called so that the
- * host process can render the {@link RemoteViews.DrawInstructions} from the provider process
- * accordingly.
- *
- * @hide
- */
-@FlaggedApi(FLAG_DRAW_DATA_PARCEL)
-public class RemoteCanvas extends View {
-
- private static final String TAG = "RemoteCanvas";
-
- @Nullable
- private SparseArray<Runnable> mCallbacks;
-
- private final IntConsumer mOnClickHandler = (viewId) -> {
- if (mCallbacks == null) {
- Log.w(TAG, "Cannot find callback for " + viewId
- + ", in fact there were no callbacks from this RemoteViews at all.");
- return;
- }
- final Runnable cb = getCallbacks().get(viewId);
- if (cb != null) {
- cb.run();
- } else {
- Log.w(TAG, "Cannot find callback for " + viewId);
- }
- };
-
- RemoteCanvas(@NonNull Context context) {
- super(context);
- }
-
- RemoteCanvas(@NonNull Context context, @Nullable AttributeSet attrs) {
- super(context, attrs);
- }
-
- RemoteCanvas(@NonNull Context context, @Nullable AttributeSet attrs,
- @AttrRes int defStyleAttr) {
- super(context, attrs, defStyleAttr);
- }
-
- RemoteCanvas(@NonNull Context context, @Nullable AttributeSet attrs,
- @AttrRes int defStyleAttr, @StyleRes int defStyleRes) {
- super(context, attrs, defStyleAttr, defStyleRes);
- }
-
- /**
- * Setter method for the {@link RemoteViews.DrawInstructions} from the provider process for
- * the host process to render accordingly.
- *
- * @param instructions {@link RemoteViews.DrawInstructions} from the provider process.
- */
- void setDrawInstructions(@NonNull final RemoteViews.DrawInstructions instructions) {
- setTag(instructions);
- // TODO: handle draw instructions
- // TODO: attach mOnClickHandler
- }
-
- /**
- * Adds a callback function to a clickable area in the RemoteCanvas.
- *
- * @param viewId the viewId of the clickable area
- * @param cb the callback function to be triggered when clicked
- */
- void addOnClickHandler(final int viewId, @NonNull final Runnable cb) {
- getCallbacks().set(viewId, cb);
- }
-
- /**
- * Returns all callbacks added to the RemoteCanvas through
- * {@link #addOnClickHandler(int, Runnable)}.
- */
- @VisibleForTesting
- public SparseArray<Runnable> getCallbacks() {
- if (mCallbacks == null) {
- mCallbacks = new SparseArray<>();
- }
- return mCallbacks;
- }
-}
diff --git a/core/java/android/widget/RemoteViews.java b/core/java/android/widget/RemoteViews.java
index 2433bd8..5bf1b5b 100644
--- a/core/java/android/widget/RemoteViews.java
+++ b/core/java/android/widget/RemoteViews.java
@@ -112,7 +112,10 @@
import com.android.internal.R;
import com.android.internal.util.Preconditions;
import com.android.internal.widget.IRemoteViewsFactory;
+import com.android.internal.widget.remotecompose.player.RemoteComposeDocument;
+import com.android.internal.widget.remotecompose.player.RemoteComposePlayer;
+import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.FileDescriptor;
import java.io.FileOutputStream;
@@ -1479,9 +1482,7 @@
@Override
public void apply(View root, ViewGroup rootParent, ActionApplyParams params) {
- if (hasDrawInstructions() && root instanceof RemoteCanvas target) {
- target.addOnClickHandler(mViewId, () ->
- mResponse.handleViewInteraction(root, params.handler));
+ if (hasDrawInstructions() && root instanceof RemoteComposePlayer) {
return;
}
final View target = root.findViewById(mViewId);
@@ -3900,8 +3901,17 @@
public void apply(View root, ViewGroup rootParent, ActionApplyParams params)
throws ActionException {
if (drawDataParcel() && mInstructions != null
- && root instanceof RemoteCanvas remoteCanvas) {
- remoteCanvas.setDrawInstructions(mInstructions);
+ && root instanceof RemoteComposePlayer player) {
+ player.setTag(mInstructions);
+ final List<byte[]> bytes = mInstructions.mInstructions;
+ if (bytes.isEmpty()) {
+ return;
+ }
+ try (ByteArrayInputStream is = new ByteArrayInputStream(bytes.get(0))) {
+ player.setDocument(new RemoteComposeDocument(is));
+ } catch (IOException e) {
+ Log.e(LOG_TAG, "Failed to render draw instructions", e);
+ }
}
}
@@ -5044,10 +5054,7 @@
* @param viewId The id of the {@link AdapterView}
* @param intent The intent of the service which will be
* providing data to the RemoteViewsAdapter
- * @deprecated use
- * {@link #setRemoteAdapter(int, android.widget.RemoteViews.RemoteCollectionItems)} instead
*/
- @Deprecated
public void setRemoteAdapter(@IdRes int viewId, Intent intent) {
if (remoteAdapterConversion()) {
addAction(new SetRemoteCollectionItemListAdapterAction(viewId, intent));
@@ -6044,6 +6051,16 @@
RemoteViews rvToApply = getRemoteViewsToApply(context, size);
View result = inflateView(context, rvToApply, directParent,
params.applyThemeResId, params.colorResources);
+ if (result instanceof RemoteComposePlayer player) {
+ player.addClickListener((viewId, metadata) -> {
+ mActions.forEach(action -> {
+ if (viewId == action.mViewId
+ && action instanceof SetOnClickResponse setOnClickResponse) {
+ setOnClickResponse.mResponse.handleViewInteraction(player, params.handler);
+ }
+ });
+ });
+ }
rvToApply.performApply(result, rootParent, params);
return result;
}
@@ -6067,7 +6084,7 @@
}
// If the RemoteViews contains draw instructions, just use it instead.
if (rv.hasDrawInstructions()) {
- return new RemoteCanvas(inflationContext);
+ return new RemoteComposePlayer(inflationContext);
}
LayoutInflater inflater = LayoutInflater.from(context);
@@ -7549,7 +7566,7 @@
public static final class DrawInstructions {
@NonNull
- private final List<byte[]> mInstructions;
+ final List<byte[]> mInstructions;
private DrawInstructions() {
throw new UnsupportedOperationException(
diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java
index 90d5140..9a4106d9 100644
--- a/core/java/android/widget/TextView.java
+++ b/core/java/android/widget/TextView.java
@@ -1259,6 +1259,7 @@
int lastBaselineToBottomHeight = -1;
float lineHeight = -1f;
int lineHeightUnit = -1;
+ boolean hasUseBoundForWidthValue = false;
readTextAppearance(context, a, attributes, true /* styleArray */);
@@ -1613,6 +1614,9 @@
lineHeight = a.getDimensionPixelSize(attr, -1);
}
break;
+ case com.android.internal.R.styleable.TextView_useBoundsForWidth:
+ mUseBoundsForWidth = a.getBoolean(attr, false);
+ hasUseBoundForWidthValue = true;
}
}
@@ -1639,10 +1643,12 @@
mUseFallbackLineSpacing = FALLBACK_LINE_SPACING_NONE;
}
- if (CompatChanges.isChangeEnabled(USE_BOUNDS_FOR_WIDTH)) {
- mUseBoundsForWidth = ClientFlags.useBoundsForWidth();
- } else {
- mUseBoundsForWidth = false;
+ if (!hasUseBoundForWidthValue) {
+ if (CompatChanges.isChangeEnabled(USE_BOUNDS_FOR_WIDTH)) {
+ mUseBoundsForWidth = ClientFlags.useBoundsForWidth();
+ } else {
+ mUseBoundsForWidth = false;
+ }
}
// TODO(b/179693024): Use a ChangeId instead.
diff --git a/core/java/android/window/ImeOnBackInvokedDispatcher.java b/core/java/android/window/ImeOnBackInvokedDispatcher.java
index 9ef6880..0cc9a0d 100644
--- a/core/java/android/window/ImeOnBackInvokedDispatcher.java
+++ b/core/java/android/window/ImeOnBackInvokedDispatcher.java
@@ -50,6 +50,8 @@
static final int RESULT_CODE_UNREGISTER = 1;
@NonNull
private final ResultReceiver mResultReceiver;
+ @NonNull
+ private final BackProgressAnimator mProgressAnimator = new BackProgressAnimator();
public ImeOnBackInvokedDispatcher(Handler handler) {
mResultReceiver = new ResultReceiver(handler) {
@@ -88,7 +90,7 @@
// cause a memory leak because the app side already clears the reference correctly.
final IOnBackInvokedCallback iCallback =
new WindowOnBackInvokedDispatcher.OnBackInvokedCallbackWrapper(
- callback, false /* useWeakRef */);
+ callback, mProgressAnimator, false /* useWeakRef */);
bundle.putBinder(RESULT_KEY_CALLBACK, iCallback.asBinder());
bundle.putInt(RESULT_KEY_PRIORITY, priority);
bundle.putInt(RESULT_KEY_ID, callback.hashCode());
@@ -179,6 +181,9 @@
}
}
mImeCallbacks.clear();
+ // We should also stop running animations since all callbacks have been removed.
+ // note: mSpring.skipToEnd(), in ProgressAnimator.reset(), requires the main handler.
+ Handler.getMain().post(mProgressAnimator::reset);
}
static class ImeOnBackInvokedCallback implements OnBackInvokedCallback {
diff --git a/core/java/android/window/SnapshotDrawerUtils.java b/core/java/android/window/SnapshotDrawerUtils.java
index cc875ad..c76c7a4 100644
--- a/core/java/android/window/SnapshotDrawerUtils.java
+++ b/core/java/android/window/SnapshotDrawerUtils.java
@@ -21,6 +21,7 @@
import static android.view.WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS;
import static android.view.WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS;
import static android.view.WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM;
+import static android.view.WindowManager.LayoutParams.FLAG_DIM_BEHIND;
import static android.view.WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED;
import static android.view.WindowManager.LayoutParams.FLAG_IGNORE_CHEEK_PRESSES;
import static android.view.WindowManager.LayoutParams.FLAG_LOCAL_FOCUS_MODE;
@@ -93,7 +94,8 @@
| FLAG_WATCH_OUTSIDE_TOUCH
| FLAG_SPLIT_TOUCH
| FLAG_SCALED
- | FLAG_SECURE;
+ | FLAG_SECURE
+ | FLAG_DIM_BEHIND;
private static final RectF sTmpSnapshotSize = new RectF();
private static final RectF sTmpDstFrame = new RectF();
diff --git a/core/java/android/window/WindowMetricsController.java b/core/java/android/window/WindowMetricsController.java
index e32c8e5..739cf0e 100644
--- a/core/java/android/window/WindowMetricsController.java
+++ b/core/java/android/window/WindowMetricsController.java
@@ -17,6 +17,7 @@
package android.window;
import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED;
+import static android.view.Surface.ROTATION_0;
import static android.view.View.SYSTEM_UI_FLAG_VISIBLE;
import static android.view.WindowManager.LayoutParams.SOFT_INPUT_ADJUST_NOTHING;
@@ -31,6 +32,7 @@
import android.os.RemoteException;
import android.util.DisplayMetrics;
import android.view.Display;
+import android.view.DisplayCutout;
import android.view.DisplayInfo;
import android.view.InsetsState;
import android.view.WindowInsets;
@@ -157,10 +159,15 @@
new Rect(0, 0, currentDisplayInfo.getNaturalWidth(),
currentDisplayInfo.getNaturalHeight()), isScreenRound,
ACTIVITY_TYPE_UNDEFINED);
- // Set the hardware-provided insets.
+ // Set the hardware-provided insets. Always with the ROTATION_0 result.
+ DisplayCutout cutout = currentDisplayInfo.displayCutout;
+ if (cutout != null && currentDisplayInfo.rotation != ROTATION_0) {
+ cutout = cutout.getRotated(
+ currentDisplayInfo.logicalWidth, currentDisplayInfo.logicalHeight,
+ currentDisplayInfo.rotation, ROTATION_0);
+ }
windowInsets = new WindowInsets.Builder(windowInsets).setRoundedCorners(
- currentDisplayInfo.roundedCorners)
- .setDisplayCutout(currentDisplayInfo.displayCutout).build();
+ currentDisplayInfo.roundedCorners).setDisplayCutout(cutout).build();
// Multiply default density scale because WindowMetrics provide the density value with
// the scaling factor for the Density Independent Pixel unit, which is the same unit
diff --git a/core/java/android/window/WindowOnBackInvokedDispatcher.java b/core/java/android/window/WindowOnBackInvokedDispatcher.java
index 65075ae..5c911f4 100644
--- a/core/java/android/window/WindowOnBackInvokedDispatcher.java
+++ b/core/java/android/window/WindowOnBackInvokedDispatcher.java
@@ -226,6 +226,9 @@
setTopOnBackInvokedCallback(null);
}
+ // We should also stop running animations since all callbacks have been removed.
+ // note: mSpring.skipToEnd(), in ProgressAnimator.reset(), requires the main handler.
+ Handler.getMain().post(mProgressAnimator::reset);
mAllCallbacks.clear();
mOnBackInvokedCallbacks.clear();
}
@@ -243,7 +246,10 @@
.ImeOnBackInvokedCallback
? ((ImeOnBackInvokedDispatcher.ImeOnBackInvokedCallback)
callback).getIOnBackInvokedCallback()
- : new OnBackInvokedCallbackWrapper(callback, this);
+ : new OnBackInvokedCallbackWrapper(
+ callback,
+ mProgressAnimator,
+ this);
callbackInfo = new OnBackInvokedCallbackInfo(
iCallback,
priority,
@@ -269,7 +275,7 @@
}
@NonNull
- private static final BackProgressAnimator mProgressAnimator = new BackProgressAnimator();
+ private final BackProgressAnimator mProgressAnimator = new BackProgressAnimator();
private boolean mIsDispatching = false;
/**
@@ -336,18 +342,24 @@
* forwarded and registered on the app's {@link WindowOnBackInvokedDispatcher}. */
@Nullable
private final WindowOnBackInvokedDispatcher mDispatcher;
+ @NonNull
+ private final BackProgressAnimator mProgressAnimator;
OnBackInvokedCallbackWrapper(
@NonNull OnBackInvokedCallback callback,
+ @NonNull BackProgressAnimator progressAnimator,
WindowOnBackInvokedDispatcher dispatcher) {
mCallbackRef = new CallbackRef(callback, true /* useWeakRef */);
+ mProgressAnimator = progressAnimator;
mDispatcher = dispatcher;
}
OnBackInvokedCallbackWrapper(
@NonNull OnBackInvokedCallback callback,
+ @NonNull BackProgressAnimator progressAnimator,
boolean useWeakRef) {
mCallbackRef = new CallbackRef(callback, useWeakRef);
+ mProgressAnimator = progressAnimator;
mDispatcher = null;
}
@@ -359,11 +371,11 @@
}
final OnBackAnimationCallback callback = getBackAnimationCallback();
if (callback != null) {
- mProgressAnimator.onBackStarted(backEvent, event ->
- callback.onBackProgressed(event));
callback.onBackStarted(new BackEvent(
backEvent.getTouchX(), backEvent.getTouchY(),
backEvent.getProgress(), backEvent.getSwipeEdge()));
+ mProgressAnimator.onBackStarted(backEvent, event ->
+ callback.onBackProgressed(event));
}
});
}
diff --git a/core/java/android/window/flags/large_screen_experiences_app_compat.aconfig b/core/java/android/window/flags/large_screen_experiences_app_compat.aconfig
index 1de77f6..82067de 100644
--- a/core/java/android/window/flags/large_screen_experiences_app_compat.aconfig
+++ b/core/java/android/window/flags/large_screen_experiences_app_compat.aconfig
@@ -66,3 +66,14 @@
bug: "314952133"
is_fixed_read_only: true
}
+
+flag {
+ name: "app_compat_refactoring"
+ namespace: "large_screen_experiences_app_compat"
+ description: "Whether the changes about app compat refactoring are enabled./n"
+ "The goal is to simplify code readability unblocking the implementation of /n"
+ "app compat feature like reachability, animations and others related to/n"
+ "freeform windowing mode."
+ bug: "309593314"
+ is_fixed_read_only: true
+}
diff --git a/core/java/android/window/flags/windowing_frontend.aconfig b/core/java/android/window/flags/windowing_frontend.aconfig
index f234637..14fb17c 100644
--- a/core/java/android/window/flags/windowing_frontend.aconfig
+++ b/core/java/android/window/flags/windowing_frontend.aconfig
@@ -84,6 +84,14 @@
}
flag {
+ name: "delegate_unhandled_drags"
+ namespace: "multitasking"
+ description: "Enables delegating unhandled drags to SystemUI"
+ bug: "320797628"
+ is_fixed_read_only: true
+}
+
+flag {
name: "insets_decoupled_configuration"
namespace: "windowing_frontend"
description: "Configuration decoupled from insets"
diff --git a/core/java/android/window/flags/windowing_sdk.aconfig b/core/java/android/window/flags/windowing_sdk.aconfig
index 4a6e8d7..bc63881 100644
--- a/core/java/android/window/flags/windowing_sdk.aconfig
+++ b/core/java/android/window/flags/windowing_sdk.aconfig
@@ -16,7 +16,7 @@
namespace: "windowing_sdk"
name: "activity_embedding_overlay_presentation_flag"
description: "Whether the overlay presentation feature is enabled"
- bug: "243518738"
+ bug: "293370683"
}
flag {
@@ -30,7 +30,7 @@
namespace: "windowing_sdk"
name: "fullscreen_dim_flag"
description: "Whether to allow showing fullscreen dim on ActivityEmbedding split"
- bug: "253533308"
+ bug: "293797706"
}
flag {
@@ -44,7 +44,7 @@
namespace: "windowing_sdk"
name: "untrusted_embedding_any_app_permission"
description: "Feature flag to enable the permission to embed any app in untrusted mode."
- bug: "289199433"
+ bug: "293647332"
is_fixed_read_only: true
}
@@ -58,7 +58,15 @@
flag {
namespace: "windowing_sdk"
+ name: "untrusted_embedding_state_sharing"
+ description: "Feature flag to enable state sharing in untrusted embedding when apps opt in."
+ bug: "293647332"
+ is_fixed_read_only: true
+}
+
+flag {
+ namespace: "windowing_sdk"
name: "embedded_activity_back_nav_flag"
description: "Refines embedded activity back navigation behavior"
- bug: "240575809"
+ bug: "293642394"
}
\ No newline at end of file
diff --git a/core/java/com/android/internal/accessibility/AccessibilityShortcutController.java b/core/java/com/android/internal/accessibility/AccessibilityShortcutController.java
index b4395a7..de0f070 100644
--- a/core/java/com/android/internal/accessibility/AccessibilityShortcutController.java
+++ b/core/java/com/android/internal/accessibility/AccessibilityShortcutController.java
@@ -18,6 +18,7 @@
import static android.accessibilityservice.AccessibilityServiceInfo.FEEDBACK_ALL_MASK;
import static android.view.WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG;
+import static android.view.accessibility.AccessibilityManager.ACCESSIBILITY_SHORTCUT_KEY;
import static com.android.internal.accessibility.dialog.AccessibilityTargetHelper.getTargets;
import static com.android.internal.os.RoSystemProperties.SUPPORT_ONE_HANDED_MODE;
@@ -328,8 +329,7 @@
}
private AlertDialog createShortcutWarningDialog(int userId) {
- List<AccessibilityTarget> targets = getTargets(mContext,
- ShortcutConstants.UserShortcutType.HARDWARE);
+ List<AccessibilityTarget> targets = getTargets(mContext, ACCESSIBILITY_SHORTCUT_KEY);
if (targets.size() == 0) {
return null;
}
@@ -541,7 +541,7 @@
private ComponentName getShortcutTargetComponentName() {
final List<String> shortcutTargets = mFrameworkObjectProvider
.getAccessibilityManagerInstance(mContext)
- .getAccessibilityShortcutTargets(ShortcutConstants.UserShortcutType.HARDWARE);
+ .getAccessibilityShortcutTargets(ACCESSIBILITY_SHORTCUT_KEY);
if (shortcutTargets.size() != 1) {
return null;
}
diff --git a/core/java/com/android/internal/accessibility/common/ShortcutConstants.java b/core/java/com/android/internal/accessibility/common/ShortcutConstants.java
index 353e182..7ec8838 100644
--- a/core/java/com/android/internal/accessibility/common/ShortcutConstants.java
+++ b/core/java/com/android/internal/accessibility/common/ShortcutConstants.java
@@ -44,27 +44,19 @@
* choose accessibility shortcut as preferred shortcut.
* {@code TRIPLETAP} for displaying specifying magnification to be toggled via quickly
* tapping screen 3 times as preferred shortcut.
- * {@code TWO_FINGERS_TRIPLE_TAP} for displaying specifying magnification to be toggled via
- * quickly tapping screen 3 times with two fingers as preferred shortcut.
*/
@Retention(RetentionPolicy.SOURCE)
- @IntDef(
- flag = true,
- value = {
- UserShortcutType.DEFAULT,
- UserShortcutType.SOFTWARE,
- UserShortcutType.HARDWARE,
- UserShortcutType.TRIPLETAP,
- UserShortcutType.TWO_FINGERS_TRIPLE_TAP,
- })
+ @IntDef({
+ UserShortcutType.DEFAULT,
+ UserShortcutType.SOFTWARE,
+ UserShortcutType.HARDWARE,
+ UserShortcutType.TRIPLETAP,
+ })
public @interface UserShortcutType {
int DEFAULT = 0;
- // LINT.IfChange(shortcut_type_intdef)
- int SOFTWARE = 1;
- int HARDWARE = 1 << 1;
- int TRIPLETAP = 1 << 2;
- int TWO_FINGERS_TRIPLE_TAP = 1 << 3;
- // LINT.ThenChange(:shortcut_type_array)
+ int SOFTWARE = 1; // 1 << 0
+ int HARDWARE = 2; // 1 << 1
+ int TRIPLETAP = 4; // 1 << 2
}
/**
@@ -72,12 +64,9 @@
* non-default IntDef types.
*/
public static final int[] USER_SHORTCUT_TYPES = {
- // LINT.IfChange(shortcut_type_array)
UserShortcutType.SOFTWARE,
UserShortcutType.HARDWARE,
- UserShortcutType.TRIPLETAP,
- UserShortcutType.TWO_FINGERS_TRIPLE_TAP,
- // LINT.ThenChange(:shortcut_type_intdef)
+ UserShortcutType.TRIPLETAP
};
diff --git a/core/java/com/android/internal/accessibility/dialog/AccessibilityActivityTarget.java b/core/java/com/android/internal/accessibility/dialog/AccessibilityActivityTarget.java
index 33048dc..063154d 100644
--- a/core/java/com/android/internal/accessibility/dialog/AccessibilityActivityTarget.java
+++ b/core/java/com/android/internal/accessibility/dialog/AccessibilityActivityTarget.java
@@ -17,13 +17,14 @@
package com.android.internal.accessibility.dialog;
import static com.android.internal.accessibility.util.ShortcutUtils.convertToKey;
+import static com.android.internal.accessibility.util.ShortcutUtils.convertToUserType;
import static com.android.internal.accessibility.util.ShortcutUtils.isShortcutContained;
import android.accessibilityservice.AccessibilityShortcutInfo;
import android.annotation.NonNull;
import android.content.Context;
+import android.view.accessibility.AccessibilityManager.ShortcutType;
-import com.android.internal.accessibility.common.ShortcutConstants;
import com.android.internal.accessibility.common.ShortcutConstants.AccessibilityFragmentType;
import com.android.internal.accessibility.common.ShortcutConstants.ShortcutMenuMode;
@@ -32,8 +33,7 @@
*/
class AccessibilityActivityTarget extends AccessibilityTarget {
- AccessibilityActivityTarget(Context context,
- @ShortcutConstants.UserShortcutType int shortcutType,
+ AccessibilityActivityTarget(Context context, @ShortcutType int shortcutType,
@NonNull AccessibilityShortcutInfo shortcutInfo) {
super(context,
shortcutType,
@@ -44,7 +44,7 @@
shortcutInfo.getActivityInfo().applicationInfo.uid,
shortcutInfo.getActivityInfo().loadLabel(context.getPackageManager()),
shortcutInfo.getActivityInfo().loadIcon(context.getPackageManager()),
- convertToKey(shortcutType));
+ convertToKey(convertToUserType(shortcutType)));
}
@Override
diff --git a/core/java/com/android/internal/accessibility/dialog/AccessibilityButtonChooserActivity.java b/core/java/com/android/internal/accessibility/dialog/AccessibilityButtonChooserActivity.java
index e084ebd..7eb09e5 100644
--- a/core/java/com/android/internal/accessibility/dialog/AccessibilityButtonChooserActivity.java
+++ b/core/java/com/android/internal/accessibility/dialog/AccessibilityButtonChooserActivity.java
@@ -17,6 +17,7 @@
package com.android.internal.accessibility.dialog;
import static android.view.WindowManagerPolicyConstants.NAV_BAR_MODE_GESTURAL;
+import static android.view.accessibility.AccessibilityManager.ACCESSIBILITY_BUTTON;
import static com.android.internal.accessibility.AccessibilityShortcutController.MAGNIFICATION_COMPONENT_NAME;
import static com.android.internal.accessibility.AccessibilityShortcutController.MAGNIFICATION_CONTROLLER_NAME;
@@ -35,7 +36,6 @@
import android.widget.TextView;
import com.android.internal.R;
-import com.android.internal.accessibility.common.ShortcutConstants;
import com.android.internal.widget.ResolverDrawerLayout;
import java.util.ArrayList;
@@ -85,7 +85,7 @@
prompt.setVisibility(View.VISIBLE);
}
- mTargets.addAll(getTargets(this, ShortcutConstants.UserShortcutType.SOFTWARE));
+ mTargets.addAll(getTargets(this, ACCESSIBILITY_BUTTON));
final GridView gridview = findViewById(R.id.accessibility_button_chooser_grid);
gridview.setAdapter(new ButtonTargetAdapter(mTargets));
diff --git a/core/java/com/android/internal/accessibility/dialog/AccessibilityServiceTarget.java b/core/java/com/android/internal/accessibility/dialog/AccessibilityServiceTarget.java
index 7406da4..2b6913c 100644
--- a/core/java/com/android/internal/accessibility/dialog/AccessibilityServiceTarget.java
+++ b/core/java/com/android/internal/accessibility/dialog/AccessibilityServiceTarget.java
@@ -17,13 +17,14 @@
package com.android.internal.accessibility.dialog;
import static com.android.internal.accessibility.util.ShortcutUtils.convertToKey;
+import static com.android.internal.accessibility.util.ShortcutUtils.convertToUserType;
import static com.android.internal.accessibility.util.ShortcutUtils.isShortcutContained;
import android.accessibilityservice.AccessibilityServiceInfo;
import android.annotation.NonNull;
import android.content.Context;
+import android.view.accessibility.AccessibilityManager.ShortcutType;
-import com.android.internal.accessibility.common.ShortcutConstants;
import com.android.internal.accessibility.common.ShortcutConstants.AccessibilityFragmentType;
import com.android.internal.accessibility.common.ShortcutConstants.ShortcutMenuMode;
@@ -35,9 +36,7 @@
private final AccessibilityServiceInfo mAccessibilityServiceInfo;
- AccessibilityServiceTarget(
- Context context,
- @ShortcutConstants.UserShortcutType int shortcutType,
+ AccessibilityServiceTarget(Context context, @ShortcutType int shortcutType,
@AccessibilityFragmentType int fragmentType,
@NonNull AccessibilityServiceInfo serviceInfo) {
super(context,
@@ -49,7 +48,7 @@
serviceInfo.getResolveInfo().serviceInfo.applicationInfo.uid,
serviceInfo.getResolveInfo().loadLabel(context.getPackageManager()),
serviceInfo.getResolveInfo().loadIcon(context.getPackageManager()),
- convertToKey(shortcutType));
+ convertToKey(convertToUserType(shortcutType)));
mAccessibilityServiceInfo = serviceInfo;
}
diff --git a/core/java/com/android/internal/accessibility/dialog/AccessibilityShortcutChooserActivity.java b/core/java/com/android/internal/accessibility/dialog/AccessibilityShortcutChooserActivity.java
index 8e2ec1b..2e80b7e 100644
--- a/core/java/com/android/internal/accessibility/dialog/AccessibilityShortcutChooserActivity.java
+++ b/core/java/com/android/internal/accessibility/dialog/AccessibilityShortcutChooserActivity.java
@@ -15,6 +15,10 @@
*/
package com.android.internal.accessibility.dialog;
+import static android.view.accessibility.AccessibilityManager.ACCESSIBILITY_BUTTON;
+import static android.view.accessibility.AccessibilityManager.ACCESSIBILITY_SHORTCUT_KEY;
+import static android.view.accessibility.AccessibilityManager.ShortcutType;
+
import static com.android.internal.accessibility.common.ShortcutConstants.ShortcutMenuMode;
import static com.android.internal.accessibility.dialog.AccessibilityTargetHelper.createEnableDialogContentView;
import static com.android.internal.accessibility.dialog.AccessibilityTargetHelper.getInstalledTargets;
@@ -38,7 +42,6 @@
import android.widget.AdapterView;
import com.android.internal.R;
-import com.android.internal.accessibility.common.ShortcutConstants;
import com.android.internal.annotations.VisibleForTesting;
import java.util.ArrayList;
@@ -49,8 +52,8 @@
* activity or allowlisting feature for volume key shortcut.
*/
public class AccessibilityShortcutChooserActivity extends Activity {
- @ShortcutConstants.UserShortcutType
- private final int mShortcutType = ShortcutConstants.UserShortcutType.HARDWARE;
+ @ShortcutType
+ private final int mShortcutType = ACCESSIBILITY_SHORTCUT_KEY;
private static final String KEY_ACCESSIBILITY_SHORTCUT_MENU_MODE =
"accessibility_shortcut_menu_mode";
private final List<AccessibilityTarget> mTargets = new ArrayList<>();
@@ -243,7 +246,7 @@
mTargetAdapter.getShortcutMenuMode() == ShortcutMenuMode.EDIT;
final int selectDialogTitleId = R.string.accessibility_select_shortcut_menu_title;
final int editDialogTitleId =
- mShortcutType == ShortcutConstants.UserShortcutType.SOFTWARE
+ mShortcutType == ACCESSIBILITY_BUTTON
? R.string.accessibility_edit_shortcut_menu_button_title
: R.string.accessibility_edit_shortcut_menu_volume_title;
diff --git a/core/java/com/android/internal/accessibility/dialog/AccessibilityTarget.java b/core/java/com/android/internal/accessibility/dialog/AccessibilityTarget.java
index 4ab1ee9..652cb52 100644
--- a/core/java/com/android/internal/accessibility/dialog/AccessibilityTarget.java
+++ b/core/java/com/android/internal/accessibility/dialog/AccessibilityTarget.java
@@ -16,6 +16,10 @@
package com.android.internal.accessibility.dialog;
+import static android.view.accessibility.AccessibilityManager.ACCESSIBILITY_BUTTON;
+import static android.view.accessibility.AccessibilityManager.ACCESSIBILITY_SHORTCUT_KEY;
+
+import static com.android.internal.accessibility.util.ShortcutUtils.convertToUserType;
import static com.android.internal.accessibility.util.ShortcutUtils.optInValueToSettings;
import static com.android.internal.accessibility.util.ShortcutUtils.optOutValueFromSettings;
@@ -26,6 +30,7 @@
import android.graphics.drawable.Drawable;
import android.view.View;
import android.view.accessibility.AccessibilityManager;
+import android.view.accessibility.AccessibilityManager.ShortcutType;
import com.android.internal.accessibility.common.ShortcutConstants;
import com.android.internal.accessibility.common.ShortcutConstants.AccessibilityFragmentType;
@@ -42,7 +47,7 @@
public abstract class AccessibilityTarget implements TargetOperations, OnTargetSelectedListener,
OnTargetCheckedChangeListener {
private Context mContext;
- @ShortcutConstants.UserShortcutType
+ @ShortcutType
private int mShortcutType;
@AccessibilityFragmentType
private int mFragmentType;
@@ -56,8 +61,7 @@
private CharSequence mStateDescription;
@VisibleForTesting
- public AccessibilityTarget(
- Context context, @ShortcutConstants.UserShortcutType int shortcutType,
+ public AccessibilityTarget(Context context, @ShortcutType int shortcutType,
@AccessibilityFragmentType int fragmentType, boolean isShortcutSwitched, String id,
int uid, CharSequence label, Drawable icon, String key) {
mContext = context;
@@ -95,10 +99,10 @@
final AccessibilityManager am =
getContext().getSystemService(AccessibilityManager.class);
switch (getShortcutType()) {
- case ShortcutConstants.UserShortcutType.SOFTWARE:
+ case ACCESSIBILITY_BUTTON:
am.notifyAccessibilityButtonClicked(getContext().getDisplayId(), getId());
return;
- case ShortcutConstants.UserShortcutType.HARDWARE:
+ case ACCESSIBILITY_SHORTCUT_KEY:
am.performAccessibilityShortcut(getId());
return;
default:
@@ -110,9 +114,9 @@
public void onCheckedChanged(boolean isChecked) {
setShortcutEnabled(isChecked);
if (isChecked) {
- optInValueToSettings(getContext(), getShortcutType(), getId());
+ optInValueToSettings(getContext(), convertToUserType(getShortcutType()), getId());
} else {
- optOutValueFromSettings(getContext(), getShortcutType(), getId());
+ optOutValueFromSettings(getContext(), convertToUserType(getShortcutType()), getId());
}
}
@@ -138,7 +142,7 @@
return mContext;
}
- public @ShortcutConstants.UserShortcutType int getShortcutType() {
+ public @ShortcutType int getShortcutType() {
return mShortcutType;
}
diff --git a/core/java/com/android/internal/accessibility/dialog/AccessibilityTargetHelper.java b/core/java/com/android/internal/accessibility/dialog/AccessibilityTargetHelper.java
index bd63e23..51a5ddf 100644
--- a/core/java/com/android/internal/accessibility/dialog/AccessibilityTargetHelper.java
+++ b/core/java/com/android/internal/accessibility/dialog/AccessibilityTargetHelper.java
@@ -16,6 +16,8 @@
package com.android.internal.accessibility.dialog;
+import static android.view.accessibility.AccessibilityManager.ACCESSIBILITY_BUTTON;
+
import static com.android.internal.accessibility.AccessibilityShortcutController.ACCESSIBILITY_HEARING_AIDS_COMPONENT_NAME;
import static com.android.internal.accessibility.AccessibilityShortcutController.COLOR_INVERSION_COMPONENT_NAME;
import static com.android.internal.accessibility.AccessibilityShortcutController.DALTONIZER_COMPONENT_NAME;
@@ -39,12 +41,12 @@
import android.view.LayoutInflater;
import android.view.View;
import android.view.accessibility.AccessibilityManager;
+import android.view.accessibility.AccessibilityManager.ShortcutType;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.TextView;
import com.android.internal.R;
-import com.android.internal.accessibility.common.ShortcutConstants;
import com.android.internal.accessibility.common.ShortcutConstants.AccessibilityFragmentType;
import java.util.ArrayList;
@@ -68,9 +70,8 @@
* @return The list of {@link AccessibilityTarget}.
* @hide
*/
- public static List<AccessibilityTarget> getTargets(
- Context context,
- @ShortcutConstants.UserShortcutType int shortcutType) {
+ public static List<AccessibilityTarget> getTargets(Context context,
+ @ShortcutType int shortcutType) {
// List all accessibility target
final List<AccessibilityTarget> installedTargets = getInstalledTargets(context,
shortcutType);
@@ -112,7 +113,7 @@
* @return The list of {@link AccessibilityTarget}.
*/
static List<AccessibilityTarget> getInstalledTargets(Context context,
- @ShortcutConstants.UserShortcutType int shortcutType) {
+ @ShortcutType int shortcutType) {
final List<AccessibilityTarget> targets = new ArrayList<>();
targets.addAll(getAccessibilityFilteredTargets(context, shortcutType));
targets.addAll(getAllowListingFeatureTargets(context, shortcutType));
@@ -121,7 +122,7 @@
}
private static List<AccessibilityTarget> getAccessibilityFilteredTargets(Context context,
- @ShortcutConstants.UserShortcutType int shortcutType) {
+ @ShortcutType int shortcutType) {
final List<AccessibilityTarget> serviceTargets =
getAccessibilityServiceTargets(context, shortcutType);
final List<AccessibilityTarget> activityTargets =
@@ -154,7 +155,7 @@
}
private static List<AccessibilityTarget> getAccessibilityServiceTargets(Context context,
- @ShortcutConstants.UserShortcutType int shortcutType) {
+ @ShortcutType int shortcutType) {
final AccessibilityManager am = (AccessibilityManager) context.getSystemService(
Context.ACCESSIBILITY_SERVICE);
final List<AccessibilityServiceInfo> installedServices =
@@ -170,7 +171,7 @@
final boolean hasRequestAccessibilityButtonFlag =
(info.flags & AccessibilityServiceInfo.FLAG_REQUEST_ACCESSIBILITY_BUTTON) != 0;
if ((targetSdk <= Build.VERSION_CODES.Q) && !hasRequestAccessibilityButtonFlag
- && (shortcutType == ShortcutConstants.UserShortcutType.SOFTWARE)) {
+ && (shortcutType == ACCESSIBILITY_BUTTON)) {
continue;
}
@@ -181,7 +182,7 @@
}
private static List<AccessibilityTarget> getAccessibilityActivityTargets(Context context,
- @ShortcutConstants.UserShortcutType int shortcutType) {
+ @ShortcutType int shortcutType) {
final AccessibilityManager am = (AccessibilityManager) context.getSystemService(
Context.ACCESSIBILITY_SERVICE);
final List<AccessibilityShortcutInfo> installedServices =
@@ -200,7 +201,7 @@
}
private static List<AccessibilityTarget> getAllowListingFeatureTargets(Context context,
- @ShortcutConstants.UserShortcutType int shortcutType) {
+ @ShortcutType int shortcutType) {
final List<AccessibilityTarget> targets = new ArrayList<>();
final int uid = context.getApplicationInfo().uid;
@@ -280,10 +281,8 @@
return targets;
}
- private static AccessibilityTarget createAccessibilityServiceTarget(
- Context context,
- @ShortcutConstants.UserShortcutType int shortcutType,
- @NonNull AccessibilityServiceInfo info) {
+ private static AccessibilityTarget createAccessibilityServiceTarget(Context context,
+ @ShortcutType int shortcutType, @NonNull AccessibilityServiceInfo info) {
switch (getAccessibilityServiceFragmentType(info)) {
case AccessibilityFragmentType.VOLUME_SHORTCUT_TOGGLE:
return new VolumeShortcutToggleAccessibilityServiceTarget(context, shortcutType,
diff --git a/core/java/com/android/internal/accessibility/dialog/InvisibleToggleAccessibilityServiceTarget.java b/core/java/com/android/internal/accessibility/dialog/InvisibleToggleAccessibilityServiceTarget.java
index 641a9f1..1bc8b84 100644
--- a/core/java/com/android/internal/accessibility/dialog/InvisibleToggleAccessibilityServiceTarget.java
+++ b/core/java/com/android/internal/accessibility/dialog/InvisibleToggleAccessibilityServiceTarget.java
@@ -16,6 +16,9 @@
package com.android.internal.accessibility.dialog;
+import static android.view.accessibility.AccessibilityManager.ACCESSIBILITY_BUTTON;
+import static android.view.accessibility.AccessibilityManager.ACCESSIBILITY_SHORTCUT_KEY;
+
import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType;
import static com.android.internal.accessibility.util.AccessibilityUtils.setAccessibilityServiceState;
import static com.android.internal.accessibility.util.ShortcutUtils.isComponentIdExistingInSettings;
@@ -25,6 +28,7 @@
import android.content.ComponentName;
import android.content.Context;
import android.os.UserHandle;
+import android.view.accessibility.AccessibilityManager.ShortcutType;
import android.view.accessibility.Flags;
import com.android.internal.accessibility.common.ShortcutConstants.AccessibilityFragmentType;
@@ -41,7 +45,7 @@
public class InvisibleToggleAccessibilityServiceTarget extends AccessibilityServiceTarget {
public InvisibleToggleAccessibilityServiceTarget(
- Context context, @UserShortcutType int shortcutType,
+ Context context, @ShortcutType int shortcutType,
@NonNull AccessibilityServiceInfo serviceInfo) {
super(context,
shortcutType,
@@ -68,10 +72,10 @@
private boolean isComponentIdExistingInOtherShortcut() {
switch (getShortcutType()) {
- case UserShortcutType.SOFTWARE:
+ case ACCESSIBILITY_BUTTON:
return isComponentIdExistingInSettings(getContext(), UserShortcutType.HARDWARE,
getId());
- case UserShortcutType.HARDWARE:
+ case ACCESSIBILITY_SHORTCUT_KEY:
return isComponentIdExistingInSettings(getContext(), UserShortcutType.SOFTWARE,
getId());
default:
diff --git a/core/java/com/android/internal/accessibility/dialog/InvisibleToggleAllowListingFeatureTarget.java b/core/java/com/android/internal/accessibility/dialog/InvisibleToggleAllowListingFeatureTarget.java
index 2204c0b..c22f17d 100644
--- a/core/java/com/android/internal/accessibility/dialog/InvisibleToggleAllowListingFeatureTarget.java
+++ b/core/java/com/android/internal/accessibility/dialog/InvisibleToggleAllowListingFeatureTarget.java
@@ -18,8 +18,8 @@
import android.content.Context;
import android.graphics.drawable.Drawable;
+import android.view.accessibility.AccessibilityManager.ShortcutType;
-import com.android.internal.accessibility.common.ShortcutConstants;
import com.android.internal.accessibility.common.ShortcutConstants.AccessibilityFragmentType;
/**
@@ -28,8 +28,7 @@
*/
class InvisibleToggleAllowListingFeatureTarget extends AccessibilityTarget {
- InvisibleToggleAllowListingFeatureTarget(Context context,
- @ShortcutConstants.UserShortcutType int shortcutType,
+ InvisibleToggleAllowListingFeatureTarget(Context context, @ShortcutType int shortcutType,
boolean isShortcutSwitched, String id, int uid, CharSequence label, Drawable icon,
String key) {
super(context, shortcutType, AccessibilityFragmentType.INVISIBLE_TOGGLE, isShortcutSwitched,
diff --git a/core/java/com/android/internal/accessibility/dialog/ToggleAccessibilityServiceTarget.java b/core/java/com/android/internal/accessibility/dialog/ToggleAccessibilityServiceTarget.java
index a6ef73e..a4ffef6 100644
--- a/core/java/com/android/internal/accessibility/dialog/ToggleAccessibilityServiceTarget.java
+++ b/core/java/com/android/internal/accessibility/dialog/ToggleAccessibilityServiceTarget.java
@@ -22,9 +22,9 @@
import android.annotation.NonNull;
import android.content.Context;
import android.view.View;
+import android.view.accessibility.AccessibilityManager.ShortcutType;
import com.android.internal.R;
-import com.android.internal.accessibility.common.ShortcutConstants;
import com.android.internal.accessibility.common.ShortcutConstants.AccessibilityFragmentType;
import com.android.internal.accessibility.common.ShortcutConstants.ShortcutMenuMode;
import com.android.internal.accessibility.dialog.TargetAdapter.ViewHolder;
@@ -45,8 +45,7 @@
float DISABLED = 0.5f;
}
- ToggleAccessibilityServiceTarget(Context context,
- @ShortcutConstants.UserShortcutType int shortcutType,
+ ToggleAccessibilityServiceTarget(Context context, @ShortcutType int shortcutType,
@NonNull AccessibilityServiceInfo serviceInfo) {
super(context,
shortcutType,
diff --git a/core/java/com/android/internal/accessibility/dialog/ToggleAllowListingFeatureTarget.java b/core/java/com/android/internal/accessibility/dialog/ToggleAllowListingFeatureTarget.java
index 2a9c555..11e668f 100644
--- a/core/java/com/android/internal/accessibility/dialog/ToggleAllowListingFeatureTarget.java
+++ b/core/java/com/android/internal/accessibility/dialog/ToggleAllowListingFeatureTarget.java
@@ -21,9 +21,9 @@
import android.graphics.drawable.Drawable;
import android.provider.Settings;
import android.view.View;
+import android.view.accessibility.AccessibilityManager.ShortcutType;
import com.android.internal.R;
-import com.android.internal.accessibility.common.ShortcutConstants;
import com.android.internal.accessibility.common.ShortcutConstants.AccessibilityFragmentType;
import com.android.internal.accessibility.common.ShortcutConstants.ShortcutMenuMode;
import com.android.internal.accessibility.dialog.TargetAdapter.ViewHolder;
@@ -34,8 +34,7 @@
*/
class ToggleAllowListingFeatureTarget extends AccessibilityTarget {
- ToggleAllowListingFeatureTarget(Context context,
- @ShortcutConstants.UserShortcutType int shortcutType,
+ ToggleAllowListingFeatureTarget(Context context, @ShortcutType int shortcutType,
boolean isShortcutSwitched, String id, int uid, CharSequence label, Drawable icon,
String key) {
super(context, shortcutType, AccessibilityFragmentType.TOGGLE, isShortcutSwitched, id,
diff --git a/core/java/com/android/internal/accessibility/dialog/VolumeShortcutToggleAccessibilityServiceTarget.java b/core/java/com/android/internal/accessibility/dialog/VolumeShortcutToggleAccessibilityServiceTarget.java
index 4926e72..04f5061 100644
--- a/core/java/com/android/internal/accessibility/dialog/VolumeShortcutToggleAccessibilityServiceTarget.java
+++ b/core/java/com/android/internal/accessibility/dialog/VolumeShortcutToggleAccessibilityServiceTarget.java
@@ -16,6 +16,9 @@
package com.android.internal.accessibility.dialog;
+import static android.view.accessibility.AccessibilityManager.ACCESSIBILITY_BUTTON;
+import static android.view.accessibility.AccessibilityManager.ACCESSIBILITY_SHORTCUT_KEY;
+
import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType;
import static com.android.internal.accessibility.util.AccessibilityUtils.setAccessibilityServiceState;
import static com.android.internal.accessibility.util.ShortcutUtils.optOutValueFromSettings;
@@ -24,6 +27,7 @@
import android.annotation.NonNull;
import android.content.ComponentName;
import android.content.Context;
+import android.view.accessibility.AccessibilityManager.ShortcutType;
import android.widget.Toast;
import com.android.internal.R;
@@ -35,8 +39,7 @@
*/
class VolumeShortcutToggleAccessibilityServiceTarget extends AccessibilityServiceTarget {
- VolumeShortcutToggleAccessibilityServiceTarget(Context context,
- @UserShortcutType int shortcutType,
+ VolumeShortcutToggleAccessibilityServiceTarget(Context context, @ShortcutType int shortcutType,
@NonNull AccessibilityServiceInfo serviceInfo) {
super(context,
shortcutType,
@@ -47,10 +50,10 @@
@Override
public void onCheckedChanged(boolean isChecked) {
switch (getShortcutType()) {
- case UserShortcutType.SOFTWARE:
+ case ACCESSIBILITY_BUTTON:
onCheckedFromAccessibilityButton(isChecked);
return;
- case UserShortcutType.HARDWARE:
+ case ACCESSIBILITY_SHORTCUT_KEY:
super.onCheckedChanged(isChecked);
return;
default:
diff --git a/core/java/com/android/internal/accessibility/util/AccessibilityStatsLogUtils.java b/core/java/com/android/internal/accessibility/util/AccessibilityStatsLogUtils.java
index 1e4bcf2..6b074a6 100644
--- a/core/java/com/android/internal/accessibility/util/AccessibilityStatsLogUtils.java
+++ b/core/java/com/android/internal/accessibility/util/AccessibilityStatsLogUtils.java
@@ -21,6 +21,8 @@
import static android.provider.Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE_ALL;
import static android.provider.Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE_FULLSCREEN;
import static android.provider.Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE_WINDOW;
+import static android.view.accessibility.AccessibilityManager.ACCESSIBILITY_BUTTON;
+import static android.view.accessibility.AccessibilityManager.ACCESSIBILITY_SHORTCUT_KEY;
import static com.android.internal.accessibility.AccessibilityShortcutController.MAGNIFICATION_COMPONENT_NAME;
import static com.android.internal.util.FrameworkStatsLog.ACCESSIBILITY_SHORTCUT_REPORTED__SERVICE_STATUS__DISABLED;
@@ -45,8 +47,9 @@
import android.content.ComponentName;
import android.content.Context;
import android.provider.Settings;
+import android.view.accessibility.AccessibilityManager;
+import android.view.accessibility.AccessibilityManager.ShortcutType;
-import com.android.internal.accessibility.common.ShortcutConstants;
import com.android.internal.util.FrameworkStatsLog;
/** Methods for logging accessibility states. */
@@ -68,15 +71,15 @@
/**
* Logs accessibility feature name that is assigned to the given {@code shortcutType}.
- * Calls this when clicking the shortcut {@link ShortcutConstants.UserShortcutType.SOFTWARE} or
- * {@link ShortcutConstants.UserShortcutType.HARDWARE}.
+ * Calls this when clicking the shortcut {@link AccessibilityManager#ACCESSIBILITY_BUTTON} or
+ * {@link AccessibilityManager#ACCESSIBILITY_SHORTCUT_KEY}.
*
* @param context context used to retrieve the {@link Settings} provider
* @param componentName component name of the accessibility feature
* @param shortcutType accessibility shortcut type
*/
public static void logAccessibilityShortcutActivated(Context context,
- ComponentName componentName, @ShortcutConstants.UserShortcutType int shortcutType) {
+ ComponentName componentName, @ShortcutType int shortcutType) {
logAccessibilityShortcutActivatedInternal(componentName,
convertToLoggingShortcutType(context, shortcutType), UNKNOWN_STATUS);
}
@@ -84,8 +87,8 @@
/**
* Logs accessibility feature name that is assigned to the given {@code shortcutType} and the
* {@code serviceEnabled} status.
- * Calls this when clicking the shortcut {@link ShortcutConstants.UserShortcutType.SOFTWARE}
- * or {@link ShortcutConstants.UserShortcutType.HARDWARE}.
+ * Calls this when clicking the shortcut {@link AccessibilityManager#ACCESSIBILITY_BUTTON}
+ * or {@link AccessibilityManager#ACCESSIBILITY_SHORTCUT_KEY}.
*
* @param context context used to retrieve the {@link Settings} provider
* @param componentName component name of the accessibility feature
@@ -93,8 +96,7 @@
* @param serviceEnabled {@code true} if the service is enabled
*/
public static void logAccessibilityShortcutActivated(Context context,
- ComponentName componentName, @ShortcutConstants.UserShortcutType int shortcutType,
- boolean serviceEnabled) {
+ ComponentName componentName, @ShortcutType int shortcutType, boolean serviceEnabled) {
logAccessibilityShortcutActivatedInternal(componentName,
convertToLoggingShortcutType(context, shortcutType),
convertToLoggingServiceStatus(serviceEnabled));
@@ -233,9 +235,9 @@
}
private static int convertToLoggingShortcutType(Context context,
- @ShortcutConstants.UserShortcutType int shortcutType) {
+ @ShortcutType int shortcutType) {
switch (shortcutType) {
- case ShortcutConstants.UserShortcutType.SOFTWARE:
+ case ACCESSIBILITY_BUTTON:
if (isAccessibilityFloatingMenuEnabled(context)) {
return ACCESSIBILITY_SHORTCUT_REPORTED__SHORTCUT_TYPE__A11Y_FLOATING_MENU;
} else if (isAccessibilityGestureEnabled(context)) {
@@ -243,7 +245,7 @@
} else {
return ACCESSIBILITY_SHORTCUT_REPORTED__SHORTCUT_TYPE__A11Y_BUTTON;
}
- case ShortcutConstants.UserShortcutType.HARDWARE:
+ case ACCESSIBILITY_SHORTCUT_KEY:
return ACCESSIBILITY_SHORTCUT_REPORTED__SHORTCUT_TYPE__VOLUME_KEY;
}
return ACCESSIBILITY_SHORTCUT_REPORTED__SHORTCUT_TYPE__UNKNOWN_TYPE;
diff --git a/core/java/com/android/internal/accessibility/util/ShortcutUtils.java b/core/java/com/android/internal/accessibility/util/ShortcutUtils.java
index 276c5c4..3fd3030 100644
--- a/core/java/com/android/internal/accessibility/util/ShortcutUtils.java
+++ b/core/java/com/android/internal/accessibility/util/ShortcutUtils.java
@@ -16,6 +16,9 @@
package com.android.internal.accessibility.util;
+import static android.view.accessibility.AccessibilityManager.ACCESSIBILITY_BUTTON;
+import static android.view.accessibility.AccessibilityManager.ACCESSIBILITY_SHORTCUT_KEY;
+
import static com.android.internal.accessibility.AccessibilityShortcutController.MAGNIFICATION_CONTROLLER_NAME;
import static com.android.internal.accessibility.common.ShortcutConstants.AccessibilityFragmentType.INVISIBLE_TOGGLE;
import static com.android.internal.accessibility.common.ShortcutConstants.SERVICES_SEPARATOR;
@@ -30,6 +33,7 @@
import android.text.TextUtils;
import android.util.ArraySet;
import android.view.accessibility.AccessibilityManager;
+import android.view.accessibility.AccessibilityManager.ShortcutType;
import java.util.Collections;
import java.util.List;
@@ -140,7 +144,7 @@
* @param componentId The component id that need to be checked.
* @return {@code true} if a component id is contained.
*/
- public static boolean isShortcutContained(Context context, @UserShortcutType int shortcutType,
+ public static boolean isShortcutContained(Context context, @ShortcutType int shortcutType,
@NonNull String componentId) {
final AccessibilityManager am = (AccessibilityManager) context.getSystemService(
Context.ACCESSIBILITY_SERVICE);
@@ -162,8 +166,6 @@
return Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_SERVICE;
case UserShortcutType.TRIPLETAP:
return Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_ENABLED;
- case UserShortcutType.TWO_FINGERS_TRIPLE_TAP:
- return Settings.Secure.ACCESSIBILITY_MAGNIFICATION_TWO_FINGER_TRIPLE_TAP_ENABLED;
default:
throw new IllegalArgumentException(
"Unsupported user shortcut type: " + type);
@@ -171,6 +173,24 @@
}
/**
+ * Converts {@link ShortcutType} to {@link UserShortcutType}.
+ *
+ * @param type The shortcut type.
+ * @return Mapping type from {@link UserShortcutType}.
+ */
+ public static @UserShortcutType int convertToUserType(@ShortcutType int type) {
+ switch (type) {
+ case ACCESSIBILITY_BUTTON:
+ return UserShortcutType.SOFTWARE;
+ case ACCESSIBILITY_SHORTCUT_KEY:
+ return UserShortcutType.HARDWARE;
+ default:
+ throw new IllegalArgumentException(
+ "Unsupported shortcut type:" + type);
+ }
+ }
+
+ /**
* Updates an accessibility state if the accessibility service is a Always-On a11y service,
* a.k.a. AccessibilityServices that has FLAG_REQUEST_ACCESSIBILITY_BUTTON
* <p>
@@ -235,13 +255,12 @@
public static Set<String> getShortcutTargetsFromSettings(
Context context, @UserShortcutType int shortcutType, int userId) {
final String targetKey = convertToKey(shortcutType);
- if (Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_ENABLED.equals(targetKey)
- || Settings.Secure.ACCESSIBILITY_MAGNIFICATION_TWO_FINGER_TRIPLE_TAP_ENABLED
- .equals(targetKey)) {
+ if (Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_ENABLED.equals(targetKey)) {
boolean magnificationEnabled = Settings.Secure.getIntForUser(
context.getContentResolver(), targetKey, /* def= */ 0, userId) == 1;
return magnificationEnabled ? Set.of(MAGNIFICATION_CONTROLLER_NAME)
: Collections.emptySet();
+
} else {
final String targetString = Settings.Secure.getStringForUser(
context.getContentResolver(), targetKey, userId);
diff --git a/core/java/com/android/internal/pm/parsing/pkg/PackageImpl.java b/core/java/com/android/internal/pm/parsing/pkg/PackageImpl.java
index 83acc47..d433ca3 100644
--- a/core/java/com/android/internal/pm/parsing/pkg/PackageImpl.java
+++ b/core/java/com/android/internal/pm/parsing/pkg/PackageImpl.java
@@ -227,6 +227,9 @@
private String requiredAccountType;
@Nullable
@DataClass.ParcelWith(ForInternedString.class)
+ private String mEmergencyInstaller;
+ @Nullable
+ @DataClass.ParcelWith(ForInternedString.class)
private String overlayTarget;
@Nullable
@DataClass.ParcelWith(ForInternedString.class)
@@ -1275,6 +1278,12 @@
return restrictedAccountType;
}
+ @Nullable
+ @Override
+ public String getEmergencyInstaller() {
+ return mEmergencyInstaller;
+ }
+
@Override
public int getRoundIconResourceId() {
return roundIconRes;
@@ -2336,6 +2345,12 @@
}
@Override
+ public PackageImpl setEmergencyInstaller(@Nullable String emergencyInstaller) {
+ this.mEmergencyInstaller = emergencyInstaller;
+ return this;
+ }
+
+ @Override
public PackageImpl setRoundIconResourceId(int value) {
roundIconRes = value;
return this;
@@ -3105,6 +3120,7 @@
dest.writeString(this.mBaseApkPath);
dest.writeString(this.restrictedAccountType);
dest.writeString(this.requiredAccountType);
+ dest.writeString(this.mEmergencyInstaller);
sForInternedString.parcel(this.overlayTarget, dest, flags);
dest.writeString(this.overlayTargetOverlayableName);
dest.writeString(this.overlayCategory);
@@ -3255,6 +3271,7 @@
this.mBaseApkPath = in.readString();
this.restrictedAccountType = in.readString();
this.requiredAccountType = in.readString();
+ this.mEmergencyInstaller = in.readString();
this.overlayTarget = sForInternedString.unparcel(in);
this.overlayTargetOverlayableName = in.readString();
this.overlayCategory = in.readString();
diff --git a/core/java/com/android/internal/pm/parsing/pkg/ParsedPackage.java b/core/java/com/android/internal/pm/parsing/pkg/ParsedPackage.java
index 7ef0b48..66cfb69 100644
--- a/core/java/com/android/internal/pm/parsing/pkg/ParsedPackage.java
+++ b/core/java/com/android/internal/pm/parsing/pkg/ParsedPackage.java
@@ -75,6 +75,11 @@
ParsedPackage setUpdatableSystem(boolean value);
+ /**
+ * Sets a system app that is allowed to update another system app
+ */
+ ParsedPackage setEmergencyInstaller(String emergencyInstaller);
+
ParsedPackage markNotActivitiesAsNotExportedIfSingleUser();
ParsedPackage setOdm(boolean odm);
diff --git a/core/java/com/android/internal/pm/pkg/component/ParsedActivity.java b/core/java/com/android/internal/pm/pkg/component/ParsedActivity.java
index b0f3578..a051c1b 100644
--- a/core/java/com/android/internal/pm/pkg/component/ParsedActivity.java
+++ b/core/java/com/android/internal/pm/pkg/component/ParsedActivity.java
@@ -89,4 +89,7 @@
*/
@Nullable
String getRequiredDisplayCategory();
+
+ /** Gets the permissions necessary for launching the activity when using content URIs. */
+ int getRequireContentUriPermissionFromCaller();
}
diff --git a/core/java/com/android/internal/pm/pkg/component/ParsedActivityImpl.java b/core/java/com/android/internal/pm/pkg/component/ParsedActivityImpl.java
index 2f977ee..1218793 100644
--- a/core/java/com/android/internal/pm/pkg/component/ParsedActivityImpl.java
+++ b/core/java/com/android/internal/pm/pkg/component/ParsedActivityImpl.java
@@ -36,9 +36,9 @@
import android.text.TextUtils;
import android.util.ArraySet;
+import com.android.internal.pm.pkg.parsing.ParsingUtils;
import com.android.internal.util.DataClass;
import com.android.internal.util.Parcelling.BuiltIn.ForInternedString;
-import com.android.internal.pm.pkg.parsing.ParsingUtils;
import java.util.Collections;
import java.util.Locale;
@@ -97,6 +97,8 @@
@Nullable
private String mRequiredDisplayCategory;
+ private int mRequireContentUriPermissionFromCaller;
+
public ParsedActivityImpl(ParsedActivityImpl other) {
super(other);
this.theme = other.theme;
@@ -124,6 +126,7 @@
this.windowLayout = other.windowLayout;
this.mKnownActivityEmbeddingCerts = other.mKnownActivityEmbeddingCerts;
this.mRequiredDisplayCategory = other.mRequiredDisplayCategory;
+ this.mRequireContentUriPermissionFromCaller = other.mRequireContentUriPermissionFromCaller;
}
/**
@@ -192,6 +195,8 @@
alias.setDirectBootAware(target.isDirectBootAware());
alias.setProcessName(target.getProcessName());
alias.setRequiredDisplayCategory(target.getRequiredDisplayCategory());
+ alias.setRequireContentUriPermissionFromCaller(
+ target.getRequireContentUriPermissionFromCaller());
return alias;
// Not all attributes from the target ParsedActivity are copied to the alias.
@@ -320,6 +325,7 @@
}
sForStringSet.parcel(this.mKnownActivityEmbeddingCerts, dest, flags);
dest.writeString8(this.mRequiredDisplayCategory);
+ dest.writeInt(this.mRequireContentUriPermissionFromCaller);
}
public ParsedActivityImpl() {
@@ -355,6 +361,7 @@
}
this.mKnownActivityEmbeddingCerts = sForStringSet.unparcel(in);
this.mRequiredDisplayCategory = in.readString8();
+ this.mRequireContentUriPermissionFromCaller = in.readInt();
}
@NonNull
@@ -412,7 +419,8 @@
int rotationAnimation,
int colorMode,
@Nullable ActivityInfo.WindowLayout windowLayout,
- @Nullable String requiredDisplayCategory) {
+ @Nullable String requiredDisplayCategory,
+ int requireContentUriPermissionFromCaller) {
this.theme = theme;
this.uiOptions = uiOptions;
this.targetActivity = targetActivity;
@@ -438,6 +446,7 @@
this.colorMode = colorMode;
this.windowLayout = windowLayout;
this.mRequiredDisplayCategory = requiredDisplayCategory;
+ this.mRequireContentUriPermissionFromCaller = requireContentUriPermissionFromCaller;
// onConstructed(); // You can define this method to get a callback
}
@@ -563,6 +572,11 @@
}
@DataClass.Generated.Member
+ public int getRequireContentUriPermissionFromCaller() {
+ return mRequireContentUriPermissionFromCaller;
+ }
+
+ @DataClass.Generated.Member
public @NonNull ParsedActivityImpl setTheme( int value) {
theme = value;
return this;
@@ -694,11 +708,17 @@
return this;
}
+ @DataClass.Generated.Member
+ public @NonNull ParsedActivityImpl setRequireContentUriPermissionFromCaller( int value) {
+ mRequireContentUriPermissionFromCaller = value;
+ return this;
+ }
+
@DataClass.Generated(
- time = 1701338377709L,
+ time = 1706180262165L,
codegenVersion = "1.0.23",
sourceFile = "frameworks/base/core/java/com/android/internal/pm/pkg/component/ParsedActivityImpl.java",
- inputSignatures = "private int theme\nprivate int uiOptions\nprivate @android.annotation.Nullable @com.android.internal.util.DataClass.ParcelWith(com.android.internal.util.Parcelling.BuiltIn.ForInternedString.class) java.lang.String targetActivity\nprivate @android.annotation.Nullable @com.android.internal.util.DataClass.ParcelWith(com.android.internal.util.Parcelling.BuiltIn.ForInternedString.class) java.lang.String parentActivityName\nprivate @android.annotation.Nullable java.lang.String taskAffinity\nprivate int privateFlags\nprivate @android.annotation.Nullable @com.android.internal.util.DataClass.ParcelWith(com.android.internal.util.Parcelling.BuiltIn.ForInternedString.class) java.lang.String permission\nprivate @android.annotation.Nullable java.util.Set<java.lang.String> mKnownActivityEmbeddingCerts\nprivate int launchMode\nprivate int documentLaunchMode\nprivate int maxRecents\nprivate int configChanges\nprivate int softInputMode\nprivate int persistableMode\nprivate int lockTaskLaunchMode\nprivate int screenOrientation\nprivate int resizeMode\nprivate float maxAspectRatio\nprivate float minAspectRatio\nprivate boolean supportsSizeChanges\nprivate @android.annotation.Nullable java.lang.String requestedVrComponent\nprivate int rotationAnimation\nprivate int colorMode\nprivate @android.annotation.Nullable android.content.pm.ActivityInfo.WindowLayout windowLayout\nprivate @android.annotation.Nullable java.lang.String mRequiredDisplayCategory\npublic static final @android.annotation.NonNull android.os.Parcelable.Creator<com.android.internal.pm.pkg.component.ParsedActivityImpl> CREATOR\npublic static @android.annotation.NonNull com.android.internal.pm.pkg.component.ParsedActivityImpl makeAppDetailsActivity(java.lang.String,java.lang.String,int,java.lang.String,boolean)\nstatic @android.annotation.NonNull com.android.internal.pm.pkg.component.ParsedActivityImpl makeAlias(java.lang.String,com.android.internal.pm.pkg.component.ParsedActivity)\npublic com.android.internal.pm.pkg.component.ParsedActivityImpl setMaxAspectRatio(int,float)\npublic com.android.internal.pm.pkg.component.ParsedActivityImpl setMinAspectRatio(int,float)\npublic com.android.internal.pm.pkg.component.ParsedActivityImpl setTargetActivity(java.lang.String)\npublic com.android.internal.pm.pkg.component.ParsedActivityImpl setPermission(java.lang.String)\npublic @android.annotation.NonNull @java.lang.Override java.util.Set<java.lang.String> getKnownActivityEmbeddingCerts()\npublic void setKnownActivityEmbeddingCerts(java.util.Set<java.lang.String>)\npublic java.lang.String toString()\npublic @java.lang.Override int describeContents()\npublic @java.lang.Override void writeToParcel(android.os.Parcel,int)\nclass ParsedActivityImpl extends com.android.internal.pm.pkg.component.ParsedMainComponentImpl implements [com.android.internal.pm.pkg.component.ParsedActivity, android.os.Parcelable]\n@com.android.internal.util.DataClass(genGetters=true, genSetters=true, genBuilder=false, genParcelable=false)")
+ inputSignatures = "private int theme\nprivate int uiOptions\nprivate @android.annotation.Nullable @com.android.internal.util.DataClass.ParcelWith(com.android.internal.util.Parcelling.BuiltIn.ForInternedString.class) java.lang.String targetActivity\nprivate @android.annotation.Nullable @com.android.internal.util.DataClass.ParcelWith(com.android.internal.util.Parcelling.BuiltIn.ForInternedString.class) java.lang.String parentActivityName\nprivate @android.annotation.Nullable java.lang.String taskAffinity\nprivate int privateFlags\nprivate @android.annotation.Nullable @com.android.internal.util.DataClass.ParcelWith(com.android.internal.util.Parcelling.BuiltIn.ForInternedString.class) java.lang.String permission\nprivate @android.annotation.Nullable java.util.Set<java.lang.String> mKnownActivityEmbeddingCerts\nprivate int launchMode\nprivate int documentLaunchMode\nprivate int maxRecents\nprivate int configChanges\nprivate int softInputMode\nprivate int persistableMode\nprivate int lockTaskLaunchMode\nprivate int screenOrientation\nprivate int resizeMode\nprivate float maxAspectRatio\nprivate float minAspectRatio\nprivate boolean supportsSizeChanges\nprivate @android.annotation.Nullable java.lang.String requestedVrComponent\nprivate int rotationAnimation\nprivate int colorMode\nprivate @android.annotation.Nullable android.content.pm.ActivityInfo.WindowLayout windowLayout\nprivate @android.annotation.Nullable java.lang.String mRequiredDisplayCategory\nprivate int mRequireContentUriPermissionFromCaller\npublic static final @android.annotation.NonNull android.os.Parcelable.Creator<com.android.internal.pm.pkg.component.ParsedActivityImpl> CREATOR\npublic static @android.annotation.NonNull com.android.internal.pm.pkg.component.ParsedActivityImpl makeAppDetailsActivity(java.lang.String,java.lang.String,int,java.lang.String,boolean)\nstatic @android.annotation.NonNull com.android.internal.pm.pkg.component.ParsedActivityImpl makeAlias(java.lang.String,com.android.internal.pm.pkg.component.ParsedActivity)\npublic com.android.internal.pm.pkg.component.ParsedActivityImpl setMaxAspectRatio(int,float)\npublic com.android.internal.pm.pkg.component.ParsedActivityImpl setMinAspectRatio(int,float)\npublic com.android.internal.pm.pkg.component.ParsedActivityImpl setTargetActivity(java.lang.String)\npublic com.android.internal.pm.pkg.component.ParsedActivityImpl setPermission(java.lang.String)\npublic @android.annotation.NonNull @java.lang.Override java.util.Set<java.lang.String> getKnownActivityEmbeddingCerts()\npublic void setKnownActivityEmbeddingCerts(java.util.Set<java.lang.String>)\npublic java.lang.String toString()\npublic @java.lang.Override int describeContents()\npublic @java.lang.Override void writeToParcel(android.os.Parcel,int)\nclass ParsedActivityImpl extends com.android.internal.pm.pkg.component.ParsedMainComponentImpl implements [com.android.internal.pm.pkg.component.ParsedActivity, android.os.Parcelable]\n@com.android.internal.util.DataClass(genGetters=true, genSetters=true, genBuilder=false, genParcelable=false)")
@Deprecated
private void __metadata() {}
diff --git a/core/java/com/android/internal/pm/pkg/component/ParsedActivityUtils.java b/core/java/com/android/internal/pm/pkg/component/ParsedActivityUtils.java
index c3f7dab..9f71d88 100644
--- a/core/java/com/android/internal/pm/pkg/component/ParsedActivityUtils.java
+++ b/core/java/com/android/internal/pm/pkg/component/ParsedActivityUtils.java
@@ -241,6 +241,10 @@
activity.setRequiredDisplayCategory(requiredDisplayCategory);
+ activity.setRequireContentUriPermissionFromCaller(sa.getInt(
+ R.styleable.AndroidManifestActivity_requireContentUriPermissionFromCaller,
+ ActivityInfo.CONTENT_URI_PERMISSION_NONE));
+
return parseActivityOrAlias(activity, pkg, tag, parser, res, sa, receiver,
false /*isAlias*/, visibleToEphemeral, input,
R.styleable.AndroidManifestActivity_parentActivityName,
diff --git a/core/java/com/android/internal/pm/pkg/component/ParsedProcess.java b/core/java/com/android/internal/pm/pkg/component/ParsedProcess.java
index e5247f9..852ed1c 100644
--- a/core/java/com/android/internal/pm/pkg/component/ParsedProcess.java
+++ b/core/java/com/android/internal/pm/pkg/component/ParsedProcess.java
@@ -51,4 +51,6 @@
@ApplicationInfo.NativeHeapZeroInitialized
int getNativeHeapZeroInitialized();
+
+ boolean isUseEmbeddedDex();
}
diff --git a/core/java/com/android/internal/pm/pkg/component/ParsedProcessImpl.java b/core/java/com/android/internal/pm/pkg/component/ParsedProcessImpl.java
index 212fb86..ff9b11a 100644
--- a/core/java/com/android/internal/pm/pkg/component/ParsedProcessImpl.java
+++ b/core/java/com/android/internal/pm/pkg/component/ParsedProcessImpl.java
@@ -54,6 +54,8 @@
@ApplicationInfo.NativeHeapZeroInitialized
private int nativeHeapZeroInitialized = ApplicationInfo.ZEROINIT_DEFAULT;
+ private boolean useEmbeddedDex;
+
public ParsedProcessImpl() {
}
@@ -65,6 +67,7 @@
gwpAsanMode = other.getGwpAsanMode();
memtagMode = other.getMemtagMode();
nativeHeapZeroInitialized = other.getNativeHeapZeroInitialized();
+ useEmbeddedDex = other.isUseEmbeddedDex();
}
public void addStateFrom(@NonNull ParsedProcess other) {
@@ -72,6 +75,7 @@
gwpAsanMode = other.getGwpAsanMode();
memtagMode = other.getMemtagMode();
nativeHeapZeroInitialized = other.getNativeHeapZeroInitialized();
+ useEmbeddedDex = other.isUseEmbeddedDex();
final ArrayMap<String, String> oacn = other.getAppClassNamesByPackage();
for (int i = 0; i < oacn.size(); i++) {
@@ -115,7 +119,8 @@
@NonNull Set<String> deniedPermissions,
@ApplicationInfo.GwpAsanMode int gwpAsanMode,
@ApplicationInfo.MemtagMode int memtagMode,
- @ApplicationInfo.NativeHeapZeroInitialized int nativeHeapZeroInitialized) {
+ @ApplicationInfo.NativeHeapZeroInitialized int nativeHeapZeroInitialized,
+ boolean useEmbeddedDex) {
this.name = name;
com.android.internal.util.AnnotationValidations.validate(
NonNull.class, null, name);
@@ -134,6 +139,7 @@
this.nativeHeapZeroInitialized = nativeHeapZeroInitialized;
com.android.internal.util.AnnotationValidations.validate(
ApplicationInfo.NativeHeapZeroInitialized.class, null, nativeHeapZeroInitialized);
+ this.useEmbeddedDex = useEmbeddedDex;
// onConstructed(); // You can define this method to get a callback
}
@@ -172,6 +178,11 @@
}
@DataClass.Generated.Member
+ public boolean isUseEmbeddedDex() {
+ return useEmbeddedDex;
+ }
+
+ @DataClass.Generated.Member
public @NonNull ParsedProcessImpl setName(@NonNull String value) {
name = value;
com.android.internal.util.AnnotationValidations.validate(
@@ -223,6 +234,12 @@
}
@DataClass.Generated.Member
+ public @NonNull ParsedProcessImpl setUseEmbeddedDex( boolean value) {
+ useEmbeddedDex = value;
+ return this;
+ }
+
+ @DataClass.Generated.Member
static Parcelling<Set<String>> sParcellingForDeniedPermissions =
Parcelling.Cache.get(
Parcelling.BuiltIn.ForInternedStringSet.class);
@@ -239,6 +256,9 @@
// You can override field parcelling by defining methods like:
// void parcelFieldName(Parcel dest, int flags) { ... }
+ byte flg = 0;
+ if (useEmbeddedDex) flg |= 0x40;
+ dest.writeByte(flg);
dest.writeString(name);
dest.writeMap(appClassNamesByPackage);
sParcellingForDeniedPermissions.parcel(deniedPermissions, dest, flags);
@@ -258,6 +278,8 @@
// You can override field unparcelling by defining methods like:
// static FieldType unparcelFieldName(Parcel in) { ... }
+ byte flg = in.readByte();
+ boolean _useEmbeddedDex = (flg & 0x40) != 0;
String _name = in.readString();
ArrayMap<String,String> _appClassNamesByPackage = new ArrayMap();
in.readMap(_appClassNamesByPackage, String.class.getClassLoader());
@@ -284,6 +306,7 @@
this.nativeHeapZeroInitialized = _nativeHeapZeroInitialized;
com.android.internal.util.AnnotationValidations.validate(
ApplicationInfo.NativeHeapZeroInitialized.class, null, nativeHeapZeroInitialized);
+ this.useEmbeddedDex = _useEmbeddedDex;
// onConstructed(); // You can define this method to get a callback
}
@@ -303,10 +326,10 @@
};
@DataClass.Generated(
- time = 1701445656489L,
+ time = 1706177189475L,
codegenVersion = "1.0.23",
sourceFile = "frameworks/base/core/java/com/android/internal/pm/pkg/component/ParsedProcessImpl.java",
- inputSignatures = "private @android.annotation.NonNull java.lang.String name\nprivate @android.annotation.NonNull android.util.ArrayMap<java.lang.String,java.lang.String> appClassNamesByPackage\nprivate @android.annotation.NonNull @com.android.internal.util.DataClass.ParcelWith(com.android.internal.util.Parcelling.BuiltIn.ForInternedStringSet.class) java.util.Set<java.lang.String> deniedPermissions\nprivate @android.content.pm.ApplicationInfo.GwpAsanMode int gwpAsanMode\nprivate @android.content.pm.ApplicationInfo.MemtagMode int memtagMode\nprivate @android.content.pm.ApplicationInfo.NativeHeapZeroInitialized int nativeHeapZeroInitialized\npublic void addStateFrom(com.android.internal.pm.pkg.component.ParsedProcess)\npublic void putAppClassNameForPackage(java.lang.String,java.lang.String)\nclass ParsedProcessImpl extends java.lang.Object implements [com.android.internal.pm.pkg.component.ParsedProcess, android.os.Parcelable]\n@com.android.internal.util.DataClass(genGetters=true, genSetters=true, genParcelable=true, genAidl=false, genBuilder=false)")
+ inputSignatures = "private @android.annotation.NonNull java.lang.String name\nprivate @android.annotation.NonNull android.util.ArrayMap<java.lang.String,java.lang.String> appClassNamesByPackage\nprivate @android.annotation.NonNull @com.android.internal.util.DataClass.ParcelWith(com.android.internal.util.Parcelling.BuiltIn.ForInternedStringSet.class) java.util.Set<java.lang.String> deniedPermissions\nprivate @android.content.pm.ApplicationInfo.GwpAsanMode int gwpAsanMode\nprivate @android.content.pm.ApplicationInfo.MemtagMode int memtagMode\nprivate @android.content.pm.ApplicationInfo.NativeHeapZeroInitialized int nativeHeapZeroInitialized\nprivate boolean useEmbeddedDex\npublic void addStateFrom(com.android.internal.pm.pkg.component.ParsedProcess)\npublic void putAppClassNameForPackage(java.lang.String,java.lang.String)\nclass ParsedProcessImpl extends java.lang.Object implements [com.android.internal.pm.pkg.component.ParsedProcess, android.os.Parcelable]\n@com.android.internal.util.DataClass(genGetters=true, genSetters=true, genParcelable=true, genAidl=false, genBuilder=false)")
@Deprecated
private void __metadata() {}
diff --git a/core/java/com/android/internal/pm/pkg/component/ParsedProcessUtils.java b/core/java/com/android/internal/pm/pkg/component/ParsedProcessUtils.java
index 3b2056e..dd58815f 100644
--- a/core/java/com/android/internal/pm/pkg/component/ParsedProcessUtils.java
+++ b/core/java/com/android/internal/pm/pkg/component/ParsedProcessUtils.java
@@ -27,6 +27,7 @@
import android.util.ArraySet;
import com.android.internal.R;
+import com.android.internal.pm.pkg.component.flags.Flags;
import com.android.internal.pm.pkg.parsing.ParsingPackage;
import com.android.internal.pm.pkg.parsing.ParsingUtils;
import com.android.internal.util.CollectionUtils;
@@ -111,6 +112,12 @@
proc.setNativeHeapZeroInitialized(
v ? ApplicationInfo.ZEROINIT_ENABLED : ApplicationInfo.ZEROINIT_DISABLED);
}
+ if (Flags.enablePerProcessUseEmbeddedDexAttr()) {
+ proc.setUseEmbeddedDex(
+ sa.getBoolean(R.styleable.AndroidManifestProcess_useEmbeddedDex, false));
+ } else {
+ proc.setUseEmbeddedDex(false);
+ }
} finally {
sa.recycle();
}
diff --git a/core/java/com/android/internal/pm/pkg/component/flags/flags.aconfig b/core/java/com/android/internal/pm/pkg/component/flags/flags.aconfig
new file mode 100644
index 0000000..ea9abdb
--- /dev/null
+++ b/core/java/com/android/internal/pm/pkg/component/flags/flags.aconfig
@@ -0,0 +1,9 @@
+package: "com.android.internal.pm.pkg.component.flags"
+
+flag {
+ name: "enable_per_process_use_embedded_dex_attr"
+ namespace: "machine_learning"
+ description: "This flag enables support for parsing per-process useEmbeddedDex attribute."
+ is_fixed_read_only: true
+ bug: "295870718"
+}
\ No newline at end of file
diff --git a/core/java/com/android/internal/pm/pkg/parsing/ParsingPackage.java b/core/java/com/android/internal/pm/pkg/parsing/ParsingPackage.java
index 6c09b7c..ef106e0 100644
--- a/core/java/com/android/internal/pm/pkg/parsing/ParsingPackage.java
+++ b/core/java/com/android/internal/pm/pkg/parsing/ParsingPackage.java
@@ -347,6 +347,11 @@
ParsingPackage setUpdatableSystem(boolean value);
+ /**
+ * Sets a system app that is allowed to update another system app
+ */
+ ParsingPackage setEmergencyInstaller(String emergencyInstaller);
+
ParsingPackage setLargeScreensSupported(int supportsLargeScreens);
ParsingPackage setNormalScreensSupported(int supportsNormalScreens);
diff --git a/core/java/com/android/internal/pm/pkg/parsing/ParsingPackageUtils.java b/core/java/com/android/internal/pm/pkg/parsing/ParsingPackageUtils.java
index f483597..e0fdbc6 100644
--- a/core/java/com/android/internal/pm/pkg/parsing/ParsingPackageUtils.java
+++ b/core/java/com/android/internal/pm/pkg/parsing/ParsingPackageUtils.java
@@ -952,6 +952,8 @@
final boolean updatableSystem = parser.getAttributeBooleanValue(null /*namespace*/,
"updatableSystem", true);
+ final String emergencyInstaller = parser.getAttributeValue(null /*namespace*/,
+ "emergencyInstaller");
pkg.setInstallLocation(anInteger(PARSE_DEFAULT_INSTALL_LOCATION,
R.styleable.AndroidManifest_installLocation, sa))
@@ -959,7 +961,8 @@
R.styleable.AndroidManifest_targetSandboxVersion, sa))
/* Set the global "on SD card" flag */
.setExternalStorage((flags & PARSE_EXTERNAL_STORAGE) != 0)
- .setUpdatableSystem(updatableSystem);
+ .setUpdatableSystem(updatableSystem)
+ .setEmergencyInstaller(emergencyInstaller);
boolean foundApp = false;
final int depth = parser.getDepth();
diff --git a/core/java/com/android/internal/policy/PhoneWindow.java b/core/java/com/android/internal/policy/PhoneWindow.java
index b4f9ee3..5239245 100644
--- a/core/java/com/android/internal/policy/PhoneWindow.java
+++ b/core/java/com/android/internal/policy/PhoneWindow.java
@@ -120,7 +120,6 @@
import android.window.ProxyOnBackInvokedDispatcher;
import com.android.internal.R;
-import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.view.menu.ContextMenuBuilder;
import com.android.internal.view.menu.IconMenuPresenter;
import com.android.internal.view.menu.ListMenuPresenter;
@@ -369,8 +368,7 @@
boolean mDecorFitsSystemWindows = true;
- @VisibleForTesting
- public final boolean mEdgeToEdgeEnforced;
+ private boolean mEdgeToEdgeEnforced;
private final ProxyOnBackInvokedDispatcher mProxyOnBackInvokedDispatcher;
@@ -390,11 +388,6 @@
mProxyOnBackInvokedDispatcher = new ProxyOnBackInvokedDispatcher(context);
mAllowFloatingWindowsFillScreen = context.getResources().getBoolean(
com.android.internal.R.bool.config_allowFloatingWindowsFillScreen);
- mEdgeToEdgeEnforced = isEdgeToEdgeEnforced(context.getApplicationInfo(), true /* local */);
- if (mEdgeToEdgeEnforced) {
- getAttributes().privateFlags |= PRIVATE_FLAG_EDGE_TO_EDGE_ENFORCED;
- mDecorFitsSystemWindows = false;
- }
}
/**
@@ -436,15 +429,18 @@
*
* @param info The application to query.
* @param local Whether this is called from the process of the given application.
+ * @param windowStyle The style of the window.
* @return {@code true} if edge-to-edge is enforced. Otherwise, {@code false}.
*/
- public static boolean isEdgeToEdgeEnforced(ApplicationInfo info, boolean local) {
- return info.targetSdkVersion >= ENFORCE_EDGE_TO_EDGE_SDK_VERSION
- || (Flags.enforceEdgeToEdge() && (local
- // Calling this doesn't require a permission.
- ? CompatChanges.isChangeEnabled(ENFORCE_EDGE_TO_EDGE)
- // Calling this requires permissions.
- : info.isChangeEnabled(ENFORCE_EDGE_TO_EDGE)));
+ public static boolean isEdgeToEdgeEnforced(ApplicationInfo info, boolean local,
+ TypedArray windowStyle) {
+ return !windowStyle.getBoolean(R.styleable.Window_windowOptOutEdgeToEdgeEnforcement, false)
+ && (info.targetSdkVersion >= ENFORCE_EDGE_TO_EDGE_SDK_VERSION
+ || (Flags.enforceEdgeToEdge() && (local
+ // Calling this doesn't require a permission.
+ ? CompatChanges.isChangeEnabled(ENFORCE_EDGE_TO_EDGE)
+ // Calling this requires permissions.
+ : info.isChangeEnabled(ENFORCE_EDGE_TO_EDGE))));
}
@Override
@@ -2470,6 +2466,13 @@
System.out.println(s);
}
+ mEdgeToEdgeEnforced = isEdgeToEdgeEnforced(
+ getContext().getApplicationInfo(), true /* local */, a);
+ if (mEdgeToEdgeEnforced) {
+ getAttributes().privateFlags |= PRIVATE_FLAG_EDGE_TO_EDGE_ENFORCED;
+ mDecorFitsSystemWindows = false;
+ }
+
mIsFloating = a.getBoolean(R.styleable.Window_windowIsFloating, false);
int flagsToUpdate = (FLAG_LAYOUT_IN_SCREEN|FLAG_LAYOUT_INSET_DECOR)
& (~getForcedWindowFlags());
diff --git a/core/java/com/android/internal/widget/ConversationLayout.java b/core/java/com/android/internal/widget/ConversationLayout.java
index a8d0d37..889434f 100644
--- a/core/java/com/android/internal/widget/ConversationLayout.java
+++ b/core/java/com/android/internal/widget/ConversationLayout.java
@@ -168,12 +168,12 @@
}
public ConversationLayout(@NonNull Context context, @Nullable AttributeSet attrs,
- @AttrRes int defStyleAttr) {
+ @AttrRes int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public ConversationLayout(@NonNull Context context, @Nullable AttributeSet attrs,
- @AttrRes int defStyleAttr, @StyleRes int defStyleRes) {
+ @AttrRes int defStyleAttr, @StyleRes int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
@@ -432,8 +432,14 @@
final List<MessagingMessage> newHistoricMessagingMessages =
createMessages(newHistoricMessages, /* isHistoric= */true, usePrecomputedText);
+ // Add our new MessagingMessages to groups
+ List<List<MessagingMessage>> groups = new ArrayList<>();
+ List<Person> senders = new ArrayList<>();
+ // Lets first find the groups (populate `groups` and `senders`)
+ findGroups(newHistoricMessagingMessages, newMessagingMessages, user, groups, senders);
+
return new MessagingData(user, showSpinner, unreadCount,
- newHistoricMessagingMessages, newMessagingMessages);
+ newHistoricMessagingMessages, newMessagingMessages, groups, senders);
}
/**
@@ -509,21 +515,13 @@
setUser(messagingData.getUser());
setUnreadCount(messagingData.getUnreadCount());
- List<MessagingMessage> messages = messagingData.getNewMessagingMessages();
- List<MessagingMessage> historicMessages = messagingData.getHistoricMessagingMessages();
// Copy our groups, before they get clobbered
ArrayList<MessagingGroup> oldGroups = new ArrayList<>(mGroups);
- // Add our new MessagingMessages to groups
- List<List<MessagingMessage>> groups = new ArrayList<>();
- List<Person> senders = new ArrayList<>();
-
- // Lets first find the groups (populate `groups` and `senders`)
- findGroups(historicMessages, messages, groups, senders);
-
// Let's now create the views and reorder them accordingly
// side-effect: updates mGroups, mAddedGroups
- createGroupViews(groups, senders, messagingData.getShowSpinner());
+ createGroupViews(messagingData.getGroups(), messagingData.getSenders(),
+ messagingData.getShowSpinner());
// Let's first check which groups were removed altogether and remove them in one animation
removeGroups(oldGroups);
@@ -536,8 +534,8 @@
historicMessage.removeMessage(mToRecycle);
}
- mMessages = messages;
- mHistoricMessages = historicMessages;
+ mMessages = messagingData.getNewMessagingMessages();
+ mHistoricMessages = messagingData.getHistoricMessagingMessages();
updateHistoricMessageVisibility();
updateTitleAndNamesDisplay();
@@ -935,7 +933,7 @@
}
private void createGroupViews(List<List<MessagingMessage>> groups,
- List<Person> senders, boolean showSpinner) {
+ List<Person> senders, boolean showSpinner) {
mGroups.clear();
for (int groupIndex = 0; groupIndex < groups.size(); groupIndex++) {
List<MessagingMessage> group = groups.get(groupIndex);
@@ -983,9 +981,12 @@
}
}
+ /**
+ * Finds groups and senders from the given messaging messages and fills outGroups and outSenders
+ */
private void findGroups(List<MessagingMessage> historicMessages,
- List<MessagingMessage> messages, List<List<MessagingMessage>> groups,
- List<Person> senders) {
+ List<MessagingMessage> messages, Person user, List<List<MessagingMessage>> outGroups,
+ List<Person> outSenders) {
CharSequence currentSenderKey = null;
List<MessagingMessage> currentGroup = null;
int histSize = historicMessages.size();
@@ -1003,14 +1004,14 @@
isNewGroup |= !TextUtils.equals(key, currentSenderKey);
if (isNewGroup) {
currentGroup = new ArrayList<>();
- groups.add(currentGroup);
+ outGroups.add(currentGroup);
if (sender == null) {
- sender = mUser;
+ sender = user;
} else {
// Remove all formatting from the sender name
sender = sender.toBuilder().setName(Objects.toString(sender.getName())).build();
}
- senders.add(sender);
+ outSenders.add(sender);
currentSenderKey = key;
}
currentGroup.add(message);
diff --git a/core/java/com/android/internal/widget/EmphasizedNotificationButton.java b/core/java/com/android/internal/widget/EmphasizedNotificationButton.java
index ce6af49..3e065bf 100644
--- a/core/java/com/android/internal/widget/EmphasizedNotificationButton.java
+++ b/core/java/com/android/internal/widget/EmphasizedNotificationButton.java
@@ -16,16 +16,30 @@
package com.android.internal.widget;
+import static android.app.Flags.evenlyDividedCallStyleActionLayout;
+import static android.app.Notification.CallStyle.DEBUG_NEW_ACTION_LAYOUT;
+import static android.text.style.DynamicDrawableSpan.ALIGN_CENTER;
+
+import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.Context;
import android.content.res.ColorStateList;
+import android.content.res.TypedArray;
import android.graphics.BlendMode;
+import android.graphics.Canvas;
+import android.graphics.Paint;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.DrawableWrapper;
import android.graphics.drawable.GradientDrawable;
import android.graphics.drawable.Icon;
import android.graphics.drawable.RippleDrawable;
+import android.text.SpannableStringBuilder;
+import android.text.TextPaint;
+import android.text.style.ImageSpan;
+import android.text.style.MetricAffectingSpan;
+import android.text.style.ReplacementSpan;
import android.util.AttributeSet;
+import android.util.Log;
import android.view.RemotableViewMethod;
import android.widget.Button;
import android.widget.RemoteViews;
@@ -43,6 +57,14 @@
private final GradientDrawable mBackground;
private boolean mPriority;
+ private int mInitialDrawablePadding;
+ private int mIconSize;
+
+ private Drawable mIconToGlue;
+ private CharSequence mLabelToGlue;
+ private int mGluedLayoutDirection = LAYOUT_DIRECTION_UNDEFINED;
+ private boolean mGluePending;
+
public EmphasizedNotificationButton(Context context) {
this(context, null);
}
@@ -58,10 +80,25 @@
public EmphasizedNotificationButton(Context context, AttributeSet attrs, int defStyleAttr,
int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
+
mRipple = (RippleDrawable) getBackground();
mRipple.mutate();
DrawableWrapper inset = (DrawableWrapper) mRipple.getDrawable(0);
mBackground = (GradientDrawable) inset.getDrawable();
+
+ mIconSize = mContext.getResources().getDimensionPixelSize(
+ R.dimen.notification_actions_icon_drawable_size);
+
+ try (TypedArray typedArray = context.obtainStyledAttributes(
+ attrs, android.R.styleable.TextView, defStyleAttr, defStyleRes)) {
+ mInitialDrawablePadding = typedArray.getDimensionPixelSize(
+ android.R.styleable.TextView_drawablePadding, 0);
+ }
+
+ if (DEBUG_NEW_ACTION_LAYOUT) {
+ Log.v(TAG, "iconSize = " + mIconSize + "px, "
+ + "initialDrawablePadding = " + mInitialDrawablePadding + "px");
+ }
}
@RemotableViewMethod
@@ -95,19 +132,248 @@
return () -> setImageDrawable(drawable);
}
- private void setImageDrawable(Drawable drawable) {
+ private void setImageDrawable(@Nullable Drawable drawable) {
if (drawable != null) {
- drawable.mutate();
- drawable.setTintList(getTextColors());
- drawable.setTintBlendMode(BlendMode.SRC_IN);
- int iconSize = mContext.getResources().getDimensionPixelSize(
- R.dimen.notification_actions_icon_drawable_size);
- drawable.setBounds(0, 0, iconSize, iconSize);
+ prepareIcon(drawable);
}
setCompoundDrawablesRelative(drawable, null, null, null);
}
/**
+ * Sets an icon to be 'glued' to the label when this button is displayed, so the icon will stay
+ * with the text if the button is wider than needed and the text isn't start-aligned.
+ *
+ * As with {@link #setImageIcon(Icon)}, the Icon will have its size constrained and will be set
+ * to the same color as the text, and this must be called after {@link #setTextColor(int)} for
+ * the latter to work.
+ *
+ * This must be called along with {@link #glueLabel(CharSequence)}, in any order, before the
+ * button is displayed.
+ */
+ @RemotableViewMethod(asyncImpl = "glueIconAsync")
+ public void glueIcon(@Nullable Icon icon) {
+ final Drawable drawable = icon == null ? null : icon.loadDrawable(mContext);
+ setIconToGlue(drawable);
+ }
+
+ /**
+ * @hide
+ */
+ @RemotableViewMethod
+ public Runnable glueIconAsync(@Nullable Icon icon) {
+ final Drawable drawable = icon == null ? null : icon.loadDrawable(mContext);
+ return () -> setIconToGlue(drawable);
+ }
+
+ private void setIconToGlue(@Nullable Drawable icon) {
+ if (!evenlyDividedCallStyleActionLayout()) {
+ Log.e(TAG, "glueIcon: new action layout disabled; doing nothing");
+ return;
+ }
+
+ prepareIcon(icon);
+
+ mIconToGlue = icon;
+ mGluePending = true;
+
+ glueIconAndLabelIfNeeded();
+ }
+
+ private void prepareIcon(@NonNull Drawable drawable) {
+ drawable.mutate();
+ drawable.setTintList(getTextColors());
+ drawable.setTintBlendMode(BlendMode.SRC_IN);
+ drawable.setBounds(0, 0, mIconSize, mIconSize);
+ }
+
+ /**
+ * Sets a label to be 'glued' to the icon when this button is displayed, so the icon will stay
+ * with the text if the button is wider than needed and the text isn't start-aligned.
+ *
+ * This must be called along with {@link #glueIcon(Icon)}, in any order, before the button is
+ * displayed.
+ */
+ @RemotableViewMethod(asyncImpl = "glueLabelAsync")
+ public void glueLabel(@Nullable CharSequence label) {
+ setLabelToGlue(label);
+ }
+
+ /**
+ * @hide
+ */
+ @RemotableViewMethod
+ public Runnable glueLabelAsync(@Nullable CharSequence label) {
+ return () -> setLabelToGlue(label);
+ }
+
+ private void setLabelToGlue(@Nullable CharSequence label) {
+ if (!evenlyDividedCallStyleActionLayout()) {
+ Log.e(TAG, "glueLabel: new action layout disabled; doing nothing");
+ return;
+ }
+
+ mLabelToGlue = label;
+ mGluePending = true;
+
+ glueIconAndLabelIfNeeded();
+ }
+
+ @Override
+ public void onRtlPropertiesChanged(int layoutDirection) {
+ super.onRtlPropertiesChanged(layoutDirection);
+
+ if (DEBUG_NEW_ACTION_LAYOUT) {
+ Log.v(TAG, "onRtlPropertiesChanged: layoutDirection = " + layoutDirection + ", "
+ + "gluedLayoutDirection = " + mGluedLayoutDirection);
+ }
+
+ if (layoutDirection != mGluedLayoutDirection) {
+ if (DEBUG_NEW_ACTION_LAYOUT) {
+ Log.d(TAG, "onRtlPropertiesChanged: layout direction changed; regluing");
+ }
+ mGluePending = true;
+ }
+
+ glueIconAndLabelIfNeeded();
+ }
+
+ private void glueIconAndLabelIfNeeded() {
+ // Don't need to glue:
+
+ if (!mGluePending) {
+ if (DEBUG_NEW_ACTION_LAYOUT) {
+ Log.v(TAG, "glueIconAndLabelIfNeeded: glue not pending; doing nothing");
+ }
+ return;
+ }
+
+ if (mIconToGlue == null && mLabelToGlue == null) {
+ if (DEBUG_NEW_ACTION_LAYOUT) {
+ Log.v(TAG, "glueIconAndLabelIfNeeded: no icon or label to glue; doing nothing");
+ }
+ mGluePending = false;
+ return;
+ }
+
+ if (!evenlyDividedCallStyleActionLayout()) {
+ Log.e(TAG, "glueIconAndLabelIfNeeded: new action layout disabled; doing nothing");
+ return;
+ }
+
+ // Not ready to glue yet:
+
+ if (!isLayoutDirectionResolved()) {
+ if (DEBUG_NEW_ACTION_LAYOUT) {
+ Log.v(TAG, "glueIconAndLabelIfNeeded: "
+ + "layout direction not resolved; doing nothing");
+ }
+ return;
+ }
+
+ // Ready to glue but don't have an icon *and* a label:
+ //
+ // (Note that this will *not* happen while the button is being initialized, since we won't
+ // be ready to glue. This can only happen if the button is initialized and displayed and
+ // *then* someone calls glueIcon or glueLabel.
+
+ if (mIconToGlue == null) {
+ Log.w(TAG, "glueIconAndLabelIfNeeded: label glued without icon; doing nothing");
+ return;
+ }
+
+ if (mLabelToGlue == null) {
+ Log.w(TAG, "glueIconAndLabelIfNeeded: icon glued without label; doing nothing");
+ return;
+ }
+
+ // Can't glue:
+
+ final int layoutDirection = getLayoutDirection();
+ if (layoutDirection != LAYOUT_DIRECTION_LTR && layoutDirection != LAYOUT_DIRECTION_RTL) {
+ Log.e(TAG, "glueIconAndLabelIfNeeded: "
+ + "resolved layout direction neither LTR nor RTL; "
+ + "doing nothing");
+ return;
+ }
+
+ // No excuses left, let's glue it!
+
+ glueIconAndLabel(layoutDirection);
+
+ mGluePending = false;
+ mGluedLayoutDirection = layoutDirection;
+ }
+
+ // Unicode replacement character
+ private static final String IMAGE_SPAN_TEXT = "\ufffd";
+
+ // Unicode no-break space
+ private static final String SPACER_SPAN_TEXT = "\u00a0";
+
+ private static final String LEFT_TO_RIGHT_ISOLATE = "\u2066";
+ private static final String RIGHT_TO_LEFT_ISOLATE = "\u2067";
+ private static final String FIRST_STRONG_ISOLATE = "\u2068";
+ private static final String POP_DIRECTIONAL_ISOLATE = "\u2069";
+
+ private void glueIconAndLabel(int layoutDirection) {
+ final boolean rtlLayout = layoutDirection == LAYOUT_DIRECTION_RTL;
+
+ if (DEBUG_NEW_ACTION_LAYOUT) {
+ Log.d(TAG, "glueIconAndLabel: "
+ + "icon = " + mIconToGlue + ", "
+ + "iconSize = " + mIconSize + "px, "
+ + "initialDrawablePadding = " + mInitialDrawablePadding + "px, "
+ + "labelToGlue.length = " + mLabelToGlue.length() + ", "
+ + "rtlLayout = " + rtlLayout);
+ }
+
+ logIfTextDirectionNotFirstStrong();
+
+ final SpannableStringBuilder builder = new SpannableStringBuilder();
+
+ // The text direction of the label might not match the layout direction of the button, so
+ // wrap the entire string in a LEFT-TO-RIGHT ISOLATE or RIGHT-TO-LEFT ISOLATE to match the
+ // layout direction. This puts the icon, padding, and label in the right order.
+ builder.append(rtlLayout ? RIGHT_TO_LEFT_ISOLATE : LEFT_TO_RIGHT_ISOLATE);
+
+ appendSpan(builder, IMAGE_SPAN_TEXT, new ImageSpan(mIconToGlue, ALIGN_CENTER));
+ appendSpan(builder, SPACER_SPAN_TEXT, new SpacerSpan(mInitialDrawablePadding));
+
+ // If the text and layout directions are different, we would end up with the *label* in the
+ // wrong direction, so wrap the label in a FIRST STRONG ISOLATE. This triggers the same
+ // automatic text direction heuristic that Android uses by default.
+ builder.append(FIRST_STRONG_ISOLATE);
+
+ appendSpan(builder, mLabelToGlue, new CenterBesideImageSpan(mIconSize));
+
+ builder.append(POP_DIRECTIONAL_ISOLATE);
+ builder.append(POP_DIRECTIONAL_ISOLATE);
+
+ setText(builder);
+ }
+
+ private void logIfTextDirectionNotFirstStrong() {
+ if (!isTextDirectionResolved()) {
+ Log.e(TAG, "glueIconAndLabel: text direction not resolved; "
+ + "letting View assume FIRST STRONG");
+ }
+ final int textDirection = getTextDirection();
+ if (textDirection != TEXT_DIRECTION_FIRST_STRONG) {
+ Log.w(TAG, "glueIconAndLabel: "
+ + "expected text direction TEXT_DIRECTION_FIRST_STRONG "
+ + "but found " + textDirection + "; "
+ + "will use a FIRST STRONG ISOLATE regardless");
+ }
+ }
+
+ private void appendSpan(SpannableStringBuilder builder, CharSequence text, Object span) {
+ final int spanStart = builder.length();
+ builder.append(text);
+ final int spanEnd = builder.length();
+ builder.setSpan(span, spanStart, spanEnd, 0);
+ }
+
+ /**
* Sets whether this view is a priority over its peers (which affects width).
* Specifically, this is used by {@link NotificationActionListLayout} to give this view width
* priority ahead of user-defined buttons when allocating horizontal space.
@@ -123,4 +389,104 @@
public boolean isPriority() {
return mPriority;
}
+
+ private static class SpacerSpan extends ReplacementSpan {
+ private int mWidth;
+
+ SpacerSpan(int width) {
+ mWidth = width;
+
+ if (DEBUG_NEW_ACTION_LAYOUT) {
+ Log.d(TAG, "width = " + mWidth + "px");
+ }
+ }
+
+
+ @Override
+ public int getSize(@NonNull Paint paint, CharSequence text, int start, int end,
+ @Nullable Paint.FontMetricsInt fontMetrics) {
+ if (DEBUG_NEW_ACTION_LAYOUT) {
+ Log.v(TAG, "getSize returning " + mWidth + "px");
+ }
+
+ return mWidth;
+ }
+
+ @Override
+ public void draw(@NonNull Canvas canvas, CharSequence text, int start, int end,
+ float x, int top, int y, int bottom, @NonNull Paint paint) {
+ if (DEBUG_NEW_ACTION_LAYOUT) {
+ Log.v(TAG, "drawing nothing");
+ }
+
+ // Draw nothing, it's a spacer.
+ }
+
+ private static final String TAG = "SpacerSpan";
+ }
+
+ private static class CenterBesideImageSpan extends MetricAffectingSpan {
+ private int mImageHeight;
+
+ private boolean mMeasured;
+ private int mBaselineShiftOffset;
+
+ CenterBesideImageSpan(int imageHeight) {
+ mImageHeight = imageHeight;
+
+ if (DEBUG_NEW_ACTION_LAYOUT) {
+ Log.d(TAG, "imageHeight = " + mImageHeight + "px");
+ }
+ }
+
+ @Override
+ public void updateMeasureState(@NonNull TextPaint textPaint) {
+ final int textHeight = (int) -textPaint.ascent();
+
+ /*
+ * We only need to shift the text *up* if the text is shorter than the image; ImageSpan
+ * with ALIGN_CENTER will shift the *image* up if the text is taller than the image.
+ */
+ if (textHeight < mImageHeight) {
+ mBaselineShiftOffset = -(mImageHeight - textHeight) / 2;
+ } else {
+ mBaselineShiftOffset = 0;
+ }
+
+ mMeasured = true;
+
+ if (DEBUG_NEW_ACTION_LAYOUT) {
+ Log.d(TAG, "updateMeasureState: "
+ + "imageHeight = " + mImageHeight + "px, "
+ + "textHeight = " + textHeight + "px, "
+ + "baselineShiftOffset = " + mBaselineShiftOffset + "px");
+ }
+
+ textPaint.baselineShift += mBaselineShiftOffset;
+ }
+
+ @Override
+ public void updateDrawState(TextPaint textPaint) {
+ if (textPaint == null) {
+ Log.e(TAG, "updateDrawState: textPaint is null; doing nothing");
+ return;
+ }
+
+ if (!mMeasured) {
+ Log.e(TAG, "updateDrawState: called without measure; doing nothing");
+ return;
+ }
+
+ if (DEBUG_NEW_ACTION_LAYOUT) {
+ Log.v(TAG, "updateDrawState: "
+ + "baselineShiftOffset = " + mBaselineShiftOffset + "px");
+ }
+
+ textPaint.baselineShift += mBaselineShiftOffset;
+ }
+
+ private static final String TAG = "CenterBesideImageSpan";
+ }
+
+ private static final String TAG = "EmphasizedNotificationButton";
}
diff --git a/core/java/com/android/internal/widget/LockSettingsInternal.java b/core/java/com/android/internal/widget/LockSettingsInternal.java
index 627e877..e591327 100644
--- a/core/java/com/android/internal/widget/LockSettingsInternal.java
+++ b/core/java/com/android/internal/widget/LockSettingsInternal.java
@@ -171,11 +171,11 @@
* Register a LockSettingsStateListener
* @param listener The listener to be registered
*/
- public abstract void registerLockSettingsStateListener(ILockSettingsStateListener listener);
+ public abstract void registerLockSettingsStateListener(LockSettingsStateListener listener);
/**
* Unregister a LockSettingsStateListener
* @param listener The listener to be unregistered
*/
- public abstract void unregisterLockSettingsStateListener(ILockSettingsStateListener listener);
+ public abstract void unregisterLockSettingsStateListener(LockSettingsStateListener listener);
}
diff --git a/core/java/com/android/internal/widget/ILockSettingsStateListener.aidl b/core/java/com/android/internal/widget/LockSettingsStateListener.java
similarity index 91%
rename from core/java/com/android/internal/widget/ILockSettingsStateListener.aidl
rename to core/java/com/android/internal/widget/LockSettingsStateListener.java
index 25e3003..869e676 100644
--- a/core/java/com/android/internal/widget/ILockSettingsStateListener.aidl
+++ b/core/java/com/android/internal/widget/LockSettingsStateListener.java
@@ -5,7 +5,7 @@
* 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
+ * 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,
@@ -21,7 +21,7 @@
* state of primary authentication (i.e. PIN/pattern/password).
* @hide
*/
-oneway interface ILockSettingsStateListener {
+public interface LockSettingsStateListener {
/**
* Defines behavior in response to a successful authentication
* @param userId The user Id for the requested authentication
@@ -33,4 +33,4 @@
* @param userId The user Id for the requested authentication
*/
void onAuthenticationFailed(int userId);
-}
\ No newline at end of file
+}
diff --git a/core/java/com/android/internal/widget/MessagingData.java b/core/java/com/android/internal/widget/MessagingData.java
index 85b0201..42de60e 100644
--- a/core/java/com/android/internal/widget/MessagingData.java
+++ b/core/java/com/android/internal/widget/MessagingData.java
@@ -28,24 +28,33 @@
private final boolean mShowSpinner;
private final List<MessagingMessage> mHistoricMessagingMessages;
private final List<MessagingMessage> mNewMessagingMessages;
+ private final List<List<MessagingMessage>> mGroups;
+ private final List<Person> mSenders;
private final int mUnreadCount;
MessagingData(Person user, boolean showSpinner,
List<MessagingMessage> historicMessagingMessages,
- List<MessagingMessage> newMessagingMessages) {
+ List<MessagingMessage> newMessagingMessages, List<List<MessagingMessage>> groups,
+ List<Person> senders) {
this(user, showSpinner, /* unreadCount= */0,
- historicMessagingMessages, newMessagingMessages);
+ historicMessagingMessages, newMessagingMessages,
+ groups,
+ senders);
}
MessagingData(Person user, boolean showSpinner,
int unreadCount,
List<MessagingMessage> historicMessagingMessages,
- List<MessagingMessage> newMessagingMessages) {
+ List<MessagingMessage> newMessagingMessages,
+ List<List<MessagingMessage>> groups,
+ List<Person> senders) {
mUser = user;
mShowSpinner = showSpinner;
mUnreadCount = unreadCount;
mHistoricMessagingMessages = historicMessagingMessages;
mNewMessagingMessages = newMessagingMessages;
+ mGroups = groups;
+ mSenders = senders;
}
public Person getUser() {
@@ -67,4 +76,12 @@
public int getUnreadCount() {
return mUnreadCount;
}
+
+ public List<Person> getSenders() {
+ return mSenders;
+ }
+
+ public List<List<MessagingMessage>> getGroups() {
+ return mGroups;
+ }
}
diff --git a/core/java/com/android/internal/widget/MessagingGroup.java b/core/java/com/android/internal/widget/MessagingGroup.java
index 30e4099..f8f1049 100644
--- a/core/java/com/android/internal/widget/MessagingGroup.java
+++ b/core/java/com/android/internal/widget/MessagingGroup.java
@@ -55,7 +55,9 @@
* A message of a {@link MessagingLayout}.
*/
@RemoteViews.RemoteView
-public class MessagingGroup extends LinearLayout implements MessagingLinearLayout.MessagingChild {
+public class MessagingGroup extends NotificationOptimizedLinearLayout implements
+ MessagingLinearLayout.MessagingChild {
+
private static final MessagingPool<MessagingGroup> sInstancePool =
new MessagingPool<>(10);
diff --git a/core/java/com/android/internal/widget/MessagingLayout.java b/core/java/com/android/internal/widget/MessagingLayout.java
index b6d7503..d000596 100644
--- a/core/java/com/android/internal/widget/MessagingLayout.java
+++ b/core/java/com/android/internal/widget/MessagingLayout.java
@@ -189,9 +189,15 @@
/* isHistoric= */true, usePrecomputedText);
final List<MessagingMessage> newMessagingMessages =
createMessages(newMessages, /* isHistoric */false, usePrecomputedText);
+ // Let's first find our groups!
+ List<List<MessagingMessage>> groups = new ArrayList<>();
+ List<Person> senders = new ArrayList<>();
+
+ // Lets first find the groups
+ findGroups(historicMessagingMessages, newMessagingMessages, groups, senders);
return new MessagingData(user, showSpinner,
- historicMessagingMessages, newMessagingMessages);
+ historicMessagingMessages, newMessagingMessages, groups, senders);
}
/**
@@ -256,10 +262,10 @@
private void bind(MessagingData messagingData) {
setUser(messagingData.getUser());
- List<MessagingMessage> historicMessages = messagingData.getHistoricMessagingMessages();
- List<MessagingMessage> messages = messagingData.getNewMessagingMessages();
+ // Let's now create the views and reorder them accordingly
ArrayList<MessagingGroup> oldGroups = new ArrayList<>(mGroups);
- addMessagesToGroups(historicMessages, messages, messagingData.getShowSpinner());
+ createGroupViews(messagingData.getGroups(), messagingData.getSenders(),
+ messagingData.getShowSpinner());
// Let's first check which groups were removed altogether and remove them in one animation
removeGroups(oldGroups);
@@ -272,8 +278,8 @@
historicMessage.removeMessage(mToRecycle);
}
- mMessages = messages;
- mHistoricMessages = historicMessages;
+ mMessages = messagingData.getNewMessagingMessages();
+ mHistoricMessages = messagingData.getHistoricMessagingMessages();
updateHistoricMessageVisibility();
updateTitleAndNamesDisplay();
@@ -451,19 +457,6 @@
}
}
- private void addMessagesToGroups(List<MessagingMessage> historicMessages,
- List<MessagingMessage> messages, boolean showSpinner) {
- // Let's first find our groups!
- List<List<MessagingMessage>> groups = new ArrayList<>();
- List<Person> senders = new ArrayList<>();
-
- // Lets first find the groups
- findGroups(historicMessages, messages, groups, senders);
-
- // Let's now create the views and reorder them accordingly
- createGroupViews(groups, senders, showSpinner);
- }
-
private void createGroupViews(List<List<MessagingMessage>> groups,
List<Person> senders, boolean showSpinner) {
mGroups.clear();
diff --git a/core/java/com/android/internal/widget/NotificationActionListLayout.java b/core/java/com/android/internal/widget/NotificationActionListLayout.java
index a7a69c9..301dc39 100644
--- a/core/java/com/android/internal/widget/NotificationActionListLayout.java
+++ b/core/java/com/android/internal/widget/NotificationActionListLayout.java
@@ -16,12 +16,16 @@
package com.android.internal.widget;
+import static android.app.Notification.CallStyle.DEBUG_NEW_ACTION_LAYOUT;
+import static android.app.Flags.evenlyDividedCallStyleActionLayout;
+
import android.annotation.DimenRes;
import android.app.Notification;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.drawable.RippleDrawable;
import android.util.AttributeSet;
+import android.util.Log;
import android.view.Gravity;
import android.view.RemotableViewMethod;
import android.view.View;
@@ -41,13 +45,13 @@
*/
@RemoteViews.RemoteView
public class NotificationActionListLayout extends LinearLayout {
-
private final int mGravity;
private int mTotalWidth = 0;
private int mExtraStartPadding = 0;
private ArrayList<TextViewInfo> mMeasureOrderTextViews = new ArrayList<>();
private ArrayList<View> mMeasureOrderOther = new ArrayList<>();
private boolean mEmphasizedMode;
+ private boolean mEvenlyDividedMode;
private int mDefaultPaddingBottom;
private int mDefaultPaddingTop;
private int mEmphasizedPaddingTop;
@@ -124,6 +128,42 @@
}
}
+ private int measureAndReturnEvenlyDividedWidth(int heightMeasureSpec, int innerWidth) {
+ final int numChildren = getChildCount();
+ int childMarginSum = 0;
+ for (int i = 0; i < numChildren; i++) {
+ final View child = getChildAt(i);
+ if (child.getVisibility() != GONE) {
+ final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
+ childMarginSum += lp.leftMargin + lp.rightMargin;
+ }
+ }
+
+ final int innerWidthMinusChildMargins = innerWidth - childMarginSum;
+ final int childWidth = innerWidthMinusChildMargins / mNumNotGoneChildren;
+ final int childWidthMeasureSpec =
+ MeasureSpec.makeMeasureSpec(childWidth, MeasureSpec.EXACTLY);
+
+ if (DEBUG_NEW_ACTION_LAYOUT) {
+ Log.v(TAG, "measuring evenly divided width: "
+ + "numChildren = " + numChildren + ", "
+ + "innerWidth = " + innerWidth + "px, "
+ + "childMarginSum = " + childMarginSum + "px, "
+ + "innerWidthMinusChildMargins = " + innerWidthMinusChildMargins + "px, "
+ + "childWidth = " + childWidth + "px, "
+ + "childWidthMeasureSpec = " + MeasureSpec.toString(childWidthMeasureSpec));
+ }
+
+ for (int i = 0; i < numChildren; i++) {
+ final View child = getChildAt(i);
+ if (child.getVisibility() != GONE) {
+ child.measure(childWidthMeasureSpec, heightMeasureSpec);
+ }
+ }
+
+ return innerWidth;
+ }
+
private int measureAndGetUsedWidth(int widthMeasureSpec, int heightMeasureSpec, int innerWidth,
boolean collapsePriorityActions) {
final int numChildren = getChildCount();
@@ -208,11 +248,16 @@
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
countAndRebuildMeasureOrder();
final int innerWidth = MeasureSpec.getSize(widthMeasureSpec) - mPaddingLeft - mPaddingRight;
- int usedWidth = measureAndGetUsedWidth(widthMeasureSpec, heightMeasureSpec, innerWidth,
- false /* collapsePriorityButtons */);
- if (mNumPriorityChildren != 0 && usedWidth >= innerWidth) {
+ int usedWidth;
+ if (mEvenlyDividedMode) {
+ usedWidth = measureAndReturnEvenlyDividedWidth(heightMeasureSpec, innerWidth);
+ } else {
usedWidth = measureAndGetUsedWidth(widthMeasureSpec, heightMeasureSpec, innerWidth,
- true /* collapsePriorityButtons */);
+ false /* collapsePriorityButtons */);
+ if (mNumPriorityChildren != 0 && usedWidth >= innerWidth) {
+ usedWidth = measureAndGetUsedWidth(widthMeasureSpec, heightMeasureSpec, innerWidth,
+ true /* collapsePriorityButtons */);
+ }
}
mTotalWidth = usedWidth + mPaddingRight + mPaddingLeft + mExtraStartPadding;
@@ -352,6 +397,38 @@
}
/**
+ * Sets whether the available width should be distributed evenly among the action buttons.
+ *
+ * When enabled, the available width (after subtracting this layout's padding and all of the
+ * buttons' margins) is divided by the number of (not-GONE) buttons, and each button is forced
+ * to that exact width, even if it is less <em>or more</em> width than they need.
+ *
+ * When disabled, the available width is allocated as buttons need; if that exceeds the
+ * available width, priority buttons are collapsed to just their icon to save space.
+ *
+ * @param evenlyDividedMode whether to enable evenly divided mode
+ */
+ @RemotableViewMethod
+ public void setEvenlyDividedMode(boolean evenlyDividedMode) {
+ if (evenlyDividedMode && !evenlyDividedCallStyleActionLayout()) {
+ Log.e(TAG, "setEvenlyDividedMode(true) called with new action layout disabled; "
+ + "leaving evenly divided mode disabled");
+ return;
+ }
+
+ if (evenlyDividedMode == mEvenlyDividedMode) {
+ return;
+ }
+
+ if (DEBUG_NEW_ACTION_LAYOUT) {
+ Log.v(TAG, "evenlyDividedMode changed to " + evenlyDividedMode + "; "
+ + "requesting layout");
+ }
+ mEvenlyDividedMode = evenlyDividedMode;
+ requestLayout();
+ }
+
+ /**
* Set whether the list is in a mode where some actions are emphasized. This will trigger an
* equal measuring where all actions are full height and change a few parameters like
* the padding.
@@ -410,4 +487,5 @@
}
}
+ private static final String TAG = "NotificationActionListLayout";
}
diff --git a/core/java/com/android/internal/widget/NotificationOptimizedLinearLayout.java b/core/java/com/android/internal/widget/NotificationOptimizedLinearLayout.java
new file mode 100644
index 0000000..b5e9b8f
--- /dev/null
+++ b/core/java/com/android/internal/widget/NotificationOptimizedLinearLayout.java
@@ -0,0 +1,578 @@
+/*
+ * Copyright (C) 2024 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.widget;
+
+import static android.widget.flags.Flags.notifLinearlayoutOptimized;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.drawable.Drawable;
+import android.os.Build;
+import android.os.Trace;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.Gravity;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.LinearLayout;
+import android.widget.RemoteViews;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * This LinearLayout customizes the measurement behavior of LinearLayout for Notification layouts.
+ * When there is exactly
+ * one child View with <code>layout_weight</code>. onMeasure methods of this LinearLayout will:
+ * 1. Measure all other children.
+ * 2. Calculate the remaining space for the View with <code>layout_weight</code>
+ * 3. Measure the weighted View using the calculated remaining width or height (based on
+ * Orientation).
+ * This ensures that the weighted View fills the remaining space in LinearLayout with only single
+ * measure.
+ *
+ * **Assumptions:**
+ * - There is *exactly one* child view with non-zero <code>layout_weight</code>.
+ * - Other views should not have weight.
+ * - LinearLayout doesn't have <code>weightSum</code>.
+ * - Horizontal LinearLayout's width should be measured EXACTLY.
+ * - Horizontal LinearLayout shouldn't need baseLineAlignment.
+ * - Vertical LinearLayout shouldn't have MATCH_PARENT children when it is not measured EXACTLY.
+ *
+ * @hide
+ */
+@RemoteViews.RemoteView
+public class NotificationOptimizedLinearLayout extends LinearLayout {
+ private static final boolean DEBUG_LAYOUT = false;
+ private static final boolean TRACE_ONMEASURE = Build.isDebuggable();
+ private static final String TAG = "NotifOptimizedLinearLayout";
+
+ private boolean mShouldUseOptimizedLayout = false;
+
+ public NotificationOptimizedLinearLayout(Context context) {
+ super(context);
+ }
+
+ public NotificationOptimizedLinearLayout(Context context, @Nullable AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public NotificationOptimizedLinearLayout(Context context, @Nullable AttributeSet attrs,
+ int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ }
+
+ public NotificationOptimizedLinearLayout(Context context, AttributeSet attrs, int defStyleAttr,
+ int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ final View weightedChildView = getSingleWeightedChild();
+ mShouldUseOptimizedLayout =
+ isUseOptimizedLinearLayoutFlagEnabled() && weightedChildView != null
+ && isLinearLayoutUsable(widthMeasureSpec, heightMeasureSpec);
+
+ if (mShouldUseOptimizedLayout) {
+ onMeasureOptimized(weightedChildView, widthMeasureSpec, heightMeasureSpec);
+ } else {
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+ }
+ }
+
+ private boolean isUseOptimizedLinearLayoutFlagEnabled() {
+ final boolean enabled = notifLinearlayoutOptimized();
+ if (!enabled) {
+ logSkipOptimizedOnMeasure("enableNotifLinearlayoutOptimized flag is off.");
+ }
+ return enabled;
+ }
+
+ /**
+ * Checks if optimizations can be safely applied to this LinearLayout during layout
+ * calculations. Optimizations might be disabled in the following cases:
+ *
+ * **weightSum**: When LinearLayout has weightSum
+ * ** MATCH_PARENT children in non EXACT dimension**
+ * **Horizontal LinearLayout with non-EXACT width**
+ * **Baseline Alignment:** If views need to align their baselines in Horizontal LinearLayout
+ *
+ * @param widthMeasureSpec The width measurement specification.
+ * @param heightMeasureSpec The height measurement specification.
+ * @return `true` if optimization is possible, `false` otherwise.
+ */
+ private boolean isLinearLayoutUsable(int widthMeasureSpec, int heightMeasureSpec) {
+ final boolean hasWeightSum = getWeightSum() > 0.0f;
+ if (hasWeightSum) {
+ logSkipOptimizedOnMeasure("Has weightSum.");
+ return false;
+ }
+
+ if (requiresMatchParentRemeasureForVerticalLinearLayout(widthMeasureSpec)) {
+ logSkipOptimizedOnMeasure(
+ "Vertical LinearLayout requires children width MATCH_PARENT remeasure ");
+ return false;
+ }
+
+ final boolean isHorizontal = getOrientation() == HORIZONTAL;
+ if (isHorizontal && MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.EXACTLY) {
+ logSkipOptimizedOnMeasure("Horizontal LinearLayout's width should be "
+ + "measured EXACTLY");
+ return false;
+ }
+
+ if (requiresBaselineAlignmentForHorizontalLinearLayout()) {
+ logSkipOptimizedOnMeasure("Need to apply baseline.");
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * @return if the vertical linearlayout requires match_parent children remeasure
+ */
+ private boolean requiresMatchParentRemeasureForVerticalLinearLayout(int widthMeasureSpec) {
+ // HORIZONTAL measuring is handled by LinearLayout. That's why we don't need to check it
+ // here.
+ if (getOrientation() == HORIZONTAL) {
+ return false;
+ }
+
+ // When the width is not EXACT, children with MATCH_PARENT width need to be double measured.
+ // This needs to be handled in LinearLayout because NotificationOptimizedLinearLayout
+ final boolean nonExactWidth =
+ MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.EXACTLY;
+ final List<View> activeChildren = getActiveChildren();
+ for (int i = 0; i < activeChildren.size(); i++) {
+ final View child = activeChildren.get(i);
+ final ViewGroup.LayoutParams lp = child.getLayoutParams();
+ if (nonExactWidth && lp.width == ViewGroup.LayoutParams.MATCH_PARENT) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * @return if this layout needs to apply baseLineAlignment.
+ */
+ private boolean requiresBaselineAlignmentForHorizontalLinearLayout() {
+ // baseLineAlignment is not important for Vertical LinearLayout.
+ if (getOrientation() == VERTICAL) {
+ return false;
+ }
+ // Early return, if it is already disabled
+ if (!isBaselineAligned()) {
+ return false;
+ }
+
+ final List<View> activeChildren = getActiveChildren();
+ final int minorGravity = getGravity() & Gravity.VERTICAL_GRAVITY_MASK;
+
+ for (int i = 0; i < activeChildren.size(); i++) {
+ final View child = activeChildren.get(i);
+ if (child.getLayoutParams() instanceof LayoutParams) {
+ final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+ int childBaseline = -1;
+
+ if (lp.height != LayoutParams.MATCH_PARENT) {
+ childBaseline = child.getBaseline();
+ }
+ if (childBaseline == -1) {
+ // This child doesn't have a baseline.
+ continue;
+ }
+ int gravity = lp.gravity;
+ if (gravity < 0) {
+ gravity = minorGravity;
+ }
+
+ final int result = gravity & Gravity.VERTICAL_GRAVITY_MASK;
+ if (result == Gravity.TOP || result == Gravity.BOTTOM) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Finds the single child view within this layout that has a non-zero weight assigned to its
+ * LayoutParams.
+ *
+ * @return The weighted child view, or null if multiple weighted children exist or no weighted
+ * children are found.
+ */
+ @Nullable
+ private View getSingleWeightedChild() {
+ final boolean isVertical = getOrientation() == VERTICAL;
+ final List<View> activeChildren = getActiveChildren();
+ View singleWeightedChild = null;
+ for (int i = 0; i < activeChildren.size(); i++) {
+ final View child = activeChildren.get(i);
+ if (child.getLayoutParams() instanceof LayoutParams) {
+ final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+ if ((!isVertical && lp.width == ViewGroup.LayoutParams.MATCH_PARENT)
+ || (isVertical && lp.height == ViewGroup.LayoutParams.MATCH_PARENT)) {
+ logSkipOptimizedOnMeasure(
+ "There is a match parent child in the related orientation.");
+ return null;
+ }
+ if (lp.weight != 0) {
+ if (singleWeightedChild == null) {
+ singleWeightedChild = child;
+ } else {
+ logSkipOptimizedOnMeasure("There is more than one weighted child.");
+ return null;
+ }
+ }
+ }
+ }
+ if (singleWeightedChild == null) {
+ logSkipOptimizedOnMeasure("There is no weighted child in this layout.");
+ } else {
+ final LayoutParams lp = (LayoutParams) singleWeightedChild.getLayoutParams();
+ boolean isHeightWrapContentOrZero =
+ lp.height == ViewGroup.LayoutParams.WRAP_CONTENT || lp.height == 0;
+ boolean isWidthWrapContentOrZero =
+ lp.width == ViewGroup.LayoutParams.WRAP_CONTENT || lp.width == 0;
+ if ((isVertical && !isHeightWrapContentOrZero)
+ || (!isVertical && !isWidthWrapContentOrZero)) {
+ logSkipOptimizedOnMeasure(
+ "Single weighted child should be either WRAP_CONTENT or 0"
+ + " in the related orientation");
+ singleWeightedChild = null;
+ }
+ }
+
+ return singleWeightedChild;
+ }
+
+ /**
+ * Optimized measurement for the single weighted child in this LinearLayout.
+ * Measures other children, calculates remaining space, then measures the weighted
+ * child using the remaining width (or height).
+ *
+ * Note: Horizontal LinearLayout doesn't need to apply baseline in optimized case @see
+ * {@link #requiresBaselineAlignmentForHorizontalLinearLayout}.
+ *
+ * @param weightedChildView The weighted child view(with `layout_weight!=0`)
+ * @param widthMeasureSpec The width MeasureSpec to use for measurement
+ * @param heightMeasureSpec The height MeasureSpec to use for measurement.
+ */
+ private void onMeasureOptimized(@NonNull View weightedChildView, int widthMeasureSpec,
+ int heightMeasureSpec) {
+ try {
+ if (TRACE_ONMEASURE) {
+ Trace.beginSection("NotifOptimizedLinearLayout#onMeasure");
+ }
+
+ if (getOrientation() == LinearLayout.HORIZONTAL) {
+ final ViewGroup.LayoutParams lp = weightedChildView.getLayoutParams();
+ final int childWidth = lp.width;
+ final boolean isBaselineAligned = isBaselineAligned();
+ // It should be marked 0 so that it use excessSpace in LinearLayout's onMeasure
+ lp.width = 0;
+
+ // It doesn't need to apply baseline. So disable it.
+ setBaselineAligned(false);
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+
+ // restore values.
+ lp.width = childWidth;
+ setBaselineAligned(isBaselineAligned);
+ } else {
+ measureVerticalOptimized(weightedChildView, widthMeasureSpec, heightMeasureSpec);
+ }
+ } finally {
+ if (TRACE_ONMEASURE) {
+ trackShouldUseOptimizedLayout();
+ Trace.endSection();
+ }
+ }
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int l, int t, int r, int b) {
+ if (mShouldUseOptimizedLayout) {
+ onLayoutOptimized(changed, l, t, r, b);
+ } else {
+ super.onLayout(changed, l, t, r, b);
+ }
+ }
+
+ private void onLayoutOptimized(boolean changed, int l, int t, int r, int b) {
+ if (getOrientation() == LinearLayout.HORIZONTAL) {
+ super.onLayout(changed, l, t, r, b);
+ } else {
+ layoutVerticalOptimized(l, t, r, b);
+ }
+ }
+
+ /**
+ * Optimized measurement for the single weighted child in this LinearLayout.
+ * Measures other children, calculates remaining space, then measures the weighted
+ * child using the exact remaining height.
+ *
+ * @param weightedChildView The weighted child view(with `layout_weight=1`
+ * @param widthMeasureSpec The width MeasureSpec to use for measurement
+ * @param heightMeasureSpec The height MeasureSpec to use for measurement.
+ */
+ private void measureVerticalOptimized(@NonNull View weightedChildView, int widthMeasureSpec,
+ int heightMeasureSpec) {
+ final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
+ final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
+ int maxWidth = 0;
+ int usedHeight = 0;
+ final List<View> activeChildren = getActiveChildren();
+ final int activeChildCount = activeChildren.size();
+
+ final boolean isContentFirstItem = !activeChildren.isEmpty() && activeChildren.get(0)
+ == weightedChildView;
+
+ final boolean isContentLastItem = !activeChildren.isEmpty() && activeChildren.get(
+ activeChildCount - 1) == weightedChildView;
+
+ final int horizontalPaddings = getPaddingLeft() + getPaddingRight();
+
+ // 1. Measure other child views.
+ for (int i = 0; i < activeChildCount; i++) {
+ final View child = activeChildren.get(i);
+ if (child == weightedChildView) {
+ continue;
+ }
+ final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
+
+ int requiredVerticalPadding = lp.topMargin + lp.bottomMargin;
+ if (!isContentFirstItem && i == 0) {
+ requiredVerticalPadding += getPaddingTop();
+ }
+ if (!isContentLastItem && i == activeChildCount - 1) {
+ requiredVerticalPadding += getPaddingBottom();
+ }
+
+ child.measure(ViewGroup.getChildMeasureSpec(widthMeasureSpec,
+ horizontalPaddings + lp.leftMargin + lp.rightMargin,
+ child.getLayoutParams().width),
+ ViewGroup.getChildMeasureSpec(heightMeasureSpec, requiredVerticalPadding,
+ lp.height));
+ maxWidth = Math.max(maxWidth,
+ child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);
+ usedHeight += child.getMeasuredHeight() + requiredVerticalPadding;
+ }
+
+ // measure content
+ final MarginLayoutParams lp = (MarginLayoutParams) weightedChildView.getLayoutParams();
+
+ int usedSpace = usedHeight + lp.topMargin + lp.bottomMargin;
+ if (isContentFirstItem) {
+ usedSpace += getPaddingTop();
+ }
+ if (isContentLastItem) {
+ usedSpace += getPaddingBottom();
+ }
+
+ final int availableWidth = MeasureSpec.getSize(widthMeasureSpec);
+ final int availableHeight = MeasureSpec.getSize(heightMeasureSpec);
+
+ final int childWidthMeasureSpec = ViewGroup.getChildMeasureSpec(widthMeasureSpec,
+ horizontalPaddings + lp.leftMargin + lp.rightMargin, lp.width);
+
+ // 2. Calculate remaining height for weightedChildView.
+ final int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(
+ Math.max(0, availableHeight - usedSpace), MeasureSpec.AT_MOST);
+
+ // 3. Measure weightedChildView with the remaining remaining space.
+ weightedChildView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
+ maxWidth = Math.max(maxWidth,
+ weightedChildView.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);
+
+ final int totalUsedHeight = usedSpace + weightedChildView.getMeasuredHeight();
+
+ final int measuredWidth;
+ if (widthMode == MeasureSpec.EXACTLY) {
+ measuredWidth = availableWidth;
+ } else {
+ measuredWidth = maxWidth + getPaddingStart() + getPaddingEnd();
+ }
+
+ final int measuredHeight;
+ if (heightMode == MeasureSpec.EXACTLY) {
+ measuredHeight = availableHeight;
+ } else {
+ measuredHeight = totalUsedHeight;
+ }
+
+ // 4. Set the container size
+ setMeasuredDimension(
+ resolveSize(Math.max(getSuggestedMinimumWidth(), measuredWidth),
+ widthMeasureSpec),
+ Math.max(getSuggestedMinimumHeight(), measuredHeight));
+ }
+
+ @NonNull
+ private List<View> getActiveChildren() {
+ final int childCount = getChildCount();
+ final List<View> activeChildren = new ArrayList<>();
+ for (int i = 0; i < childCount; i++) {
+ final View child = getChildAt(i);
+ if (child == null || child.getVisibility() == View.GONE) {
+ continue;
+ }
+ activeChildren.add(child);
+ }
+ return activeChildren;
+ }
+
+ //region LinearLayout copy methods
+
+ /**
+ * layoutVerticalOptimized is a version of LinearLayout's layoutVertical method that
+ * excludes
+ * TableRow-related functionalities.
+ *
+ * @see LinearLayout#onLayout(boolean, int, int, int, int)
+ */
+ private void layoutVerticalOptimized(int left, int top, int right,
+ int bottom) {
+ final int paddingLeft = mPaddingLeft;
+ final int mTotalLength = getMeasuredHeight();
+ int childTop;
+ int childLeft;
+
+ // Where right end of child should go
+ final int width = right - left;
+ int childRight = width - mPaddingRight;
+
+ // Space available for child
+ int childSpace = width - paddingLeft - mPaddingRight;
+
+ final int count = getChildCount();
+
+ final int majorGravity = getGravity() & Gravity.VERTICAL_GRAVITY_MASK;
+ final int minorGravity = getGravity() & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK;
+
+ switch (majorGravity) {
+ case Gravity.BOTTOM:
+ // mTotalLength contains the padding already
+ childTop = mPaddingTop + bottom - top - mTotalLength;
+ break;
+
+ // mTotalLength contains the padding already
+ case Gravity.CENTER_VERTICAL:
+ childTop = mPaddingTop + (bottom - top - mTotalLength) / 2;
+ break;
+
+ case Gravity.TOP:
+ default:
+ childTop = mPaddingTop;
+ break;
+ }
+ final int dividerHeight = getDividerHeight();
+ for (int i = 0; i < count; i++) {
+ final View child = getChildAt(i);
+ if (child != null && child.getVisibility() != GONE) {
+ final int childWidth = child.getMeasuredWidth();
+ final int childHeight = child.getMeasuredHeight();
+
+ final LinearLayout.LayoutParams lp =
+ (LinearLayout.LayoutParams) child.getLayoutParams();
+
+ int gravity = lp.gravity;
+ if (gravity < 0) {
+ gravity = minorGravity;
+ }
+ final int layoutDirection = getLayoutDirection();
+ final int absoluteGravity = Gravity.getAbsoluteGravity(gravity,
+ layoutDirection);
+ switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
+ case Gravity.CENTER_HORIZONTAL:
+ childLeft =
+ paddingLeft + ((childSpace - childWidth) / 2) + lp.leftMargin
+ - lp.rightMargin;
+ break;
+
+ case Gravity.RIGHT:
+ childLeft = childRight - childWidth - lp.rightMargin;
+ break;
+
+ case Gravity.LEFT:
+ default:
+ childLeft = paddingLeft + lp.leftMargin;
+ break;
+ }
+
+ if (hasDividerBeforeChildAt(i)) {
+ childTop += dividerHeight;
+ }
+
+ childTop += lp.topMargin;
+ child.layout(childLeft, childTop, childLeft + childWidth,
+ childTop + childHeight);
+ childTop += childHeight + lp.bottomMargin;
+
+ }
+ }
+ }
+
+ /**
+ * Used in laying out views vertically.
+ *
+ * @see #layoutVerticalOptimized
+ * @see LinearLayout#onLayout(boolean, int, int, int, int)
+ */
+ private int getDividerHeight() {
+ final Drawable dividerDrawable = getDividerDrawable();
+ if (dividerDrawable == null) {
+ return 0;
+ } else {
+ return dividerDrawable.getIntrinsicHeight();
+ }
+ }
+ //endregion
+
+ //region Logging&Tracing
+ private void trackShouldUseOptimizedLayout() {
+ if (TRACE_ONMEASURE) {
+ Trace.setCounter("NotifOptimizedLinearLayout#shouldUseOptimizedLayout",
+ mShouldUseOptimizedLayout ? 1 : 0);
+ }
+ }
+
+ private void logSkipOptimizedOnMeasure(String reason) {
+ if (DEBUG_LAYOUT) {
+ final StringBuilder logMessage = new StringBuilder();
+ int layoutId = getId();
+ if (layoutId != NO_ID) {
+ final Resources resources = getResources();
+ if (resources != null) {
+ logMessage.append("[");
+ logMessage.append(resources.getResourceName(layoutId));
+ logMessage.append("] ");
+ }
+ }
+ logMessage.append("Going to skip onMeasureOptimized reason:");
+ logMessage.append(reason);
+
+ Log.d(TAG, logMessage.toString());
+ }
+ }
+ //endregion
+}
diff --git a/core/java/com/android/internal/widget/RemeasuringLinearLayout.java b/core/java/com/android/internal/widget/RemeasuringLinearLayout.java
index 7b154a5..bddad94 100644
--- a/core/java/com/android/internal/widget/RemeasuringLinearLayout.java
+++ b/core/java/com/android/internal/widget/RemeasuringLinearLayout.java
@@ -30,7 +30,7 @@
* MessagingLayouts where groups need to be able to snap it's height to.
*/
@RemoteViews.RemoteView
-public class RemeasuringLinearLayout extends LinearLayout {
+public class RemeasuringLinearLayout extends NotificationOptimizedLinearLayout {
private ArrayList<View> mMatchParentViews = new ArrayList<>();
@@ -79,7 +79,7 @@
int exactHeightSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY);
for (View child : mMatchParentViews) {
child.measure(getChildMeasureSpec(
- widthMeasureSpec, getPaddingStart() + getPaddingEnd(),
+ widthMeasureSpec, getPaddingStart() + getPaddingEnd(),
child.getLayoutParams().width),
exactHeightSpec);
}
@@ -87,4 +87,5 @@
mMatchParentViews.clear();
setMeasuredDimension(getMeasuredWidth(), height);
}
+
}
diff --git a/core/java/com/android/internal/widget/remotecompose/OWNERS b/core/java/com/android/internal/widget/remotecompose/OWNERS
new file mode 100644
index 0000000..54facab
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/OWNERS
@@ -0,0 +1,8 @@
+nicolasroard@google.com
+hoford@google.com
+jnichol@google.com
+sihua@google.com
+sunnygoyal@google.com
+oscarad@google.com
+pinyaoting@google.com
+zakcohen@google.com
diff --git a/packages/SoundPicker2/src/com/android/soundpicker/RingtonePickerApplication.java b/core/java/com/android/internal/widget/remotecompose/core/CompanionOperation.java
similarity index 66%
copy from packages/SoundPicker2/src/com/android/soundpicker/RingtonePickerApplication.java
copy to core/java/com/android/internal/widget/remotecompose/core/CompanionOperation.java
index 48fd4fe..ce8ca0d 100644
--- a/packages/SoundPicker2/src/com/android/soundpicker/RingtonePickerApplication.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/CompanionOperation.java
@@ -13,16 +13,18 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+package com.android.internal.widget.remotecompose.core;
-package com.android.soundpicker;
-
-import android.app.Application;
-
-import dagger.hilt.android.HiltAndroidApp;
+import java.util.List;
/**
- * The main application class for the project.
+ * Interface for the companion operations
*/
-@HiltAndroidApp(Application.class)
-public class RingtonePickerApplication extends Hilt_RingtonePickerApplication {
+public interface CompanionOperation {
+ void read(WireBuffer buffer, List<Operation> operations);
+
+ // Debugging / Documentation utility functions
+ String name();
+ int id();
}
+
diff --git a/core/java/com/android/internal/widget/remotecompose/core/CoreDocument.java b/core/java/com/android/internal/widget/remotecompose/core/CoreDocument.java
new file mode 100644
index 0000000..0e4c743
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/core/CoreDocument.java
@@ -0,0 +1,494 @@
+/*
+ * Copyright (C) 2023 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.widget.remotecompose.core;
+
+import com.android.internal.widget.remotecompose.core.operations.RootContentBehavior;
+import com.android.internal.widget.remotecompose.core.operations.Theme;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * Represents a platform independent RemoteCompose document,
+ * containing RemoteCompose operations + state
+ */
+public class CoreDocument {
+
+ ArrayList<Operation> mOperations;
+ RemoteComposeState mRemoteComposeState = new RemoteComposeState();
+
+ // Semantic version of the document
+ Version mVersion = new Version(0, 1, 0);
+
+ String mContentDescription; // text description of the document (used for accessibility)
+
+ long mRequiredCapabilities = 0L; // bitmask indicating needed capabilities of the player(unused)
+ int mWidth = 0; // horizontal dimension of the document in pixels
+ int mHeight = 0; // vertical dimension of the document in pixels
+
+ int mContentScroll = RootContentBehavior.NONE;
+ int mContentSizing = RootContentBehavior.NONE;
+ int mContentMode = RootContentBehavior.NONE;
+
+ int mContentAlignment = RootContentBehavior.ALIGNMENT_CENTER;
+
+ RemoteComposeBuffer mBuffer = new RemoteComposeBuffer(mRemoteComposeState);
+
+ public String getContentDescription() {
+ return mContentDescription;
+ }
+
+ public void setContentDescription(String contentDescription) {
+ this.mContentDescription = contentDescription;
+ }
+
+ public long getRequiredCapabilities() {
+ return mRequiredCapabilities;
+ }
+
+ public void setRequiredCapabilities(long requiredCapabilities) {
+ this.mRequiredCapabilities = requiredCapabilities;
+ }
+
+ public int getWidth() {
+ return mWidth;
+ }
+
+ public void setWidth(int width) {
+ this.mWidth = width;
+ }
+
+ public int getHeight() {
+ return mHeight;
+ }
+
+ public void setHeight(int height) {
+ this.mHeight = height;
+ }
+
+ public RemoteComposeBuffer getBuffer() {
+ return mBuffer;
+ }
+
+ public void setBuffer(RemoteComposeBuffer buffer) {
+ this.mBuffer = buffer;
+ }
+
+ public RemoteComposeState getRemoteComposeState() {
+ return mRemoteComposeState;
+ }
+
+ public void setRemoteComposeState(RemoteComposeState remoteComposeState) {
+ this.mRemoteComposeState = remoteComposeState;
+ }
+
+ public int getContentScroll() {
+ return mContentScroll;
+ }
+
+ public int getContentSizing() {
+ return mContentSizing;
+ }
+
+ public int getContentMode() {
+ return mContentMode;
+ }
+
+ /**
+ * Sets the way the player handles the content
+ *
+ * @param scroll set the horizontal behavior (NONE|SCROLL_HORIZONTAL|SCROLL_VERTICAL)
+ * @param alignment set the alignment of the content (TOP|CENTER|BOTTOM|START|END)
+ * @param sizing set the type of sizing for the content (NONE|SIZING_LAYOUT|SIZING_SCALE)
+ * @param mode set the mode of sizing, either LAYOUT modes or SCALE modes
+ * the LAYOUT modes are:
+ * - LAYOUT_MATCH_PARENT
+ * - LAYOUT_WRAP_CONTENT
+ * or adding an horizontal mode and a vertical mode:
+ * - LAYOUT_HORIZONTAL_MATCH_PARENT
+ * - LAYOUT_HORIZONTAL_WRAP_CONTENT
+ * - LAYOUT_HORIZONTAL_FIXED
+ * - LAYOUT_VERTICAL_MATCH_PARENT
+ * - LAYOUT_VERTICAL_WRAP_CONTENT
+ * - LAYOUT_VERTICAL_FIXED
+ * The LAYOUT_*_FIXED modes will use the intrinsic document size
+ */
+ public void setRootContentBehavior(int scroll, int alignment, int sizing, int mode) {
+ this.mContentScroll = scroll;
+ this.mContentAlignment = alignment;
+ this.mContentSizing = sizing;
+ this.mContentMode = mode;
+ }
+
+ /**
+ * Given dimensions w x h of where to paint the content, returns the corresponding scale factor
+ * according to the contentSizing information
+ *
+ * @param w horizontal dimension of the rendering area
+ * @param h vertical dimension of the rendering area
+ * @param scaleOutput will contain the computed scale factor
+ */
+ public void computeScale(float w, float h, float[] scaleOutput) {
+ float contentScaleX = 1f;
+ float contentScaleY = 1f;
+ if (mContentSizing == RootContentBehavior.SIZING_SCALE) {
+ // we need to add canvas transforms ops here
+ switch (mContentMode) {
+ case RootContentBehavior.SCALE_INSIDE: {
+ float scaleX = w / mWidth;
+ float scaleY = h / mHeight;
+ float scale = Math.min(1f, Math.min(scaleX, scaleY));
+ contentScaleX = scale;
+ contentScaleY = scale;
+ } break;
+ case RootContentBehavior.SCALE_FIT: {
+ float scaleX = w / mWidth;
+ float scaleY = h / mHeight;
+ float scale = Math.min(scaleX, scaleY);
+ contentScaleX = scale;
+ contentScaleY = scale;
+ } break;
+ case RootContentBehavior.SCALE_FILL_WIDTH: {
+ float scale = w / mWidth;
+ contentScaleX = scale;
+ contentScaleY = scale;
+ } break;
+ case RootContentBehavior.SCALE_FILL_HEIGHT: {
+ float scale = h / mHeight;
+ contentScaleX = scale;
+ contentScaleY = scale;
+ } break;
+ case RootContentBehavior.SCALE_CROP: {
+ float scaleX = w / mWidth;
+ float scaleY = h / mHeight;
+ float scale = Math.max(scaleX, scaleY);
+ contentScaleX = scale;
+ contentScaleY = scale;
+ } break;
+ case RootContentBehavior.SCALE_FILL_BOUNDS: {
+ float scaleX = w / mWidth;
+ float scaleY = h / mHeight;
+ contentScaleX = scaleX;
+ contentScaleY = scaleY;
+ } break;
+ default:
+ // nothing
+ }
+ }
+ scaleOutput[0] = contentScaleX;
+ scaleOutput[1] = contentScaleY;
+ }
+
+ /**
+ * Given dimensions w x h of where to paint the content, returns the corresponding translation
+ * according to the contentAlignment information
+ *
+ * @param w horizontal dimension of the rendering area
+ * @param h vertical dimension of the rendering area
+ * @param contentScaleX the horizontal scale we are going to use for the content
+ * @param contentScaleY the vertical scale we are going to use for the content
+ * @param translateOutput will contain the computed translation
+ */
+ private void computeTranslate(float w, float h, float contentScaleX, float contentScaleY,
+ float[] translateOutput) {
+ int horizontalContentAlignment = mContentAlignment & 0xF0;
+ int verticalContentAlignment = mContentAlignment & 0xF;
+ float translateX = 0f;
+ float translateY = 0f;
+ float contentWidth = mWidth * contentScaleX;
+ float contentHeight = mHeight * contentScaleY;
+
+ switch (horizontalContentAlignment) {
+ case RootContentBehavior.ALIGNMENT_START: {
+ // nothing
+ } break;
+ case RootContentBehavior.ALIGNMENT_HORIZONTAL_CENTER: {
+ translateX = (w - contentWidth) / 2f;
+ } break;
+ case RootContentBehavior.ALIGNMENT_END: {
+ translateX = w - contentWidth;
+ } break;
+ default:
+ // nothing (same as alignment_start)
+ }
+ switch (verticalContentAlignment) {
+ case RootContentBehavior.ALIGNMENT_TOP: {
+ // nothing
+ } break;
+ case RootContentBehavior.ALIGNMENT_VERTICAL_CENTER: {
+ translateY = (h - contentHeight) / 2f;
+ } break;
+ case RootContentBehavior.ALIGNMENT_BOTTOM: {
+ translateY = h - contentHeight;
+ } break;
+ default:
+ // nothing (same as alignment_top)
+ }
+
+ translateOutput[0] = translateX;
+ translateOutput[1] = translateY;
+ }
+
+ public Set<ClickAreaRepresentation> getClickAreas() {
+ return mClickAreas;
+ }
+
+ public interface ClickCallbacks {
+ void click(int id, String metadata);
+ }
+
+ HashSet<ClickCallbacks> mClickListeners = new HashSet<>();
+ HashSet<ClickAreaRepresentation> mClickAreas = new HashSet<>();
+
+ static class Version {
+ public final int major;
+ public final int minor;
+ public final int patchLevel;
+
+ Version(int major, int minor, int patchLevel) {
+ this.major = major;
+ this.minor = minor;
+ this.patchLevel = patchLevel;
+ }
+ }
+
+ public static class ClickAreaRepresentation {
+ int mId;
+ String mContentDescription;
+ float mLeft;
+ float mTop;
+ float mRight;
+ float mBottom;
+ String mMetadata;
+
+ public ClickAreaRepresentation(int id,
+ String contentDescription,
+ float left,
+ float top,
+ float right,
+ float bottom,
+ String metadata) {
+ this.mId = id;
+ this.mContentDescription = contentDescription;
+ this.mLeft = left;
+ this.mTop = top;
+ this.mRight = right;
+ this.mBottom = bottom;
+ this.mMetadata = metadata;
+ }
+
+ public boolean contains(float x, float y) {
+ return x >= mLeft && x < mRight
+ && y >= mTop && y < mBottom;
+ }
+
+ public float getLeft() {
+ return mLeft;
+ }
+
+ public float getTop() {
+ return mTop;
+ }
+
+ public float width() {
+ return Math.max(0, mRight - mLeft);
+ }
+
+ public float height() {
+ return Math.max(0, mBottom - mTop);
+ }
+
+ public int getId() {
+ return mId;
+ }
+
+ public String getContentDescription() {
+ return mContentDescription;
+ }
+
+ public String getMetadata() {
+ return mMetadata;
+ }
+ }
+
+ /**
+ * Load operations from the given buffer
+ */
+ public void initFromBuffer(RemoteComposeBuffer buffer) {
+ mOperations = new ArrayList<Operation>();
+ buffer.inflateFromBuffer(mOperations);
+ }
+
+ /**
+ * Called when an initialization is needed, allowing the document to eg load
+ * resources / cache them.
+ */
+ public void initializeContext(RemoteContext context) {
+ mRemoteComposeState.reset();
+ mClickAreas.clear();
+
+ context.mDocument = this;
+ context.mRemoteComposeState = mRemoteComposeState;
+
+ // mark context to be in DATA mode, which will skip the painting ops.
+ context.mMode = RemoteContext.ContextMode.DATA;
+ for (Operation op: mOperations) {
+ op.apply(context);
+ }
+ context.mMode = RemoteContext.ContextMode.UNSET;
+ }
+
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+ // Document infos
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * Returns true if the document can be displayed given this version of the player
+ *
+ * @param majorVersion the max major version supported by the player
+ * @param minorVersion the max minor version supported by the player
+ * @param capabilities a bitmask of capabilities the player supports (unused for now)
+ */
+ public boolean canBeDisplayed(int majorVersion, int minorVersion, long capabilities) {
+ return mVersion.major <= majorVersion && mVersion.minor <= minorVersion;
+ }
+
+ /**
+ * Set the document version, following semantic versioning.
+ *
+ * @param majorVersion major version number, increased upon changes breaking the compatibility
+ * @param minorVersion minor version number, increased when adding new features
+ * @param patch patch level, increased upon bugfixes
+ */
+ void setVersion(int majorVersion, int minorVersion, int patch) {
+ mVersion = new Version(majorVersion, minorVersion, patch);
+ }
+
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+ // Click handling
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * Add a click area to the document, in root coordinates. We are not doing any specific sorting
+ * through the declared areas on click detections, which means that the first one containing
+ * the click coordinates will be the one reported; the order of addition of those click areas
+ * is therefore meaningful.
+ *
+ * @param id the id of the area, which will be reported on click
+ * @param contentDescription the content description (used for accessibility)
+ * @param left the left coordinate of the click area (in pixels)
+ * @param top the top coordinate of the click area (in pixels)
+ * @param right the right coordinate of the click area (in pixels)
+ * @param bottom the bottom coordinate of the click area (in pixels)
+ * @param metadata arbitrary metadata associated with the are, also reported on click
+ */
+ public void addClickArea(int id, String contentDescription,
+ float left, float top, float right, float bottom, String metadata) {
+ mClickAreas.add(new ClickAreaRepresentation(id,
+ contentDescription, left, top, right, bottom, metadata));
+ }
+
+ /**
+ * Add a click listener. This will get called when a click is detected on the document
+ *
+ * @param callback called when a click area has been hit, passing the click are id and metadata.
+ */
+ public void addClickListener(ClickCallbacks callback) {
+ mClickListeners.add(callback);
+ }
+
+ /**
+ * Passing a click event to the document. This will possibly result in calling the click
+ * listeners.
+ */
+ public void onClick(float x, float y) {
+ for (ClickAreaRepresentation clickArea: mClickAreas) {
+ if (clickArea.contains(x, y)) {
+ warnClickListeners(clickArea);
+ }
+ }
+ }
+
+ /**
+ * Programmatically trigger the click response for the given id
+ *
+ * @param id the click area id
+ */
+ public void performClick(int id) {
+ for (ClickAreaRepresentation clickArea: mClickAreas) {
+ if (clickArea.mId == id) {
+ warnClickListeners(clickArea);
+ }
+ }
+ }
+
+ /**
+ * Warn click listeners when a click area is activated
+ */
+ private void warnClickListeners(ClickAreaRepresentation clickArea) {
+ for (ClickCallbacks listener: mClickListeners) {
+ listener.click(clickArea.mId, clickArea.mMetadata);
+ }
+ }
+
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+ // Painting
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+
+ private final float[] mScaleOutput = new float[2];
+ private final float[] mTranslateOutput = new float[2];
+
+ /**
+ * Paint the document
+ *
+ * @param context the provided PaintContext
+ * @param theme the theme we want to use for this document.
+ */
+ public void paint(RemoteContext context, int theme) {
+ context.mMode = RemoteContext.ContextMode.PAINT;
+
+ // current theme starts as UNSPECIFIED, until a Theme setter
+ // operation gets executed and modify it.
+ context.setTheme(Theme.UNSPECIFIED);
+
+ context.mRemoteComposeState = mRemoteComposeState;
+ if (mContentSizing == RootContentBehavior.SIZING_SCALE) {
+ // we need to add canvas transforms ops here
+ computeScale(context.mWidth, context.mHeight, mScaleOutput);
+ computeTranslate(context.mWidth, context.mHeight,
+ mScaleOutput[0], mScaleOutput[1], mTranslateOutput);
+ context.mPaintContext.translate(mTranslateOutput[0], mTranslateOutput[1]);
+ context.mPaintContext.scale(mScaleOutput[0], mScaleOutput[1]);
+ }
+ for (Operation op : mOperations) {
+ // operations will only be executed if no theme is set (ie UNSPECIFIED)
+ // or the theme is equal as the one passed in argument to paint.
+ boolean apply = true;
+ if (theme != Theme.UNSPECIFIED) {
+ apply = op instanceof Theme // always apply a theme setter
+ || context.getTheme() == theme
+ || context.getTheme() == Theme.UNSPECIFIED;
+ }
+ if (apply) {
+ op.apply(context);
+ }
+ }
+ context.mMode = RemoteContext.ContextMode.UNSET;
+ }
+
+}
+
diff --git a/core/java/com/android/internal/widget/remotecompose/core/Operation.java b/core/java/com/android/internal/widget/remotecompose/core/Operation.java
new file mode 100644
index 0000000..7cb9a42
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/core/Operation.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2023 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.widget.remotecompose.core;
+
+/**
+ * Base interface for RemoteCompose operations
+ */
+public interface Operation {
+
+ /**
+ * add the operation to the buffer
+ */
+ void write(WireBuffer buffer);
+
+ /**
+ * paint an operation
+ *
+ * @param context the paint context used to paint the operation
+ */
+ void apply(RemoteContext context);
+
+ /**
+ * Debug utility to display an operation + indentation
+ */
+ String deepToString(String indent);
+}
+
diff --git a/core/java/com/android/internal/widget/remotecompose/core/Operations.java b/core/java/com/android/internal/widget/remotecompose/core/Operations.java
new file mode 100644
index 0000000..b8bb1f0
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/core/Operations.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2023 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.widget.remotecompose.core;
+
+import com.android.internal.widget.remotecompose.core.operations.BitmapData;
+import com.android.internal.widget.remotecompose.core.operations.ClickArea;
+import com.android.internal.widget.remotecompose.core.operations.DrawBitmapInt;
+import com.android.internal.widget.remotecompose.core.operations.Header;
+import com.android.internal.widget.remotecompose.core.operations.RootContentBehavior;
+import com.android.internal.widget.remotecompose.core.operations.RootContentDescription;
+import com.android.internal.widget.remotecompose.core.operations.TextData;
+import com.android.internal.widget.remotecompose.core.operations.Theme;
+import com.android.internal.widget.remotecompose.core.operations.utilities.IntMap;
+
+/**
+ * List of operations supported in a RemoteCompose document
+ */
+public class Operations {
+
+ ////////////////////////////////////////
+ // Protocol
+ ////////////////////////////////////////
+ public static final int HEADER = 0;
+ public static final int LOAD_BITMAP = 4;
+ public static final int THEME = 63;
+ public static final int CLICK_AREA = 64;
+ public static final int ROOT_CONTENT_BEHAVIOR = 65;
+ public static final int ROOT_CONTENT_DESCRIPTION = 103;
+
+ ////////////////////////////////////////
+ // Draw commands
+ ////////////////////////////////////////
+ public static final int DRAW_BITMAP = 44;
+ public static final int DRAW_BITMAP_INT = 66;
+ public static final int DATA_BITMAP = 101;
+ public static final int DATA_TEXT = 102;
+
+
+ public static IntMap<CompanionOperation> map = new IntMap<>();
+
+ static {
+ map.put(HEADER, Header.COMPANION);
+ map.put(DRAW_BITMAP_INT, DrawBitmapInt.COMPANION);
+ map.put(DATA_BITMAP, BitmapData.COMPANION);
+ map.put(DATA_TEXT, TextData.COMPANION);
+ map.put(THEME, Theme.COMPANION);
+ map.put(CLICK_AREA, ClickArea.COMPANION);
+ map.put(ROOT_CONTENT_BEHAVIOR, RootContentBehavior.COMPANION);
+ map.put(ROOT_CONTENT_DESCRIPTION, RootContentDescription.COMPANION);
+ }
+
+}
diff --git a/core/java/com/android/internal/widget/remotecompose/core/PaintContext.java b/core/java/com/android/internal/widget/remotecompose/core/PaintContext.java
new file mode 100644
index 0000000..6999cde
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/core/PaintContext.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2023 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.widget.remotecompose.core;
+
+/**
+ * Specify an abstract paint context used by RemoteCompose commands to draw
+ */
+public abstract class PaintContext {
+ protected RemoteContext mContext;
+
+ public PaintContext(RemoteContext context) {
+ this.mContext = context;
+ }
+
+ public void setContext(RemoteContext context) {
+ this.mContext = context;
+ }
+
+ public abstract void drawBitmap(int imageId,
+ int srcLeft, int srcTop, int srcRight, int srcBottom,
+ int dstLeft, int dstTop, int dstRight, int dstBottom,
+ int cdId);
+
+ public abstract void scale(float scaleX, float scaleY);
+ public abstract void translate(float translateX, float translateY);
+}
+
diff --git a/core/java/com/android/internal/widget/remotecompose/core/PaintOperation.java b/core/java/com/android/internal/widget/remotecompose/core/PaintOperation.java
new file mode 100644
index 0000000..2f3fe57
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/core/PaintOperation.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2023 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.widget.remotecompose.core;
+
+/**
+ * PaintOperation interface, used for operations aimed at painting
+ * (while any operation _can_ paint, this make it a little more explicit)
+ */
+public abstract class PaintOperation implements Operation {
+
+ @Override
+ public void apply(RemoteContext context) {
+ if (context.getMode() == RemoteContext.ContextMode.PAINT
+ && context.getPaintContext() != null) {
+ paint((PaintContext) context.getPaintContext());
+ }
+ }
+
+ @Override
+ public String deepToString(String indent) {
+ return indent + toString();
+ }
+
+ public abstract void paint(PaintContext context);
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/animation/ActivityLaunchAnimatorKosmos.kt b/core/java/com/android/internal/widget/remotecompose/core/Platform.java
similarity index 73%
copy from packages/SystemUI/tests/utils/src/com/android/systemui/animation/ActivityLaunchAnimatorKosmos.kt
copy to core/java/com/android/internal/widget/remotecompose/core/Platform.java
index 128f58b..abda0c0 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/animation/ActivityLaunchAnimatorKosmos.kt
+++ b/core/java/com/android/internal/widget/remotecompose/core/Platform.java
@@ -13,9 +13,12 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+package com.android.internal.widget.remotecompose.core;
-package com.android.systemui.animation
+/**
+ * Services that are needed to be provided by the platform during encoding.
+ */
+public interface Platform {
+ byte[] imageToByteArray(Object image);
+}
-import com.android.systemui.kosmos.Kosmos
-
-val Kosmos.activityLaunchAnimator by Kosmos.Fixture { ActivityLaunchAnimator() }
diff --git a/core/java/com/android/internal/widget/remotecompose/core/RemoteComposeBuffer.java b/core/java/com/android/internal/widget/remotecompose/core/RemoteComposeBuffer.java
new file mode 100644
index 0000000..3ab6c47
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/core/RemoteComposeBuffer.java
@@ -0,0 +1,317 @@
+/*
+ * Copyright (C) 2023 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.widget.remotecompose.core;
+
+import com.android.internal.widget.remotecompose.core.operations.BitmapData;
+import com.android.internal.widget.remotecompose.core.operations.ClickArea;
+import com.android.internal.widget.remotecompose.core.operations.DrawBitmapInt;
+import com.android.internal.widget.remotecompose.core.operations.Header;
+import com.android.internal.widget.remotecompose.core.operations.RootContentBehavior;
+import com.android.internal.widget.remotecompose.core.operations.RootContentDescription;
+import com.android.internal.widget.remotecompose.core.operations.TextData;
+import com.android.internal.widget.remotecompose.core.operations.Theme;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.Arrays;
+
+/**
+ * Provides an abstract buffer to encode/decode RemoteCompose operations
+ */
+public class RemoteComposeBuffer {
+ WireBuffer mBuffer = new WireBuffer();
+ Platform mPlatform = null;
+ RemoteComposeState mRemoteComposeState;
+
+ /**
+ * Provides an abstract buffer to encode/decode RemoteCompose operations
+ *
+ * @param remoteComposeState the state used while encoding on the buffer
+ */
+ public RemoteComposeBuffer(RemoteComposeState remoteComposeState) {
+ this.mRemoteComposeState = remoteComposeState;
+ }
+
+ public void reset() {
+ mBuffer.reset();
+ mRemoteComposeState.reset();
+ }
+
+ public Platform getPlatform() {
+ return mPlatform;
+ }
+
+ public void setPlatform(Platform platform) {
+ this.mPlatform = platform;
+ }
+
+ public WireBuffer getBuffer() {
+ return mBuffer;
+ }
+
+ public void setBuffer(WireBuffer buffer) {
+ this.mBuffer = buffer;
+ }
+
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+ // Supported operations on the buffer
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * Insert a header
+ *
+ * @param width the width of the document in pixels
+ * @param height the height of the document in pixels
+ * @param contentDescription content description of the document
+ * @param capabilities bitmask indicating needed capabilities (unused for now)
+ */
+ public void header(int width, int height, String contentDescription, long capabilities) {
+ Header.COMPANION.apply(mBuffer, width, height, capabilities);
+ int contentDescriptionId = 0;
+ if (contentDescription != null) {
+ contentDescriptionId = addText(contentDescription);
+ RootContentDescription.COMPANION.apply(mBuffer, contentDescriptionId);
+ }
+ }
+
+ /**
+ * Insert a header
+ *
+ * @param width the width of the document in pixels
+ * @param height the height of the document in pixels
+ * @param contentDescription content description of the document
+ */
+ public void header(int width, int height, String contentDescription) {
+ header(width, height, contentDescription, 0);
+ }
+
+ /**
+ * Insert a bitmap
+ *
+ * @param image an opaque image that we'll add to the buffer
+ * @param imageWidth the width of the image
+ * @param imageHeight the height of the image
+ * @param srcLeft left coordinate of the source area
+ * @param srcTop top coordinate of the source area
+ * @param srcRight right coordinate of the source area
+ * @param srcBottom bottom coordinate of the source area
+ * @param dstLeft left coordinate of the destination area
+ * @param dstTop top coordinate of the destination area
+ * @param dstRight right coordinate of the destination area
+ * @param dstBottom bottom coordinate of the destination area
+ */
+ public void drawBitmap(Object image,
+ int imageWidth, int imageHeight,
+ int srcLeft, int srcTop, int srcRight, int srcBottom,
+ int dstLeft, int dstTop, int dstRight, int dstBottom,
+ String contentDescription) {
+ int imageId = mRemoteComposeState.dataGetId(image);
+ if (imageId == -1) {
+ imageId = mRemoteComposeState.cache(image);
+ byte[] data = mPlatform.imageToByteArray(image);
+ BitmapData.COMPANION.apply(mBuffer, imageId, imageWidth, imageHeight, data);
+ }
+ int contentDescriptionId = 0;
+ if (contentDescription != null) {
+ contentDescriptionId = addText(contentDescription);
+ }
+ DrawBitmapInt.COMPANION.apply(
+ mBuffer, imageId, srcLeft, srcTop, srcRight, srcBottom,
+ dstLeft, dstTop, dstRight, dstBottom, contentDescriptionId
+ );
+ }
+
+ /**
+ * Adds a text string data to the stream and returns its id
+ * Will be used to insert string with bitmaps etc.
+ *
+ * @param text the string to inject in the buffer
+ */
+ int addText(String text) {
+ int id = mRemoteComposeState.dataGetId(text);
+ if (id == -1) {
+ id = mRemoteComposeState.cache(text);
+ TextData.COMPANION.apply(mBuffer, id, text);
+ }
+ return id;
+ }
+
+ /**
+ * Add a click area to the document
+ *
+ * @param id the id of the click area, reported in the click listener callback
+ * @param contentDescription the content description of that click area (accessibility)
+ * @param left left coordinate of the area bounds
+ * @param top top coordinate of the area bounds
+ * @param right right coordinate of the area bounds
+ * @param bottom bottom coordinate of the area bounds
+ * @param metadata associated metadata, user-provided
+ */
+ public void addClickArea(
+ int id,
+ String contentDescription,
+ float left,
+ float top,
+ float right,
+ float bottom,
+ String metadata
+ ) {
+ int contentDescriptionId = 0;
+ if (contentDescription != null) {
+ contentDescriptionId = addText(contentDescription);
+ }
+ int metadataId = 0;
+ if (metadata != null) {
+ metadataId = addText(metadata);
+ }
+ ClickArea.COMPANION.apply(mBuffer, id, contentDescriptionId,
+ left, top, right, bottom, metadataId);
+ }
+
+ /**
+ * Sets the way the player handles the content
+ *
+ * @param scroll set the horizontal behavior (NONE|SCROLL_HORIZONTAL|SCROLL_VERTICAL)
+ * @param alignment set the alignment of the content (TOP|CENTER|BOTTOM|START|END)
+ * @param sizing set the type of sizing for the content (NONE|SIZING_LAYOUT|SIZING_SCALE)
+ * @param mode set the mode of sizing, either LAYOUT modes or SCALE modes
+ * the LAYOUT modes are:
+ * - LAYOUT_MATCH_PARENT
+ * - LAYOUT_WRAP_CONTENT
+ * or adding an horizontal mode and a vertical mode:
+ * - LAYOUT_HORIZONTAL_MATCH_PARENT
+ * - LAYOUT_HORIZONTAL_WRAP_CONTENT
+ * - LAYOUT_HORIZONTAL_FIXED
+ * - LAYOUT_VERTICAL_MATCH_PARENT
+ * - LAYOUT_VERTICAL_WRAP_CONTENT
+ * - LAYOUT_VERTICAL_FIXED
+ * The LAYOUT_*_FIXED modes will use the intrinsic document size
+ */
+ public void setRootContentBehavior(int scroll, int alignment, int sizing, int mode) {
+ RootContentBehavior.COMPANION.apply(mBuffer, scroll, alignment, sizing, mode);
+ }
+
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+
+ public void inflateFromBuffer(ArrayList<Operation> operations) {
+ mBuffer.setIndex(0);
+ while (mBuffer.available()) {
+ int opId = mBuffer.readByte();
+ CompanionOperation operation = Operations.map.get(opId);
+ if (operation == null) {
+ throw new RuntimeException("Unknown operation encountered");
+ }
+ operation.read(mBuffer, operations);
+ }
+ }
+
+ RemoteComposeBuffer copy() {
+ ArrayList<Operation> operations = new ArrayList<>();
+ inflateFromBuffer(operations);
+ RemoteComposeBuffer buffer = new RemoteComposeBuffer(mRemoteComposeState);
+ return copyFromOperations(operations, buffer);
+ }
+
+ public void setTheme(int theme) {
+ Theme.COMPANION.apply(mBuffer, theme);
+ }
+
+
+ static String version() {
+ return "v1.0";
+ }
+
+ public static RemoteComposeBuffer fromFile(String path,
+ RemoteComposeState remoteComposeState)
+ throws IOException {
+ RemoteComposeBuffer buffer = new RemoteComposeBuffer(remoteComposeState);
+ read(new File(path), buffer);
+ return buffer;
+ }
+
+ public RemoteComposeBuffer fromFile(File file,
+ RemoteComposeState remoteComposeState) throws IOException {
+ RemoteComposeBuffer buffer = new RemoteComposeBuffer(remoteComposeState);
+ read(file, buffer);
+ return buffer;
+ }
+
+ public static RemoteComposeBuffer fromInputStream(InputStream inputStream,
+ RemoteComposeState remoteComposeState) {
+ RemoteComposeBuffer buffer = new RemoteComposeBuffer(remoteComposeState);
+ read(inputStream, buffer);
+ return buffer;
+ }
+
+ RemoteComposeBuffer copyFromOperations(ArrayList<Operation> operations,
+ RemoteComposeBuffer buffer) {
+
+ for (Operation operation : operations) {
+ operation.write(buffer.mBuffer);
+ }
+ return buffer;
+ }
+
+ public void write(RemoteComposeBuffer buffer, File file) {
+ try {
+ FileOutputStream fd = new FileOutputStream(file);
+ fd.write(buffer.mBuffer.getBuffer(), 0, buffer.mBuffer.getSize());
+ fd.flush();
+ fd.close();
+ } catch (Exception ex) {
+ ex.printStackTrace();
+ }
+ }
+
+ static void read(File file, RemoteComposeBuffer buffer) throws IOException {
+ FileInputStream fd = new FileInputStream(file);
+ read(fd, buffer);
+ }
+
+ public static void read(InputStream fd, RemoteComposeBuffer buffer) {
+ try {
+ byte[] bytes = readAllBytes(fd);
+ buffer.reset();
+ System.arraycopy(bytes, 0, buffer.mBuffer.mBuffer, 0, bytes.length);
+ buffer.mBuffer.mSize = bytes.length;
+ } catch (Exception e) {
+ e.printStackTrace();
+ // todo decide how to handel this stuff
+ }
+ }
+
+ private static byte[] readAllBytes(InputStream is) throws IOException {
+ byte[] buff = new byte[32 * 1024]; // moderate size buff to start
+ int red = 0;
+ while (true) {
+ int ret = is.read(buff, red, buff.length - red);
+ if (ret == -1) {
+ is.close();
+ return Arrays.copyOf(buff, red);
+ }
+ red += ret;
+ if (red == buff.length) {
+ buff = Arrays.copyOf(buff, buff.length * 2);
+ }
+ }
+ }
+
+}
+
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/animation/ActivityLaunchAnimatorKosmos.kt b/core/java/com/android/internal/widget/remotecompose/core/RemoteComposeOperation.java
similarity index 78%
copy from packages/SystemUI/tests/utils/src/com/android/systemui/animation/ActivityLaunchAnimatorKosmos.kt
copy to core/java/com/android/internal/widget/remotecompose/core/RemoteComposeOperation.java
index 128f58b..c7ec3359 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/animation/ActivityLaunchAnimatorKosmos.kt
+++ b/core/java/com/android/internal/widget/remotecompose/core/RemoteComposeOperation.java
@@ -13,9 +13,8 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+package com.android.internal.widget.remotecompose.core;
-package com.android.systemui.animation
+public interface RemoteComposeOperation extends Operation {
-import com.android.systemui.kosmos.Kosmos
-
-val Kosmos.activityLaunchAnimator by Kosmos.Fixture { ActivityLaunchAnimator() }
+}
diff --git a/core/java/com/android/internal/widget/remotecompose/core/RemoteComposeState.java b/core/java/com/android/internal/widget/remotecompose/core/RemoteComposeState.java
new file mode 100644
index 0000000..17e8c83
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/core/RemoteComposeState.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright (C) 2023 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.widget.remotecompose.core;
+
+import com.android.internal.widget.remotecompose.core.operations.utilities.IntMap;
+
+import java.util.HashMap;
+
+/**
+ * Represents runtime state for a RemoteCompose document
+ */
+public class RemoteComposeState {
+
+ private final IntMap<Object> mIntDataMap = new IntMap<>();
+ private final IntMap<Boolean> mIntWrittenMap = new IntMap<>();
+ private final HashMap<Object, Integer> mDataIntMap = new HashMap();
+
+ private static int sNextId = 42;
+
+ public Object getFromId(int id) {
+ return mIntDataMap.get(id);
+ }
+
+ public boolean containsId(int id) {
+ return mIntDataMap.get(id) != null;
+ }
+
+ /**
+ * Return the id of an item from the cache.
+ */
+ public int dataGetId(Object image) {
+ Integer res = mDataIntMap.get(image);
+ if (res == null) {
+ return -1;
+ }
+ return res;
+ }
+
+ /**
+ * Add an image to the cache. Generates an id for the image and adds it to the cache based on
+ * that id.
+ */
+ public int cache(Object image) {
+ int id = nextId();
+ mDataIntMap.put(image, id);
+ mIntDataMap.put(id, image);
+ return id;
+ }
+
+ /**
+ * Insert an item in the cache
+ */
+ public void cache(int id, Object item) {
+ mDataIntMap.put(item, id);
+ mIntDataMap.put(id, item);
+ }
+
+ /**
+ * Method to determine if a cached value has been written to the documents WireBuffer based on
+ * its id.
+ */
+ public boolean wasNotWritten(int id) {
+ return !mIntWrittenMap.get(id);
+ }
+
+ /**
+ * Method to mark that a value, represented by its id, has been written to the WireBuffer
+ */
+ public void markWritten(int id) {
+ mIntWrittenMap.put(id, true);
+ }
+
+ /**
+ * Clear the record of the values that have been written to the WireBuffer.
+ */
+ void reset() {
+ mIntWrittenMap.clear();
+ }
+
+ public static int nextId() {
+ return sNextId++;
+ }
+ public static void setNextId(int id) {
+ sNextId = id;
+ }
+
+}
diff --git a/core/java/com/android/internal/widget/remotecompose/core/RemoteContext.java b/core/java/com/android/internal/widget/remotecompose/core/RemoteContext.java
new file mode 100644
index 0000000..1b7c6fd
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/core/RemoteContext.java
@@ -0,0 +1,154 @@
+/*
+ * Copyright (C) 2023 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.widget.remotecompose.core;
+
+import com.android.internal.widget.remotecompose.core.operations.Theme;
+
+/**
+ * Specify an abstract context used to playback RemoteCompose documents
+ *
+ * This allows us to intercept the different operations in a document and react to them.
+ *
+ * We also contain a PaintContext, so that any operation can draw as needed.
+ */
+public abstract class RemoteContext {
+ protected CoreDocument mDocument;
+ public RemoteComposeState mRemoteComposeState;
+
+ protected PaintContext mPaintContext = null;
+ ContextMode mMode = ContextMode.UNSET;
+
+ boolean mDebug = false;
+ private int mTheme = Theme.UNSPECIFIED;
+
+ public float mWidth = 0f;
+ public float mHeight = 0f;
+
+ /**
+ * The context can be used in a few different mode, allowing operations to skip being executed:
+ * - UNSET : all operations will get executed
+ * - DATA : only operations dealing with DATA (eg loading a bitmap) should execute
+ * - PAINT : only operations painting should execute
+ */
+ public enum ContextMode {
+ UNSET, DATA, PAINT
+ }
+
+ public int getTheme() {
+ return mTheme;
+ }
+
+ public void setTheme(int theme) {
+ this.mTheme = theme;
+ }
+
+ public ContextMode getMode() {
+ return mMode;
+ }
+
+ public void setMode(ContextMode mode) {
+ this.mMode = mode;
+ }
+
+ public PaintContext getPaintContext() {
+ return mPaintContext;
+ }
+
+ public void setPaintContext(PaintContext paintContext) {
+ this.mPaintContext = paintContext;
+ }
+
+ public CoreDocument getDocument() {
+ return mDocument;
+ }
+
+ public boolean isDebug() {
+ return mDebug;
+ }
+
+ public void setDebug(boolean debug) {
+ this.mDebug = debug;
+ }
+
+ public void setDocument(CoreDocument document) {
+ this.mDocument = document;
+ }
+
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+ // Operations
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+
+ public void header(int majorVersion, int minorVersion, int patchVersion,
+ int width, int height, long capabilities
+ ) {
+ mDocument.setVersion(majorVersion, minorVersion, patchVersion);
+ mDocument.setWidth(width);
+ mDocument.setHeight(height);
+ mDocument.setRequiredCapabilities(capabilities);
+ }
+
+ /**
+ * Sets the way the player handles the content
+ *
+ * @param scroll set the horizontal behavior (NONE|SCROLL_HORIZONTAL|SCROLL_VERTICAL)
+ * @param alignment set the alignment of the content (TOP|CENTER|BOTTOM|START|END)
+ * @param sizing set the type of sizing for the content (NONE|SIZING_LAYOUT|SIZING_SCALE)
+ * @param mode set the mode of sizing, either LAYOUT modes or SCALE modes
+ * the LAYOUT modes are:
+ * - LAYOUT_MATCH_PARENT
+ * - LAYOUT_WRAP_CONTENT
+ * or adding an horizontal mode and a vertical mode:
+ * - LAYOUT_HORIZONTAL_MATCH_PARENT
+ * - LAYOUT_HORIZONTAL_WRAP_CONTENT
+ * - LAYOUT_HORIZONTAL_FIXED
+ * - LAYOUT_VERTICAL_MATCH_PARENT
+ * - LAYOUT_VERTICAL_WRAP_CONTENT
+ * - LAYOUT_VERTICAL_FIXED
+ * The LAYOUT_*_FIXED modes will use the intrinsic document size
+ */
+ public void setRootContentBehavior(int scroll, int alignment, int sizing, int mode) {
+ mDocument.setRootContentBehavior(scroll, alignment, sizing, mode);
+ }
+
+ /**
+ * Set a content description for the document
+ * @param contentDescriptionId the text id pointing at the description
+ */
+ public void setDocumentContentDescription(int contentDescriptionId) {
+ String contentDescription = (String) mRemoteComposeState.getFromId(contentDescriptionId);
+ mDocument.setContentDescription(contentDescription);
+ }
+
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+ // Data handling
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+ public abstract void loadBitmap(int imageId, int width, int height, byte[] bitmap);
+ public abstract void loadText(int id, String text);
+
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+ // Click handling
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+ public abstract void addClickArea(
+ int id,
+ int contentDescription,
+ float left,
+ float top,
+ float right,
+ float bottom,
+ int metadataId
+ );
+}
+
diff --git a/core/java/com/android/internal/widget/remotecompose/core/WireBuffer.java b/core/java/com/android/internal/widget/remotecompose/core/WireBuffer.java
new file mode 100644
index 0000000..3e701c1
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/core/WireBuffer.java
@@ -0,0 +1,254 @@
+/*
+ * Copyright (C) 2023 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.widget.remotecompose.core;
+
+import java.util.Arrays;
+
+/**
+ * The base communication buffer capable of encoding and decoding various types
+ */
+public class WireBuffer {
+ private static final int BUFFER_SIZE = 1024 * 1024 * 1;
+ int mMaxSize;
+ byte[] mBuffer;
+ int mIndex = 0;
+ int mStartingIndex = 0;
+ int mSize = 0;
+
+ public WireBuffer(int size) {
+ mMaxSize = size;
+ mBuffer = new byte[mMaxSize];
+ }
+
+ public WireBuffer() {
+ this(BUFFER_SIZE);
+ }
+
+ private void resize(int need) {
+ if (mSize + need >= mMaxSize) {
+ mMaxSize = Math.max(mMaxSize * 2, mSize + need);
+ mBuffer = Arrays.copyOf(mBuffer, mMaxSize);
+ }
+ }
+
+ public byte[] getBuffer() {
+ return mBuffer;
+ }
+
+ public int getMax_size() {
+ return mMaxSize;
+ }
+
+ public int getIndex() {
+ return mIndex;
+ }
+
+ public int getSize() {
+ return mSize;
+ }
+
+ public void setIndex(int index) {
+ this.mIndex = index;
+ }
+
+ public void start(int type) {
+ mStartingIndex = mIndex;
+ writeByte(type);
+ }
+
+ public void startWithSize(int type) {
+ mStartingIndex = mIndex;
+ writeByte(type);
+ mIndex += 4; // skip ahead for the future size
+ }
+
+ public void endWithSize() {
+ int size = mIndex - mStartingIndex;
+ int currentIndex = mIndex;
+ mIndex = mStartingIndex + 1; // (type)
+ writeInt(size);
+ mIndex = currentIndex;
+ }
+
+ public void reset() {
+ mIndex = 0;
+ mStartingIndex = 0;
+ mSize = 0;
+ }
+
+ public int size() {
+ return mSize;
+ }
+
+ public boolean available() {
+ return mSize - mIndex > 0;
+ }
+
+ ///////////////////////////////////////////////////////////////////////////
+ // Read values
+ ///////////////////////////////////////////////////////////////////////////
+
+ public int readOperationType() {
+ return readByte();
+ }
+
+ public boolean readBoolean() {
+ byte value = mBuffer[mIndex];
+ mIndex++;
+ return (value == 1);
+ }
+
+ public int readByte() {
+ byte value = mBuffer[mIndex];
+ mIndex++;
+ return value;
+ }
+
+ public int readShort() {
+ int v1 = (mBuffer[mIndex++] & 0xFF) << 8;
+ int v2 = (mBuffer[mIndex++] & 0xFF) << 0;
+ return v1 + v2;
+ }
+
+ public int readInt() {
+ int v1 = (mBuffer[mIndex++] & 0xFF) << 24;
+ int v2 = (mBuffer[mIndex++] & 0xFF) << 16;
+ int v3 = (mBuffer[mIndex++] & 0xFF) << 8;
+ int v4 = (mBuffer[mIndex++] & 0xFF) << 0;
+ return v1 + v2 + v3 + v4;
+ }
+
+ public long readLong() {
+ long v1 = (mBuffer[mIndex++] & 0xFFL) << 56;
+ long v2 = (mBuffer[mIndex++] & 0xFFL) << 48;
+ long v3 = (mBuffer[mIndex++] & 0xFFL) << 40;
+ long v4 = (mBuffer[mIndex++] & 0xFFL) << 32;
+ long v5 = (mBuffer[mIndex++] & 0xFFL) << 24;
+ long v6 = (mBuffer[mIndex++] & 0xFFL) << 16;
+ long v7 = (mBuffer[mIndex++] & 0xFFL) << 8;
+ long v8 = (mBuffer[mIndex++] & 0xFFL) << 0;
+ return v1 + v2 + v3 + v4 + v5 + v6 + v7 + v8;
+ }
+
+ public float readFloat() {
+ return java.lang.Float.intBitsToFloat(readInt());
+ }
+
+ public double readDouble() {
+ return java.lang.Double.longBitsToDouble(readLong());
+ }
+
+ public byte[] readBuffer() {
+ int count = readInt();
+ byte[] b = Arrays.copyOfRange(mBuffer, mIndex, mIndex + count);
+ mIndex += count;
+ return b;
+ }
+
+ public byte[] readBuffer(int maxSize) {
+ int count = readInt();
+ if (count < 0 || count > maxSize) {
+ throw new RuntimeException("attempt read a buff of invalid size 0 <= "
+ + count + " > " + maxSize);
+ }
+ byte[] b = Arrays.copyOfRange(mBuffer, mIndex, mIndex + count);
+ mIndex += count;
+ return b;
+ }
+
+ public String readUTF8() {
+ byte[] stringBuffer = readBuffer();
+ return new String(stringBuffer);
+ }
+
+ public String readUTF8(int maxSize) {
+ byte[] stringBuffer = readBuffer(maxSize);
+ return new String(stringBuffer);
+ }
+
+ ///////////////////////////////////////////////////////////////////////////
+ // Write values
+ ///////////////////////////////////////////////////////////////////////////
+
+ public void writeBoolean(boolean value) {
+ resize(1);
+ mBuffer[mIndex++] = (byte) ((value) ? 1 : 0);
+ mSize++;
+ }
+
+ public void writeByte(int value) {
+ resize(1);
+ mBuffer[mIndex++] = (byte) value;
+ mSize++;
+ }
+
+ public void writeShort(int value) {
+ int need = 2;
+ resize(need);
+ mBuffer[mIndex++] = (byte) (value >>> 8 & 0xFF);
+ mBuffer[mIndex++] = (byte) (value & 0xFF);
+ mSize += need;
+ }
+
+ public void writeInt(int value) {
+ int need = 4;
+ resize(need);
+ mBuffer[mIndex++] = (byte) (value >>> 24 & 0xFF);
+ mBuffer[mIndex++] = (byte) (value >>> 16 & 0xFF);
+ mBuffer[mIndex++] = (byte) (value >>> 8 & 0xFF);
+ mBuffer[mIndex++] = (byte) (value & 0xFF);
+ mSize += need;
+ }
+
+ public void writeLong(long value) {
+ int need = 8;
+ resize(need);
+ mBuffer[mIndex++] = (byte) (value >>> 56 & 0xFF);
+ mBuffer[mIndex++] = (byte) (value >>> 48 & 0xFF);
+ mBuffer[mIndex++] = (byte) (value >>> 40 & 0xFF);
+ mBuffer[mIndex++] = (byte) (value >>> 32 & 0xFF);
+ mBuffer[mIndex++] = (byte) (value >>> 24 & 0xFF);
+ mBuffer[mIndex++] = (byte) (value >>> 16 & 0xFF);
+ mBuffer[mIndex++] = (byte) (value >>> 8 & 0xFF);
+ mBuffer[mIndex++] = (byte) (value & 0xFF);
+ mSize += need;
+ }
+
+ public void writeFloat(float value) {
+ writeInt(Float.floatToRawIntBits(value));
+ }
+
+ public void writeDouble(double value) {
+ writeLong(Double.doubleToRawLongBits(value));
+ }
+
+ public void writeBuffer(byte[] b) {
+ resize(b.length + 4);
+ writeInt(b.length);
+ for (int i = 0; i < b.length; i++) {
+ mBuffer[mIndex++] = b[i];
+
+ }
+ mSize += b.length;
+ }
+
+ public void writeUTF8(String content) {
+ byte[] buffer = content.getBytes();
+ writeBuffer(buffer);
+ }
+
+}
+
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/BitmapData.java b/core/java/com/android/internal/widget/remotecompose/core/operations/BitmapData.java
new file mode 100644
index 0000000..4bfdc59
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/BitmapData.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright (C) 2023 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.widget.remotecompose.core.operations;
+
+import com.android.internal.widget.remotecompose.core.CompanionOperation;
+import com.android.internal.widget.remotecompose.core.Operation;
+import com.android.internal.widget.remotecompose.core.Operations;
+import com.android.internal.widget.remotecompose.core.RemoteContext;
+import com.android.internal.widget.remotecompose.core.WireBuffer;
+
+import java.util.List;
+
+/**
+ * Operation to deal with bitmap data
+ * On getting an Image during a draw call the bitmap is compressed and saved
+ * in playback the image is decompressed
+ */
+public class BitmapData implements Operation {
+ int mImageId;
+ int mImageWidth;
+ int mImageHeight;
+ byte[] mBitmap;
+ public static final int MAX_IMAGE_DIMENSION = 6000;
+
+ public static final Companion COMPANION = new Companion();
+
+ public BitmapData(int imageId, int width, int height, byte[] bitmap) {
+ this.mImageId = imageId;
+ this.mImageWidth = width;
+ this.mImageHeight = height;
+ this.mBitmap = bitmap;
+ }
+
+ @Override
+ public void write(WireBuffer buffer) {
+ COMPANION.apply(buffer, mImageId, mImageWidth, mImageHeight, mBitmap);
+ }
+
+ @Override
+ public String toString() {
+ return "BITMAP DATA $imageId";
+ }
+
+ public static class Companion implements CompanionOperation {
+ private Companion() {}
+
+ @Override
+ public String name() {
+ return "BitmapData";
+ }
+
+ @Override
+ public int id() {
+ return Operations.DATA_BITMAP;
+ }
+
+ public void apply(WireBuffer buffer, int imageId, int width, int height, byte[] bitmap) {
+ buffer.start(Operations.DATA_BITMAP);
+ buffer.writeInt(imageId);
+ buffer.writeInt(width);
+ buffer.writeInt(height);
+ buffer.writeBuffer(bitmap);
+ }
+
+ @Override
+ public void read(WireBuffer buffer, List<Operation> operations) {
+ int imageId = buffer.readInt();
+ int width = buffer.readInt();
+ int height = buffer.readInt();
+ if (width < 1
+ || height < 1
+ || height > MAX_IMAGE_DIMENSION
+ || width > MAX_IMAGE_DIMENSION) {
+ throw new RuntimeException("Dimension of image is invalid " + width + "x" + height);
+ }
+ byte[] bitmap = buffer.readBuffer();
+ operations.add(new BitmapData(imageId, width, height, bitmap));
+ }
+ }
+
+ @Override
+ public void apply(RemoteContext context) {
+ context.loadBitmap(mImageId, mImageWidth, mImageHeight, mBitmap);
+ }
+
+ @Override
+ public String deepToString(String indent) {
+ return indent + toString();
+ }
+
+}
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/ClickArea.java b/core/java/com/android/internal/widget/remotecompose/core/operations/ClickArea.java
new file mode 100644
index 0000000..a3cd197
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/ClickArea.java
@@ -0,0 +1,132 @@
+/*
+ * Copyright (C) 2024 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.widget.remotecompose.core.operations;
+
+import com.android.internal.widget.remotecompose.core.CompanionOperation;
+import com.android.internal.widget.remotecompose.core.Operation;
+import com.android.internal.widget.remotecompose.core.Operations;
+import com.android.internal.widget.remotecompose.core.RemoteComposeOperation;
+import com.android.internal.widget.remotecompose.core.RemoteContext;
+import com.android.internal.widget.remotecompose.core.WireBuffer;
+
+import java.util.List;
+
+/**
+ * Add a click area to the document
+ */
+public class ClickArea implements RemoteComposeOperation {
+ int mId;
+ int mContentDescription;
+ float mLeft;
+ float mTop;
+ float mRight;
+ float mBottom;
+ int mMetadata;
+
+ public static final Companion COMPANION = new Companion();
+
+ /**
+ * Add a click area to the document
+ *
+ * @param id the id of the click area, which will be reported in the listener
+ * callback on the player
+ * @param contentDescription the content description (used for accessibility, as a textID)
+ * @param left left coordinate of the area bounds
+ * @param top top coordinate of the area bounds
+ * @param right right coordinate of the area bounds
+ * @param bottom bottom coordinate of the area bounds
+ * @param metadata associated metadata, user-provided (as a textID, pointing to a string)
+ */
+ public ClickArea(int id, int contentDescription,
+ float left, float top,
+ float right, float bottom,
+ int metadata) {
+ this.mId = id;
+ this.mContentDescription = contentDescription;
+ this.mLeft = left;
+ this.mTop = top;
+ this.mRight = right;
+ this.mBottom = bottom;
+ this.mMetadata = metadata;
+ }
+
+ @Override
+ public void write(WireBuffer buffer) {
+ COMPANION.apply(buffer, mId, mContentDescription, mLeft, mTop, mRight, mBottom, mMetadata);
+ }
+
+ @Override
+ public String toString() {
+ return "CLICK_AREA <" + mId + " <" + mContentDescription + "> "
+ + "<" + mMetadata + ">+" + mLeft + " "
+ + mTop + " " + mRight + " " + mBottom + "+"
+ + " (" + (mRight - mLeft) + " x " + (mBottom - mTop) + " }";
+ }
+
+ @Override
+ public void apply(RemoteContext context) {
+ if (context.getMode() != RemoteContext.ContextMode.DATA) {
+ return;
+ }
+ context.addClickArea(mId, mContentDescription, mLeft, mTop, mRight, mBottom, mMetadata);
+ }
+
+ @Override
+ public String deepToString(String indent) {
+ return indent + toString();
+ }
+
+ public static class Companion implements CompanionOperation {
+ private Companion() {}
+
+ @Override
+ public String name() {
+ return "ClickArea";
+ }
+
+ @Override
+ public int id() {
+ return Operations.CLICK_AREA;
+ }
+
+ public void apply(WireBuffer buffer, int id, int contentDescription,
+ float left, float top, float right, float bottom,
+ int metadata) {
+ buffer.start(Operations.CLICK_AREA);
+ buffer.writeInt(id);
+ buffer.writeInt(contentDescription);
+ buffer.writeFloat(left);
+ buffer.writeFloat(top);
+ buffer.writeFloat(right);
+ buffer.writeFloat(bottom);
+ buffer.writeInt(metadata);
+ }
+
+ @Override
+ public void read(WireBuffer buffer, List<Operation> operations) {
+ int id = buffer.readInt();
+ int contentDescription = buffer.readInt();
+ float left = buffer.readFloat();
+ float top = buffer.readFloat();
+ float right = buffer.readFloat();
+ float bottom = buffer.readFloat();
+ int metadata = buffer.readInt();
+ ClickArea clickArea = new ClickArea(id, contentDescription,
+ left, top, right, bottom, metadata);
+ operations.add(clickArea);
+ }
+ }
+}
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/DrawBitmapInt.java b/core/java/com/android/internal/widget/remotecompose/core/operations/DrawBitmapInt.java
new file mode 100644
index 0000000..3fbdf94
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/DrawBitmapInt.java
@@ -0,0 +1,132 @@
+/*
+ * Copyright (C) 2023 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.widget.remotecompose.core.operations;
+
+import com.android.internal.widget.remotecompose.core.CompanionOperation;
+import com.android.internal.widget.remotecompose.core.Operation;
+import com.android.internal.widget.remotecompose.core.Operations;
+import com.android.internal.widget.remotecompose.core.PaintContext;
+import com.android.internal.widget.remotecompose.core.PaintOperation;
+import com.android.internal.widget.remotecompose.core.WireBuffer;
+
+import java.util.List;
+
+/**
+ * Operation to draw a given cached bitmap
+ */
+public class DrawBitmapInt extends PaintOperation {
+ int mImageId;
+ int mSrcLeft;
+ int mSrcTop;
+ int mSrcRight;
+ int mSrcBottom;
+ int mDstLeft;
+ int mDstTop;
+ int mDstRight;
+ int mDstBottom;
+ int mContentDescId = 0;
+ public static final Companion COMPANION = new Companion();
+
+ public DrawBitmapInt(int imageId,
+ int srcLeft,
+ int srcTop,
+ int srcRight,
+ int srcBottom,
+ int dstLeft,
+ int dstTop,
+ int dstRight,
+ int dstBottom,
+ int cdId) {
+ this.mImageId = imageId;
+ this.mSrcLeft = srcLeft;
+ this.mSrcTop = srcTop;
+ this.mSrcRight = srcRight;
+ this.mSrcBottom = srcBottom;
+ this.mDstLeft = dstLeft;
+ this.mDstTop = dstTop;
+ this.mDstRight = dstRight;
+ this.mDstBottom = dstBottom;
+ this.mContentDescId = cdId;
+ }
+
+ @Override
+ public void write(WireBuffer buffer) {
+ COMPANION.apply(buffer, mImageId, mSrcLeft, mSrcTop, mSrcRight, mSrcBottom,
+ mDstLeft, mDstTop, mDstRight, mDstBottom, mContentDescId);
+ }
+
+ @Override
+ public String toString() {
+ return "DRAW_BITMAP_INT " + mImageId + " on " + mSrcLeft + " " + mSrcTop
+ + " " + mSrcRight + " " + mSrcBottom + " "
+ + "- " + mDstLeft + " " + mDstTop + " " + mDstRight + " " + mDstBottom + ";";
+ }
+
+ public static class Companion implements CompanionOperation {
+ private Companion() {}
+
+ @Override
+ public String name() {
+ return "DrawBitmapInt";
+ }
+
+ @Override
+ public int id() {
+ return Operations.DRAW_BITMAP;
+ }
+
+ public void apply(WireBuffer buffer, int imageId,
+ int srcLeft, int srcTop, int srcRight, int srcBottom,
+ int dstLeft, int dstTop, int dstRight, int dstBottom,
+ int cdId) {
+ buffer.start(Operations.DRAW_BITMAP_INT);
+ buffer.writeInt(imageId);
+ buffer.writeInt(srcLeft);
+ buffer.writeInt(srcTop);
+ buffer.writeInt(srcRight);
+ buffer.writeInt(srcBottom);
+ buffer.writeInt(dstLeft);
+ buffer.writeInt(dstTop);
+ buffer.writeInt(dstRight);
+ buffer.writeInt(dstBottom);
+ buffer.writeInt(cdId);
+ }
+
+ @Override
+ public void read(WireBuffer buffer, List<Operation> operations) {
+ int imageId = buffer.readInt();
+ int sLeft = buffer.readInt();
+ int srcTop = buffer.readInt();
+ int srcRight = buffer.readInt();
+ int srcBottom = buffer.readInt();
+ int dstLeft = buffer.readInt();
+ int dstTop = buffer.readInt();
+ int dstRight = buffer.readInt();
+ int dstBottom = buffer.readInt();
+ int cdId = buffer.readInt();
+ DrawBitmapInt op = new DrawBitmapInt(imageId, sLeft, srcTop, srcRight, srcBottom,
+ dstLeft, dstTop, dstRight, dstBottom, cdId);
+
+ operations.add(op);
+ }
+ }
+
+ @Override
+ public void paint(PaintContext context) {
+ context.drawBitmap(mImageId, mSrcLeft, mSrcTop, mSrcRight, mSrcBottom,
+ mDstLeft, mDstTop, mDstRight, mDstBottom, mContentDescId);
+ }
+}
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/Header.java b/core/java/com/android/internal/widget/remotecompose/core/operations/Header.java
new file mode 100644
index 0000000..eca43c5
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/Header.java
@@ -0,0 +1,127 @@
+/*
+ * Copyright (C) 2023 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.widget.remotecompose.core.operations;
+
+import com.android.internal.widget.remotecompose.core.CompanionOperation;
+import com.android.internal.widget.remotecompose.core.Operation;
+import com.android.internal.widget.remotecompose.core.Operations;
+import com.android.internal.widget.remotecompose.core.RemoteComposeOperation;
+import com.android.internal.widget.remotecompose.core.RemoteContext;
+import com.android.internal.widget.remotecompose.core.WireBuffer;
+
+import java.util.List;
+
+/**
+ * Describe some basic information for a RemoteCompose document
+ *
+ * It encodes the version of the document (following semantic versioning) as well
+ * as the dimensions of the document in pixels.
+ */
+public class Header implements RemoteComposeOperation {
+ public static final int MAJOR_VERSION = 0;
+ public static final int MINOR_VERSION = 1;
+ public static final int PATCH_VERSION = 0;
+
+ int mMajorVersion;
+ int mMinorVersion;
+ int mPatchVersion;
+
+ int mWidth;
+ int mHeight;
+ long mCapabilities;
+
+ public static final Companion COMPANION = new Companion();
+
+ /**
+ * It encodes the version of the document (following semantic versioning) as well
+ * as the dimensions of the document in pixels.
+ *
+ * @param majorVersion the major version of the RemoteCompose document API
+ * @param minorVersion the minor version of the RemoteCompose document API
+ * @param patchVersion the patch version of the RemoteCompose document API
+ * @param width the width of the RemoteCompose document
+ * @param height the height of the RemoteCompose document
+ * @param capabilities bitmask field storing needed capabilities (unused for now)
+ */
+ public Header(int majorVersion, int minorVersion, int patchVersion,
+ int width, int height, long capabilities) {
+ this.mMajorVersion = majorVersion;
+ this.mMinorVersion = minorVersion;
+ this.mPatchVersion = patchVersion;
+ this.mWidth = width;
+ this.mHeight = height;
+ this.mCapabilities = capabilities;
+ }
+
+ @Override
+ public void write(WireBuffer buffer) {
+ COMPANION.apply(buffer, mWidth, mHeight, mCapabilities);
+ }
+
+ @Override
+ public String toString() {
+ return "HEADER v" + mMajorVersion + "."
+ + mMinorVersion + "." + mPatchVersion + ", "
+ + mWidth + " x " + mHeight + " [" + mCapabilities + "]";
+ }
+
+ @Override
+ public void apply(RemoteContext context) {
+ context.header(mMajorVersion, mMinorVersion, mPatchVersion, mWidth, mHeight, mCapabilities);
+ }
+
+ @Override
+ public String deepToString(String indent) {
+ return toString();
+ }
+
+ public static class Companion implements CompanionOperation {
+ private Companion() {}
+
+ @Override
+ public String name() {
+ return "Header";
+ }
+
+ @Override
+ public int id() {
+ return Operations.HEADER;
+ }
+
+ public void apply(WireBuffer buffer, int width, int height, long capabilities) {
+ buffer.start(Operations.HEADER);
+ buffer.writeInt(MAJOR_VERSION); // major version number of the protocol
+ buffer.writeInt(MINOR_VERSION); // minor version number of the protocol
+ buffer.writeInt(PATCH_VERSION); // patch version number of the protocol
+ buffer.writeInt(width);
+ buffer.writeInt(height);
+ buffer.writeLong(capabilities);
+ }
+
+ @Override
+ public void read(WireBuffer buffer, List<Operation> operations) {
+ int majorVersion = buffer.readInt();
+ int minorVersion = buffer.readInt();
+ int patchVersion = buffer.readInt();
+ int width = buffer.readInt();
+ int height = buffer.readInt();
+ long capabilities = buffer.readLong();
+ Header header = new Header(majorVersion, minorVersion, patchVersion,
+ width, height, capabilities);
+ operations.add(header);
+ }
+ }
+}
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/RootContentBehavior.java b/core/java/com/android/internal/widget/remotecompose/core/operations/RootContentBehavior.java
new file mode 100644
index 0000000..ad4caea
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/RootContentBehavior.java
@@ -0,0 +1,234 @@
+/*
+ * Copyright (C) 2024 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.widget.remotecompose.core.operations;
+
+import android.util.Log;
+
+import com.android.internal.widget.remotecompose.core.CompanionOperation;
+import com.android.internal.widget.remotecompose.core.Operation;
+import com.android.internal.widget.remotecompose.core.Operations;
+import com.android.internal.widget.remotecompose.core.RemoteComposeOperation;
+import com.android.internal.widget.remotecompose.core.RemoteContext;
+import com.android.internal.widget.remotecompose.core.WireBuffer;
+
+import java.util.List;
+
+/**
+ * Describe some basic information for a RemoteCompose document
+ *
+ * It encodes the version of the document (following semantic versioning) as well
+ * as the dimensions of the document in pixels.
+ */
+public class RootContentBehavior implements RemoteComposeOperation {
+
+ int mScroll = NONE;
+ int mSizing = NONE;
+
+ int mAlignment = ALIGNMENT_CENTER;
+
+ int mMode = NONE;
+
+ protected static final String TAG = "RootContentBehavior";
+
+ public static final int NONE = 0;
+
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+ // Scrolling
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+ public static final int SCROLL_HORIZONTAL = 1;
+ public static final int SCROLL_VERTICAL = 2;
+
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+ // Sizing
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+ public static final int SIZING_LAYOUT = 1;
+ public static final int SIZING_SCALE = 2;
+
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+ // Sizing
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+ public static final int ALIGNMENT_TOP = 1;
+ public static final int ALIGNMENT_VERTICAL_CENTER = 2;
+ public static final int ALIGNMENT_BOTTOM = 4;
+ public static final int ALIGNMENT_START = 16;
+ public static final int ALIGNMENT_HORIZONTAL_CENTER = 32;
+ public static final int ALIGNMENT_END = 64;
+ public static final int ALIGNMENT_CENTER = ALIGNMENT_HORIZONTAL_CENTER
+ + ALIGNMENT_VERTICAL_CENTER;
+
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+ // Layout mode
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+ public static final int LAYOUT_HORIZONTAL_MATCH_PARENT = 1;
+ public static final int LAYOUT_HORIZONTAL_WRAP_CONTENT = 2;
+ public static final int LAYOUT_HORIZONTAL_FIXED = 4;
+ public static final int LAYOUT_VERTICAL_MATCH_PARENT = 8;
+ public static final int LAYOUT_VERTICAL_WRAP_CONTENT = 16;
+ public static final int LAYOUT_VERTICAL_FIXED = 32;
+ public static final int LAYOUT_MATCH_PARENT =
+ LAYOUT_HORIZONTAL_MATCH_PARENT + LAYOUT_VERTICAL_MATCH_PARENT;
+ public static final int LAYOUT_WRAP_CONTENT =
+ LAYOUT_HORIZONTAL_WRAP_CONTENT + LAYOUT_VERTICAL_WRAP_CONTENT;
+
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+ // Sizing mode
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+
+ public static final int SCALE_INSIDE = 1;
+ public static final int SCALE_FILL_WIDTH = 2;
+ public static final int SCALE_FILL_HEIGHT = 3;
+ public static final int SCALE_FIT = 4;
+ public static final int SCALE_CROP = 5;
+ public static final int SCALE_FILL_BOUNDS = 6;
+
+
+ public static final Companion COMPANION = new Companion();
+
+ /**
+ * Sets the way the player handles the content
+ *
+ * @param scroll set the horizontal behavior (NONE|SCROLL_HORIZONTAL|SCROLL_VERTICAL)
+ * @param alignment set the alignment of the content (TOP|CENTER|BOTTOM|START|END)
+ * @param sizing set the type of sizing for the content (NONE|SIZING_LAYOUT|SIZING_SCALE)
+ * @param mode set the mode of sizing, either LAYOUT modes or SCALE modes
+ * the LAYOUT modes are:
+ * - LAYOUT_MATCH_PARENT
+ * - LAYOUT_WRAP_CONTENT
+ * or adding an horizontal mode and a vertical mode:
+ * - LAYOUT_HORIZONTAL_MATCH_PARENT
+ * - LAYOUT_HORIZONTAL_WRAP_CONTENT
+ * - LAYOUT_HORIZONTAL_FIXED
+ * - LAYOUT_VERTICAL_MATCH_PARENT
+ * - LAYOUT_VERTICAL_WRAP_CONTENT
+ * - LAYOUT_VERTICAL_FIXED
+ * The LAYOUT_*_FIXED modes will use the intrinsic document size
+ */
+ public RootContentBehavior(int scroll, int alignment, int sizing, int mode) {
+ switch (scroll) {
+ case NONE:
+ case SCROLL_HORIZONTAL:
+ case SCROLL_VERTICAL:
+ mScroll = scroll;
+ break;
+ default: {
+ Log.e(TAG, "incorrect scroll value " + scroll);
+ }
+ }
+ if (alignment == ALIGNMENT_CENTER) {
+ mAlignment = alignment;
+ } else {
+ int horizontalContentAlignment = alignment & 0xF0;
+ int verticalContentAlignment = alignment & 0xF;
+ boolean validHorizontalAlignment = horizontalContentAlignment == ALIGNMENT_START
+ || horizontalContentAlignment == ALIGNMENT_HORIZONTAL_CENTER
+ || horizontalContentAlignment == ALIGNMENT_END;
+ boolean validVerticalAlignment = verticalContentAlignment == ALIGNMENT_TOP
+ || verticalContentAlignment == ALIGNMENT_VERTICAL_CENTER
+ || verticalContentAlignment == ALIGNMENT_BOTTOM;
+ if (validHorizontalAlignment && validVerticalAlignment) {
+ mAlignment = alignment;
+ } else {
+ Log.e(TAG, "incorrect alignment "
+ + " h: " + horizontalContentAlignment
+ + " v: " + verticalContentAlignment);
+ }
+ }
+ switch (sizing) {
+ case SIZING_LAYOUT: {
+ Log.e(TAG, "sizing_layout is not yet supported");
+ } break;
+ case SIZING_SCALE: {
+ mSizing = sizing;
+ } break;
+ default: {
+ Log.e(TAG, "incorrect sizing value " + sizing);
+ }
+ }
+ if (mSizing == SIZING_LAYOUT) {
+ if (mode != NONE) {
+ Log.e(TAG, "mode for sizing layout is not yet supported");
+ }
+ } else if (mSizing == SIZING_SCALE) {
+ switch (mode) {
+ case SCALE_INSIDE:
+ case SCALE_FIT:
+ case SCALE_FILL_WIDTH:
+ case SCALE_FILL_HEIGHT:
+ case SCALE_CROP:
+ case SCALE_FILL_BOUNDS:
+ mMode = mode;
+ break;
+ default: {
+ Log.e(TAG, "incorrect mode for scale sizing, mode: " + mode);
+ }
+ }
+ }
+ }
+
+ @Override
+ public void write(WireBuffer buffer) {
+ COMPANION.apply(buffer, mScroll, mAlignment, mSizing, mMode);
+ }
+
+ @Override
+ public String toString() {
+ return "ROOT_CONTENT_BEHAVIOR scroll: " + mScroll
+ + " sizing: " + mSizing + " mode: " + mMode;
+ }
+
+ @Override
+ public void apply(RemoteContext context) {
+ context.setRootContentBehavior(mScroll, mAlignment, mSizing, mMode);
+ }
+
+ @Override
+ public String deepToString(String indent) {
+ return toString();
+ }
+
+ public static class Companion implements CompanionOperation {
+ private Companion() {}
+
+ @Override
+ public String name() {
+ return "RootContentBehavior";
+ }
+
+ @Override
+ public int id() {
+ return Operations.ROOT_CONTENT_BEHAVIOR;
+ }
+
+ public void apply(WireBuffer buffer, int scroll, int alignment, int sizing, int mode) {
+ buffer.start(Operations.ROOT_CONTENT_BEHAVIOR);
+ buffer.writeInt(scroll);
+ buffer.writeInt(alignment);
+ buffer.writeInt(sizing);
+ buffer.writeInt(mode);
+ }
+
+ @Override
+ public void read(WireBuffer buffer, List<Operation> operations) {
+ int scroll = buffer.readInt();
+ int alignment = buffer.readInt();
+ int sizing = buffer.readInt();
+ int mode = buffer.readInt();
+ RootContentBehavior rootContentBehavior =
+ new RootContentBehavior(scroll, alignment, sizing, mode);
+ operations.add(rootContentBehavior);
+ }
+ }
+}
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/RootContentDescription.java b/core/java/com/android/internal/widget/remotecompose/core/operations/RootContentDescription.java
new file mode 100644
index 0000000..64c7f3e
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/RootContentDescription.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2024 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.widget.remotecompose.core.operations;
+
+import com.android.internal.widget.remotecompose.core.CompanionOperation;
+import com.android.internal.widget.remotecompose.core.Operation;
+import com.android.internal.widget.remotecompose.core.Operations;
+import com.android.internal.widget.remotecompose.core.RemoteComposeOperation;
+import com.android.internal.widget.remotecompose.core.RemoteContext;
+import com.android.internal.widget.remotecompose.core.WireBuffer;
+
+import java.util.List;
+
+/**
+ * Describe a content description for the document
+ */
+public class RootContentDescription implements RemoteComposeOperation {
+ int mContentDescription;
+
+ public static final Companion COMPANION = new Companion();
+
+ /**
+ * Encodes a content description for the document
+ *
+ * @param contentDescription content description for the document
+ */
+ public RootContentDescription(int contentDescription) {
+ this.mContentDescription = contentDescription;
+ }
+
+ @Override
+ public void write(WireBuffer buffer) {
+ COMPANION.apply(buffer, mContentDescription);
+ }
+
+ @Override
+ public String toString() {
+ return "ROOT_CONTENT_DESCRIPTION " + mContentDescription;
+ }
+
+ @Override
+ public void apply(RemoteContext context) {
+ context.setDocumentContentDescription(mContentDescription);
+ }
+
+ @Override
+ public String deepToString(String indent) {
+ return toString();
+ }
+
+ public static class Companion implements CompanionOperation {
+ private Companion() {}
+
+ @Override
+ public String name() {
+ return "RootContentDescription";
+ }
+
+ @Override
+ public int id() {
+ return Operations.ROOT_CONTENT_DESCRIPTION;
+ }
+
+ public void apply(WireBuffer buffer, int contentDescription) {
+ buffer.start(Operations.ROOT_CONTENT_DESCRIPTION);
+ buffer.writeInt(contentDescription);
+ }
+
+ @Override
+ public void read(WireBuffer buffer, List<Operation> operations) {
+ int contentDescription = buffer.readInt();
+ RootContentDescription header = new RootContentDescription(contentDescription);
+ operations.add(header);
+ }
+ }
+}
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/TextData.java b/core/java/com/android/internal/widget/remotecompose/core/operations/TextData.java
new file mode 100644
index 0000000..5b622ae
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/TextData.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 2023 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.widget.remotecompose.core.operations;
+
+import com.android.internal.widget.remotecompose.core.CompanionOperation;
+import com.android.internal.widget.remotecompose.core.Operation;
+import com.android.internal.widget.remotecompose.core.Operations;
+import com.android.internal.widget.remotecompose.core.RemoteContext;
+import com.android.internal.widget.remotecompose.core.WireBuffer;
+
+import java.util.List;
+
+/**
+ * Operation to deal with Text data
+ */
+public class TextData implements Operation {
+ public int mTextId;
+ public String mText;
+ public static final Companion COMPANION = new Companion();
+ public static final int MAX_STRING_SIZE = 4000;
+
+ public TextData(int textId, String text) {
+ this.mTextId = textId;
+ this.mText = text;
+ }
+
+ @Override
+ public void write(WireBuffer buffer) {
+ COMPANION.apply(buffer, mTextId, mText);
+ }
+
+ @Override
+ public String toString() {
+ return "TEXT DATA " + mTextId + "\"" + mText + "\"";
+ }
+
+ public static class Companion implements CompanionOperation {
+ private Companion() {}
+
+ @Override
+ public String name() {
+ return "TextData";
+ }
+
+ @Override
+ public int id() {
+ return Operations.DATA_TEXT;
+ }
+
+ public void apply(WireBuffer buffer, int textId, String text) {
+ buffer.start(Operations.DATA_TEXT);
+ buffer.writeInt(textId);
+ buffer.writeUTF8(text);
+ }
+
+ @Override
+ public void read(WireBuffer buffer, List<Operation> operations) {
+ int textId = buffer.readInt();
+
+ String text = buffer.readUTF8(MAX_STRING_SIZE);
+ operations.add(new TextData(textId, text));
+ }
+ }
+
+ @Override
+ public void apply(RemoteContext context) {
+ context.loadText(mTextId, mText);
+ }
+
+ @Override
+ public String deepToString(String indent) {
+ return indent + toString();
+ }
+}
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/Theme.java b/core/java/com/android/internal/widget/remotecompose/core/operations/Theme.java
new file mode 100644
index 0000000..cbe9c12
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/Theme.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright (C) 2024 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.widget.remotecompose.core.operations;
+
+import com.android.internal.widget.remotecompose.core.CompanionOperation;
+import com.android.internal.widget.remotecompose.core.Operation;
+import com.android.internal.widget.remotecompose.core.Operations;
+import com.android.internal.widget.remotecompose.core.RemoteComposeOperation;
+import com.android.internal.widget.remotecompose.core.RemoteContext;
+import com.android.internal.widget.remotecompose.core.WireBuffer;
+
+import java.util.List;
+
+/**
+ * Set a current theme, applied to the following operations in the document.
+ * This can be used to "tag" the subsequent operations to a given theme. On playback,
+ * we can then filter operations depending on the chosen theme.
+ *
+ */
+public class Theme implements RemoteComposeOperation {
+ int mTheme;
+ public static final int UNSPECIFIED = -1;
+ public static final int DARK = -2;
+ public static final int LIGHT = -3;
+
+ public static final Companion COMPANION = new Companion();
+
+ /**
+ * we can then filter operations depending on the chosen theme.
+ *
+ * @param theme the theme we are interested in:
+ * - Theme.UNSPECIFIED
+ * - Theme.DARK
+ * - Theme.LIGHT
+ */
+ public Theme(int theme) {
+ this.mTheme = theme;
+ }
+
+ @Override
+ public void write(WireBuffer buffer) {
+ COMPANION.apply(buffer, mTheme);
+ }
+
+ @Override
+ public String toString() {
+ return "SET_THEME " + mTheme;
+ }
+
+ @Override
+ public void apply(RemoteContext context) {
+ context.setTheme(mTheme);
+ }
+
+ @Override
+ public String deepToString(String indent) {
+ return indent + toString();
+ }
+
+ public static class Companion implements CompanionOperation {
+ private Companion() {}
+
+ @Override
+ public String name() {
+ return "SetTheme";
+ }
+
+ @Override
+ public int id() {
+ return Operations.THEME;
+ }
+
+ public void apply(WireBuffer buffer, int theme) {
+ buffer.start(Operations.THEME);
+ buffer.writeInt(theme);
+ }
+
+ @Override
+ public void read(WireBuffer buffer, List<Operation> operations) {
+ int theme = buffer.readInt();
+ operations.add(new Theme(theme));
+ }
+ }
+}
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/utilities/IntMap.java b/core/java/com/android/internal/widget/remotecompose/core/operations/utilities/IntMap.java
new file mode 100644
index 0000000..8051ef1
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/utilities/IntMap.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright (C) 2023 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.widget.remotecompose.core.operations.utilities;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+
+public class IntMap<T> {
+
+ private static final int DEFAULT_CAPACITY = 16;
+ private static final float LOAD_FACTOR = 0.75f;
+ private static final int NOT_PRESENT = Integer.MIN_VALUE;
+ private int[] mKeys;
+ private ArrayList<T> mValues;
+ int mSize;
+
+ public IntMap() {
+ mKeys = new int[DEFAULT_CAPACITY];
+ Arrays.fill(mKeys, NOT_PRESENT);
+ mValues = new ArrayList<T>(DEFAULT_CAPACITY);
+ for (int i = 0; i < DEFAULT_CAPACITY; i++) {
+ mValues.add(null);
+ }
+ }
+
+ public void clear() {
+ Arrays.fill(mKeys, NOT_PRESENT);
+ mValues.clear();
+ mSize = 0;
+ }
+
+ public T put(int key, T value) {
+ if (key == NOT_PRESENT) throw new IllegalArgumentException("Key cannot be NOT_PRESENT");
+ if (mSize > mKeys.length * LOAD_FACTOR) {
+ resize();
+ }
+ return insert(key, value);
+ }
+
+
+ public T get(int key) {
+ int index = findKey(key);
+ if (index == -1) {
+ return null;
+ } else
+ return mValues.get(index);
+ }
+
+ public int size() {
+ return mSize;
+ }
+
+ private T insert(int key, T value) {
+ int index = hash(key) % mKeys.length;
+ while (mKeys[index] != NOT_PRESENT && mKeys[index] != key) {
+ index = (index + 1) % mKeys.length;
+ }
+ T oldValue = null;
+ if (mKeys[index] == NOT_PRESENT) {
+ mSize++;
+ } else {
+ oldValue = mValues.get(index);
+ }
+ mKeys[index] = key;
+ mValues.set(index, value);
+ return oldValue;
+ }
+
+ private int findKey(int key) {
+ int index = hash(key) % mKeys.length;
+ while (mKeys[index] != NOT_PRESENT) {
+ if (mKeys[index] == key) {
+ return index;
+ }
+ index = (index + 1) % mKeys.length;
+ }
+ return -1;
+ }
+
+ private int hash(int key) {
+ return key;
+ }
+
+ private void resize() {
+ int[] oldKeys = mKeys;
+ ArrayList<T> oldValues = mValues;
+ mKeys = new int[(oldKeys.length * 2)];
+ for (int i = 0; i < mKeys.length; i++) {
+ mKeys[i] = NOT_PRESENT;
+ }
+ mValues = new ArrayList<T>(oldKeys.length * 2);
+ for (int i = 0; i < oldKeys.length * 2; i++) {
+ mValues.add(null);
+ }
+ mSize = 0;
+ for (int i = 0; i < oldKeys.length; i++) {
+ if (oldKeys[i] != NOT_PRESENT) {
+ put(oldKeys[i], oldValues.get(i));
+ }
+ }
+ }
+}
diff --git a/core/java/com/android/internal/widget/remotecompose/player/RemoteComposeDocument.java b/core/java/com/android/internal/widget/remotecompose/player/RemoteComposeDocument.java
new file mode 100644
index 0000000..bcda27a
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/player/RemoteComposeDocument.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) 2024 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.widget.remotecompose.player;
+
+import com.android.internal.widget.remotecompose.core.CoreDocument;
+import com.android.internal.widget.remotecompose.core.RemoteComposeBuffer;
+import com.android.internal.widget.remotecompose.core.RemoteContext;
+
+import java.io.InputStream;
+
+/**
+ * Public API to create a new RemoteComposeDocument coming from an input stream
+ */
+public class RemoteComposeDocument {
+
+ CoreDocument mDocument = new CoreDocument();
+
+ public RemoteComposeDocument(InputStream inputStream) {
+ RemoteComposeBuffer buffer =
+ RemoteComposeBuffer.fromInputStream(inputStream, mDocument.getRemoteComposeState());
+ mDocument.initFromBuffer(buffer);
+ }
+
+ public CoreDocument getDocument() {
+ return mDocument;
+ }
+
+ public void setDocument(CoreDocument document) {
+ this.mDocument = document;
+ }
+
+ /**
+ * Called when an initialization is needed, allowing the document to eg load
+ * resources / cache them.
+ */
+ public void initializeContext(RemoteContext context) {
+ mDocument.initializeContext(context);
+ }
+
+ /**
+ * Returns the width of the document in pixels
+ */
+ public int getWidth() {
+ return mDocument.getWidth();
+ }
+
+ /**
+ * Returns the height of the document in pixels
+ */
+ public int getHeight() {
+ return mDocument.getHeight();
+ }
+
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+ // Painting
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * Paint the document
+ *
+ * @param context the provided PaintContext
+ * @param theme the theme we want to use for this document.
+ */
+ public void paint(RemoteContext context, int theme) {
+ mDocument.paint(context, theme);
+ }
+
+ /**
+ * Returns true if the document can be displayed given this version of the player
+ *
+ * @param majorVersion the max major version supported by the player
+ * @param minorVersion the max minor version supported by the player
+ * @param capabilities a bitmask of capabilities the player supports (unused for now)
+ */
+ public boolean canBeDisplayed(int majorVersion, int minorVersion, long capabilities) {
+ return mDocument.canBeDisplayed(majorVersion, minorVersion, capabilities);
+ }
+
+}
+
diff --git a/core/java/com/android/internal/widget/remotecompose/player/RemoteComposePlayer.java b/core/java/com/android/internal/widget/remotecompose/player/RemoteComposePlayer.java
new file mode 100644
index 0000000..cc1f3dd
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/player/RemoteComposePlayer.java
@@ -0,0 +1,177 @@
+/*
+ * Copyright (C) 2024 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.widget.remotecompose.player;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.ViewGroup;
+import android.widget.FrameLayout;
+import android.widget.HorizontalScrollView;
+import android.widget.ScrollView;
+
+import com.android.internal.widget.remotecompose.core.operations.RootContentBehavior;
+import com.android.internal.widget.remotecompose.player.platform.RemoteComposeCanvas;
+
+/**
+ * A view to to display and play RemoteCompose documents
+ */
+public class RemoteComposePlayer extends FrameLayout {
+ private RemoteComposeCanvas mInner;
+
+ private static final int MAX_SUPPORTED_MAJOR_VERSION = 0;
+ private static final int MAX_SUPPORTED_MINOR_VERSION = 1;
+
+ public RemoteComposePlayer(Context context) {
+ super(context);
+ init(context, null, 0);
+ }
+
+ public RemoteComposePlayer(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ init(context, attrs, 0);
+ }
+
+ public RemoteComposePlayer(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ init(context, attrs, defStyleAttr);
+ }
+
+ /**
+ * Turn on debug information
+ * @param debugFlags 1 to set debug on
+ */
+ public void setDebug(int debugFlags) {
+ if (debugFlags == 1) {
+ mInner.setDebug(true);
+ } else {
+ mInner.setDebug(false);
+ }
+ }
+
+ public void setDocument(RemoteComposeDocument value) {
+ if (value != null) {
+ if (value.canBeDisplayed(
+ MAX_SUPPORTED_MAJOR_VERSION,
+ MAX_SUPPORTED_MINOR_VERSION, 0L
+ )
+ ) {
+ mInner.setDocument(value);
+ int contentBehavior = value.getDocument().getContentScroll();
+ applyContentBehavior(contentBehavior);
+ } else {
+ Log.e("RemoteComposePlayer", "Unsupported document ");
+ }
+ } else {
+ mInner.setDocument(null);
+ }
+ }
+
+ /**
+ * Apply the content behavior (NONE|SCROLL_HORIZONTAL|SCROLL_VERTICAL) to the player,
+ * adding or removing scrollviews as needed.
+ *
+ * @param contentBehavior document content behavior (NONE|SCROLL_HORIZONTAL|SCROLL_VERTICAL)
+ */
+ private void applyContentBehavior(int contentBehavior) {
+ switch (contentBehavior) {
+ case RootContentBehavior.SCROLL_HORIZONTAL: {
+ if (!(mInner.getParent() instanceof HorizontalScrollView)) {
+ ((ViewGroup) mInner.getParent()).removeView(mInner);
+ removeAllViews();
+ LayoutParams layoutParamsInner = new LayoutParams(
+ LayoutParams.WRAP_CONTENT,
+ LayoutParams.MATCH_PARENT);
+ HorizontalScrollView horizontalScrollView =
+ new HorizontalScrollView(getContext());
+ horizontalScrollView.setFillViewport(true);
+ horizontalScrollView.addView(mInner, layoutParamsInner);
+ LayoutParams layoutParams = new LayoutParams(
+ LayoutParams.MATCH_PARENT,
+ LayoutParams.MATCH_PARENT);
+ addView(horizontalScrollView, layoutParams);
+ }
+ } break;
+ case RootContentBehavior.SCROLL_VERTICAL: {
+ if (!(mInner.getParent() instanceof ScrollView)) {
+ ((ViewGroup) mInner.getParent()).removeView(mInner);
+ removeAllViews();
+ LayoutParams layoutParamsInner = new LayoutParams(
+ LayoutParams.MATCH_PARENT,
+ LayoutParams.WRAP_CONTENT);
+ ScrollView scrollView = new ScrollView(getContext());
+ scrollView.setFillViewport(true);
+ scrollView.addView(mInner, layoutParamsInner);
+ LayoutParams layoutParams = new LayoutParams(
+ LayoutParams.MATCH_PARENT,
+ LayoutParams.MATCH_PARENT);
+ addView(scrollView, layoutParams);
+ }
+ } break;
+ default:
+ if (mInner.getParent() != this) {
+ ((ViewGroup) mInner.getParent()).removeView(mInner);
+ removeAllViews();
+ LayoutParams layoutParams = new LayoutParams(
+ LayoutParams.MATCH_PARENT,
+ LayoutParams.MATCH_PARENT);
+ addView(mInner, layoutParams);
+ }
+ }
+ }
+
+ private void init(Context context, AttributeSet attrs, int defStyleAttr) {
+ LayoutParams layoutParams = new LayoutParams(LayoutParams.MATCH_PARENT,
+ LayoutParams.MATCH_PARENT);
+ mInner = new RemoteComposeCanvas(context, attrs, defStyleAttr);
+ addView(mInner, layoutParams);
+ }
+
+ public interface ClickCallbacks {
+ void click(int id, String metadata);
+ }
+
+ /**
+ * Add a callback for handling click events on the document
+ *
+ * @param callback the callback lambda that will be used when a click is detected
+ * <p>
+ * The parameter of the callback are:
+ * id : the id of the clicked area
+ * metadata: a client provided unstructured string associated with that area
+ */
+ public void addClickListener(ClickCallbacks callback) {
+ mInner.addClickListener((id, metadata) -> callback.click(id, metadata));
+ }
+
+ /**
+ * Set the playback theme for the document. This allows to filter operations in order
+ * to have the document adapt to the given theme. This method is intended to be used
+ * to support night/light themes (system or app level), not custom themes.
+ *
+ * @param theme the theme used for playing the document. Possible values for theme are:
+ * - Theme.UNSPECIFIED -- all instructions in the document will be executed
+ * - Theme.DARK -- only executed NON Light theme instructions
+ * - Theme.LIGHT -- only executed NON Dark theme instructions
+ */
+ public void setTheme(int theme) {
+ if (mInner.getTheme() != theme) {
+ mInner.setTheme(theme);
+ mInner.invalidate();
+ }
+ }
+}
+
diff --git a/core/java/com/android/internal/widget/remotecompose/player/platform/AndroidPaintContext.java b/core/java/com/android/internal/widget/remotecompose/player/platform/AndroidPaintContext.java
new file mode 100644
index 0000000..3799cf6
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/player/platform/AndroidPaintContext.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) 2023 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.widget.remotecompose.player.platform;
+
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Rect;
+
+import com.android.internal.widget.remotecompose.core.PaintContext;
+import com.android.internal.widget.remotecompose.core.RemoteContext;
+
+/**
+ * An implementation of PaintContext for the Android Canvas.
+ * This is used to play the RemoteCompose operations on Android.
+ */
+public class AndroidPaintContext extends PaintContext {
+ Paint mPaint = new Paint();
+ Canvas mCanvas;
+
+ public AndroidPaintContext(RemoteContext context, Canvas canvas) {
+ super(context);
+ this.mCanvas = canvas;
+ }
+
+ public Canvas getCanvas() {
+ return mCanvas;
+ }
+
+ public void setCanvas(Canvas canvas) {
+ this.mCanvas = canvas;
+ }
+
+ /**
+ * Draw an image onto the canvas
+ *
+ * @param imageId the id of the image
+ * @param srcLeft left coordinate of the source area
+ * @param srcTop top coordinate of the source area
+ * @param srcRight right coordinate of the source area
+ * @param srcBottom bottom coordinate of the source area
+ * @param dstLeft left coordinate of the destination area
+ * @param dstTop top coordinate of the destination area
+ * @param dstRight right coordinate of the destination area
+ * @param dstBottom bottom coordinate of the destination area
+ */
+
+ @Override
+ public void drawBitmap(int imageId,
+ int srcLeft,
+ int srcTop,
+ int srcRight,
+ int srcBottom,
+ int dstLeft,
+ int dstTop,
+ int dstRight,
+ int dstBottom,
+ int cdId) {
+ AndroidRemoteContext androidContext = (AndroidRemoteContext) mContext;
+ if (androidContext.mRemoteComposeState.containsId(imageId)) {
+ Bitmap bitmap = (Bitmap) androidContext.mRemoteComposeState.getFromId(imageId);
+ mCanvas.drawBitmap(
+ bitmap,
+ new Rect(srcLeft, srcTop, srcRight, srcBottom),
+ new Rect(dstLeft, dstTop, dstRight, dstBottom), mPaint
+ );
+ }
+ }
+
+ @Override
+ public void scale(float scaleX, float scaleY) {
+ mCanvas.scale(scaleX, scaleY);
+ }
+
+ @Override
+ public void translate(float translateX, float translateY) {
+ mCanvas.translate(translateX, translateY);
+ }
+}
+
diff --git a/core/java/com/android/internal/widget/remotecompose/player/platform/AndroidRemoteContext.java b/core/java/com/android/internal/widget/remotecompose/player/platform/AndroidRemoteContext.java
new file mode 100644
index 0000000..ce15855
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/player/platform/AndroidRemoteContext.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 2023 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.widget.remotecompose.player.platform;
+
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Canvas;
+
+import com.android.internal.widget.remotecompose.core.RemoteContext;
+
+/**
+ * An implementation of Context for Android.
+ *
+ * This is used to play the RemoteCompose operations on Android.
+ */
+class AndroidRemoteContext extends RemoteContext {
+
+ public void useCanvas(Canvas canvas) {
+ if (mPaintContext == null) {
+ mPaintContext = new AndroidPaintContext(this, canvas);
+ } else {
+ // need to make sure to update the canvas for the current one
+ ((AndroidPaintContext) mPaintContext).setCanvas(canvas);
+ }
+ mWidth = canvas.getWidth();
+ mHeight = canvas.getHeight();
+ }
+
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+ // Data handling
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * Decode a byte array into an image and cache it using the given imageId
+ *
+ * @oaram imageId the id of the image
+ * @param width with of image to be loaded
+ * @param height height of image to be loaded
+ * @param bitmap a byte array containing the image information
+ */
+ @Override
+ public void loadBitmap(int imageId, int width, int height, byte[] bitmap) {
+ if (!mRemoteComposeState.containsId(imageId)) {
+ Bitmap image = BitmapFactory.decodeByteArray(bitmap, 0, bitmap.length);
+ mRemoteComposeState.cache(imageId, image);
+ }
+ }
+
+ @Override
+ public void loadText(int id, String text) {
+ if (!mRemoteComposeState.containsId(id)) {
+ mRemoteComposeState.cache(id, text);
+ }
+ }
+
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+ // Click handling
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+
+
+ @Override
+ public void addClickArea(int id,
+ int contentDescriptionId,
+ float left,
+ float top,
+ float right,
+ float bottom,
+ int metadataId) {
+ String contentDescription = (String) mRemoteComposeState.getFromId(contentDescriptionId);
+ String metadata = (String) mRemoteComposeState.getFromId(metadataId);
+ mDocument.addClickArea(id, contentDescription, left, top, right, bottom, metadata);
+ }
+}
+
diff --git a/core/java/com/android/internal/widget/remotecompose/player/platform/ClickAreaView.java b/core/java/com/android/internal/widget/remotecompose/player/platform/ClickAreaView.java
new file mode 100644
index 0000000..672dae3
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/player/platform/ClickAreaView.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2024 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.widget.remotecompose.player.platform;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.view.View;
+
+
+/**
+ * Implementation for the click handling
+ */
+class ClickAreaView extends View {
+ private int mId;
+ private String mMetadata;
+ Paint mPaint = new Paint();
+
+ private boolean mDebug;
+
+ ClickAreaView(Context context, boolean debug, int id,
+ String contentDescription, String metadata) {
+ super(context);
+ this.mId = id;
+ this.mMetadata = metadata;
+ this.mDebug = debug;
+ setContentDescription(contentDescription);
+ }
+
+
+ public void setDebug(boolean value) {
+ if (mDebug != value) {
+ mDebug = value;
+ invalidate();
+ }
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ super.onDraw(canvas);
+ if (mDebug) {
+ mPaint.setARGB(200, 200, 0, 0);
+ mPaint.setStrokeWidth(3f);
+ canvas.drawLine(0, 0, getWidth(), 0, mPaint);
+ canvas.drawLine(getWidth(), 0, getWidth(), getHeight(), mPaint);
+ canvas.drawLine(getWidth(), getHeight(), 0, getHeight(), mPaint);
+ canvas.drawLine(0, getHeight(), 0, 0, mPaint);
+
+ mPaint.setTextSize(20f);
+ canvas.drawText("id: " + mId + " : " + mMetadata, 4, 22, mPaint);
+ }
+ }
+}
diff --git a/core/java/com/android/internal/widget/remotecompose/player/platform/RemoteComposeCanvas.java b/core/java/com/android/internal/widget/remotecompose/player/platform/RemoteComposeCanvas.java
new file mode 100644
index 0000000..a3bb73e
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/player/platform/RemoteComposeCanvas.java
@@ -0,0 +1,230 @@
+/*
+ * Copyright (C) 2023 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.widget.remotecompose.player.platform;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Point;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.view.View;
+import android.widget.FrameLayout;
+
+import com.android.internal.widget.remotecompose.core.CoreDocument;
+import com.android.internal.widget.remotecompose.core.operations.RootContentBehavior;
+import com.android.internal.widget.remotecompose.core.operations.Theme;
+import com.android.internal.widget.remotecompose.player.RemoteComposeDocument;
+
+import java.util.Set;
+
+/**
+ * Internal view handling the actual painting / interactions
+ */
+public class RemoteComposeCanvas extends FrameLayout implements View.OnAttachStateChangeListener {
+
+ static final boolean USE_VIEW_AREA_CLICK = true; // Use views to represent click areas
+ RemoteComposeDocument mDocument = null;
+ int mTheme = Theme.LIGHT;
+ boolean mInActionDown = false;
+ boolean mDebug = false;
+ Point mActionDownPoint = new Point(0, 0);
+
+ public RemoteComposeCanvas(Context context) {
+ super(context);
+ if (USE_VIEW_AREA_CLICK) {
+ addOnAttachStateChangeListener(this);
+ }
+ }
+
+ public RemoteComposeCanvas(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ if (USE_VIEW_AREA_CLICK) {
+ addOnAttachStateChangeListener(this);
+ }
+ }
+
+ public RemoteComposeCanvas(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ setBackgroundColor(Color.WHITE);
+ if (USE_VIEW_AREA_CLICK) {
+ addOnAttachStateChangeListener(this);
+ }
+ }
+
+ public void setDebug(boolean value) {
+ if (mDebug != value) {
+ mDebug = value;
+ if (USE_VIEW_AREA_CLICK) {
+ for (int i = 0; i < getChildCount(); i++) {
+ View child = getChildAt(i);
+ if (child instanceof ClickAreaView) {
+ ((ClickAreaView) child).setDebug(mDebug);
+ }
+ }
+ }
+ invalidate();
+ }
+ }
+
+ public void setDocument(RemoteComposeDocument value) {
+ mDocument = value;
+ mDocument.initializeContext(mARContext);
+ setContentDescription(mDocument.getDocument().getContentDescription());
+ requestLayout();
+ }
+
+ AndroidRemoteContext mARContext = new AndroidRemoteContext();
+
+ @Override
+ public void onViewAttachedToWindow(View view) {
+ if (mDocument == null) {
+ return;
+ }
+ Set<CoreDocument.ClickAreaRepresentation> clickAreas = mDocument
+ .getDocument().getClickAreas();
+ removeAllViews();
+ for (CoreDocument.ClickAreaRepresentation area : clickAreas) {
+ ClickAreaView viewArea = new ClickAreaView(getContext(), mDebug,
+ area.getId(), area.getContentDescription(),
+ area.getMetadata());
+ int w = (int) area.width();
+ int h = (int) area.height();
+ FrameLayout.LayoutParams param = new FrameLayout.LayoutParams(w, h);
+ param.width = w;
+ param.height = h;
+ param.leftMargin = (int) area.getLeft();
+ param.topMargin = (int) area.getTop();
+ viewArea.setOnClickListener(view1
+ -> mDocument.getDocument().performClick(area.getId()));
+ addView(viewArea, param);
+ }
+ }
+
+ @Override
+ public void onViewDetachedFromWindow(View view) {
+ removeAllViews();
+ }
+
+
+ public interface ClickCallbacks {
+ void click(int id, String metadata);
+ }
+
+ public void addClickListener(ClickCallbacks callback) {
+ if (mDocument == null) {
+ return;
+ }
+ mDocument.getDocument().addClickListener((id, metadata) -> callback.click(id, metadata));
+ }
+
+ public int getTheme() {
+ return mTheme;
+ }
+
+ public void setTheme(int theme) {
+ this.mTheme = theme;
+ }
+
+ public boolean onTouchEvent(MotionEvent event) {
+ if (USE_VIEW_AREA_CLICK) {
+ return super.onTouchEvent(event);
+ }
+ switch (event.getActionMasked()) {
+ case MotionEvent.ACTION_DOWN: {
+ mActionDownPoint.x = (int) event.getX();
+ mActionDownPoint.y = (int) event.getY();
+ mInActionDown = true;
+ return true;
+ }
+ case MotionEvent.ACTION_CANCEL: {
+ mInActionDown = false;
+ return true;
+ }
+ case MotionEvent.ACTION_UP: {
+ mInActionDown = false;
+ performClick();
+ return true;
+ }
+ case MotionEvent.ACTION_MOVE: {
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public boolean performClick() {
+ if (USE_VIEW_AREA_CLICK) {
+ return super.performClick();
+ }
+ mDocument.getDocument().onClick((float) mActionDownPoint.x, (float) mActionDownPoint.y);
+ super.performClick();
+ return true;
+ }
+
+ public int measureDimension(int measureSpec, int intrinsicSize) {
+ int result = intrinsicSize;
+ int mode = MeasureSpec.getMode(measureSpec);
+ int size = MeasureSpec.getSize(measureSpec);
+ switch (mode) {
+ case MeasureSpec.EXACTLY:
+ result = size;
+ break;
+ case MeasureSpec.AT_MOST:
+ result = Integer.min(size, intrinsicSize);
+ break;
+ case MeasureSpec.UNSPECIFIED:
+ result = intrinsicSize;
+ }
+ return result;
+ }
+
+ private static final float[] sScaleOutput = new float[2];
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+ if (mDocument == null) {
+ return;
+ }
+ int w = measureDimension(widthMeasureSpec, mDocument.getWidth());
+ int h = measureDimension(heightMeasureSpec, mDocument.getHeight());
+
+ if (!USE_VIEW_AREA_CLICK) {
+ if (mDocument.getDocument().getContentSizing() == RootContentBehavior.SIZING_SCALE) {
+ mDocument.getDocument().computeScale(w, h, sScaleOutput);
+ w = (int) (mDocument.getWidth() * sScaleOutput[0]);
+ h = (int) (mDocument.getHeight() * sScaleOutput[1]);
+ }
+ }
+ setMeasuredDimension(w, h);
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ super.onDraw(canvas);
+ if (mDocument == null) {
+ return;
+ }
+ mARContext.setDebug(mDebug);
+ mARContext.useCanvas(canvas);
+ mARContext.mWidth = getWidth();
+ mARContext.mHeight = getHeight();
+ mDocument.paint(mARContext, mTheme);
+ }
+
+}
+
diff --git a/core/java/com/android/server/pm/pkg/AndroidPackage.java b/core/java/com/android/server/pm/pkg/AndroidPackage.java
index adb0c69..096f246 100644
--- a/core/java/com/android/server/pm/pkg/AndroidPackage.java
+++ b/core/java/com/android/server/pm/pkg/AndroidPackage.java
@@ -273,6 +273,13 @@
String getRestrictedAccountType();
/**
+ * @see R.styleable#AndroidManifestApplication_emergencyInstaller
+ * @hide
+ */
+ @Nullable
+ String getEmergencyInstaller();
+
+ /**
* @see ApplicationInfo#roundIconRes
* @see R.styleable#AndroidManifestApplication_roundIcon
*/
diff --git a/core/jni/android_hardware_input_InputWindowHandle.cpp b/core/jni/android_hardware_input_InputWindowHandle.cpp
index ae23942..bed7768 100644
--- a/core/jni/android_hardware_input_InputWindowHandle.cpp
+++ b/core/jni/android_hardware_input_InputWindowHandle.cpp
@@ -75,6 +75,7 @@
jfieldID windowToken;
jfieldID focusTransferTarget;
jfieldID alpha;
+ jfieldID canOccludePresentation;
} gInputWindowHandleClassInfo;
static struct {
@@ -327,6 +328,8 @@
javaObjectForIBinder(env, windowInfo.windowToken));
env->SetFloatField(inputWindowHandle, gInputWindowHandleClassInfo.alpha, windowInfo.alpha);
+ env->SetBooleanField(inputWindowHandle, gInputWindowHandleClassInfo.canOccludePresentation,
+ windowInfo.canOccludePresentation);
return inputWindowHandle;
}
@@ -451,6 +454,9 @@
GET_FIELD_ID(gInputWindowHandleClassInfo.alpha, clazz, "alpha", "F");
+ GET_FIELD_ID(gInputWindowHandleClassInfo.canOccludePresentation, clazz,
+ "canOccludePresentation", "Z");
+
jclass surfaceControlClazz;
FIND_CLASS(surfaceControlClazz, "android/view/SurfaceControl");
GET_FIELD_ID(gInputWindowHandleClassInfo.touchableRegionSurfaceControl.mNativeObject,
diff --git a/core/jni/android_opengl_EGL14.cpp b/core/jni/android_opengl_EGL14.cpp
index 2f29cae..917d283 100644
--- a/core/jni/android_opengl_EGL14.cpp
+++ b/core/jni/android_opengl_EGL14.cpp
@@ -17,6 +17,7 @@
// This source file is automatically generated
#pragma GCC diagnostic ignored "-Wunused-variable"
+#pragma GCC diagnostic ignored "-Wunused-but-set-variable"
#pragma GCC diagnostic ignored "-Wunused-function"
#include "jni.h"
diff --git a/core/jni/android_opengl_EGL15.cpp b/core/jni/android_opengl_EGL15.cpp
index b9c36b9..447b8ec 100644
--- a/core/jni/android_opengl_EGL15.cpp
+++ b/core/jni/android_opengl_EGL15.cpp
@@ -17,6 +17,7 @@
// This source file is automatically generated
#pragma GCC diagnostic ignored "-Wunused-variable"
+#pragma GCC diagnostic ignored "-Wunused-but-set-variable"
#pragma GCC diagnostic ignored "-Wunused-function"
#include "jni.h"
diff --git a/core/jni/android_opengl_EGLExt.cpp b/core/jni/android_opengl_EGLExt.cpp
index cdc9852..ffd75ea 100644
--- a/core/jni/android_opengl_EGLExt.cpp
+++ b/core/jni/android_opengl_EGLExt.cpp
@@ -17,6 +17,7 @@
// This source file is automatically generated
#pragma GCC diagnostic ignored "-Wunused-variable"
+#pragma GCC diagnostic ignored "-Wunused-but-set-variable"
#pragma GCC diagnostic ignored "-Wunused-function"
#include "jni.h"
@@ -54,11 +55,11 @@
jclass eglsurfaceClassLocal = _env->FindClass("android/opengl/EGLSurface");
eglsurfaceClass = (jclass) _env->NewGlobalRef(eglsurfaceClassLocal);
jclass eglsyncClassLocal = _env->FindClass("android/opengl/EGLSync");
- eglsyncClass = (jclass)_env->NewGlobalRef(eglsyncClassLocal);
+ eglsyncClass = (jclass) _env->NewGlobalRef(eglsyncClassLocal);
egldisplayGetHandleID = _env->GetMethodID(egldisplayClass, "getNativeHandle", "()J");
eglsurfaceGetHandleID = _env->GetMethodID(eglsurfaceClass, "getNativeHandle", "()J");
- eglsyncGetHandleID = _env->GetMethodID(eglsyncClassLocal, "getNativeHandle", "()J");
+ eglsyncGetHandleID = _env->GetMethodID(eglsyncClass, "getNativeHandle", "()J");
}
static void *
@@ -72,6 +73,14 @@
return reinterpret_cast<void*>(_env->CallLongMethod(obj, mid));
}
+// TODO: this should be generated from the .spec file, but needs to be renamed and made private
+static jint android_eglDupNativeFenceFDANDROID(JNIEnv *env, jobject, jobject dpy, jobject sync) {
+ EGLDisplay dpy_native = (EGLDisplay)fromEGLHandle(env, egldisplayGetHandleID, dpy);
+ EGLSync sync_native = (EGLSync)fromEGLHandle(env, eglsyncGetHandleID, sync);
+
+ return eglDupNativeFenceFDANDROID(dpy_native, sync_native);
+}
+
// --------------------------------------------------------------------------
/* EGLBoolean eglPresentationTimeANDROID ( EGLDisplay dpy, EGLSurface sur, EGLnsecsANDROID time ) */
static jboolean
@@ -89,21 +98,12 @@
return (jboolean)_returnValue;
}
-static jint android_eglDupNativeFenceFDANDROID(JNIEnv *env, jobject, jobject dpy, jobject sync) {
- EGLDisplay dpy_native = (EGLDisplay)fromEGLHandle(env, egldisplayGetHandleID, dpy);
- EGLSync sync_native = (EGLSync)fromEGLHandle(env, eglsyncGetHandleID, sync);
-
- return eglDupNativeFenceFDANDROID(dpy_native, sync_native);
-}
-
static const char *classPathName = "android/opengl/EGLExt";
static const JNINativeMethod methods[] = {
- {"_nativeClassInit", "()V", (void *)nativeClassInit},
- {"eglPresentationTimeANDROID", "(Landroid/opengl/EGLDisplay;Landroid/opengl/EGLSurface;J)Z",
- (void *)android_eglPresentationTimeANDROID},
- {"eglDupNativeFenceFDANDROIDImpl", "(Landroid/opengl/EGLDisplay;Landroid/opengl/EGLSync;)I",
- (void *)android_eglDupNativeFenceFDANDROID},
+{"_nativeClassInit", "()V", (void*)nativeClassInit },
+{"eglPresentationTimeANDROID", "(Landroid/opengl/EGLDisplay;Landroid/opengl/EGLSurface;J)Z", (void *) android_eglPresentationTimeANDROID },
+{"eglDupNativeFenceFDANDROIDImpl", "(Landroid/opengl/EGLDisplay;Landroid/opengl/EGLSync;)I", (void *)android_eglDupNativeFenceFDANDROID },
};
int register_android_opengl_jni_EGLExt(JNIEnv *_env)
diff --git a/core/jni/android_opengl_GLES10.cpp b/core/jni/android_opengl_GLES10.cpp
index d65b498..2d921ad 100644
--- a/core/jni/android_opengl_GLES10.cpp
+++ b/core/jni/android_opengl_GLES10.cpp
@@ -18,6 +18,7 @@
// This source file is automatically generated
#pragma GCC diagnostic ignored "-Wunused-variable"
+#pragma GCC diagnostic ignored "-Wunused-but-set-variable"
#pragma GCC diagnostic ignored "-Wunused-function"
#include <GLES/gl.h>
diff --git a/core/jni/android_opengl_GLES10Ext.cpp b/core/jni/android_opengl_GLES10Ext.cpp
index 3638b87..35a9a68 100644
--- a/core/jni/android_opengl_GLES10Ext.cpp
+++ b/core/jni/android_opengl_GLES10Ext.cpp
@@ -18,6 +18,7 @@
// This source file is automatically generated
#pragma GCC diagnostic ignored "-Wunused-variable"
+#pragma GCC diagnostic ignored "-Wunused-but-set-variable"
#pragma GCC diagnostic ignored "-Wunused-function"
#include <GLES/gl.h>
diff --git a/core/jni/android_opengl_GLES11.cpp b/core/jni/android_opengl_GLES11.cpp
index 9724e6c..e04b56e 100644
--- a/core/jni/android_opengl_GLES11.cpp
+++ b/core/jni/android_opengl_GLES11.cpp
@@ -18,6 +18,7 @@
// This source file is automatically generated
#pragma GCC diagnostic ignored "-Wunused-variable"
+#pragma GCC diagnostic ignored "-Wunused-but-set-variable"
#pragma GCC diagnostic ignored "-Wunused-function"
#include <GLES/gl.h>
diff --git a/core/jni/android_opengl_GLES11Ext.cpp b/core/jni/android_opengl_GLES11Ext.cpp
index 1ffa4ec..bccbda6 100644
--- a/core/jni/android_opengl_GLES11Ext.cpp
+++ b/core/jni/android_opengl_GLES11Ext.cpp
@@ -18,6 +18,7 @@
// This source file is automatically generated
#pragma GCC diagnostic ignored "-Wunused-variable"
+#pragma GCC diagnostic ignored "-Wunused-but-set-variable"
#pragma GCC diagnostic ignored "-Wunused-function"
#include <GLES/gl.h>
diff --git a/core/jni/android_opengl_GLES20.cpp b/core/jni/android_opengl_GLES20.cpp
index d832558..165262e 100644
--- a/core/jni/android_opengl_GLES20.cpp
+++ b/core/jni/android_opengl_GLES20.cpp
@@ -18,6 +18,7 @@
// This source file is automatically generated
#pragma GCC diagnostic ignored "-Wunused-variable"
+#pragma GCC diagnostic ignored "-Wunused-but-set-variable"
#pragma GCC diagnostic ignored "-Wunused-function"
#include <GLES2/gl2.h>
diff --git a/core/jni/android_opengl_GLES30.cpp b/core/jni/android_opengl_GLES30.cpp
index 719c6b3..d3fe439 100644
--- a/core/jni/android_opengl_GLES30.cpp
+++ b/core/jni/android_opengl_GLES30.cpp
@@ -18,6 +18,7 @@
// This source file is automatically generated
#pragma GCC diagnostic ignored "-Wunused-variable"
+#pragma GCC diagnostic ignored "-Wunused-but-set-variable"
#pragma GCC diagnostic ignored "-Wunused-function"
#include <GLES3/gl3.h>
diff --git a/core/jni/android_opengl_GLES31.cpp b/core/jni/android_opengl_GLES31.cpp
index afe7c63..b123f9d 100644
--- a/core/jni/android_opengl_GLES31.cpp
+++ b/core/jni/android_opengl_GLES31.cpp
@@ -17,6 +17,7 @@
// This source file is automatically generated
#pragma GCC diagnostic ignored "-Wunused-variable"
+#pragma GCC diagnostic ignored "-Wunused-but-set-variable"
#pragma GCC diagnostic ignored "-Wunused-function"
#include <stdint.h>
diff --git a/core/jni/android_opengl_GLES31Ext.cpp b/core/jni/android_opengl_GLES31Ext.cpp
index 8127433..1e4049b 100644
--- a/core/jni/android_opengl_GLES31Ext.cpp
+++ b/core/jni/android_opengl_GLES31Ext.cpp
@@ -17,6 +17,7 @@
// This source file is automatically generated
#pragma GCC diagnostic ignored "-Wunused-variable"
+#pragma GCC diagnostic ignored "-Wunused-but-set-variable"
#pragma GCC diagnostic ignored "-Wunused-function"
#include <GLES3/gl31.h>
diff --git a/core/jni/android_opengl_GLES32.cpp b/core/jni/android_opengl_GLES32.cpp
index 7ed7548..e0175f0 100644
--- a/core/jni/android_opengl_GLES32.cpp
+++ b/core/jni/android_opengl_GLES32.cpp
@@ -17,6 +17,7 @@
// This source file is automatically generated
#pragma GCC diagnostic ignored "-Wunused-variable"
+#pragma GCC diagnostic ignored "-Wunused-but-set-variable"
#pragma GCC diagnostic ignored "-Wunused-function"
#include <stdint.h>
diff --git a/core/jni/android_view_InputDevice.cpp b/core/jni/android_view_InputDevice.cpp
index 239c626..aae0da9 100644
--- a/core/jni/android_view_InputDevice.cpp
+++ b/core/jni/android_view_InputDevice.cpp
@@ -14,17 +14,16 @@
* limitations under the License.
*/
-#include <input/Input.h>
+#include "android_view_InputDevice.h"
#include <android_runtime/AndroidRuntime.h>
+#include <com_android_input_flags.h>
+#include <input/Input.h>
#include <jni.h>
#include <nativehelper/JNIHelp.h>
-
#include <nativehelper/ScopedLocalRef.h>
-#include "android_view_InputDevice.h"
#include "android_view_KeyCharacterMap.h"
-
#include "core_jni_helpers.h"
namespace android {
@@ -34,6 +33,7 @@
jmethodID ctor;
jmethodID addMotionRange;
+ jmethodID setShouldSmoothScroll;
} gInputDeviceClassInfo;
jobject android_view_InputDevice_create(JNIEnv* env, const InputDeviceInfo& deviceInfo) {
@@ -103,6 +103,18 @@
}
}
+ if (com::android::input::flags::input_device_view_behavior_api()) {
+ const InputDeviceViewBehavior& viewBehavior = deviceInfo.getViewBehavior();
+ std::optional<bool> defaultSmoothScroll = viewBehavior.shouldSmoothScroll;
+ if (defaultSmoothScroll.has_value()) {
+ env->CallVoidMethod(inputDeviceObj.get(), gInputDeviceClassInfo.setShouldSmoothScroll,
+ *defaultSmoothScroll);
+ if (env->ExceptionCheck()) {
+ return NULL;
+ }
+ }
+ }
+
return env->NewLocalRef(inputDeviceObj.get());
}
@@ -118,6 +130,8 @@
gInputDeviceClassInfo.addMotionRange =
GetMethodIDOrDie(env, gInputDeviceClassInfo.clazz, "addMotionRange", "(IIFFFFF)V");
+ gInputDeviceClassInfo.setShouldSmoothScroll =
+ GetMethodIDOrDie(env, gInputDeviceClassInfo.clazz, "setShouldSmoothScroll", "(Z)V");
return 0;
}
diff --git a/core/jni/android_view_InputEventSender.cpp b/core/jni/android_view_InputEventSender.cpp
index 323f7b6..b5fbb22 100644
--- a/core/jni/android_view_InputEventSender.cpp
+++ b/core/jni/android_view_InputEventSender.cpp
@@ -73,7 +73,7 @@
uint32_t mNextPublishedSeq;
const std::string getInputChannelName() {
- return mInputPublisher.getChannel()->getName();
+ return mInputPublisher.getChannel().getName();
}
int handleEvent(int receiveFd, int events, void* data) override;
@@ -102,7 +102,7 @@
}
status_t NativeInputEventSender::initialize() {
- const int receiveFd = mInputPublisher.getChannel()->getFd();
+ const int receiveFd = mInputPublisher.getChannel().getFd();
mMessageQueue->getLooper()->addFd(receiveFd, 0, ALOOPER_EVENT_INPUT, this, NULL);
return OK;
}
@@ -112,7 +112,7 @@
LOG(DEBUG) << "channel '" << getInputChannelName() << "' ~ Disposing input event sender.";
}
- mMessageQueue->getLooper()->removeFd(mInputPublisher.getChannel()->getFd());
+ mMessageQueue->getLooper()->removeFd(mInputPublisher.getChannel().getFd());
}
status_t NativeInputEventSender::sendKeyEvent(uint32_t seq, const KeyEvent* event) {
diff --git a/core/jni/com_android_internal_os_Zygote.cpp b/core/jni/com_android_internal_os_Zygote.cpp
index 7e325a5..0938ce17 100644
--- a/core/jni/com_android_internal_os_Zygote.cpp
+++ b/core/jni/com_android_internal_os_Zygote.cpp
@@ -667,8 +667,9 @@
}
}
-static void DropCapabilitiesBoundingSet(fail_fn_t fail_fn) {
+static void DropCapabilitiesBoundingSet(fail_fn_t fail_fn, jlong bounding_capabilities) {
for (int i = 0; prctl(PR_CAPBSET_READ, i, 0, 0, 0) >= 0; i++) {;
+ if ((1LL << i) & bounding_capabilities) continue;
if (prctl(PR_CAPBSET_DROP, i, 0, 0, 0) == -1) {
if (errno == EINVAL) {
ALOGE("prctl(PR_CAPBSET_DROP) failed with EINVAL. Please verify "
@@ -680,6 +681,27 @@
}
}
+static bool MatchGid(JNIEnv* env, jintArray gids, jint gid, jint gid_to_find) {
+ if (gid == gid_to_find) return true;
+
+ if (gids == nullptr) return false;
+
+ jsize gids_num = env->GetArrayLength(gids);
+ ScopedIntArrayRO native_gid_proxy(env, gids);
+
+ if (native_gid_proxy.get() == nullptr) {
+ RuntimeAbort(env, __LINE__, "Bad gids array");
+ }
+
+ for (int gids_index = 0; gids_index < gids_num; ++gids_index) {
+ if (native_gid_proxy[gids_index] == gid_to_find) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
static void SetInheritable(uint64_t inheritable, fail_fn_t fail_fn) {
__user_cap_header_struct capheader;
memset(&capheader, 0, sizeof(capheader));
@@ -1875,9 +1897,9 @@
// Utility routine to specialize a zygote child process.
static void SpecializeCommon(JNIEnv* env, uid_t uid, gid_t gid, jintArray gids, jint runtime_flags,
jobjectArray rlimits, jlong permitted_capabilities,
- jlong effective_capabilities, jint mount_external,
- jstring managed_se_info, jstring managed_nice_name,
- bool is_system_server, bool is_child_zygote,
+ jlong effective_capabilities, jlong bounding_capabilities,
+ jint mount_external, jstring managed_se_info,
+ jstring managed_nice_name, bool is_system_server, bool is_child_zygote,
jstring managed_instruction_set, jstring managed_app_data_dir,
bool is_top_app, jobjectArray pkg_data_info_list,
jobjectArray allowlisted_data_info_list, bool mount_data_dirs,
@@ -1891,6 +1913,9 @@
auto instruction_set = extract_fn(managed_instruction_set);
auto app_data_dir = extract_fn(managed_app_data_dir);
+ // Permit bounding capabilities
+ permitted_capabilities |= bounding_capabilities;
+
// Keep capabilities across UID change, unless we're staying root.
if (uid != 0) {
EnableKeepCapabilities(fail_fn);
@@ -1898,7 +1923,7 @@
SetInheritable(permitted_capabilities, fail_fn);
- DropCapabilitiesBoundingSet(fail_fn);
+ DropCapabilitiesBoundingSet(fail_fn, bounding_capabilities);
bool need_pre_initialize_native_bridge = !is_system_server && instruction_set.has_value() &&
android::NativeBridgeAvailable() &&
@@ -2165,6 +2190,23 @@
return capdata[0].effective | (static_cast<uint64_t>(capdata[1].effective) << 32);
}
+static jlong CalculateBoundingCapabilities(JNIEnv* env, jint uid, jint gid, jintArray gids) {
+ jlong capabilities = 0;
+
+ /*
+ * Grant CAP_SYS_NICE to CapInh/CapPrm/CapBnd for processes that can spawn
+ * VMs. This enables processes to execve on binaries with elevated
+ * capabilities if its file capability bits are set. This does not grant
+ * capability to the parent process(that spawns the VM) as the effective
+ * bits are not set.
+ */
+ if (MatchGid(env, gids, gid, AID_VIRTUALMACHINE)) {
+ capabilities |= (1LL << CAP_SYS_NICE);
+ }
+
+ return capabilities;
+}
+
static jlong CalculateCapabilities(JNIEnv* env, jint uid, jint gid, jintArray gids,
bool is_child_zygote) {
jlong capabilities = 0;
@@ -2198,26 +2240,7 @@
* Grant CAP_BLOCK_SUSPEND to processes that belong to GID "wakelock"
*/
- bool gid_wakelock_found = false;
- if (gid == AID_WAKELOCK) {
- gid_wakelock_found = true;
- } else if (gids != nullptr) {
- jsize gids_num = env->GetArrayLength(gids);
- ScopedIntArrayRO native_gid_proxy(env, gids);
-
- if (native_gid_proxy.get() == nullptr) {
- RuntimeAbort(env, __LINE__, "Bad gids array");
- }
-
- for (int gids_index = 0; gids_index < gids_num; ++gids_index) {
- if (native_gid_proxy[gids_index] == AID_WAKELOCK) {
- gid_wakelock_found = true;
- break;
- }
- }
- }
-
- if (gid_wakelock_found) {
+ if (MatchGid(env, gids, gid, AID_WAKELOCK)) {
capabilities |= (1LL << CAP_BLOCK_SUSPEND);
}
@@ -2393,6 +2416,11 @@
const std::vector<int>& fds_to_ignore,
bool is_priority_fork,
bool purge) {
+ ATRACE_CALL();
+ if (is_priority_fork) {
+ setpriority(PRIO_PROCESS, 0, PROCESS_PRIORITY_MAX);
+ }
+
SetSignalHandlers();
// Curry a failure function.
@@ -2478,6 +2506,10 @@
// We blocked SIGCHLD prior to a fork, we unblock it here.
UnblockSignal(SIGCHLD, fail_fn);
+ if (is_priority_fork && pid != 0) {
+ setpriority(PRIO_PROCESS, 0, PROCESS_PRIORITY_DEFAULT);
+ }
+
return pid;
}
@@ -2494,6 +2526,7 @@
jobjectArray pkg_data_info_list, jobjectArray allowlisted_data_info_list,
jboolean mount_data_dirs, jboolean mount_storage_dirs, jboolean mount_sysprop_overrides) {
jlong capabilities = CalculateCapabilities(env, uid, gid, gids, is_child_zygote);
+ jlong bounding_capabilities = CalculateBoundingCapabilities(env, uid, gid, gids);
if (UNLIKELY(managed_fds_to_close == nullptr)) {
zygote::ZygoteFailure(env, "zygote", nice_name,
@@ -2532,10 +2565,11 @@
if (pid == 0) {
SpecializeCommon(env, uid, gid, gids, runtime_flags, rlimits, capabilities, capabilities,
- mount_external, se_info, nice_name, false, is_child_zygote == JNI_TRUE,
- instruction_set, app_data_dir, is_top_app == JNI_TRUE, pkg_data_info_list,
- allowlisted_data_info_list, mount_data_dirs == JNI_TRUE,
- mount_storage_dirs == JNI_TRUE, mount_sysprop_overrides == JNI_TRUE);
+ bounding_capabilities, mount_external, se_info, nice_name, false,
+ is_child_zygote == JNI_TRUE, instruction_set, app_data_dir,
+ is_top_app == JNI_TRUE, pkg_data_info_list, allowlisted_data_info_list,
+ mount_data_dirs == JNI_TRUE, mount_storage_dirs == JNI_TRUE,
+ mount_sysprop_overrides == JNI_TRUE);
}
return pid;
}
@@ -2545,6 +2579,7 @@
JNIEnv* env, jclass, uid_t uid, gid_t gid, jintArray gids,
jint runtime_flags, jobjectArray rlimits, jlong permitted_capabilities,
jlong effective_capabilities) {
+ ATRACE_CALL();
std::vector<int> fds_to_close(MakeUsapPipeReadFDVector()),
fds_to_ignore(fds_to_close);
@@ -2568,7 +2603,7 @@
// System server prcoess does not need data isolation so no need to
// know pkg_data_info_list.
SpecializeCommon(env, uid, gid, gids, runtime_flags, rlimits, permitted_capabilities,
- effective_capabilities, MOUNT_EXTERNAL_DEFAULT, nullptr, nullptr, true,
+ effective_capabilities, 0, MOUNT_EXTERNAL_DEFAULT, nullptr, nullptr, true,
false, nullptr, nullptr, /* is_top_app= */ false,
/* pkg_data_info_list */ nullptr,
/* allowlisted_data_info_list */ nullptr, false, false, false);
@@ -2620,6 +2655,7 @@
jintArray managed_session_socket_fds,
jboolean args_known,
jboolean is_priority_fork) {
+ ATRACE_CALL();
std::vector<int> session_socket_fds =
ExtractJIntArray(env, "USAP", nullptr, managed_session_socket_fds)
.value_or(std::vector<int>());
@@ -2635,6 +2671,7 @@
bool args_known,
bool is_priority_fork,
bool purge) {
+ ATRACE_CALL();
std::vector<int> fds_to_close(MakeUsapPipeReadFDVector()),
fds_to_ignore(fds_to_close);
@@ -2725,12 +2762,14 @@
jobjectArray allowlisted_data_info_list, jboolean mount_data_dirs,
jboolean mount_storage_dirs, jboolean mount_sysprop_overrides) {
jlong capabilities = CalculateCapabilities(env, uid, gid, gids, is_child_zygote);
+ jlong bounding_capabilities = CalculateBoundingCapabilities(env, uid, gid, gids);
SpecializeCommon(env, uid, gid, gids, runtime_flags, rlimits, capabilities, capabilities,
- mount_external, se_info, nice_name, false, is_child_zygote == JNI_TRUE,
- instruction_set, app_data_dir, is_top_app == JNI_TRUE, pkg_data_info_list,
- allowlisted_data_info_list, mount_data_dirs == JNI_TRUE,
- mount_storage_dirs == JNI_TRUE, mount_sysprop_overrides == JNI_TRUE);
+ bounding_capabilities, mount_external, se_info, nice_name, false,
+ is_child_zygote == JNI_TRUE, instruction_set, app_data_dir,
+ is_top_app == JNI_TRUE, pkg_data_info_list, allowlisted_data_info_list,
+ mount_data_dirs == JNI_TRUE, mount_storage_dirs == JNI_TRUE,
+ mount_sysprop_overrides == JNI_TRUE);
}
/**
diff --git a/core/jni/com_google_android_gles_jni_GLImpl.cpp b/core/jni/com_google_android_gles_jni_GLImpl.cpp
index 21de723..ef29c88 100644
--- a/core/jni/com_google_android_gles_jni_GLImpl.cpp
+++ b/core/jni/com_google_android_gles_jni_GLImpl.cpp
@@ -18,6 +18,7 @@
// This source file is automatically generated
#pragma GCC diagnostic ignored "-Wunused-variable"
+#pragma GCC diagnostic ignored "-Wunused-but-set-variable"
#pragma GCC diagnostic ignored "-Wunused-function"
#include "jni.h"
diff --git a/core/proto/OWNERS b/core/proto/OWNERS
index 381580b..c65794e 100644
--- a/core/proto/OWNERS
+++ b/core/proto/OWNERS
@@ -19,6 +19,7 @@
per-file android/hardware/sensorprivacy.proto = ntmyren@google.com,evanseverson@google.com
per-file background_install_control.proto = wenhaowang@google.com,georgechan@google.com,billylau@google.com
per-file android/content/intent.proto = file:/PACKAGE_MANAGER_OWNERS
+per-file android/hardware/location/context_hub_info.proto = file:/services/core/java/com/android/server/location/contexthub/OWNERS
# Biometrics
jaggies@google.com
diff --git a/core/proto/android/providers/settings/global.proto b/core/proto/android/providers/settings/global.proto
index d3f3af7..2861858 100644
--- a/core/proto/android/providers/settings/global.proto
+++ b/core/proto/android/providers/settings/global.proto
@@ -723,6 +723,7 @@
// encoded as a key=value list separated by commas.
optional SettingProto smart_suggestions_in_notifications_flags = 5 [ (android.privacy).dest = DEST_AUTOMATIC ];
optional SettingProto bubbles = 6 [ (android.privacy).dest = DEST_AUTOMATIC ];
+ optional SettingProto disable_screen_share_protections_for_apps_and_notifications = 7 [ (android.privacy).dest = DEST_AUTOMATIC ];
}
optional Notification notification = 82;
diff --git a/core/proto/android/providers/settings/secure.proto b/core/proto/android/providers/settings/secure.proto
index 104c023..10f75d0 100644
--- a/core/proto/android/providers/settings/secure.proto
+++ b/core/proto/android/providers/settings/secure.proto
@@ -612,6 +612,7 @@
}
optional Sounds sounds = 72;
+ optional SettingProto stylus_pointer_icon_enabled = 99 [ (android.privacy).dest = DEST_AUTOMATIC ];
optional SettingProto swipe_bottom_to_notification_enabled = 82 [ (android.privacy).dest = DEST_AUTOMATIC ];
// Defines whether managed profile ringtones should be synced from its
// parent profile.
@@ -720,5 +721,5 @@
// Please insert fields in alphabetical order and group them into messages
// if possible (to avoid reaching the method limit).
- // Next tag = 99;
+ // Next tag = 100;
}
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index 21d2bf2..374c312 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -2645,6 +2645,13 @@
android:description="@string/permdesc_detectScreenCapture"
android:protectionLevel="normal" />
+ <!-- Allows an application to get notified when it is being recorded.
+ <p>Protection level: normal
+ @FlaggedApi("com.android.window.flags.screen_recording_callbacks")
+ -->
+ <permission android:name="android.permission.DETECT_SCREEN_RECORDING"
+ android:protectionLevel="normal" />
+
<!-- ======================================== -->
<!-- Permissions for factory reset protection -->
<!-- ======================================== -->
@@ -3209,6 +3216,13 @@
android:description="@string/permdesc_accessHiddenProfile"
android:protectionLevel="normal" />
+ <!-- @SystemApi @hide Allows privileged applications to get details about hidden profile
+ users.
+ @FlaggedApi("android.multiuser.flags.enable_permission_to_access_hidden_profiles") -->
+ <permission
+ android:name="android.permission.ACCESS_HIDDEN_PROFILES_FULL"
+ android:protectionLevel="signature|privileged" />
+
<!-- @SystemApi @hide Allows starting activities across profiles in the same profile group. -->
<permission android:name="android.permission.START_CROSS_PROFILE_ACTIVITIES"
android:protectionLevel="signature|role" />
@@ -5584,6 +5598,7 @@
of a session based install.
<p>Not for use by third-party applications.
@hide
+ @FlaggedApi("android.content.pm.get_resolved_apk_path")
-->
<permission android:name="android.permission.READ_INSTALLED_SESSION_PATHS"
android:protectionLevel="signature|installer" />
@@ -7547,6 +7562,11 @@
<permission android:name="android.permission.RESET_APP_ERRORS"
android:protectionLevel="signature" />
+ <!-- @hide Allows ThemeOverlayController to delay launch of Home / SetupWizard on boot, ensuring
+ Theme Palettes and Colors are ready -->
+ <permission android:name="android.permission.SET_THEME_OVERLAY_CONTROLLER_READY"
+ android:protectionLevel="signature|setup" />
+
<!-- @hide Allows an application to create/destroy input consumer. -->
<permission android:name="android.permission.INPUT_CONSUMER"
android:protectionLevel="signature" />
diff --git a/core/res/assets/geoid_height_map/README.md b/core/res/assets/geoid_height_map/README.md
deleted file mode 100644
index 849d32e..0000000
--- a/core/res/assets/geoid_height_map/README.md
+++ /dev/null
@@ -1,2 +0,0 @@
-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_map/README.md b/core/res/assets/geoid_map/README.md
new file mode 100644
index 0000000..5d480c1
--- /dev/null
+++ b/core/res/assets/geoid_map/README.md
@@ -0,0 +1,2 @@
+These binary protos are generated at runtime from the text protos in ../../geoid_map_assets and
+using aprotoc.
\ No newline at end of file
diff --git a/core/res/assets/geoid_map/expiration-distance-disk-tile-1.pb b/core/res/assets/geoid_map/expiration-distance-disk-tile-1.pb
new file mode 100644
index 0000000..c2ec5d9
--- /dev/null
+++ b/core/res/assets/geoid_map/expiration-distance-disk-tile-1.pb
Binary files differ
diff --git a/core/res/assets/geoid_map/expiration-distance-disk-tile-3.pb b/core/res/assets/geoid_map/expiration-distance-disk-tile-3.pb
new file mode 100644
index 0000000..e4b3b0c
--- /dev/null
+++ b/core/res/assets/geoid_map/expiration-distance-disk-tile-3.pb
Binary files differ
diff --git a/core/res/assets/geoid_map/expiration-distance-disk-tile-5.pb b/core/res/assets/geoid_map/expiration-distance-disk-tile-5.pb
new file mode 100644
index 0000000..95f54c2
--- /dev/null
+++ b/core/res/assets/geoid_map/expiration-distance-disk-tile-5.pb
Binary files differ
diff --git a/core/res/assets/geoid_map/expiration-distance-disk-tile-7.pb b/core/res/assets/geoid_map/expiration-distance-disk-tile-7.pb
new file mode 100644
index 0000000..896bbc1
--- /dev/null
+++ b/core/res/assets/geoid_map/expiration-distance-disk-tile-7.pb
Binary files differ
diff --git a/core/res/assets/geoid_map/expiration-distance-disk-tile-9.pb b/core/res/assets/geoid_map/expiration-distance-disk-tile-9.pb
new file mode 100644
index 0000000..ddc687d
--- /dev/null
+++ b/core/res/assets/geoid_map/expiration-distance-disk-tile-9.pb
Binary files differ
diff --git a/core/res/assets/geoid_map/expiration-distance-disk-tile-b.pb b/core/res/assets/geoid_map/expiration-distance-disk-tile-b.pb
new file mode 100644
index 0000000..eb405f6
--- /dev/null
+++ b/core/res/assets/geoid_map/expiration-distance-disk-tile-b.pb
Binary files differ
diff --git a/core/res/assets/geoid_map/expiration-distance-params.pb b/core/res/assets/geoid_map/expiration-distance-params.pb
new file mode 100644
index 0000000..e160120
--- /dev/null
+++ b/core/res/assets/geoid_map/expiration-distance-params.pb
Binary files differ
diff --git a/core/res/assets/geoid_height_map/tile-1.pb b/core/res/assets/geoid_map/geoid-height-disk-tile-1.pb
similarity index 100%
rename from core/res/assets/geoid_height_map/tile-1.pb
rename to core/res/assets/geoid_map/geoid-height-disk-tile-1.pb
Binary files differ
diff --git a/core/res/assets/geoid_height_map/tile-3.pb b/core/res/assets/geoid_map/geoid-height-disk-tile-3.pb
similarity index 100%
rename from core/res/assets/geoid_height_map/tile-3.pb
rename to core/res/assets/geoid_map/geoid-height-disk-tile-3.pb
Binary files differ
diff --git a/core/res/assets/geoid_height_map/tile-5.pb b/core/res/assets/geoid_map/geoid-height-disk-tile-5.pb
similarity index 100%
rename from core/res/assets/geoid_height_map/tile-5.pb
rename to core/res/assets/geoid_map/geoid-height-disk-tile-5.pb
Binary files differ
diff --git a/core/res/assets/geoid_height_map/tile-7.pb b/core/res/assets/geoid_map/geoid-height-disk-tile-7.pb
similarity index 100%
rename from core/res/assets/geoid_height_map/tile-7.pb
rename to core/res/assets/geoid_map/geoid-height-disk-tile-7.pb
Binary files differ
diff --git a/core/res/assets/geoid_height_map/tile-9.pb b/core/res/assets/geoid_map/geoid-height-disk-tile-9.pb
similarity index 100%
rename from core/res/assets/geoid_height_map/tile-9.pb
rename to core/res/assets/geoid_map/geoid-height-disk-tile-9.pb
Binary files differ
diff --git a/core/res/assets/geoid_height_map/tile-b.pb b/core/res/assets/geoid_map/geoid-height-disk-tile-b.pb
similarity index 100%
rename from core/res/assets/geoid_height_map/tile-b.pb
rename to core/res/assets/geoid_map/geoid-height-disk-tile-b.pb
Binary files differ
diff --git a/core/res/assets/geoid_height_map/map-params.pb b/core/res/assets/geoid_map/geoid-height-params.pb
similarity index 100%
rename from core/res/assets/geoid_height_map/map-params.pb
rename to core/res/assets/geoid_map/geoid-height-params.pb
Binary files differ
diff --git a/core/res/geoid_height_map_assets/README.md b/core/res/geoid_map_assets/README.md
similarity index 100%
rename from core/res/geoid_height_map_assets/README.md
rename to core/res/geoid_map_assets/README.md
diff --git a/core/res/geoid_map_assets/expiration-distance-disk-tile-1.textpb b/core/res/geoid_map_assets/expiration-distance-disk-tile-1.textpb
new file mode 100644
index 0000000..a4170e6
--- /dev/null
+++ b/core/res/geoid_map_assets/expiration-distance-disk-tile-1.textpb
@@ -0,0 +1,2 @@
+tile_key: "1"
+byte_png: "\211PNG\r\n\032\n\000\000\000\rIHDR\000\000\000@\000\000\000@\b\000\000\000\000\217\002.\002\000\000\v\361IDATx^\035WYo\034Ir\256\256\252\274\357\314:\262\316\276\233\315S$E\211\244\244\341H\262$\256V\243\3359W\322\314\216\027Z\f\026\006\326\360\313b\001\277\032~1\f\333\257\376\t\376\233\216\232\006\371\322\310\214\216\214\370\342\373\276H^~\367\037\377\365\343\325\361\320\307\252\352\207\276\237o\327]w~zr~\262\026Xe\271\t\224*\257\352\3233\253\257\317\317W\265\337\305e\325\354\272X9k\271L\256_\376\353\377~z\264\031\372\246\254\272E\333\265\355vX\274y\376\360\226s\225\347i\232et?r\226#\032\312\217\377p\273)|\027\027\355\3204}\033\242\310E\322\275\372\374\267g\347\273\305\242\201\200E\263\034\347\235v\217\317\037.\373\276\330\342\024\"\220\343\315|\254,\301t}\376\207C\347\\\025\302f\fq\331\264\002\311$^\235\277\177|u\260j\272\262\366E]\327\373m=\177y\276\"\234 \256\226.\307\354v=\216\273\001\347DJiL\251\255\032\206\255\026m\355\030\226\311\345\343\375\301uU\314\245d\314\225\363\272\033\026\266\352{.p\236\271j\377\305\273\273\273\367\317\317v\333M\236s\257\363\034\033]\354[c\004\215\215\"9M\212b\277\252\215\366%Ms\241\275[\217\205=\350\021\211\224z\234\243\347w\217\317\316\036\270\233\'\373\363\262\222\230\"\312\225\365\255\022\234\370\261T\210&\262\210ma\031!\2311U\260\306\327\205\320\273\262\334P\302Q\216\324\227\257ONN\355\356\365\333\037\357;iJ\2458\227\\\325N\223\320\365\216\242\204\037VJ2\244T\325\325c\335-J\277h\254\332=;\333s\212\020!\264\177\365\366\313\353G_\276\376\366FR*\030\306\224b\214KK\nhD\215\022RW\222Jf\v\353\252]U\257v\273\335v.\352\373o\276~F\363\034!\210\342N\256\037=\277\270Xs\316\021\245\324\300W\310\030B\353\266\245y\302\245\307H.\252J\n3T\325\260^\257\016O\255n?\377r9\245\200q\236\273\3475\272|x\244\324\004\251\002\242d\024\v\217\3110o\263<!\232\321~uRE@\201\017\313\305f\263Z\227\326\276\372\373\377\374\346\351r\036%\244\301\337\f\362\346\341R(\316\nk\332\016\nF\030\000t\230w9K\000\037\230-\240\227\'CQ\270\242k\206\361h\241\324\307\377\373\317\237\337|\361t_j\226\323\313\363w\357\026\255n\2056\206\005G1\200CJ\347\232\np \210a\f\253\027w\267\217\226M\b}\323l\326\313\260\375\360O\277|\377\273\267O\216N\217;D\212\352\315\317sc\234\320\256\f\212\301\313x\200\000\b\033B \000\315YF]\374\355\267\227\243\325\306\205aQ\371\360\325\307\367/\256\276\370\342v3\366\232\241\374\372\273^k\313\353UQ*\306\b\346^J\236cII\022\004C8KS\230\226\243\273y\f\316\373\022\336\362\325\217\367O\316\326C\327-\326\253R\220\366\266S\232[\331\350\252l\265\244\\HB\255\343B%\326\230\250R\n\030#\b\337n\327\313U\277,\030\343\337\274{\275\335U]\327\036,\312\202\242S\350\222\206\264\213X;\3079F\n\263Fc\302\222\312\2320\r\335\315\315WU\375\351d\273\202\000}d\354\247\177\377\343\315~5^\235mG\2559UU\201\275Q\316\311\340\205\224yn9a\022\347i\262\232j\230e\352\305\375\037\276\377\360\303\3610\214\355\342\240\256\207\357\376\355\277_\234_\337=~r\fE\200\251\320V;\003\237\322a\")\312\t\003@`F\222BC\000\024\353\243W\177\375\376\307\'\253\266\353\233\345B\252\323g\227\177\377\370\364\370\260\276zy\266\\\356e\216\204\016ee]\343\001\2370w\204\331\n0\226%\232\023\301\371\372\240\264\277\375\353\037\317\226]\273\030\227\203\224\2671\036\331\agg\333\353O\357.\317\317\216\f\343&l\233PuNA\301`\256\265s\204\247i\242\005Q\212\001\312\252\235x\362hy8\366m\003Q\236\235X\234\271p\377\315\317\037\177\370\352\351\305\221\221\210\273v4f\t\224D\020t\016\273\300h\n\031(\350N\016\300\242\356\360\351\243\343]?\f\233\332\356\277y\342Lk\364\376\376\273\037~\372\363\207\017\227\000x\331\372B\312\256*\203\311Q\216I\027\020\274#\341\212k\231+\026\335\321\347_\316\317\017\026\243\2130K\217\237/\004\242\230l\337\376\360\347\337\377\356\356\346\264f\332\020m\255\365Ji\201S\344\"B\314W\211\022\006Per67\027\257n6c\315\3750D\021\027\341\031eK\202\312\373\237_==\336\f\212rBli\005gB(\234f\250@\244\036\253$X)\006S d\271zv\270\352-\005}\210\315\260\360\',\000\027\347\375O\257\237\356\227\205\207\031\240\2057BcLr`\373\214\bQ\214CR\303\273\204\26087kwz8\326\3327\274!j\275\334_\\?\210\221g\370\371\277\274\277\3314^B\000\243\225\344\224\300m\v\275\307\322\037\306\244,\225T\312\020\276\235o\340~\251t\031\243c\276\256\216\257_\275X\223<\257\257\276\275\177\030\f\a|s8\313\215\247\316\002\236\201\360\344|\225h\v\365Uy\206\351|\263\265]\3356M\354\326>\324E\r\2742\027@J2\336\003-0-!\202p\241A\310\353\236\003;2\336m\022ea<s\224ehy\260[\217UW5\303r\345\233\373\233\365`UW\032\t\334\265P@xAs\030(UD\240\264\302\243,\205b\2641a\224\021\224Swp\262Z\265Pc.\250\250\273z\371\351\353\177\376\374\217\237o\035\345p\016\006\330h#\245\226\302\215pW\351\036\000\r\331\371\022\002PH`:B!\213,\233\002`&n\277\376\345\303\357\377\364\351\315\233\301\v`q\216\271\001\366\347J\355;\21105!\317\250\226\210T\t\002zq\276m\v\a\332\006\001r\370c\f\b\357\376\371\335\207\277\275\177s$\303\031\312\004\234\221\202\251r\034Z#2n\f(\237\362\004\253\004M}\005\232b\224P_\272\240\200\236r\220\016\346\317\216\037\335\\\375\345\323\361\303\337\344)\343\032\002pS\017CiU\232\351<C9\267\001\243D\224\205\026\206K\355\005k+]\233)\006Dc\246l\226\'Ww\037\236\235\035\027\0316\023\004\204T^k]\344\251\230\332\306\214\205\000\276\357u,CQxyr~tq\321UUY(\023\255\244\312r\036\016\017j\306L\312\314\257\252\b\315\320a\250\2630=X\206\322\362d\021\035\020\245\v\222\210\376d\267]\305xryr\2727\030\224\025\230\a\2031\341\302f)e\034\304\031h\251\177\262h\025\241\360h\306}\341\023\005<Y\026\336{1?\326\240\032\321\025\017\256\326\f\223\246\206\252\023\002\322\206\244\225\030[\a\327\234/\264\367]e%\313\031(\302\302\'@T\021<\213Sj\354\r\364\220\270\366\270\202\206:)\232\346\242\001\n\205\237\016s\201\tUF\031\b`l\347z\r\336\201a\352}R\324\261\264zRmi\254\323\032$\234\242\f\340k\325\341\246\357\275o\036\200\376P2M\214\323!\024\306\354\306jY\021\202\322\224)\233\204\222\302\\\201\356V6\314\227\313\000\272\207\177\305\277sc\177pX\201_hAL\t\b\216\205Y\016U\335\306\020j\270\003\307\250\220@i\224 \230\275\252j\232!\004\353\n\016=\243f\330-\373.\356\367c\250|\\\fu\214!\026N\311\020\274\325\241 D\020`$\316 \300\204P@\272\203\361+\003\0302\231;\307t\327\035=]tm\035\373\"\366\273\323\355\252\033\326\261\356\242\322p\243\360\226L\211\303x&`B0\017\261\254\212\002\322h\332\345\240\362**\277\a\273V\v \301f\230\257\267\333\261,\227\307\203\265Q\262\322\201v\226\r$\316\255\222\tB\200\242\276.\n\253\224\367\263\031\352\025\251[\356\273q\230\357\025\030\002kO\327\213yl\332\341\374\240.\332\2713UU\270\261\342\031\210\023\322\t\341\215\373\265@Pd/\263Y\006\001\000tE7\276\275:X\037\201+\260\363\323\226\272P\327M,\233E\337\2128\200D\367,\233l\006\001\233W\200\244S\351\255\247U\226fxs\004-\221\252x\360\362\305\227 \254\240\030E\327\022k\353\246\261\266\005\301\232\017]]\373\025\320\020\206\366\'\n\222\a\315\005(r\3329\273Z_J*\240\030\253f\274\273*K\306J\003\364&Z0p0\250b\263:\214q\\T\332\026$+{\312\223\326;\037@\256\234\t\213\365|9\237_p*c\324u3\202?(\313\342`\tz\016\361\243\3078#\355a\023\233n\327T5\340\260\251\021N`<\240\002\360q\325f\034\227\3077\003\307Z\201g+\333\"n\273\262\b\001\b\325KW\031msjw24GkW\004\230\324&\315I\002\371\033\353\'s\324\235\354\346\353\265\303\036\340\021\240\253\315<\266uo\200\'\v\3707m\031F-MW\024m\333\307\326\001\364iJp\342\nm\001\3406T\363#\350\367\246A9 \3054\020\266o+\230\327((\210\231\\\215\253\312\205\t\207e_\307C\017_\211,c`\266\341\370\364\000\200FW\223f\025\201\243g\251lm\230Z\v\302Y\273\336\272ry\321\3250\036\301\273X\265 \333\225r\255\206\005\202\'\240k\022\350Jy\230\220\272\356\373\276Ig\263Y\306&\036\000/\006\303\\m\316\206\330\\,\232X\204\241/\344<\364\026s\006b\006\236\217\'\002\370\006\234\2323\316\227\343|9\324M\232\242,\237\364s\n\000\203\"T7V\256l\343\256\f][J_\0160\377 G,\002\235&R\201W\361Jh\355\2624\036\rCU\v\020#\006\002\nH\003s\016Dh\333\302\000Zb\333\004 K1\275-\a\006\200{Z\'\030\224\325Z L\256\263\324\271\266Wt\006\213\226I\'\225\311\375D\370\004\244E\256\226e\321F\340BHO\030\224\247\324.\n+mBA\260\004\237\350\306\000\221I\v\030\303<\313R\002{\005X\224T\t\006\313\037\202\031t\214)\224O\231\375\332\001\002\364\315\031O\200\235\341G\024\224Q\346`\031\246ta\337B\263\351 \254X \273\323\035\316 PJ\300\241\202\'\245B\030\223\206iR\201\017&\021\202{\234Y\0209PZx\266\244\031\232a\340c\f\244CA\375a\371,5\312f\222\344\320\241Y*`G@\\O\223j\022\006\v\"T\032\312\n\002\2073\304\b\2255\005\357\312*\247\240Z\006\030\276\346\024{\221\315R\232\315f\371,\231eS\247\241w\300N\t\345\034\004\036\274#\370\357<\317\230\201j\002e\202t\201;e\006\222\2438\202|\002\315\247\263\3515\f\315\222d\006/\341\240\367@\252\223\302Lp\221\300\274\210p\300%`\317Z\006\313\200\001#\bN\217\360\250\263\334\341i\a\313&\271\302\2239O\341q\23239\001\t~E\022\246\b\330\377XX\320/\251\'\242\304\320G:\351\axs\020\032\260e\be\b\002z\230\2114\005\356\206\276\210dB\233\202E\024\224O\214\233\276\204\212h\312 \327\251\\\031\355\300u\n\232M\276\016\201\'\310\t&\260n\321\276\201&C\3219\375\177\264\213Dt\003\247\373\213\000\000\000\000IEND\256B`\202"
diff --git a/core/res/geoid_map_assets/expiration-distance-disk-tile-3.textpb b/core/res/geoid_map_assets/expiration-distance-disk-tile-3.textpb
new file mode 100644
index 0000000..684f848
--- /dev/null
+++ b/core/res/geoid_map_assets/expiration-distance-disk-tile-3.textpb
@@ -0,0 +1,2 @@
+tile_key: "3"
+byte_png: "\211PNG\r\n\032\n\000\000\000\rIHDR\000\000\000@\000\000\000@\b\000\000\000\000\217\002.\002\000\000\nKIDATx^-\227\351r\334\326\222\204\321\000\316\276c\355\225-\262%Q\242e[\226=\023s\035w\376\315<\302\274\377\253\314W\224\021\022\311\350\006\352\324\222\231\225\350.9\2720\244\266?=\335?\377\366\353\257\277?\266\323\375\345\345\345~:\356\333\365r\332\267m\235\226u\331\266\375|\272<=\276\375\370\365\227\267\177\377\353\307\375\323\307O\331\253\261\253\245\346\320\353\232\333\343\345\333\357\337~\273\177\270\235\326\333\313\371t.q;\235\217+\327<M\353\266\357\307\343\361\345\323\217\377\370\376\371\351\327\307\365|\334\2753\312w\271\315%\253\301\304\370\366\343\317\317\237\277_\256\267\333:=_\326\375\030\333:\267y\221\213\237\004\270^_\036\277\377\361\\S\276\234\317k\265\326\030\327\265\251\225\354\265\n\351\374\367\377\374\365\365\355\371\3065\307\351\264\315\307\224f\276n\004X\313r<\236\316\257\363Zk\2529W;\216\243\266\326\271\316\247\\r\364F\353\351\371\374\372\361\345z\275=\335\357)\305\343\266\257\363\334\262\344\277\254\373v\332\256)Y\233\3628h5\216\303h\254\367\316w6\3449\325\350\3061\247\355r\273]>\\n\347\263\317>\255\253D(\265\266\266\256\324\337\234\3621\0305*m\346Z\366\205\032\274\357|))M\261\270\321\370\363v\272]\256\227\215\303\213w\333:\257$_\033u\320Nr\f\3319\247\207u\231\225M\353\322\2345\241\313\311\232\312\025\2656e\335./\267\023\331\226Vb\\\346u\233\032\001\326m\273*\345\252q\24193<\355\307\311\306\326\30282\005\357\\\\j\245\rvp\247\323\351z\341\374\2472\325\342B\235\246\245\265i\t6\270\336\224`\326x\016z\240\2711\347l\006\002\204\316j}<m\323\266\2478\230\371r9S\355\266\022r\362\332\245:\255-*:fM\324\346\357\377{\373B\367\227\331\247Z\3658\322\022\335\031m\246\345\366t\2716o\2142\361|</\265\3442M\245j\343<\335\035c=?\331I\333\277\277\177\340v?\371@~\306f;\364#\001t.\3556=\036{p\301\030\263\357\205\004\351\3754\027\346\024\322\3246\372\374\232\036\371\3655Z\347\223\347\344\032|`\230c\350\264V\246\224f\355~j5\246`|H\2450\274y\231C\320\336O\241A\027\377\343\217\377\345\303hcM\216(6\020\\\006\323)\257\3241\323\320v\234\353\266V\372S\tX\005\200\233\323\241l\347k>U\333\376s\032M\n\256\272\030Rt6\371`m\bd\340\364x\334\303\3410\026\206>M-\346\024K\256\251\021o\266`z\177\334\336\356\326\236\325h\346\350L.\211,\271\311\333\030c\356\254r\332\335\250\3460f_\'\322\3659\223\202\0241Mn\252\347\327\267\327\327\257\317$\352\335\f\032\rh4\201\216S\r\207u\203R\306<\335N\266\357\025\370\246i\021v\200\254R\251C\307v}\373\366\366\375\323\251\030\337\246\b\370\225v\316\222\211\3211s\\\247\264\002\t`\vTYg|\"\257\230J\232B\3661\372\330\226/\277\277\375\327\027\357\366\375>Qs\320Z[\320\347\034E\004\237\273\301\214F\257\273Y\\?\250@^!\270\030K\230\005\315\204\234S\234\257\217\353\2162\355\313\334\254Q\0262\305\034 \371\344\256\'u5\327\251&\376\360<\021\234\v1E\232\220\321\t\355\266h\323\224R\233\270%\025\v\270`\215\241H\222`\344\235\006M\343\232i\317\330k9\222\001\323\005`$\200Dr\246\225qE\310\237\224BB\024\263\244\212\362s\214y\351fm\225:\241]K\033\r\375%j\200) \241\026\370n\034\277\350:\337(\351\367;\001h\2406i\"\325\\\273\241\357\315\250S\323\220[Ec8\337\225w(\002\206\310\304KA\200\220O-B\002\365\2143}O\336\203\314,\227N\037z\37024\327\254\032\231\016\205\t\t\247\372N\031[L*^\217<\3073\374\327c\337\037\270h]?\360q\352\3740h;\f\v\002\251\031\022\303\f&\320\261i\231Y\026\261\0017\243-\223\367\326jy\362\237\377\\\350A\257\272\017M\353\3630\304q\360\016\322E\215\276\2326K\027\3624\307HO\351\031\245 \177]w u\216>\034h\346\330K\244\356\313\307\264\337\206^\217>4\3211\023\36480\002`\224\352\342b\0024\3223cx\\)Z\357#\220\355G\272\"\271t\257\327\371\374\351Hm*5&\237\265\347k\221s0M\001\376]Gy\\\221\262@#\346\"Z\006x\224\264c\354N\267\260\277\035\3350\000\240\234J#\003=\242\270)\204\352\337ik\2048#i\243Ss\211.* \'\225\310\245\273u9\316\237n\254\306\242ucz\336\347F\200\t\244\227@\302\321\312\006\223\261\225\005\211.M\311\337#\317\313$\206\241k\3554\177\374\364z\254\227\251\262\267\002\210k\352tZr\244\243<\034\344y\305\371\242\321%\006\320e\373\303 \r\355G\272\320M\323\275}\371\353-\3733hd\346\256 \330\363\207OW\000\031\377\311\300\312\201!\325\":\226\253S\222|w\240\023\374*\365>}\375\353kV\363\372\256y\001\021\320\323\343\363S\220\376y\331\177\326\364`\021\365\251\311z\220H\003\016\335\241cE\252\330wFT\366\257\257\237t-,\342\311;Zo\362/_\276\325\020x\026|y\326\302\240\223,a>1\022\200\342\031 \264h\266\023\364\253\360\362\366\262-\313\004l\310\322\350\374#\337\317\305\000@\300\312\363\207\301jH\204\334CG\002\"+T?V\223\367\256\220\207\262\341\365\204\207@\"b&\200\232\252\247\215\034\346$\000\250\265Q\231,P\315,\037\253\347RPa\355\224u\335\354\371\251\307G\314GXD\241i\325\372\243\207\"\357\v\300\272L\253\006\266\210\003\002!Z\n\207\230E\213\020e\304\255\023\263\241.(\217GbR\016\271m\260T\353a\240m\310\247O\303\240\244\260\224\250/ \b#\372;\312\030uT\030\fF3\016\331\216i5L\276\324u\277\264&\232\253k\f41z\264\222A\220=\002\202\036\222\200\021.v\207p4\252\357\210\313\214\026b\t{Xh\373\222\263S\305\340B\002\024\026\2615\304\201&\"\305\026T\213+\350\207\256\257-\333C\027 \3400\224\204\334\343\251`\321\262\004$G8\335\320\2078\271\236\305\201\266@O-\v\234\347\025P\354\273a\237\202\004\000\360\330\275\261\037l\201\305\204\340.\f\324\250\346\211\246\346\t\236\3118\304\316\35018\005Yu\031u\327\251+\256Kw\322\027>\034\374\240\300\321\304\277\314\234\210\235J\264`gNj@\355\213\243\243`\006`\215Jd\264\353,\226rg\n\000\216\274\006H\252Y\206S\362\2633-r\222i\354\226TV\255*6*\240\346#c\025D\365\a\335\003\243\375t{\262\035\000\003\233 \230T\341+\367\221p\016\3163HKW\247gv\2213\301\266du\320\360J\t\262\244\342\355x\272\263\332\320&\245\262K\366p\300\017\261BD\363e\005q\247fIj\271b\322@0#MZ\224\350\320\365v\336\326\333\275\305\016\025\347\004\211;\036f\331\236\332j\251_\240/{\200x\243\032\003\304P5(\330\355\255d\240|\251,\327e\241\004.\347M\204\335\036c\240D\303!q\360\340\300\344\373\307\244\330}X\203\240\004\231\340\3219\304\210N\370\020\327-\201\003t\322\317\333\344!\035\033@\205\202S\202\273,\372:\227\307\243(\200\340\021we3V\212\304R\355\a\276/\367\313\355\342;\027B\331\237\177y\333\3430\034\000m\000\333\016j\037q\0232\265\201~\t\366\234/\016\253)\253\327\207~\264e===\177xD\234\252\257\307\307\237\'\3618\350u\336\321c\343\360\327%\tc:\331@4\b\347\360\3442\273\323\b?`\242\366\363\307/?~\334;\fcZ_\376\273\262+\304\221*[\353\202\016\002\261\237\227\260\206\030\270\002\334\021_\200G\223\230\002\366s\373\376\307\237\277\211\321\364\355\371\273\353\031\226/bj\323\374!\305\303\373\303\357^\206\241\217\006\2760?\353\212\370\v\314\037U\341\356\277\355\027\\\232\366\365\305\t\0270?Z\344\276\355N\236GR\347\272n\307$\253Y\226\001P\323\r\v\002,\2128Kp\025\r\001\214+Jt\033<)/\001R\2265\030\352\276c\025q\357\373)\023\263?\340\364U\364\342L0\000Y\020\005\273:\006\344\211F\000=\312\224\315a\320\224=\232\270my\332\2666\037\357\217|\274\264(\376F\211=\301@c\360\210 \306\216\000A\354\225\321=6\026\v\034\336\267\277\366\370\373\232\347#\372\222\004Y\3503\257\227\303\0302 \003\'\274Ol\200\267\312\033K\362\357\365\243\261\002kY\372h\336\273\203\350\255\000\341\247\027Aa\034\n\217\274\002\234\234dKji$\001j\337Ch#/n\2622\031\273\260E\026\027?{\001\001\257;\210){R\314N@\372\n\257\023\204 l\355bR\207^\001\016G;\3317\034\357\320;Z\237\262\037d\203\276\257!\222\250\220\201C!N\nXapI\320\326\201*\314\216A\326q[!\312\306~\277\177\320\310\t\344\025\025$1x\235\331?J\2746D\203\256y.\204s\330\274Q\017\302OV\315v\333\225\314Q\206\322u\222\r\241C2!?>P7\246\206V\330\214\367`\214\336\204\331*\333\211d\2767\3540,\274\t*\375\263\213\377x)\311\205\031\211K`M\263\0224\320\267\031?\305\213\036\236T\333nTRx\210\200l?6\270&f\346=\310\317AH\376\300\\d\231W \322\207P\354s\\\234\227\265\341\376\037O\340\276\251\345{,\253\000\000\000\000IEND\256B`\202"
diff --git a/core/res/geoid_map_assets/expiration-distance-disk-tile-5.textpb b/core/res/geoid_map_assets/expiration-distance-disk-tile-5.textpb
new file mode 100644
index 0000000..387bbdf
--- /dev/null
+++ b/core/res/geoid_map_assets/expiration-distance-disk-tile-5.textpb
@@ -0,0 +1,2 @@
+tile_key: "5"
+byte_png: "\211PNG\r\n\032\n\000\000\000\rIHDR\000\000\000@\000\000\000@\b\000\000\000\000\217\002.\002\000\000\n\321IDATx^\035W\331n\034\327\025\354\351\345\356\373\332\353\364,$E\312\262eYN\340\004\b\022 H\200<\345)y\310\377\177H\352z$\200\304\260\373\334\263\324\251\252\333\r\303p\271\020:pa\274\325BP\252V\311\331\320_\372~\230(\031\206~\030\307\2110\356\202\025BI\251\230\225B2\374&\204\024\3350\210\313\3452NBh\217\317,\204O)Y\301\373\021\257\222\021/O#\002\311\024\360-^\221\332z\211s\264\261Bq\322!8R\350G%\215I5\347\325*_\222\346|\034h;}\232\2504\316\030c\255U\222s)\255w\0229R\'\270$}G\b\233\220\0023\306;\263\247T\274k\037\211T\351H(\245\023ERQ\031\'\244UJI\245\2037\222\021z0A\306\251\233\210\230\372\3762m\b\020pzL\336k\245\263\215\206M\212SJF\202s\225\261J \023\305\225O\316\021\374q\341B\022\004\020\222\242cC\310\321\aA\2716\305\332\020l\216\316K$L\31041:\216\375\200\323\245A\023|\b\224\2502\261\273\245S\337i\206\363Q\202m\265\033\212\b\331ZcB}\256\321%B\2317\255\363|\030\230t\355W\031\202\"$\032\356\320\335\313\245\313S\337\232(\275\224Q3J\031\367i\311!\326d]\n\204\350\2424\343\312![T\201\367\215\211\322G)\270\306{\227\241\323\343@.=\221\2221L\bEse\274\317\271.\347\313\227\212\202\244v&\205(\306Q\002\'R\271\020]\016x\234\221\276G\023\345\200aO\n\337p\f\211\v\005@\270RkJ\363\333Ok\314\316\343\273\270l\023\006\n\374h\035\262r\263\021\030\204\354\273\201v\300\3330L\321jF\bN\307\a/\204\\\312\272\034\333\222\225\322\202)\357+&:R\207\371X\340\360TZ+A.\335 \273\221+\264P0\3464\227\316\031\035$\362A\204\224\352\226}\233\274\213\245\324@\350\324\340jP\005\002\006\215\351t]\357;\f\211\231\3409_f\340\a\030\260\025C\262\321\307\230\213\327\022\370\325\353q_7dH8N\002$\262vR\0231\"@\355(\302\342M\316\317-\3703\006k\023\233\260)*\2259\205y\251\301\250\354k\3354E\217(a\322\370yc,\3232t\227\276t\350\277N\3006w\253s\325\270T\243\003\274\215\ni\306<\2637\016p\002\222\221\201\3268\316\355%z\306\3537\004\350I@\200q\342Q\243RlK\002\n\263\266\250\227\331\020\327T\003\n6\336x\355\"&\254\214\301>\334fi\343\017\347\251\373\256O\b0\364\224T\f\305\274\027\233\262\301\256\242\217\300.\323\347\271g\r\340\350\274\254\331\246H\031\023\202\a\357YY~x\276\242\005\223J\000\322\210\366z\246\3463\247\325\257{\260\0163\002N\364\272\037\017\311\265\326V\352\315\' wD\031W\245j\231\327\0053\030\274(\035M\321\003\"\000H\315\327\325\324Z\202\326\036\375\310\347-\227\031\033,\230\021z7\323\210\305\234\250\306d\347\307\327c\356\273\v\t\321wT\337%\266\336\316aG\325.\327b\271\325sT\363\313\236r\335@%&\273\307*\247)i\037\255;\364<\247\354Dw\031S\b\241\303\366e\n\214\316\031T\220\313\262Zp\225FS\000\337\353\016\fY4u\373t:\356\025\021:f\273\311\3031\n\f`/c\001\241p\251\251H\261\350\342\264{\0244\336\315\311`/\\.\332,\005\240\215\327\347uI\206b`N\345h%x\3402\201\000\347\243S#cm_\254\223\331\351\370\251l\321\206\224b\006\353Qn]\215\326\272uY\346e\251\025}\004\r\001j}+\000\200\366\241\033zD\305x\215\301\237r\315\t\345$P\t\bA\213h@\216&\256~\aKm\327\343~\r\306\201\033\030\210`T@\313\0000\r\323\202\247\202\307\256\203\254\021Er\254\202\v\350&\202\362\236\331|\004\303\365\276\275\274|\376\274\204\300$\260\323u#\003\357\203\221@&q\2432z\360zKZ\240\335`4\353\234\002\251s>I3\227\022l\375\351?\377\375\355\313\373\025\260\360\303\340\372\016\370\301\003\330\207Q\250*\315\001\226\002\243\000i\2145Z\000\232\220\037\aU\006\347\327\305hu>\377\371\333\307\016\262\213\001Y\311\3412\200p\255G&!ke\347\b\342 \2244\362\034\301\201\034\324\001!\313\331H\236\367\245||\256\373\277\376q\265\353\\\226\271G\365\364\202\001\222$:n\326\303\224u}(I\'\256\344d\025\301\331\200\226\02018.\364\022\342\2535\267\373\313/\337\357\321\317>\223\236\3228\\\2721\351y\355\244)\217\347\363\361\372\260\320\f\r\3162K\242d\302?\272Y=\030_9Hh1\366\361\375\333\367\017\264#*F\336~\261\340\343\236\356\327{g\322\355\375\353G\254\333\262\326\240\005\200n,\345\214pB\002z\032\b\2045`\321\324\355\373_\317O\261\240K\343\364\370+\277H\214r=_\273\262]\313\275\306P\227\nj\021\272\361\201\223\236P\264\001Z63Hc\331\316y\271\237\277\236[\314\2336\343\270\376\252\240&\335p?\216\356\374\355\036R\024\302\004\220\212\a\232\231\213N@L\024\265\244TNh9\224|\375\364ct\217k\235\363q{>P_\201\236u\303~\273u\354\347%@\361\204\242 B\020\263\3454y\201.\203%(\264\210\031\177\b\377i\261I\306\363\353\227O\037\307\037J\036\311G\316\320\263\375~\353\210\311\240\035\031L\333zA,\304\224\031\002\222\201\f\030\327T\300%?\257-\276\316_\377\3748_\337k\232\006\366\"\273\313\360\274_;Ld) @\r\032\220\006\v\v\345\204\016\202:\360\237\0064\035b\026V\006\321t\347\237\376v\034oo\337\236\274\357o\023(\365\231\317\016\310\335Jz\333\r\332\276;CY5\212M\200\223\2021\020^kc\300\357\205B4Y\2126\226\374\271^\177\240\203\206\250\311u\333:NG\a\031kk\240\017FD\333c,(P$\247\021z\310\245\307>\307\345\372\351d\251i\263\311\363\376\305\022\215)\232u\237;.F\352C!v\"\362\235\303n\350\276G\000\370\"K\306\2364\320*\025\303\266?\036\302\300\244\300+\335o\277|\301.\\\206\373\272\272fq\230\327\261%\24061\255B\f=\021D\203\t\246\2619<LCs\235\036\217D\024\245\316\302\312\325\363s\343dqn\233\353\bS\002-7\020\226|\354\233\267\313@\032A\301A\300#\"\000\003:a\225\324+,\025q\331V\b\330\233nk|;\257\313\334\021\237\257\213\211\320e\377~}\024\374\200\247T\026K\335\354\325\000\345\204\037\204\254I\243\246Q\201L\b\277\375\314\246\3762\272s\001\323u\023\315\v\3100K\236\316\375H\300\003\244\025v\004\026\223\264.\362\006/\346Tss2S\314\a\313\214}\035\371|\234\327}\2076\272#{\301\264\370x\277\302\300a\232\232\343m\006\217:\342\034\n\216\263\312\020\206\246R\005!\355G\344\006\253\260\316\327s_\266\316\305\274\276\336\035\277}\375;\002\271\310&\312\301\024\214\2148\3532\001?\214Y\240t\302\020aaF\006\252\000\021\313|\255q\313y\353t*\361\330\266e\377\337_\016\255\334\332\234k\363\307xyl\346\003\006GYX\003\252\034\322RS3V\200(,-\244tC\tT\224X\217E\313\267\177kx\027d>\301\341\2168\037\001xK@\231\320\354\2230=\261r\274\364hpS\235\230\363\262\264\000t;L\000\237\001\rJ%\336J\'\315f\343ui\031\\\2327\271\315Eil\031\347\227K\005\227\032\201\304S)\307\326\255\353\341a\001\274\237\b\350L\260\t\375\341\b\000\262\266\001\302\256|\330\022\024\336Y\016Di\336_\nq\024N2\305T\353\262v\373\331\354\271\017p\3050\324L4\267\b\310\264\372\261\231\315tBs`M\300\f\fh\a\363\216\274$E\361N\252\245\334\272\027@\023\"\202*\030of\000M\227\034\345\203\vy\320\316\266\345\t\306\307\002j\025\020b\314\200\315p\3331\304\245n\307\336\025\a\345H\360\217\b\000%\300\032\312\001\307s\017K\027\363\022-lo\366\241\031\263\306\266\022\312\301f\v\017\345\363z\\\257o\235\306M\210\206\200\214\220?t\221\300|C\362\261y\214\032\034q\334\326:\033W\022\207Tk\351\030\357\207\232\275\214qC\200\333\263kv\002F\023E0,\001o\327\003\\G\230\005\305JW\217\307c\376\275F\357\271\366\t>\026No\2341\200\274m\353\365\361,\235\001\361q\301\201U\2600\003\337\302\236\201\312Q\274\267\371\343\207\367+\304\020w\261\371jK\205\3278\024 \022\204\317\333\276\\\257\307\223w \016`\177\262\300\017\372\213\342\'A\235w\311\241\266u\373\214\273\213\365\326-\271\306\265\270F.Xr\3643\347\373\375v\234\216v\220k`\2259\034\215\344\261dp&\020{\330\237\365\276=vx\356\202\025\263\266\304\274k\320\016\240\312\337\245\\\326\217\327\347\313\025^\t\306]\033\335,$F\300\a\240\tXb\277g\275/0\230\355*\345\004s\270\202\354\226R\220\317\300\326\323\3477\004x\205\355\354\004n((\002\003F\v\241\354\231\214\226\363\022\340\223\2269\302\345z\017\303NS^p\363\304\245L4\323~+\371\307\337>\037\270\234<;\b\242\006\327\215m\005\031\247~\265\243\344\246@\356\"4\023\001\000\006\334cay*\236\244\0327\233f\203\365\217\267s\021b[:\270\0214\001\3149\265\335Wa\211\2362L&n>>\301u\0338\264x\\\037\316Sb\345\024\310\002I\320\363\r\367.\361\361\366\271\003\3341rJ`6q\321\004\355G\300,\315 <\037\236\031\240\026i\251\256\372t\336\223F\005p\337x\234Vp\234Y\326\237\337[\000l\232\342\006\227qBc\211\021\303\202E\203q.\245\235E\345z=p\213H+\\\231\024V\227E\2206\257p\256\267?|oc\204\362\340\256d|\301\315\t\350\270\303\272\342\276u\274\024\334\221\005b\324\375\226\332\025\022\202C<\020&\232\016\310y\275\375\361\227o\337\376\017\034\345\351>\bI\311Z\000\000\000\000IEND\256B`\202"
diff --git a/core/res/geoid_map_assets/expiration-distance-disk-tile-7.textpb b/core/res/geoid_map_assets/expiration-distance-disk-tile-7.textpb
new file mode 100644
index 0000000..371e9ec
--- /dev/null
+++ b/core/res/geoid_map_assets/expiration-distance-disk-tile-7.textpb
@@ -0,0 +1,2 @@
+tile_key: "7"
+byte_png: "\211PNG\r\n\032\n\000\000\000\rIHDR\000\000\000@\000\000\000@\b\000\000\000\000\217\002.\002\000\000\v\366IDATx^\035Wi\217\033Ir-\326\221WefeUf\335\367A\026o6\373RkZ\323\032i\244\031Y\353\321`\327\213Y\333\203\005\f\333\037\0260\f\370\323\002\376\356\037\356\250n\200\r\202dFF\274x\357E\224E\034\227\307\rG\036&\n3\021V\233\306D\230\272\016W\346rIl\333u\354\025N\353$\020\001\027\201\220B\006\234 J%\245\2142d!\a\251 \b\302\204\370\204\a\375fh\207\276T\276e\371Eys3i\027\002X\250\254\313<\tC-\204\362\205\020\234P\001\247\024c\330\362<\027c\314\202&\321\252)\212\276\236\306`\265ZY\253@\266\367\267\247\320u<\307r\213\266\310b\245u\030\006\234\371\214R!\002\202\ba\334\362\034\033a\354ka\302\270\216\005\347e\340\257V\266\307\022\271\276\273^2\265\004\260\315P\025\"4\252\f\245`\f\002@\322K\034\346[\266mY\360\317qm\333\017\363\276\331n\367\215\263\262]\027\a\373\375u\277m\345\n\257l3\347B\206J\206\220C\310|\312\003\035\004\234+E-\033\322\265^_\020Ep\345+%mk\005\230\266\233\355T\2175\266=w\345\326\261\222\034\362\206\3735\367\001H\315\031\225y@\254\005f\370{\r\000\021\204\037H\t)aB\342y=\024\225\353\n\f\371\311T\"\017q.1\026\320\f\025\302/#c$\265\260\a_\344\271\362\035\aJ\267\021\300\vE\301\255\2104\327\241\031\034\307\307\253\225\313\b\203\337I\036P\3423\310]2\306\303(\"\334\n\212:g\332\204\304u\\\3575\035\033Z\200\260CI=\2366\265\v\367\332+{\345\320\bc\"\341b\037\316\252\000\370\300\002\204\250\225\313$%\236\313(vl\202 \ndbCM.\203.\303\275\256\207\005\204\264lG\'J\022m\000\n*#m|\237\272\b\031+\226*\361=\327\v\211g\273\236\01788.\362\201=\216\347A\213\001c\034\"@\331vd\026\032\252CH\037\002\b_P\212<\a[\261\016\363Dk\024C$\307\021p\320\226\302\037 gD\025\\o\257<\271\202^\001\267\v\225\2624OC\310\vN\373\v\366(\263L\234\026\271NQj !\a/\314\265\223|Cm\333\343&\t\031]0\201^y\216\250\241(\225%I\nU\340\245wI\354\373\226\311\322\024\302\246\245\353r\274\\\004iUu!\035(!L\323X\331\356+\323\210\203\363*\004\372s\017\200Z\216\307I\000\\\264\242\254(\333)\003\032\253H\272\310\361\b\360\201$\312\261)aY\020T9\301P\002\206vJ\343c\212\\\017H\v\360x\032)\337O,S5U7\366\371\020\344\201D\020\230\247hA\022\030C\002\255u\222$\001\265\035P\275 \212\003\377\005\v\274\245N\300KI\251\255\270\254\233\"M\265\016\f\021K\000GRH_\204\256M\002\223\306)\004\b^\333*\025\205n\271DJ\001\tp%D\034\a\322\312\313nHu\026\252\"\3174\360\300q\215\v\327\273 Q\270L\307F+B\\\240\222\343b\272t\026\000\000\vp\b\362#\235\305\211\225u\323<wy\224\344\271\314)\244\340\202\033-8!\344\363\250L\301F\200\370\340\020\200N\300\200zQ\030FI\022\a\334W&\016#+k\327=\257\213\".s\311)\206\b\b\356X\364m\034\251\313\301H\002\225C\177\034\365z\261\200\262tb\f\030\v\242\222\t\253\354\352\232\247\355\330\224\215V\212\260\305_\210\r\025\240\330\017\223v*\300\351$\363\220\215\000\177\344\330<JU\004\257@P\304\245\344\326i\236\323\315\361\374\207\276\316\022\024A\'\201\320hQ\225[B\215\331d\350b\177\016\202\"VD{+[i\031)\342\005\332\347\234\tbi\223\232\350x\377\353\313\241\017\301+\"\327A.\206\343\266\303\220\211MN\2120\213\002@\177\265\250\035 \362\021 \f\355\320\320R\306l\v\a\202\233\254\352\373\355]\344IN\210/\201\345\256\a\036\347\270@$\223\304U\035\270\216/@Z\213\246\204\324\230 \206p\b\375\024`^\313\247+\307\t\262b\312\263\fd\206\004%ha\212\343W`\200\240\262\f\354\306%\311\240(H\035\371`\t\004\234\\*\025x\236\365\032\326^B\230\260\355\213\210\270.[l\323\003Y\262\004h\020\345q\026\n\220\202\254\347\347\306\261\203(\242\f\006\021\230,\006\223\262\300 \302\252\360V\256M\351\335P\b\344\272\0043?\037C\320\005P\022\306N\232@/\301\026\213\3762\256VI\000\212`\034#I\340{l\261fw=\236z\035\300\251\247MDaha\237\245E\271N`z\001\a\235@\230h\251AVQ\221\331\356\020\004\221\240\212\222E\372\016\2678\225w\317\337\017\025\363\221\237]#p\243\240\344\030\2479\363`6`\360&\214\\\026H\027\314\b\212v\344\371L\022 B\340/\302!\324Z\v\321\275\177\274\3139\'\352pl\221Gi\323EC\b(-\204\b]\0271\327\365!r\342/\236\331\036oN\323iN94\n\344\302\254\315)\b~\270\333]\204@\264\377\330&\036%\244\243\024\\`\333\355n\266\003\345u\203\020R6a\320\033\327\230Sw\254F\243@0\204\004>\265v\363V\326w\273\301\207\301\241\036\037\214\217PP\270nH\366\327\251\251\347d4\231\300@\201\305k\001\372\354~\206\241(\"\240*!*\344\304\032\242\310g\277\274\277\2251\f\372\341\232QN\375\224\223r\267=\314\365\310\351&3\245\037a\350\252\207\021\321i\252\200\200\257\214\242\221\346!\266\272$g\230\315-\371\256\265m\246\312\"\364I\232\232i\274V\227&dMi\342\006\320\023 \321@h\2356\031s\026]xD)\030\264\310\352B\363\214I\"\231\274\001\362\365=\005\032\362\244\234\372\2428\233\020b\353\333\271\216\242\b\364%\3454g\257\232\000s\240Z0%\205k\245\252\373p\242\006\023O2\333\215\272\327\241\257\362\247k\b\236\274O|\330}\250)\f,\032\266\320\022Y\313(\267\035\254\264y\335x\\K\211\361\355ou\2312p+\272B\351\f>\001\323o\374v\366Y<|z\027x\030j\027\322[d\000\307-\210\344\211\310\204\341\"g\216\255\324d\017\177\330\375\365\247\207\2200\300\026\027\330\367=\302\374\340\223\241\261\326\227K\311=h \270\032\334m\301\2145zq&f\264\f\300\326\251\025\267\335\365\224\375\372\361\274\216\250\267Z\325\224\320\034F;fo\277\277\275\257\304\216s\327\215Z\204\300\324l\317\366sE}/\004\2150X\024L\b\001\262\252}h\364\327\227\323\224\004d\265\002_\005\272x=\346\333\367OO\037\276l\375\f\372\235C\363\"\310\334\363L\304}\304L\no]\016C\324\aS\335\336\253$y|\334\200\323\332v\264`\341\322\230\370\347O\335\347o\337\376\344:\337\375\372\241\031M\0369\204x\036K\005\327a\226\226\315R\207\017\333L7\354\036\032\251e8d\006\bb\263h\v\275c\204l\177|8\234\177\177\304hx\336\327u\227\372\230A\366qV\024\221\n\362\242*`\332\201\301[\315\270\335?\223\241\016\353L\033\360\377\344\323Ow\030a\317\r\363\254\3041\246\017\260\234v\275pi\003n*\362\274.\263\\\253</X\35092\006[\357n2\366\242\003\216u\334\332\253\247\317_.\227\f\374\302#\323H\250K~\221\222r_\226y`r\332u-,X0\000\312\274\314[\030y\332*\207\365\273<\377\334\373\024\023\352\333\316\323\371!2\225\241\036\f\241\254\244\364\351\347\215$@\317.\244\252@c\267\036\272\256\033\273\315\241\355\206.\002Kk\372\323\307\351\335\373\036\306\317k\000\357\374$\373\302\003\006z\024+J\263\227\257;@\204xJ\251\315~\\\017u=\232\271\352\367\363\361<M\205k\325}\237_\376\3742Dc\004s\002\255\274\315\341\361\002\213\000\362\370\342m\214\252+\354C\304\213\342\346f\n`b%Qg\352\252\336\314\323N\244io\325\2722\367\377\374\262\237\207D\371)0\0012\255a\332{\005-\030\301\"?=\207b\366\300\031\313\227\3073\270T\035\365\335\270-\273y\322A\252\215\325\036\313\352\370\365\237^\316y\036\207<\002\262\213\3656\355\327&2\361\236\300\200\224IDi\342\373\317\317\206\217\022E\211\330v\335y{\272\036\016Q\320u\326\266\251\252\313\345\315\343\017M\225\3022\017\214\257\373\252\032\347u_\027o\267\233\247P\200_S\231\252\362\361\3100=2\221>\236\177\376\362\307\317\327\3635\n\306\321\352\232\246\233\017\303\372\373\244\252\264\n\224\215\2735\004\030\247\272\236\267\333\246}\363\3764&\346\232\212\354\315\004\217\bqHy\365\366\333\227\367\315\375\365r\177\332\214\226R\311\346\334\367\323\224\314c\236$\371\264\335\314\035D\205y?u\305\364\360\371\353}\2479\330/\321\023 \340jLw\325\370\017\247\346\376\341\315a?\367\226T\325\371i\250`S:\315\323\220\227\323|}\231\306\016\306\3550\365\365\372\341\360\365\347A)\2148\314\034h-b`0t\275\237n>\276\177\270\207\307\t\213%\331x\031+\243\217w\247\r4\371|\\\247f\267\353\212\262\200M\346\346p\363\365\307Q\353n\006\371\202\312m\312\017\260\"\005\273\346z\177s\205G\242\275\305\362z\272L\231P\363\345\274)\233qsX\aj\277]7Y<\304\351\256\031\276{7f\315x\231G\030\352\016\246|\247`=\310\273c\267\271\336\277{woA\213\306\313V\262b:\2346%<\262\265kU\314\353y\275n\212l\3276\347\207\267\333\264\336^\242<I\227\r,\352\006H\206\234/\333\215\276{{}\003k^U\237\267\360\254\323\355\200\344m\337\266*\0306\343\272\357\273<\337\256\247\335\371q\277\333\357Ng\370\304\'`v<\204G3\017?\037\367\233\247g\bPt\335\356\224\250\034\"w]\277n\253^\210v\275^\217\375\030\367\303p\234\367\217\207\375\343<\034.C\b\236\200s?\252\236\300\273\365\2737Oo~\370\341\027k<\356\217\263\t\373\256\005\241M}[\305\242lZ\310\240\322z\354\272\303a}>\337<\315\204\356\206\242\027\332C~~\374\320\003\347>\374\362\345\343\277\177\376h\355\016\347c]\224\033\310\244\a\016\f\225\f\272\266m\247>\325\3618\r\343M\\\336?\274\257\240\374|J\313\030\212\240\364\363wm#\304\257\177\374\333\337>}\263\266\207=\220`\a\246\3255\303t\032r\2215uS\217e\031V\303\270\356N&\233\357\036\207e\205-\356\302d6\034v\263\337^NB\374\364\347\377\375\257/\237 @_4\331~\000\362\255\207ah\205(\3726\a0\263\244\354\341Qz\275[_\316\267\267\267\260\242\347\2730\034G\001\0327\277}\272\273=\377\313\177\374\337_\376jm6\345\202\3434\364M\335\017]\315\343\242)\263\242j\222\f>\337\214\233\371p\271}\270\273\275+U\276?\353\307\207sH\224\234~\377\323\315\371\307\177\375\373\357\177\261|Q\f}\267\333\326y\\7}\247\2022\352\212\242,\001\227\022\366p>\3177\217\367\017\267w\367o\263\354\374>\311\376\361\366\201s\217\374\333\177~}\371\370\333\337\377\373\177\376\037\341QF\003\316-\374\237\000\000\000\000IEND\256B`\202"
diff --git a/core/res/geoid_map_assets/expiration-distance-disk-tile-9.textpb b/core/res/geoid_map_assets/expiration-distance-disk-tile-9.textpb
new file mode 100644
index 0000000..59f386c
--- /dev/null
+++ b/core/res/geoid_map_assets/expiration-distance-disk-tile-9.textpb
@@ -0,0 +1,2 @@
+tile_key: "9"
+byte_png: "\211PNG\r\n\032\n\000\000\000\rIHDR\000\000\000@\000\000\000@\b\000\000\000\000\217\002.\002\000\000\r[IDATx^5Wi\217#\327u-\262\226W\373\253}/\026\311b\261\212\373\316f\223\275\261\327\351\236\356\236\255g\353\2215[\317\264\244Y\002[\036\317\b\032[\262#X\260\244\004v\004\003\006\002Y\261\215\bH\020\177\n\034(\210\201$\310\017\310\347\374\201|\310\207\374\205\344r\202\024\032\315\356b\275S\367\235s\357\271\367\021v9\311\274\240\352\031Q\330(\205\236lxa\241\340\273\236\027\024\303\260\022\363\270>h\264F\375\301`\326J\306\263\357tO\317\256\246\032\305\336\373\344\371\326\356[70&\246u\216K\313AR\216\263F3\364\024\354\373^!\f\035\307\213\213Q\034W\332i\332joL\247k\263\225\255\315\235\017\276\367\303\337\235~\250i\301\a\377\360\335\323\353\327\353\234H\354\257!6\255\005QV)\226\322^\325\346#?\b\202\320s\355\250\024\224\253\365N\2559\233\314V\246;\307G\317~\370\352\344\352\223o~\367\331N\\}\347\263O\316&\206\b\000[\373SQk\325\242\254T\324\rl\271f1*D\205\330\327u\277X,\307q\226e\263\331\374\362\361\255\243\363\217\276\370\323\253\'\357\334\371\350\267\333\242\370\340\343\363-\214\305\332c\242\377\326\275fk\263\337n5\353Y\022\225\375b\245\222D\205B\301\266\254(.\306q\265^mLgW.\037\037\177\367\337\376\353\253\2337\357\274\375\342\225$\341G/\256o\031\262x\364gDxx\375\b\261{\275V\253]7\263\310\217\232q\\.\024\002X_J\313\345z-\255\246\323\325\243\203\345\235\367\377\365?\277\272q\343\376\323\323G<\277\372\344\301}\001\253\352\325\367\b\247R\277f\232\223Ng8JM3\254Em\340\263\\\364\\7\212\312\245R\265\326h4F\223\265\265\345\351\367\277\375\303/\256\\\3329\270\371|\272\367\370\301\307\027x\314\362\203\267\b\241\230\244\353\273\343\321t\270\232Zx\031WG\255,.\227=\327\017+\225\254\004,v\232\343\345\311\326\372\336\367\276\376\342\213m}y\266vOn\234}\370\325S\211a\351\372\207\004E!\2771\331.5\216g+aa\273\3409\353Y#\251\304\236\347\201\262\245RVk\267\226\016/\366]w\371G/^\354k\323\311\344\244r\371\326\213/\233\b\245\224\362\212\240\031:\317z-\303\033+\322\225\375\253\363\336\326\326\264\331\250\305\216\027\226\223z\271\334h$\315\346\305y\2672\330\334??\277u\375h2\306f\353\352\273\337Ld\272Ij\237\0214M\347\021\315\272\245zEn<|v\255\323\275\340\212i\263\354\004q\234f\265j\255Vi\265\326\346\235\366hc\343\370\370\354\372\321\312\216\026\215\266^~\275*\321}m\363SB\346)\222$9\006\313eM]I\333\206\276]gq;\016\213I\265\232ei\257Wk.\215\a\265\332\260\331\334?~\373\356\325\375}3\234\035?x\376P\022g\351\315\227\204 \n$E\321\034f\264\314\262\246\375\272\251\254otZI\241\224\266:Y\326\207\374h72\000\350\342f\351\364\354\335G\207G]<>\275s\366\035I\330\377\350\313\217\tI\342a=\r?$\203P#K\315b\271\325l\271n\263\035\247\203\341\270\327nw\a\235NT+\371\355\335\323\363\027\217\037^.\340\321\335\207\357?\341\330\316\355\317\177L\3602/2\034\017\313I\022\211I\232\262\272\323H\nZ\320\356\325\352\355\366\240\333m\265\246\355\222_i8\311\375\363\037\374\352\316\366\266e\325o\\>\277\205E\376\312\3133\202\323%\216bX\021\277\371P\v\301\"\217\323B\030\025\374f\243\327\353-\365\373\343\236\343\304\265\376\362\205w_}\375\347smo\307K/\236_\2724\226\233\247\257\317\b\236Eo.I\323x\236a\n>g\362\274\345`\323Q\232\315f\177<\031\216\333P\330\215\341\322\366\205W\277\370\303\247E\376\356js~\347\331n\372\'\361\203\237~\372K\002BG\274\310!N\221%I`\220\303\231\262J!N/hP`\375\321h8\364\313\335vky~x\373\365_\377\363\317.\237>\027\304\225my\347\306\347O_\377\376\037\377H\220\034M#V\346y\236e\020\'\3506S\346|\327\246\2644\031\366\006\323\351\372t\2206W\227\226\326V\217\357\274\367\344\357\177\365\345\227\317\020\332\334\022\006\233\177\371\373o~\375\233o\t\032!P\200AJ`\b\240\005\304#\250\222dRl\326\036M\273\335\265\255\371\2606\\]]\032\3166\217V\367\377\346\237\376\345\357$)=8\273W\362\036\377\350\247\257\376\3427\004\313\200\212\360rJ\r\\\215\241\251\034I\353\262\343\331F\022\330KK\223\303\033\a\313\275\336\336\336p\320\235\205t\353o\377\347\277\277\020\330`\351\336\275r\264uv\366\352\'\237\020\210a\030\212\345\005\201\2255\323\341h\212\314\363N\350DA\254\362\343\371\315\273o\235\256\257\357\356\316*q\255\035\222K\337\376\307\277\277\322\225\332\360\306\353\206\233\234\\\277\330|B0\262c\250\222\240\v\002\306*V\025\236\3163\256\356\224\223\2701\030M\227\017\236\237\335\276}R\213*\365\345F\273}\364\321\037?\337\344\315\321\225g?\351E\303\223\023O\275F\b\300[h\250*\306:fi\321\320\0312/D\305,\213[\235\221\353\036\234\277\376\371\'\247k\275\245\361r\265\334\374\301\213\237\235\v\254=\277t\377\323\241>\272\375t\264:%D\321\365\322\202\312aY\321$\212\242\030\206&\251b\322\254\325{\243v\253\266\371\350\303_\377\325\367o\034\017\207\255V\375\321\263w\236>\344\271z\260u\357\243\211\236%\005\256\232\022\026v<\307)\262\202*\b\232L/ (\0227\032\315l0\252\367\202\361\335w\177\374\333\3077\257\354l\256\016\252\357]?99\342\270yp\341\326\343-/I0I#B\2678\026\271\021\317+\252\253\360,\317\347I2\307W\333\355l4\033\266\312\315;\367\037\177\360\313\314\236ot\033\223\273\307{\'\207\310\335\336n]\275q\242w\032\001\217\004BW\031F\nC\216u\303\032\206\232@\240C\216I\332\235\341\322\244Q/\225\016o\335>\377\371dP\233u\272;\253\263\371\316\026\207\366\326g\343\v\227}\273\337o\016SBE4StM\216u\034K\026\260\242\212\024\324%\327\031\367FK\215\206m\367\347\363\313\357?\036)\223\331\265K\256R_\217X\2661\356\325\372\273\205\352\325a{V#\030\222\212\352\246\215\241\'\232XPE\222\024\251<IW\'\275\336\250\336\t\303di\351\360\316{\233\363\235\265\333\207\243\301\332\301!\317E\355\226\273\262\267\273\262\262\334\336\274\004~ 7B\313Q\203\330\267\025\335\'\363\244L\346\363$^\356\r\373q]\247\362T6>89YYY;\270\270\2661\337?\340\370\203V\327\335~\371r\377B\275\267w\221\020$<\016\203\240\033\227\035G\261\003\222rl\rx\244{\375^1\256\327(\222J{\263\365\275\331ly\272\273\2666\037-q\334\315\365\336\356\375\227\017\036\337\213\212k+\204`\325\227</L\n\005M\020\255|^\320\r\021\264\244\300\020\255\300\355\0264\272\336\356O66V\246\313\233\233\033\353\235u\206\331\336\330>8|\373\322\245{~X\353\020f\251Rr\242$q\034\215\345q>oBy\223 \005SOeA\217\352\252\030\307\315\316dyc\343\322\366\346\312\372n\233\221\306\303\313\233;+;\223ke\243V#T;)8\232\203\261\355\vH@4IR\271\034\v\205m\030\222\350\'\rU\215\343\244\326\233\254m\316W\267&\223\251\3070\243\341\346\366|l\364g\2111\034\001\200\241\204\032/\033\330u\301\027\240GH\371<\a\326 \b\266b\206a\030\024\313\225j\265=\234\255\365z\223I\237C\364\366x2\036\225\304~\346\330\315.a\031\272nK\034\247\342\202\312/\354]\244sH1\205<2\rE\324tE\325M/N\222r\251Q\253\266\352\"\313\242Aw\324\357\370\270\227\205N\334$*\266-I.B\206\353\253\252\203)\232g\362\274!\260y\222b\031N\341E\225\027\005\245\030\333a\251X\256z\nES\265\356R\307\257d\255\206g\307\036\221\264l[7Y\326\326uW\222\305E\177@,\317 H\006\212\226d\0268\311\347 9t\337\363\213%\v\f\024\325kI\235R\262V\315+4\034\242*i\206l\361\212\214ea\341\254\024\223gX$\36292\217\2400\350\305\362\305\305\b\226\345\030\262\352\245\264\254\2242J\252\267\352z\320\342\bC\322\261$\211\022\v\216\fmV\240\250\034B\264,\345\310\034\aa\344\340\202`\026 \274a8qV)\033v \227H\266\226&v\230I\204\256\351\222\"+2Z\004\235#\311<\221c)J\342\026+\t\202\200\350\363t~\201\000\030zP\210\334\202\256k\305\200\346\3234q\213\231O@O\223\024\f;\345\200\236\034<\a\000\214\0046\235_\000\344(\017\213\242\310\0024\000\346\215B\340)\262\252\206\241a\201<\340\245\210\020y\254\260,\t\357\024\\\323z\0233\022\305\034\221\177\003@9E}\001\200\026\\\300\277\276\242\b\252\351\232\nb ,\020\035\021\f\257 d\031~Q\221-\313F\213\307h\016\345\341\345\260\001$\341\310/\006*x6\005%J@\225\261\314\302\371\027\033\202~D3\fa\033\b\331e\337M{^P\362\235E\0044hG/\366/\210\"\326\213\206\241\a\272&\347\t\366\377y\201\337\377\a\300\212\"\341\250H\260\312^\034\204\361\240\227\306\034\354\226\221\241?\345)]\221d\350\231:h\204\r\325b\240D\026\b9\002</\267x\214\341\355\000\023&\315\270\336b:\207\361\274\332o\2339\350\3272\231\313\2034\264\310\261\234\252\2100\331\252\212\252Bp`6\020\034\363f\a\210\346\004M\023\bf\001\340\332\212\342\205\225l\330\347\340\005\202I\021y^\000\365\030\0309\f\003\026K\274\0007r\204\240a\235\002\206I\232\325t#\264E\000\340\030\023\000r9\277\022W6VX\370\026\033t\216b\270\005I4m(2\v\\\262\254 *y\320\006\362$O\220<B\262\356\205\256\253\023\252N\t\016F\224dY\305\356d\b\3743\220\201@\001\202\254\206\334v\f\314s\242 \t\260\223EF\345i\006v\304q\234\244\326=\'s\t\0171\310\320\220k\233\226\327\350\017\021I\344@s\000`\363\210\341e\310\177\210A\020$\340C\223\260\246\311\330\367\242\002\334\265J\315\310\315\nD\bC\205\t\246\bY\3444z\211F\202N \021\303\210y\206\023Dlj\006\213\241q\202\021\360\274\210E\216\263l\017\232a\026\265\266\002\325.\020\216\315p \227\016\230NRv\035(\300|\216\345i\312\242\031Q\226\025\254\251\334\342S\002\3258\216\027E\311\002\325\312A\351xR\vU3\"\300\rD\004\226bi\246\346E\216f\"\n\b\202\351\023\251\002\'\233\340\321\"/*0\177-\254\232\203H\004H/\331\264\315`\354\353\226U \260\241a\244j\246\251)\262a\352\272\203(lTL\021\261\034\317\361A\344p\234 I2\017^\bB\300p\t\224Bj\351~X\322u\025\313\004\206\r\312\n\374\255\302>4WW\0051(\245)b\221\ni\350\225\r\221\227\3404\006\"\202\313\300jN\340\004Y\226|`Q\024\025\216\203i\035r\004\313\252\002e*\033j\200E8)\264j\220\347\n\f-\020\221!\b\226\347\330JQ\326T\025\024\0211V4M\003r8(S\226\220)\222\226EPY\204\344\325\261\255H\\\220e\253\"\f\177\252j\351z\002/\212\003\253\0001\300\005Ly\005\005\212\003.Ma9Q%$\030\362\336\254\207\200D,\353\222\310\332~\273;V9\232\321\f]Q\324bQs\364EY\211`\006\222\022T -\025\211\246$\240\t\353\000@\211\210\227%pT\260&\360F\226\225p\251\232$.\213t\003\016\271`\327@\273,[XT\004^\342%\253\314B\222\222\024\202!_\367\t\004-\b\350\021\200r\360\177\036\000\200}\257\3408\256$X\274\345/\316\260\262]\006\211\0240^I\325\260\350c\226\001$\206\201\246\224\021\213u\034\224\032\214\230\022\270:\3148\220\373\256g\331\201\252\332\210\346\035M\325e\253\344\273\v\32689\322T\350\036<\'-\306\251(\2126\b(\024\216\005u`\342\205[0n\210\222\300q\006\f<V!\320(\312\021 N\327vAe\320\003\a\232\001;Ep\346\304\232\td\315\377\027US\b\376Q\205\227\315\000\000\000\000IEND\256B`\202"
diff --git a/core/res/geoid_map_assets/expiration-distance-disk-tile-b.textpb b/core/res/geoid_map_assets/expiration-distance-disk-tile-b.textpb
new file mode 100644
index 0000000..e65c433
--- /dev/null
+++ b/core/res/geoid_map_assets/expiration-distance-disk-tile-b.textpb
@@ -0,0 +1,2 @@
+tile_key: "b"
+byte_png: "\211PNG\r\n\032\n\000\000\000\rIHDR\000\000\000@\000\000\000@\b\000\000\000\000\217\002.\002\000\000\f\246IDATx^\035W[\217\034\307un\356tw]\272\272\252\253\253\252\357\327\271\317\354\314\354\354\314\354}\271\313%E\212\022\245\345M\261CF\026M\213\022\035;J\240\004\326C,\3312\002\b\016\024\304Q\002\333@\000?\344\321\017\t\220<$O\201\003\377\202\374\240<\344\214\v\v,\260\330:]u\316w+\353\343W\337y\377\331\aO\357^l\206M3\354\016c\335\241q\024G\nS\3548\210PJ=\337u\361\216\205#\023&a\230r\237\270J:\333\325\331\331\261\256\277\377\352\365\313\017\336~\360\344j4\355\017\273\032\333<I\213\266T<\240\036,\036\312@*\344:\0262q\231\006y\3059\241\310\266]B\220G\250u\375\345_\276z\364\255\323[\267\036\237\314\247\343E\200;q\335M\212\266\212t\030*\3440\250\021\033\352\302\367\250(e\320d\260\261\323\261\211\312\222$Ic\353\325\307?\371\344O\036\235\237^^^\234\036\356\356!D\213*K\212\274\310\263\210\375\341\032\224\371Q\354\271;\256\027\232\244\256)\305P@N\016\372\335\254lz\326\313\227\037~\365\321{o\335|\363\215\343\243\335\315~(\313\262*\262,\317\313X3\027{\310q=!d\350\334pYh\312\272\241\236A\366\364\250\031\317\206e\2577\264~\374\352\243\257?\371\344\331\203\367\337\275y\274\020Xe&\321:\323Y\246\265\024\001\\_\270.\017\003\321\331\261Ud\2469cR\372\aom\366\027\263i\333\033\355Z\277\374\217\257\276\376\352\363[E1{|\266\2448\315L\024\310L\253Xi\225\306\320\b\311\035\233R\206\350\r\326\016\017\030\363et~}y\260\331\237\017\207\363\365\306\372\337\377\374\371\317\177\363\301\3030\224\'\327GI\234T\025\v\362<\326J\'Y\032\307F\033\351\332\016\241.\276\341U&\225A\232\256\207\303\363\263\343\365\341r\357\360\370\330\372\375\377|\363\243\037}\364\302\230\372\344\2155T\357k\246\3132\366\0057\2216\211\201\341k\317q0\362o\220\222\207=c\232~;\274}qv<[\035\036\037\036Z\277\377\267?\377\344\263\357>\274{xx;\223\204\017\206\224\226\320\002\311\230\021\302\030\023\307q\030\n\2372\262\223\026EAy\262\314\373\367\357\337\271:\231NO\217V\a\326\027\237\177\362\027\177\375\342\342[\213\305\323\232\272\274\325\272\312t\b\337U\234\347A\270\355C\226D\222\241\235\235\270jK\252f&?\276\177\375\356\331\301\374\346\301jub}\374\342\263/\377\354\351\336j\271w\f\005p\035\364\212\272PZ\006\312\204Q\256T\232\304\275\312\243\324\336q\374(\017a:\311\336\315\353\3537\316N/.N\217\217\247\326\353\027?\370\341\367\357\355\255\327\207\353\034\341\2249mY$E\242T\022\372Q\331DY*\220\213P`\333\236\'\343D\t=X\335{v\275Z,\366OON\216f\326O\276\374\364\207_~\373\376\311\362p\025\243\246Ki\006W\315b\255\203 \316\353\321(A\030c\025(\025p\316u\244D\177y|\377\275\273w\356\\\236\235\236\034\034\034[\177\365\017\277\372\342\357\236]]\355o\326\221\335\317\240I2\2118\221\222\005I\"\201t\000\356 2>\254@EQ\334\233\256.\336z\370\340\361\345xx\262Y\002\016>\375\346\353_\377\355\207Ww\316\226{\330\256]WDId\002\306\003\251\225\364:\035GaL\t\361=*#\025De;>\270\371\350\217\036\234u\307\213\311\376l2\261\376\3767\377\370\253\177\372\336\367\336\276s$)\315\267\005B-}.\225\366C\017u\\\204\t\331\036\203P\356y\304\317\253\341\352\366\335\213[\253n\253\273\375~\277g\375\356\267\277\375\367\037\277\376\350\365\2433\201\363\2242\243\000\376~ \302\220\205\036\365\335,%\230`\004\324\307\036\264!\256&\363\325\371\371r}\030\351n\267\251+\353\277~\367\177\377\372d\361\374\365\273\207\324-3\354\233P\026\000\001\005\3437>\363|\311\260\2131\v\002!\240R`L9\201~/\227\323\262i\353\"N\255o~\361\337\377\362Vr\365\342\366\024\3434!\332\230\240\210\245.\253r>\034\017=\337\'\202\261Pk\317\023I\210\210H\263f2_\356\016\2065p%1\251\365\305\207?\373\347\'\253w^\034\016\b\312\025\253\373y\221\304i^tg\363i+\204\247\"\356P\035C\003|\277$(\310\223\004X\\\265\2756\213\322,M\255\017\177\360\371\337\374\354\326\305\323\303\222\3406\225\331`\\\032/2i\224\324%c\214J\031`\3004\363\b\347\036A8\212\343\254i\252\246\327M\245\214@\322\236\376\361\263\227\337\275{\361\370\264\360\360\356$-gsD\\\315@E\341\356\274\322Jy\314O\205\357\021\341Q\217:4\326qQ\225m\267\fu\004\262a]^^\335{xu\276Yw9\313\247\243\341l\202l\037\3336\362|\023A#\241\200\n\225\020A\bZb\244\203\203\300\310\274\004b\2524RZ[\213\361\351\311\333\253\325b>b^\bm\033\f<\2121\310\001\363U]\267U\325\024\202+\nl\30042\n\376\3145+\267\005\322\n*\030k\301vO\201\226\253\275\232`9\237\f\3522r]bL$d\244\363\242\314\244\317\004\005,\206\210\202<\b\307\023*\313\n\231V\371\226r\306\"\336\350h\275\332\270\330\347,\231\257\207\335\n\271\000C\245b\020D\035\v\317\205\376\203\305\300\017X\004\020\203\372\306d \330y\235\'q\034Xlrx~uk\317Eu\025\244\323\275\321\250v\034_\203\254\307)T!\016\200\333\367=\214\335-\236a\321\216\243\303\004Q\267(\213<\217\204\345\317\216\216\036=\216\202\242\024<\352O\206\243\310\rd\230\344i\242\r\243\330&q\022\202\227`\327\205Z\310\303\302vd\250\2024.\313<\317Sc\371\233\325\362\374\r#J\342$iY\002`\355\004\006d@\246\003\020\"RT\302\'P\000l\020#\360)\346\272\241\202\0236M\236gA\230Z\263\345\260\036\037\224\246\326^\225\2266\354\267\325\326S$ \b\272R\026!\a\fl\vx\214\201M\371\276\220A\230\345eS\306\240=\231\325N\274z\264\230\251@J^\360\216\323\351\270J)\237r]\005A\325tc\016\256\002\222\3401\356sJ\f\361\214\224:-\300\377\212\n\016a\345\211\337m\247k\2603\346\305}\a \350\202)\205\201N\353\241\347E\025\210\000\314\0202\002\343\202\3630\206yJ\035\205U]d\025\024(\254\243d5\351\215\346\275\270\210}\335/R\215\267n\300y\326\214\a\333\001b8:\024\360Y\240\022\316M\212\020P\302gmS\302)\312\242\264\312\375\305\270\327\353\366t\030g&MR\201\271\017\212\304\263\272`\004\222\t\310\310v\263\002:\307\200\342\032\n\000\320\251\204\t\024u\223fV\332\237\f&m^WJ7i\2524w\2740\304Xd\261\330\026\240>\245\234\v\005\247\221J\206\265p=\037CBA\016-\300\353\201\316\224t\347\223(\364\202:\315@\313\300\212\271\016\b\021Q \240\257&+)`TT)cQ\024\004>E~\004\234B\330Ei\331VIj\251Q\177>l\233\314\000(\244\340\304\366\211\307\t\220A\212L\206\206\373\b1\256\"\3511\nj\300\020\311\262P\021\217\270\016\257\2522)-\321\355\315g\275A\257\n#\207E1\340\330g\200}\352\a\251\342>\364\017#\225l\351,\340\340\230\021\256{\231\240\222\0030\241\215um\341\244\233B\346\002\301\361\034\b\037.\374k\000\362A}\002\212\312\231\307\224\311\245T*\227@\361\314uT\006\361K%\2328;Y^\024\201\345+\300\216\275\263s\003\f\034\003N\001h.\2061\370\220\217<\026\000\214@@dd\222-4\267\005\306\375\214\023\245m\326\301\271\b\032\213\271;P@0\310\214@:\210r\022\344$\364!S\000jA\204\240\300\326\024U\226\202[F\210\325u\323\3476\204.\233\330\221\027\344\026\206\000\323q9s\311\016\270\250M\005B\232\'Q\200\221K\030e\324\v\005\360Y\372\n\320 b\024\017F\303\222\332l\340\332\340\272(\212\255m\022\304\254U\004\232\r\001\320w\0211R\233(\f\267\262\352\362\255\222\200\034\000\023<\037\001\216\367G\203\032\344r\223:\016\301T\265\026\306\016\341qQp\314B\210\240@z?\224*\f\033\02586\000\t>\203\200\214\\\303n\004P\\\357\257./\217\306\304\023\016p\254(,\300\216\'FE!\034\307#\247vG\021\2601P\203\250\351\"\2332\016_\247\340\225\304v)\213\253\272\267\\,\366\232^\004\373\035\020\3562\262\240Qy\3357\322\a\'G=\262\323\261=\0004\310M\275[\340\255\fz\002N1\033x\216\315\222\301\254\005\354\224\2521\f,\0330\252j\v\220\337^w\v-;;\304\t\332\316\215\035\204\303 Lz\343\223\253\241\221\245\226E\244\322\246j\225\r\023\3304e\222j-<\221\303\261hP\314,\226d\317{\303$3\004\206\351\031\300\207\323\201\301\311\274\232\235\034\354O\002_Va\200L;\3547I\331\353\017\247u\336\351\220\320\317z\322\204\371d\317J\346\267!K\a\261\362\341\360\244\002\241\332\366\016C\236\031\235L&\3730C\024\v\001Tm\362\272l\233\246-\323\216\335AHM\353\254j\366\346\326d\005-\026\324e\002Y;N\324n\272\333h\337A\240D\252\350\357GL\326!\260\027^\a\345\260\256\363,6\021\274T\306\373\273\323\0062=\204\355\nb\003(\260\353z\330\202\373;~]\214m\000\247\213\231\257\343\351f\005\342\035!\264_0oP7\031\350\241c\343\342\374\341\361xR\346iVY\r\034 \2326\005T\340\235\033\026\220\302\207\337\035\016\023`2\206\320:\352\326uU\245=\243\023 \217\321\312\261\365\360\374]@\342\266@fQ\327O\027\213YQ\"\004/\213\033\360\212\202\265\343\205`E G\002\2744\020m\253\266\316\fQ\245\256\323\304\201\334\264nl\333i\267\252j\271n\260\\\036\334;l\023\307\021\322w\211\v\324tU\257\206\247\032\240\035t\325\363\303\030\b\r\357\f\327\005\220\a\262\030S\222\330\266\335\213UUY\230N\216\317n\336|1j\024\t\3008@\274]\207\362\252\255\343\214\001\321t\000\352\245\3456\"xn\247CM\024\247\203\201\313M\2344\020\350\n\v\313\365\275\247\227w\327\273-${\001)\"\020ntro\261;\311\263\256t\021p\306v<\276\365f\006\202\351`\316{\314\261\355\274\035O\024\v\245E\207\307\267f\263\305b3\356\305\261&\310\305f4[\354\206\273\253\275\335\274{\362\246A^\207\330dk\r<\t8r:6\356\330\324\357\217g\213ZPf\245\343\313\223\371|q\262\\\036\227\2311\240\006q\336\037\227$.\027\367\317\357\235\254\020C\341\032\366\2410\212K\023\372x\a\026\a\227D\302OkE\254j\363\350\370p}x\377p\267\017\266\020\301kI\207\331\240\354U\020M\252M\372\agv1\310L\030\325\373F\244\001E\2003\240\222\335\001=\300\304\332}\362\336\267\337y\347\346f5h\341\221\224\024E\336\326:\025\243\204\261\264\032\000\177\027\021u\201\313H\226\325\336\305\301\365\036 \312\335\032\216v8\001)\262\356>\177\376\374\263\a\347\233\301 R\220\034\213,-\253n#\260\201h0\335_\236\336\\\267\333\356\273>I\232\325\315w>\373\352l.\266\262\317|\354n\245\336\272~\365\362O\177\372\346\301\254\237\201\n\304\020\001\301\261\262\25071\030\321l}\177\021^\0344\020\227\021#A6\334\177\362\301\373o.\227\0201\003\220[W\362PX\337\371\364\247\277x\276Y\316\341\245\253!\274\345y\267\216\223\371x\272\267\036g~\020q\244\300\256\274z\274\337\204bw\266\351v\207\220\004w\223\b\002\f\260^\350\377\a/i\247w\337\243[\370\000\000\000\000IEND\256B`\202"
diff --git a/core/res/geoid_map_assets/expiration-distance-params.textpb b/core/res/geoid_map_assets/expiration-distance-params.textpb
new file mode 100644
index 0000000..b3bbddb
--- /dev/null
+++ b/core/res/geoid_map_assets/expiration-distance-params.textpb
@@ -0,0 +1,6 @@
+map_s2_level: 6
+cache_tile_s2_level: 2
+disk_tile_s2_level: 0
+model_a_meters: 322622.0
+model_b_meters: 2828.0
+model_rmse_meters: 0.707
diff --git a/core/res/geoid_height_map_assets/tile-1.textpb b/core/res/geoid_map_assets/geoid-height-disk-tile-1.textpb
similarity index 100%
rename from core/res/geoid_height_map_assets/tile-1.textpb
rename to core/res/geoid_map_assets/geoid-height-disk-tile-1.textpb
diff --git a/core/res/geoid_height_map_assets/tile-3.textpb b/core/res/geoid_map_assets/geoid-height-disk-tile-3.textpb
similarity index 100%
rename from core/res/geoid_height_map_assets/tile-3.textpb
rename to core/res/geoid_map_assets/geoid-height-disk-tile-3.textpb
diff --git a/core/res/geoid_height_map_assets/tile-5.textpb b/core/res/geoid_map_assets/geoid-height-disk-tile-5.textpb
similarity index 100%
rename from core/res/geoid_height_map_assets/tile-5.textpb
rename to core/res/geoid_map_assets/geoid-height-disk-tile-5.textpb
diff --git a/core/res/geoid_height_map_assets/tile-7.textpb b/core/res/geoid_map_assets/geoid-height-disk-tile-7.textpb
similarity index 100%
rename from core/res/geoid_height_map_assets/tile-7.textpb
rename to core/res/geoid_map_assets/geoid-height-disk-tile-7.textpb
diff --git a/core/res/geoid_height_map_assets/tile-9.textpb b/core/res/geoid_map_assets/geoid-height-disk-tile-9.textpb
similarity index 100%
rename from core/res/geoid_height_map_assets/tile-9.textpb
rename to core/res/geoid_map_assets/geoid-height-disk-tile-9.textpb
diff --git a/core/res/geoid_height_map_assets/tile-b.textpb b/core/res/geoid_map_assets/geoid-height-disk-tile-b.textpb
similarity index 100%
rename from core/res/geoid_height_map_assets/tile-b.textpb
rename to core/res/geoid_map_assets/geoid-height-disk-tile-b.textpb
diff --git a/core/res/geoid_height_map_assets/map-params.textpb b/core/res/geoid_map_assets/geoid-height-params.textpb
similarity index 100%
rename from core/res/geoid_height_map_assets/map-params.textpb
rename to core/res/geoid_map_assets/geoid-height-params.textpb
diff --git a/core/res/res/drawable/ic_notification_summary_auto.xml b/core/res/res/drawable/ic_notification_summary_auto.xml
new file mode 100644
index 0000000..908bba8
--- /dev/null
+++ b/core/res/res/drawable/ic_notification_summary_auto.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="48dp"
+ android:height="48dp"
+ android:viewportWidth="960"
+ android:viewportHeight="960"
+ android:tint="?attr/colorControlNormal">
+ <path
+ android:fillColor="@android:color/white"
+ android:pathData="M260,760Q236,760 218,742Q200,724 200,700L200,140Q200,116 218,98Q236,80 260,80L820,80Q844,80 862,98Q880,116 880,140L880,700Q880,724 862,742Q844,760 820,760L260,760ZM260,700L820,700Q820,700 820,700Q820,700 820,700L820,140Q820,140 820,140Q820,140 820,140L260,140Q260,140 260,140Q260,140 260,140L260,700Q260,700 260,700Q260,700 260,700ZM140,880Q116,880 98,862Q80,844 80,820L80,200L140,200L140,820Q140,820 140,820Q140,820 140,820L760,820L760,880L140,880ZM260,140L260,140Q260,140 260,140Q260,140 260,140L260,700Q260,700 260,700Q260,700 260,700L260,700Q260,700 260,700Q260,700 260,700L260,140Q260,140 260,140Q260,140 260,140Z"/>
+</vector>
diff --git a/core/res/res/layout/autofill_save.xml b/core/res/res/layout/autofill_save.xml
index ddedca2..50ff7c9 100644
--- a/core/res/res/layout/autofill_save.xml
+++ b/core/res/res/layout/autofill_save.xml
@@ -79,16 +79,16 @@
android:layout_height="1dp"
style="@style/AutofillHalfSheetDivider" />
- <com.android.internal.widget.ButtonBarLayout
+ <com.android.server.autofill.ui.BottomSheetButtonBarLayout
+ android:id="@+id/autofill_save_button_bar"
android:layout_width="match_parent"
- android:layout_height="48dp"
+ android:layout_height="wrap_content"
android:layout_gravity="end"
android:clipToPadding="false"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"
android:theme="@style/Theme.DeviceDefault.AutofillHalfScreenDialogButton"
android:orientation="horizontal"
- android:gravity="center_vertical"
android:layout_marginStart="@dimen/autofill_save_outer_margin"
android:layout_marginEnd="@dimen/autofill_save_outer_margin"
>
@@ -100,12 +100,15 @@
android:paddingEnd="12dp"
android:paddingTop="0dp"
android:paddingBottom="0dp"
+ android:layout_marginTop="4dp"
+ android:layout_marginBottom="4dp"
android:minWidth="0dp"
style="?android:attr/borderlessButtonStyle"
android:text="@string/autofill_save_no">
</Button>
<Space
+ android:id="@+id/autofill_button_bar_spacer"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_weight="1"
@@ -116,12 +119,14 @@
android:id="@+id/autofill_save_yes"
android:layout_width="wrap_content"
android:layout_height="40dp"
+ android:layout_marginTop="4dp"
+ android:layout_marginBottom="4dp"
android:minWidth="0dp"
style="@style/AutofillHalfSheetTonalButton"
android:text="@string/autofill_save_yes">
</Button>
- </com.android.internal.widget.ButtonBarLayout>
+ </com.android.server.autofill.ui.BottomSheetButtonBarLayout>
</com.android.server.autofill.ui.BottomSheetLayout>
</LinearLayout>
\ No newline at end of file
diff --git a/core/res/res/values/attrs.xml b/core/res/res/values/attrs.xml
index 2eb28eb..908eeeb 100644
--- a/core/res/res/values/attrs.xml
+++ b/core/res/res/values/attrs.xml
@@ -2292,7 +2292,17 @@
{@link android.R.attr#windowDrawsSystemBarBackgrounds} and the status bar must not
have been requested to be translucent with
{@link android.R.attr#windowTranslucentStatus}.
- Corresponds to {@link android.view.Window#setStatusBarColor(int)}. -->
+ Corresponds to {@link android.view.Window#setStatusBarColor(int)}.
+ <p>If the color is transparent and the window enforces the status bar contrast, the
+ system will determine whether a scrim is necessary and draw one on behalf of the app to
+ ensure that the status bar has enough contrast with the contents of this app, and set
+ an appropriate effective bar background accordingly.
+ See: {@link android.R.attr#enforceStatusBarContrast}
+ <p>If the app targets
+ {@link android.os.Build.VERSION_CODES#VANILLA_ICE_CREAM VANILLA_ICE_CREAM} or above,
+ this attribute is ignored.
+ @deprecated Draw proper background behind
+ {@link android.view.WindowInsets.Type#statusBars()}} instead. -->
<attr name="statusBarColor" format="color" />
<!-- The color for the navigation bar. If the color is not opaque, consider setting
@@ -2302,7 +2312,18 @@
{@link android.R.attr#windowDrawsSystemBarBackgrounds} and the navigation bar must not
have been requested to be translucent with
{@link android.R.attr#windowTranslucentNavigation}.
- Corresponds to {@link android.view.Window#setNavigationBarColor(int)}. -->
+ Corresponds to {@link android.view.Window#setNavigationBarColor(int)}.
+ <p>If the color is transparent and the window enforces the navigation bar contrast, the
+ system will determine whether a scrim is necessary and draw one on behalf of the app to
+ ensure that the navigation bar has enough contrast with the contents of this app, and
+ set an appropriate effective bar background accordingly.
+ See: {@link android.R.attr#enforceNavigationBarContrast}
+ <p>If the app targets
+ {@link android.os.Build.VERSION_CODES#VANILLA_ICE_CREAM VANILLA_ICE_CREAM} or above,
+ this attribute is ignored.
+ @deprecated Draw proper background behind
+ {@link android.view.WindowInsets.Type#navigationBars()} or
+ {@link android.view.WindowInsets.Type#tappableElement()} instead. -->
<attr name="navigationBarColor" format="color" />
<!-- Shows a thin line of the specified color between the navigation bar and the app
@@ -2311,7 +2332,13 @@
{@link android.R.attr#windowDrawsSystemBarBackgrounds} and the navigation bar must not
have been requested to be translucent with
{@link android.R.attr#windowTranslucentNavigation}.
- Corresponds to {@link android.view.Window#setNavigationBarDividerColor(int)}. -->
+ Corresponds to {@link android.view.Window#setNavigationBarDividerColor(int)}.
+ <p>If the app targets
+ {@link android.os.Build.VERSION_CODES#VANILLA_ICE_CREAM VANILLA_ICE_CREAM} or above,
+ this attribute is ignored.
+ @deprecated Draw proper background behind
+ {@link android.view.WindowInsets.Type#navigationBars()} or
+ {@link android.view.WindowInsets.Type#tappableElement()} instead. -->
<attr name="navigationBarDividerColor" format="color" />
<!-- Sets whether the system should ensure that the status bar has enough
@@ -2327,7 +2354,9 @@
<p>If the app does not target at least {@link android.os.Build.VERSION_CODES#Q Q},
this attribute is ignored.
- @see android.view.Window#setStatusBarContrastEnforced -->
+ @see android.view.Window#setStatusBarContrastEnforced
+ @deprecated Draw proper background behind
+ {@link android.view.WindowInsets.Type#statusBars()}} instead. -->
<attr name="enforceStatusBarContrast" format="boolean" />
<!-- Sets whether the system should ensure that the navigation bar has enough
@@ -2483,6 +2512,31 @@
<!-- The icon is shown unless the launching app specified SPLASH_SCREEN_STYLE_EMPTY -->
<enum name="icon_preferred" value="1" />
</attr>
+
+ <!-- Flag indicating whether this window would opt-out the edge-to-edge enforcement.
+
+ <p>If this is false, the edge-to-edge enforcement will be applied to the window if its
+ app targets
+ {@link android.os.Build.VERSION_CODES#VANILLA_ICE_CREAM VANILLA_ICE_CREAM} or above.
+ The affected behaviors are:
+ <ul>
+ <li>The framework will not fit the content view to the insets and will just pass
+ through the {@link android.view.WindowInsets} to the content view, as if calling
+ {@link android.view.Window#setDecorFitsSystemWindows(boolean)} with false.
+ <li>{@link android.view.WindowManager.LayoutParams#layoutInDisplayCutoutMode} of
+ the non-floating windows will be set to {@link
+ android.view.WindowManager.LayoutParams#LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS}.
+ Changing it to other values will cause {@link lang.IllegalArgumentException}.
+ <li>The framework will set {@link android.R.attr#statusBarColor},
+ {@link android.R.attr#navigationBarColor}, and
+ {@link android.R.attr#navigationBarDividerColor} to transparent.
+ </ul>
+
+ <p>If this is true, the edge-to-edge enforcement won't be applied. However, this
+ attribute will be deprecated and disabled in a future SDK level.
+
+ <p>This is false by default. -->
+ <attr name="windowOptOutEdgeToEdgeEnforcement" format="boolean"/>
</declare-styleable>
<!-- The set of attributes that describe a AlertDialog's theme. -->
@@ -4375,9 +4429,14 @@
<!-- Specify one or more <code>polling-loop-filter</code> elements inside a
<code>host-apdu-service</code> to indicate polling loop frames that
your service can handle. -->
+ <!-- @FlaggedApi("android.nfc.Flags.FLAG_NFC_READ_POLLING_LOOP") -->
<declare-styleable name="PollingLoopFilter">
<!-- The polling loop frame. This attribute is mandatory. -->
<attr name="name" />
+ <!-- Whether or not the system should automatically start a transaction when this polling
+ loop filter matches. If not set, default value is false. -->
+ <!-- @FlaggedApi("android.nfc.Flags.FLAG_NFC_READ_POLLING_LOOP") -->
+ <attr name="autoTransact" format="boolean"/>
</declare-styleable>
<!-- Use <code>host-nfcf-service</code> as the root tag of the XML resource that
@@ -5783,7 +5842,17 @@
<enum name="none" value="0" />
<!-- Justification by stretching word spacing. -->
<enum name="inter_word" value = "1" />
+ <!-- Justification by stretching letter spacing. -->
+ <!-- @FlaggedApi("com.android.text.flags.inter_character_justification") -->
+ <enum name="inter_character" value = "2" />
</attr>
+ <!-- Whether to use width of bounding box as a source of automatic line breaking and
+ drawing.
+ If this value is false, the TextView determines the View width, drawing offset and
+ automatic line breaking based on total advances as text widths. By setting true,
+ use glyph bound's as a source of text width. -->
+ <!-- @FlaggedApi("com.android.text.flags.use_bounds_for_width") -->
+ <attr name="useBoundsForWidth" format="boolean" />
</declare-styleable>
<declare-styleable name="TextViewAppearance">
<!-- Base text color, typeface, size, and style. -->
diff --git a/core/res/res/values/attrs_manifest.xml b/core/res/res/values/attrs_manifest.xml
index 6884fc0..4741012 100644
--- a/core/res/res/values/attrs_manifest.xml
+++ b/core/res/res/values/attrs_manifest.xml
@@ -1273,11 +1273,17 @@
null to indicate no split types are offered. -->
<attr name="splitTypes" format="string" />
- <!-- Flag to specify if this app wants to run the dex within its APK but not extracted or
- locally compiled variants. This keeps the dex code protected by the APK signature. Such
- apps will always run in JIT mode (same when they are first installed), and the system will
- never generate ahead-of-time compiled code for them. Depending on the app's workload,
- there may be some run time performance change, noteably the cold start time. -->
+ <!-- Flag to specify if this app (or process) wants to run the dex within its APK but not
+ extracted or locally compiled variants. This keeps the dex code protected by the APK
+ signature. Such apps (or processes) will always run in JIT mode (same when they are first
+ installed). If enabled at the app level, the system will never generate ahead-of-time
+ compiled code for the app. Depending on the app's workload, there may be some run time
+ performance change, noteably the cold start time.
+
+ <p>This attribute can be applied to either
+ {@link android.R.styleable#AndroidManifestProcess process} or
+ {@link android.R.styleable#AndroidManifestApplication application} tags. If enabled at the
+ app level, any process level attribute is effectively ignored. -->
<attr name="useEmbeddedDex" format="boolean" />
<!-- Extra options for an activity's UI. Applies to either the {@code <activity>} or
@@ -1602,6 +1608,10 @@
This is a private attribute, used without android: namespace. -->
<attr name="updatableSystem" format="boolean" />
+ <!-- Allows each installer in the system image to designate another app in the system image to
+ update the installer. -->
+ <attr name="emergencyInstaller" format="string" />
+
<!-- Specify the type of foreground service. Multiple types can be specified by ORing the flags
together. -->
<attr name="foregroundServiceType">
@@ -2793,6 +2803,7 @@
<attr name="gwpAsanMode" />
<attr name="memtagMode" />
<attr name="nativeHeapZeroInitialized" />
+ <attr name="useEmbeddedDex" />
</declare-styleable>
<!-- The <code>deny-permission</code> tag specifies that a permission is to be denied
@@ -3267,6 +3278,31 @@
<p> By default, the behavior is configured by the same attribute in application.
-->
<attr name="enableOnBackInvokedCallback" format="boolean"/>
+
+ <!-- Specifies permissions necessary to launch this activity via
+ {@link android.content.Context#startActivity} when passing content URIs. The default
+ value is {@code none}, meaning no specific permissions are required. Setting this
+ attribute restricts activity invocation based on the invoker's permissions. If the
+ invoker doesn't have the required permissions, the activity start will be denied via a
+ {@link java.lang.SecurityException}.
+
+ <p> Note that the enforcement works for content URIs inside
+ {@link android.content.Intent#getData} and {@link android.content.Intent#getClipData}.
+ @FlaggedApi("android.security.content_uri_permission_apis") -->
+ <attr name="requireContentUriPermissionFromCaller" format="string">
+ <!-- Default, no specific permissions are required. -->
+ <enum name="none" value="0" />
+ <!-- Enforces the invoker to have read access to the passed content URIs. -->
+ <enum name="read" value="1" />
+ <!-- Enforces the invoker to have write access to the passed content URIs. -->
+ <enum name="write" value="2" />
+ <!-- Enforces the invoker to have either read or write access to the passed content
+ URIs. -->
+ <enum name="readOrWrite" value="3" />
+ <!-- Enforces the invoker to have both read and write access to the passed content
+ URIs. -->
+ <enum name="readAndWrite" value="4" />
+ </attr>
</declare-styleable>
<!-- The <code>activity-alias</code> tag declares a new
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index 238f242..d4e727e 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -2998,12 +2998,13 @@
will be locked. -->
<bool name="config_multiuserDelayUserDataLocking">false</bool>
- <!-- Whether the device allows users to start in background visible on displays.
+ <!-- Whether the device allows full users to start in background visible on displays.
+ Note: this flag does NOT control the Communal Profile, which is not a full user.
Should be false for all devices in production. Can be enabled only for development use
in automotive vehicles with passenger displays. -->
<bool name="config_multiuserVisibleBackgroundUsers">false</bool>
- <!-- Whether the device allows users to start in background visible on the default display.
+ <!-- Whether the device allows full users to start in background visible on the default display.
Should be false for all devices in production. Can be enabled only for development use
in passenger-only automotive build (i.e., when Android runs in a separate system in the
back seat to manage the passenger displays).
@@ -4054,16 +4055,22 @@
<bool name="config_maskMainBuiltInDisplayCutout">false</bool>
<!-- This string array provide override side of each rotation of the given insets.
- Array of "[rotation],[side]".
- Undefined rotation will apply the default behavior.
+ Array of [side] for rotation 0, 90, 180 and 270 in order.
+ The options of [side] are:
+ - Option 0 - Left.
+ - Option 1 - Top.
+ - Option 2 - Right.
+ - Option 3 - Bottom.
When there are cutouts on multiple edges of the display, the override won't take any
effect. -->
- <string-array name="config_mainBuiltInDisplayCutoutSideOverride" translatable="false">
- <!-- Example:
- <item>90,top</item>
- <item>270,bottom</item>
+ <integer-array name="config_mainBuiltInDisplayCutoutSideOverride">
+ <!-- Example of at top for rotation 0 and 90, and at bottom for rotation 180 and 270:
+ <item>1</item>
+ <item>1</item>
+ <item>3</item>
+ <item>3</item>
-->
- </string-array>
+ </integer-array>
<!-- Ultrasound support for Mic/speaker path -->
<!-- Whether the default microphone audio source supports near-ultrasound frequencies
@@ -5378,19 +5385,20 @@
and a second time clipped to the fill level to indicate charge -->
<bool name="config_batterymeterDualTone">false</bool>
- <!-- The default refresh rate for a given device. This value is used to set the
- global refresh rate vote, and when set to zero it has no effect on the vote.
- If this value is non-zero but the hardware composer on the device supports
- display modes with higher refresh rates, the framework may use those higher
- refresh rate modes if an app chooses one by setting preferredDisplayModeId
- or calling setFrameRate().-->
- <integer name="config_defaultRefreshRate">0</integer>
+ <!-- The default refresh rate for a given device. Change this value to set a higher default
+ refresh rate. If the hardware composer on the device supports display modes with a higher
+ refresh rate than the default value specified here, the framework may use those higher
+ refresh rate modes if an app chooses one by setting preferredDisplayModeId or calling
+ setFrameRate().
+ If a non-zero value is set for config_defaultPeakRefreshRate, then
+ config_defaultRefreshRate may be set to 0, in which case the value set for
+ config_defaultPeakRefreshRate will act as the default frame rate. -->
+ <integer name="config_defaultRefreshRate">60</integer>
- <!-- The default peak refresh rate for a given device. This value is used to set the
- global peak refresh rate vote, and when set to zero it has no effect on the vote.
- Change this value to non-zero if you want to prevent the framework from using higher
- refresh rates, even if display modes with higher refresh rates are available from
- hardware composer. -->
+ <!-- The default peak refresh rate for a given device. Change this value if you want to prevent
+ the framework from using higher refresh rates, even if display modes with higher refresh
+ rates are available from hardware composer. Only has an effect if the value is
+ non-zero. -->
<integer name="config_defaultPeakRefreshRate">0</integer>
<!-- External display peak refresh rate for the given device. Change this value if you want to
@@ -6943,4 +6951,7 @@
<!-- Name of the starting activity for DisplayCompat host. specific to automotive.-->
<string name="config_defaultDisplayCompatHostActivity" translatable="false"></string>
+
+ <!-- Whether to use file hashes cache in watchlist-->
+ <bool name="config_watchlistUseFileHashesCache">false</bool>
</resources>
diff --git a/core/res/res/values/dimens.xml b/core/res/res/values/dimens.xml
index 96c4bf4..0acccee 100644
--- a/core/res/res/values/dimens.xml
+++ b/core/res/res/values/dimens.xml
@@ -887,6 +887,8 @@
<dimen name="autofill_save_scroll_view_top_margin">16dp</dimen>
<dimen name="autofill_save_button_bar_padding">16dp</dimen>
<dimen name="autofill_dialog_corner_radius">24dp</dimen>
+ <dimen name="autofill_button_bar_spacer_width">12dp</dimen>
+ <dimen name="autofill_button_bar_spacer_height">4dp</dimen>
<!-- How much extra space should be left around the autofill dialog -->
<dimen name="autofill_dialog_offset">72dp</dimen>
diff --git a/core/res/res/values/public-staging.xml b/core/res/res/values/public-staging.xml
index 830e99c..f9cf28c 100644
--- a/core/res/res/values/public-staging.xml
+++ b/core/res/res/values/public-staging.xml
@@ -143,6 +143,14 @@
<public name="fragmentAdvancedPattern"/>
<!-- @FlaggedApi("android.content.pm.relative_reference_intent_filters") -->
<public name="fragmentSuffix"/>
+ <!-- @FlaggedApi("com.android.text.flags.use_bounds_for_width") -->
+ <public name="useBoundsForWidth"/>
+ <!-- @FlaggedApi("android.nfc.Flags.FLAG_NFC_READ_POLLING_LOOP") -->
+ <public name="autoTransact"/>
+ <!-- @FlaggedApi("com.android.window.flags.enforce_edge_to_edge") -->
+ <public name="windowOptOutEdgeToEdgeEnforcement"/>
+ <!-- @FlaggedApi("android.security.content_uri_permission_apis") -->
+ <public name="requireContentUriPermissionFromCaller" />
</staging-public-group>
<staging-public-group type="id" first-id="0x01bc0000">
diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml
index ec47313..be96cc2 100644
--- a/core/res/res/values/strings.xml
+++ b/core/res/res/values/strings.xml
@@ -396,6 +396,27 @@
<!-- Displayed when the call forwarding query was set but forwarding is not enabled. -->
<string name="cfTemplateRegisteredTime"><xliff:g id="bearer_service_code">{0}</xliff:g>: Not forwarded</string>
+ <!-- Title of the cellular network security safety center source's status. -->
+ <string name="scCellularNetworkSecurityTitle">Cellular network security</string>
+ <!-- Summary of the cellular network security safety center source's status. -->
+ <string name="scCellularNetworkSecuritySummary">Review settings</string>
+ <!-- Title of the safety center issue and notification when the phone's identifier is shared over the network. -->
+ <string name="scIdentifierDisclosureIssueTitle">Device identifier accessed</string>
+ <!-- Summary of the safety center issue and notification when the phone's identifier is shared over the network. -->
+ <string name="scIdentifierDisclosureIssueSummary">A network on the <xliff:g id="disclosure_network">%4$s</xliff:g> connection recorded your device\'s unique identifier (IMSI) <xliff:g id="disclosure_count">%1$d</xliff:g> times in the period between <xliff:g id="disclosure_window_start_time">%2$tr</xliff:g> and <xliff:g id="disclosure_window_end_time">%3$tr</xliff:g>.</string>
+ <!-- Title of the safety center issue and notification when the phone restores an encrypted connection to the network. -->
+ <string name="scNullCipherIssueEncryptedTitle">Encrypted connection to <xliff:g id="network_name">%1$s</xliff:g></string>
+ <!-- Summary of the safety center issue and notification when the phone restores an encrypted connection to the network. -->
+ <string name="scNullCipherIssueEncryptedSummary">You\'re now connected to a more secure cellular network.</string>
+ <!-- Title of the safety center issue and notification when a connected network is not using encryption. -->
+ <string name="scNullCipherIssueNonEncryptedTitle">Non-encrypted connection to <xliff:g id="network_name">%1$s</xliff:g></string>
+ <!-- Summary of the safety center issue and notification when a connected network is not using encryption. -->
+ <string name="scNullCipherIssueNonEncryptedSummary">You\'re connected to a non-encrypted cellular network. Your calls, messages, and data are vulnerable to interception.</string>
+ <!-- Label for the button that links to the cellular network security settings. -->
+ <string name="scNullCipherIssueActionSettings">Cellular security settings</string>
+ <!-- Label for the button that link to education resourcess about cellular network security settings. -->
+ <string name="scNullCipherIssueActionLearnMore">Learn more</string>
+
<!-- android.net.http Error strings --> <skip />
<!-- Displayed when a feature code (non-phone number) is dialed and completes successfully. -->
<string name="fcComplete">Feature code complete.</string>
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index 699c8ac..58427b1 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -954,6 +954,16 @@
<java-symbol type="string" name="roamingText8" />
<java-symbol type="string" name="roamingText9" />
<java-symbol type="string" name="roamingTextSearching" />
+ <java-symbol type="string" name="scCellularNetworkSecuritySummary" />
+ <java-symbol type="string" name="scCellularNetworkSecurityTitle" />
+ <java-symbol type="string" name="scIdentifierDisclosureIssueSummary" />
+ <java-symbol type="string" name="scIdentifierDisclosureIssueTitle" />
+ <java-symbol type="string" name="scNullCipherIssueActionLearnMore" />
+ <java-symbol type="string" name="scNullCipherIssueActionSettings" />
+ <java-symbol type="string" name="scNullCipherIssueEncryptedSummary" />
+ <java-symbol type="string" name="scNullCipherIssueEncryptedTitle" />
+ <java-symbol type="string" name="scNullCipherIssueNonEncryptedSummary" />
+ <java-symbol type="string" name="scNullCipherIssueNonEncryptedTitle" />
<java-symbol type="string" name="selected" />
<java-symbol type="string" name="sendText" />
<java-symbol type="string" name="sending" />
@@ -3136,6 +3146,7 @@
<java-symbol type="drawable" name="ic_collapse_notification" />
<java-symbol type="drawable" name="ic_expand_bundle" />
<java-symbol type="drawable" name="ic_collapse_bundle" />
+ <java-symbol type="drawable" name="ic_notification_summary_auto" />
<java-symbol type="dimen" name="notification_header_shrink_min_width" />
<java-symbol type="dimen" name="notification_header_shrink_hide_width" />
<java-symbol type="dimen" name="notification_content_margin_start" />
@@ -3717,6 +3728,7 @@
<java-symbol type="id" name="autofill_save_no" />
<java-symbol type="id" name="autofill_save_title" />
<java-symbol type="id" name="autofill_save_yes" />
+ <java-symbol type="id" name="autofill_button_bar_spacer" />
<java-symbol type="id" name="autofill_service_icon" />
<java-symbol type="id" name="autofill_dialog_picker"/>
<java-symbol type="id" name="autofill_dialog_header"/>
@@ -3763,6 +3775,8 @@
<java-symbol type="dimen" name="autofill_dialog_max_width" />
<java-symbol type="dimen" name="autofill_dialog_offset"/>
<java-symbol type="dimen" name="autofill_save_outer_margin"/>
+ <java-symbol type="dimen" name="autofill_button_bar_spacer_width"/>
+ <java-symbol type="dimen" name="autofill_button_bar_spacer_height"/>
<java-symbol type="bool" name="autofill_dialog_horizontal_space_included"/>
@@ -5336,4 +5350,6 @@
<java-symbol type="string" name="satellite_notification_open_message" />
<java-symbol type="string" name="satellite_notification_how_it_works" />
<java-symbol type="drawable" name="ic_satellite_alt_24px" />
+
+ <java-symbol type="bool" name="config_watchlistUseFileHashesCache" />
</resources>
diff --git a/core/res/res/xml/sms_short_codes.xml b/core/res/res/xml/sms_short_codes.xml
index 9bb2499..61e6a36 100644
--- a/core/res/res/xml/sms_short_codes.xml
+++ b/core/res/res/xml/sms_short_codes.xml
@@ -127,7 +127,7 @@
<!-- France: 5 digits, free: 3xxxx, premium [4-8]xxxx, plus EU:
http://clients.txtnation.com/entries/161972-france-premium-sms-short-code-requirements,
visual voicemail code for Orange: 21101 -->
- <shortcode country="fr" premium="[4-8]\\d{4}" free="3\\d{4}|116\\d{3}|21101|20366|555|2051" />
+ <shortcode country="fr" premium="[4-8]\\d{4}" free="3\\d{4}|116\\d{3}|21101|20366|555|2051|33033" />
<!-- United Kingdom (Great Britain): 4-6 digits, common codes [5-8]xxxx, plus EU:
http://www.short-codes.com/media/Co-regulatoryCodeofPracticeforcommonshortcodes170206.pdf,
@@ -150,6 +150,9 @@
http://clients.txtnation.com/entries/209633-hungary-premium-sms-short-code-regulations -->
<shortcode country="hu" pattern="[01](?:\\d{3}|\\d{9})" premium="0691227910|1784" free="116\\d{3}" />
+ <!-- Honduras -->
+ <shortcode country="hn" pattern="\\d{4,6}" free="466453" />
+
<!-- India: 1-5 digits (standard system default, not country specific) -->
<shortcode country="in" pattern="\\d{1,5}" free="59336|53969" />
@@ -171,7 +174,7 @@
<shortcode country="jp" pattern="\\d{1,5}" free="8083" />
<!-- Kenya: 5 digits, known premium codes listed -->
- <shortcode country="ke" pattern="\\d{5}" free="21725|21562|40520|23342|40023" />
+ <shortcode country="ke" pattern="\\d{5}" free="21725|21562|40520|23342|40023|24088|23054" />
<!-- Kyrgyzstan: 4 digits, known premium codes listed -->
<shortcode country="kg" pattern="\\d{4}" premium="415[2367]|444[69]" />
@@ -183,7 +186,7 @@
<shortcode country="kz" pattern="\\d{4}" premium="335[02]|4161|444[469]|77[2359]0|8444|919[3-5]|968[2-5]" />
<!-- Kuwait: 1-5 digits (standard system default, not country specific) -->
- <shortcode country="kw" pattern="\\d{1,5}" free="1378|50420|94006|55991" />
+ <shortcode country="kw" pattern="\\d{1,5}" free="1378|50420|94006|55991|50976" />
<!-- Lithuania: 3-5 digits, known premium codes listed, plus EU -->
<shortcode country="lt" pattern="\\d{3,5}" premium="13[89]1|1394|16[34]5" free="116\\d{3}|1399|1324" />
@@ -195,9 +198,18 @@
<!-- Latvia: 4 digits, known premium codes listed, plus EU -->
<shortcode country="lv" pattern="\\d{4}" premium="18(?:19|63|7[1-4])" free="116\\d{3}|1399" />
+ <!-- Morocco: 1-5 digits (standard system default, not country specific) -->
+ <shortcode country="ma" pattern="\\d{1,5}" free="53819" />
+
<!-- Macedonia: 1-6 digits (not confirmed), known premium codes listed -->
<shortcode country="mk" pattern="\\d{1,6}" free="129005|122" />
+ <!-- Malawi: 1-5 digits (standard system default, not country specific) -->
+ <shortcode country="mw" pattern="\\d{1,5}" free="4276" />
+
+ <!-- Mozambique: 1-5 digits (standard system default, not country specific) -->
+ <shortcode country="mz" pattern="\\d{1,5}" free="1714" />
+
<!-- Mexico: 4-5 digits (not confirmed), known premium codes listed -->
<shortcode country="mx" pattern="\\d{4,6}" premium="53035|7766" free="26259|46645|50025|50052|5050|76551|88778|9963|91101|45453|550346" />
@@ -207,6 +219,9 @@
<!-- Namibia: 1-5 digits (standard system default, not country specific) -->
<shortcode country="na" pattern="\\d{1,5}" free="40005" />
+ <!-- Nicaragua -->
+ <shortcode country="ni" pattern="\\d{4,6}" free="466453" />
+
<!-- The Netherlands, 4 digits, known premium codes listed, plus EU -->
<shortcode country="nl" pattern="\\d{4}" premium="4466|5040" free="116\\d{3}|2223|6225|2223|1662" />
@@ -219,8 +234,8 @@
<!-- New Zealand: 3-4 digits, known premium codes listed -->
<shortcode country="nz" pattern="\\d{3,4}" premium="3903|8995|4679" free="1737|176|2141|3067|3068|3110|3876|4006|4053|4061|4062|4202|4300|4334|4412|4575|5626|8006|8681" />
- <!-- Peru: 4-5 digits (not confirmed), known premium codes listed -->
- <shortcode country="pe" pattern="\\d{4,5}" free="9963|40778" />
+ <!-- Peru: 4-6 digits (not confirmed), known premium codes listed -->
+ <shortcode country="pe" pattern="\\d{4,6}" free="9963|40778|301303" />
<!-- Philippines -->
<shortcode country="ph" pattern="\\d{1,5}" free="2147|5495|5496" />
@@ -269,6 +284,12 @@
<!-- Slovakia: 4 digits (premium), plus EU: http://www.cmtelecom.com/premium-sms/slovakia -->
<shortcode country="sk" premium="\\d{4}" free="116\\d{3}|8000" />
+ <!-- Senegal(SN): 1-5 digits (standard system default, not country specific) -->
+ <shortcode country="sn" pattern="\\d{1,5}" free="21215" />
+
+ <!-- El Salvador(SV): 1-5 digits (standard system default, not country specific) -->
+ <shortcode country="sv" pattern="\\d{4,6}" free="466453" />
+
<!-- Taiwan -->
<shortcode country="tw" pattern="\\d{4}" free="1922" />
@@ -278,15 +299,21 @@
<!-- Tajikistan: 4 digits, known premium codes listed -->
<shortcode country="tj" pattern="\\d{4}" premium="11[3-7]1|4161|4333|444[689]" />
+ <!-- Tanzania: 1-5 digits (standard system default, not country specific) -->
+ <shortcode country="tz" pattern="\\d{1,5}" free="15046|15234" />
+
<!-- Turkey -->
<shortcode country="tr" pattern="\\d{1,5}" free="7529|5528|6493|3193" />
<!-- Ukraine: 4 digits, known premium codes listed -->
<shortcode country="ua" pattern="\\d{4}" premium="444[3-9]|70[579]4|7540" />
+ <!-- Uganda(UG): 4 digits (standard system default, not country specific) -->
+ <shortcode country="ug" pattern="\\d{4}" free="8000" />
+
<!-- USA: 5-6 digits (premium codes from https://www.premiumsmsrefunds.com/ShortCodes.htm),
visual voicemail code for T-Mobile: 122 -->
- <shortcode country="us" pattern="\\d{5,6}" premium="20433|21(?:344|472)|22715|23(?:333|847)|24(?:15|28)0|25209|27(?:449|606|663)|28498|305(?:00|83)|32(?:340|941)|33(?:166|786|849)|34746|35(?:182|564)|37975|38(?:135|146|254)|41(?:366|463)|42335|43(?:355|500)|44(?:578|711|811)|45814|46(?:157|173|327)|46666|47553|48(?:221|277|669)|50(?:844|920)|51(?:062|368)|52944|54(?:723|892)|55928|56483|57370|59(?:182|187|252|342)|60339|61(?:266|982)|62478|64(?:219|898)|65(?:108|500)|69(?:208|388)|70877|71851|72(?:078|087|465)|73(?:288|588|882|909|997)|74(?:034|332|815)|76426|79213|81946|83177|84(?:103|685)|85797|86(?:234|236|666)|89616|90(?:715|842|938)|91(?:362|958)|94719|95297|96(?:040|666|835|969)|97(?:142|294|688)|99(?:689|796|807)" standard="44567|244444" free="122|87902|21696|24614|28003|30356|33669|40196|41064|41270|43753|44034|46645|52413|56139|57969|61785|66975|75136|76227|81398|83952|85140|86566|86799|95737|96684|99245|611611" />
+ <shortcode country="us" pattern="\\d{5,6}" premium="20433|21(?:344|472)|22715|23(?:333|847)|24(?:15|28)0|25209|27(?:449|606|663)|28498|305(?:00|83)|32(?:340|941)|33(?:166|786|849)|34746|35(?:182|564)|37975|38(?:135|146|254)|41(?:366|463)|42335|43(?:355|500)|44(?:578|711|811)|45814|46(?:157|173|327)|46666|47553|48(?:221|277|669)|50(?:844|920)|51(?:062|368)|52944|54(?:723|892)|55928|56483|57370|59(?:182|187|252|342)|60339|61(?:266|982)|62478|64(?:219|898)|65(?:108|500)|69(?:208|388)|70877|71851|72(?:078|087|465)|73(?:288|588|882|909|997)|74(?:034|332|815)|76426|79213|81946|83177|84(?:103|685)|85797|86(?:234|236|666)|89616|90(?:715|842|938)|91(?:362|958)|94719|95297|96(?:040|666|835|969)|97(?:142|294|688)|99(?:689|796|807)" standard="44567|244444" free="122|87902|21696|24614|28003|30356|33669|40196|41064|41270|43753|44034|46645|52413|56139|57969|61785|66975|75136|76227|81398|83952|85140|86566|86799|95737|96684|99245|611611|96831" />
<!-- Vietnam: 1-5 digits (standard system default, not country specific) -->
<shortcode country="vn" pattern="\\d{1,5}" free="5001|9055" />
diff --git a/core/tests/BroadcastRadioTests/OWNERS b/core/tests/BroadcastRadioTests/OWNERS
index d2bdd64..51a85e4 100644
--- a/core/tests/BroadcastRadioTests/OWNERS
+++ b/core/tests/BroadcastRadioTests/OWNERS
@@ -1,3 +1,3 @@
xuweilin@google.com
oscarazu@google.com
-keunyoung@google.com
+ericjeong@google.com
diff --git a/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/aidl/TunerSessionTest.java b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/aidl/TunerSessionTest.java
index 296c451..262f167 100644
--- a/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/aidl/TunerSessionTest.java
+++ b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/aidl/TunerSessionTest.java
@@ -31,6 +31,7 @@
import static org.junit.Assert.assertThrows;
+import android.Manifest;
import android.app.compat.CompatChanges;
import android.graphics.Bitmap;
import android.hardware.broadcastradio.ConfigFlag;
@@ -57,6 +58,8 @@
import android.util.ArrayMap;
import android.util.ArraySet;
+import androidx.test.platform.app.InstrumentationRegistry;
+
import com.android.dx.mockito.inline.extended.StaticMockitoSessionBuilder;
import com.android.server.broadcastradio.ExtendedRadioMockitoTestCase;
import com.android.server.broadcastradio.RadioServiceUserController;
@@ -171,6 +174,10 @@
@Before
public void setup() throws Exception {
+ InstrumentationRegistry.getInstrumentation().getUiAutomation()
+ .adoptShellPermissionIdentity(Manifest.permission.LOG_COMPAT_CHANGE,
+ Manifest.permission.READ_COMPAT_CHANGE_CONFIG);
+
doReturn(true).when(() -> CompatChanges.isChangeEnabled(
eq(ConversionUtils.RADIO_U_VERSION_REQUIRED), anyInt()));
doReturn(USER_ID_1).when(mUserHandleMock).getIdentifier();
diff --git a/core/tests/coretests/Android.bp b/core/tests/coretests/Android.bp
index 513e022..ee1a4ac 100644
--- a/core/tests/coretests/Android.bp
+++ b/core/tests/coretests/Android.bp
@@ -66,6 +66,7 @@
"android.view.accessibility.flags-aconfig-java",
"androidx.core_core",
"androidx.core_core-ktx",
+ "androidx.test.core",
"androidx.test.espresso.core",
"androidx.test.ext.junit",
"androidx.test.runner",
@@ -90,6 +91,7 @@
"flickerlib-parsers",
"flickerlib-trace_processor_shell",
"mockito-target-extended-minus-junit4",
+ "TestParameterInjector",
],
libs: [
@@ -208,6 +210,7 @@
"testng",
],
srcs: [
+ "src/android/app/ActivityManagerTest.java",
"src/android/content/pm/PackageManagerTest.java",
"src/android/content/pm/UserInfoTest.java",
"src/android/database/CursorWindowTest.java",
diff --git a/packages/SoundPicker2/src/com/android/soundpicker/RingtonePickerApplication.java b/core/tests/coretests/res/drawable/adaptiveicon_drawable.xml
similarity index 64%
copy from packages/SoundPicker2/src/com/android/soundpicker/RingtonePickerApplication.java
copy to core/tests/coretests/res/drawable/adaptiveicon_drawable.xml
index 48fd4fe..dcffe75 100644
--- a/packages/SoundPicker2/src/com/android/soundpicker/RingtonePickerApplication.java
+++ b/core/tests/coretests/res/drawable/adaptiveicon_drawable.xml
@@ -1,4 +1,5 @@
-/*
+<?xml version="1.0" encoding="utf-8"?>
+<!--
* Copyright (C) 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
@@ -12,17 +13,10 @@
* 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.soundpicker;
-
-import android.app.Application;
-
-import dagger.hilt.android.HiltAndroidApp;
-
-/**
- * The main application class for the project.
- */
-@HiltAndroidApp(Application.class)
-public class RingtonePickerApplication extends Hilt_RingtonePickerApplication {
-}
+<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
+ <background android:drawable="@android:color/white"/>
+ <foreground android:drawable="@android:color/black"/>
+ <monochrome android:drawable="@android:color/system_accent2_800"/>
+</adaptive-icon>
\ No newline at end of file
diff --git a/core/tests/coretests/src/android/app/ActivityManagerTest.java b/core/tests/coretests/src/android/app/ActivityManagerTest.java
new file mode 100644
index 0000000..d930e4d
--- /dev/null
+++ b/core/tests/coretests/src/android/app/ActivityManagerTest.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app;
+
+import static android.app.ActivityManager.PROCESS_STATE_SERVICE;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import android.os.UserHandle;
+import android.platform.test.ravenwood.RavenwoodRule;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.ByteArrayOutputStream;
+import java.io.PrintWriter;
+
+@RunWith(AndroidJUnit4.class)
+public class ActivityManagerTest {
+ @Rule
+ public final RavenwoodRule mRavenwood = new RavenwoodRule();
+
+ @Test
+ public void testSimple() throws Exception {
+ assertTrue(ActivityManager.isSystemReady());
+ assertFalse(ActivityManager.isUserAMonkey());
+ assertNotEquals(UserHandle.USER_NULL, ActivityManager.getCurrentUser());
+ }
+
+ @Test
+ public void testCapabilities() throws Exception {
+ // For the moment mostly want to confirm we don't crash
+ assertNotNull(ActivityManager.getCapabilitiesSummary(~0));
+ ActivityManager.printCapabilitiesFull(new PrintWriter(new ByteArrayOutputStream()), ~0);
+ ActivityManager.printCapabilitiesSummary(new PrintWriter(new ByteArrayOutputStream()), ~0);
+ ActivityManager.printCapabilitiesSummary(new StringBuilder(), ~0);
+ }
+
+ @Test
+ public void testProcState() throws Exception {
+ // For the moment mostly want to confirm we don't crash
+ assertNotNull(ActivityManager.procStateToString(PROCESS_STATE_SERVICE));
+ assertNotNull(ActivityManager.processStateAmToProto(PROCESS_STATE_SERVICE));
+ assertTrue(ActivityManager.isProcStateBackground(PROCESS_STATE_SERVICE));
+ assertFalse(ActivityManager.isProcStateCached(PROCESS_STATE_SERVICE));
+ assertFalse(ActivityManager.isForegroundService(PROCESS_STATE_SERVICE));
+ assertFalse(ActivityManager.isProcStateConsideredInteraction(PROCESS_STATE_SERVICE));
+ }
+
+ @Test
+ public void testStartResult() throws Exception {
+ // For the moment mostly want to confirm we don't crash
+ assertTrue(ActivityManager.isStartResultSuccessful(50));
+ assertTrue(ActivityManager.isStartResultFatalError(-50));
+ }
+
+ @Test
+ public void testRestrictionLevel() throws Exception {
+ // For the moment mostly want to confirm we don't crash
+ assertNotNull(ActivityManager.restrictionLevelToName(
+ ActivityManager.RESTRICTION_LEVEL_HIBERNATION));
+ }
+}
diff --git a/core/tests/coretests/src/android/app/LaunchCookieTest.java b/core/tests/coretests/src/android/app/LaunchCookieTest.java
new file mode 100644
index 0000000..9325391
--- /dev/null
+++ b/core/tests/coretests/src/android/app/LaunchCookieTest.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+
+import android.app.ActivityOptions.LaunchCookie;
+import android.os.Parcel;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class LaunchCookieTest {
+
+ @Test
+ public void parcelNonNullLaunchCookie() {
+ LaunchCookie launchCookie = new LaunchCookie();
+ Parcel parcel = Parcel.obtain();
+ LaunchCookie.writeToParcel(launchCookie, parcel);
+ parcel.setDataPosition(0);
+ LaunchCookie unparceledLaunchCookie = LaunchCookie.readFromParcel(parcel);
+ assertEquals(launchCookie, unparceledLaunchCookie);
+ }
+
+ @Test
+ public void parcelNullLaunchCookie() {
+ Parcel parcel = Parcel.obtain();
+ LaunchCookie.writeToParcel(/*launchCookie*/null, parcel);
+ LaunchCookie unparceledLaunchCookie = LaunchCookie.readFromParcel(parcel);
+ assertNull(unparceledLaunchCookie);
+ }
+
+}
diff --git a/core/tests/coretests/src/android/app/NotificationChannelTest.java b/core/tests/coretests/src/android/app/NotificationChannelTest.java
index d8305f0..56ab034 100644
--- a/core/tests/coretests/src/android/app/NotificationChannelTest.java
+++ b/core/tests/coretests/src/android/app/NotificationChannelTest.java
@@ -19,6 +19,9 @@
import static com.google.common.truth.Truth.assertThat;
import static junit.framework.TestCase.assertEquals;
+import static junit.framework.TestCase.assertFalse;
+import static junit.framework.TestCase.assertNull;
+import static junit.framework.TestCase.assertTrue;
import static org.junit.Assert.assertEquals;
import static org.mockito.ArgumentMatchers.any;
@@ -42,6 +45,8 @@
import android.os.Parcel;
import android.os.RemoteCallback;
import android.os.RemoteException;
+import android.os.VibrationEffect;
+import android.platform.test.flag.junit.SetFlagsRule;
import android.provider.MediaStore.Audio.AudioColumns;
import android.test.mock.MockContentResolver;
import android.util.Xml;
@@ -55,6 +60,7 @@
import com.google.common.base.Strings;
import org.junit.Before;
+import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -63,10 +69,15 @@
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.lang.reflect.Field;
+import java.util.Arrays;
+import java.util.function.Consumer;
@RunWith(AndroidJUnit4.class)
@SmallTest
public class NotificationChannelTest {
+ @Rule
+ public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+
private final String CLASS = "android.app.NotificationChannel";
Context mContext;
@@ -294,28 +305,12 @@
NotificationChannel channel = new NotificationChannel("id", "name", 3);
channel.setSound(uriToBeRestoredCanonicalized, mAudioAttributes);
- TypedXmlSerializer serializer = Xml.newFastSerializer();
- ByteArrayOutputStream baos = new ByteArrayOutputStream();
- serializer.setOutput(new BufferedOutputStream(baos), "utf-8");
- serializer.startDocument(null, true);
-
// mock the canonicalize in writeXmlForBackup -> getSoundForBackup
when(mIContentProvider.canonicalize(any(), eq(uriToBeRestoredUncanonicalized)))
.thenReturn(uriToBeRestoredCanonicalized);
when(mIContentProvider.canonicalize(any(), eq(uriToBeRestoredCanonicalized)))
.thenReturn(uriToBeRestoredCanonicalized);
- channel.writeXmlForBackup(serializer, mContext);
- serializer.endDocument();
- serializer.flush();
-
- TypedXmlPullParser parser = Xml.newFastPullParser();
- byte[] byteArray = baos.toByteArray();
- parser.setInput(new BufferedInputStream(new ByteArrayInputStream(byteArray)), null);
- parser.nextTag();
-
- NotificationChannel targetChannel = new NotificationChannel("id", "name", 3);
-
MatrixCursor cursor = new MatrixCursor(new String[] {"_id"});
cursor.addRow(new Object[] {100L});
@@ -350,7 +345,263 @@
when(mIContentProvider.canonicalize(any(), eq(uriAfterRestoredUncanonicalized)))
.thenReturn(uriAfterRestoredCanonicalized);
- targetChannel.populateFromXmlForRestore(parser, true, mContext);
- assertThat(targetChannel.getSound()).isEqualTo(uriAfterRestoredCanonicalized);
+ NotificationChannel restoredChannel = backUpAndRestore(channel);
+ assertThat(restoredChannel.getSound()).isEqualTo(uriAfterRestoredCanonicalized);
+ }
+
+ @Test
+ public void testVibrationGetters_nonPatternBasedVibrationEffect_waveform() throws Exception {
+ mSetFlagsRule.enableFlags(Flags.FLAG_NOTIFICATION_CHANNEL_VIBRATION_EFFECT_API);
+ NotificationChannel channel = new NotificationChannel("id", "name", 3);
+ // Note that the amplitude used (1) is not the default amplitude, meaning that this effect
+ // does not have an equivalent pattern based effect.
+ VibrationEffect effect = VibrationEffect.createOneShot(123, 1);
+
+ channel.setVibrationEffect(effect);
+
+ Consumer<NotificationChannel> assertions = (testedChannel) -> {
+ assertThat(testedChannel.getVibrationEffect()).isEqualTo(effect);
+ assertNull(testedChannel.getVibrationPattern());
+ assertTrue(testedChannel.shouldVibrate());
+ };
+ assertions.accept(channel);
+ assertions.accept(writeToAndReadFromParcel(channel));
+ assertions.accept(backUpAndRestore(channel));
+ }
+
+ @Test
+ public void testVibrationGetters_nonPatternBasedVibrationEffect_nonWaveform() throws Exception {
+ mSetFlagsRule.enableFlags(Flags.FLAG_NOTIFICATION_CHANNEL_VIBRATION_EFFECT_API);
+ NotificationChannel channel = new NotificationChannel("id", "name", 3);
+ VibrationEffect effect =
+ VibrationEffect
+ .startComposition()
+ .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK)
+ .compose();
+
+ channel.setVibrationEffect(effect);
+
+ Consumer<NotificationChannel> assertions = (testedChannel) -> {
+ assertThat(testedChannel.getVibrationEffect()).isEqualTo(effect);
+ assertNull(testedChannel.getVibrationPattern()); // amplitude not default.
+ assertTrue(testedChannel.shouldVibrate());
+ };
+ assertions.accept(channel);
+ assertions.accept(writeToAndReadFromParcel(channel));
+ assertions.accept(backUpAndRestore(channel));
+ }
+
+ @Test
+ public void testVibrationGetters_patternBasedVibrationEffect_nonRepeating() throws Exception {
+ mSetFlagsRule.enableFlags(Flags.FLAG_NOTIFICATION_CHANNEL_VIBRATION_EFFECT_API);
+ NotificationChannel channel = new NotificationChannel("id", "name", 3);
+ long[] pattern = new long[] {1, 2};
+ VibrationEffect effect = VibrationEffect.createWaveform(pattern, /* repeatIndex= */ -1);
+
+ channel.setVibrationEffect(effect);
+
+ Consumer<NotificationChannel> assertions = (testedChannel) -> {
+ assertThat(testedChannel.getVibrationEffect()).isEqualTo(effect);
+ assertTrue(Arrays.equals(pattern, testedChannel.getVibrationPattern()));
+ assertTrue(testedChannel.shouldVibrate());
+ };
+ assertions.accept(channel);
+ assertions.accept(writeToAndReadFromParcel(channel));
+ assertions.accept(backUpAndRestore(channel));
+ }
+
+ @Test
+ public void testVibrationGetters_patternBasedVibrationEffect_wholeRepeating() throws Exception {
+ mSetFlagsRule.enableFlags(Flags.FLAG_NOTIFICATION_CHANNEL_VIBRATION_EFFECT_API);
+ NotificationChannel channel = new NotificationChannel("id", "name", 3);
+ long[] pattern = new long[] {1, 2};
+ VibrationEffect effect = VibrationEffect.createWaveform(pattern, /* repeatIndex= */ 0);
+
+ channel.setVibrationEffect(effect);
+
+ Consumer<NotificationChannel> assertions = (testedChannel) -> {
+ assertThat(testedChannel.getVibrationEffect()).isEqualTo(effect);
+ assertNull(testedChannel.getVibrationPattern());
+ assertTrue(testedChannel.shouldVibrate());
+ };
+ assertions.accept(channel);
+ assertions.accept(writeToAndReadFromParcel(channel));
+ assertions.accept(backUpAndRestore(channel));
+ }
+
+ @Test
+ public void testVibrationGetters_patternBasedVibrationEffect_partialRepeating()
+ throws Exception {
+ mSetFlagsRule.enableFlags(Flags.FLAG_NOTIFICATION_CHANNEL_VIBRATION_EFFECT_API);
+ NotificationChannel channel = new NotificationChannel("id", "name", 3);
+ long[] pattern = new long[] {1, 2, 3, 4};
+ VibrationEffect effect = VibrationEffect.createWaveform(pattern, /* repeatIndex= */ 2);
+
+ channel.setVibrationEffect(effect);
+
+ Consumer<NotificationChannel> assertions = (testedChannel) -> {
+ assertThat(testedChannel.getVibrationEffect()).isEqualTo(effect);
+ assertNull(testedChannel.getVibrationPattern());
+ assertTrue(testedChannel.shouldVibrate());
+ };
+ assertions.accept(channel);
+ assertions.accept(writeToAndReadFromParcel(channel));
+ assertions.accept(backUpAndRestore(channel));
+ }
+
+ @Test
+ public void testVibrationGetters_nullVibrationEffect() throws Exception {
+ mSetFlagsRule.enableFlags(Flags.FLAG_NOTIFICATION_CHANNEL_VIBRATION_EFFECT_API);
+ NotificationChannel channel = new NotificationChannel("id", "name", 3);
+
+ channel.setVibrationEffect(null);
+
+ Consumer<NotificationChannel> assertions = (testedChannel) -> {
+ assertNull(channel.getVibrationEffect());
+ assertNull(channel.getVibrationPattern());
+ assertFalse(channel.shouldVibrate());
+ };
+ assertions.accept(channel);
+ assertions.accept(writeToAndReadFromParcel(channel));
+ assertions.accept(backUpAndRestore(channel));
+ }
+
+ @Test
+ public void testVibrationGetters_nullPattern() throws Exception {
+ mSetFlagsRule.enableFlags(Flags.FLAG_NOTIFICATION_CHANNEL_VIBRATION_EFFECT_API);
+ NotificationChannel channel = new NotificationChannel("id", "name", 3);
+
+ channel.setVibrationPattern(null);
+
+ Consumer<NotificationChannel> assertions = (testedChannel) -> {
+ assertThat(testedChannel.getVibrationEffect()).isNull();
+ assertNull(testedChannel.getVibrationPattern());
+ assertFalse(testedChannel.shouldVibrate());
+ };
+ assertions.accept(channel);
+ assertions.accept(writeToAndReadFromParcel(channel));
+ assertions.accept(backUpAndRestore(channel));
+ }
+
+ @Test
+ public void testVibrationGetters_setEffectOverridesSetPattern() throws Exception {
+ mSetFlagsRule.enableFlags(Flags.FLAG_NOTIFICATION_CHANNEL_VIBRATION_EFFECT_API);
+ NotificationChannel channel = new NotificationChannel("id", "name", 3);
+ VibrationEffect effect =
+ VibrationEffect.createOneShot(123, VibrationEffect.DEFAULT_AMPLITUDE);
+
+ channel.setVibrationPattern(new long[] {60, 80});
+ channel.setVibrationEffect(effect);
+
+ assertThat(channel.getVibrationEffect()).isEqualTo(effect);
+ assertTrue(Arrays.equals(new long[] {0, 123}, channel.getVibrationPattern()));
+ assertTrue(channel.shouldVibrate());
+
+ channel.setVibrationEffect(null);
+
+ assertNull(channel.getVibrationEffect());
+ assertNull(channel.getVibrationPattern());
+ assertFalse(channel.shouldVibrate());
+ }
+
+ @Test
+ public void testVibrationGetters_setPatternOverridesSetEffect() throws Exception {
+ mSetFlagsRule.enableFlags(Flags.FLAG_NOTIFICATION_CHANNEL_VIBRATION_EFFECT_API);
+ NotificationChannel channel = new NotificationChannel("id", "name", 3);
+ long[] pattern = new long[] {0, 123};
+
+ channel.setVibrationEffect(
+ VibrationEffect.createOneShot(123, VibrationEffect.DEFAULT_AMPLITUDE));
+ channel.setVibrationPattern(pattern);
+
+ assertThat(channel.getVibrationEffect())
+ .isEqualTo(VibrationEffect.createWaveform(pattern, -1));
+ assertTrue(Arrays.equals(pattern, channel.getVibrationPattern()));
+ assertTrue(channel.shouldVibrate());
+
+ channel.setVibrationPattern(null);
+
+ assertNull(channel.getVibrationEffect());
+ assertNull(channel.getVibrationPattern());
+ assertFalse(channel.shouldVibrate());
+ }
+
+ @Test
+ public void testEqualityDependsOnVibration() throws Exception {
+ mSetFlagsRule.enableFlags(Flags.FLAG_NOTIFICATION_CHANNEL_VIBRATION_EFFECT_API);
+ NotificationChannel channel1 = new NotificationChannel("id", "name", 3);
+ NotificationChannel channel2 = new NotificationChannel("id", "name", 3);
+ assertThat(channel1).isEqualTo(channel2);
+
+ VibrationEffect effect = VibrationEffect.createPredefined(VibrationEffect.EFFECT_POP);
+ long[] pattern = new long[] {1, 2, 3};
+ channel1.setVibrationEffect(effect);
+ channel2.setVibrationEffect(effect);
+ assertThat(channel1).isEqualTo(channel2);
+
+ channel1.setVibrationPattern(pattern);
+ channel2.setVibrationPattern(pattern);
+ assertThat(channel1).isEqualTo(channel2);
+
+ channel1.setVibrationPattern(pattern);
+ channel2.setVibrationEffect(VibrationEffect.createWaveform(pattern, /* repeat= */ -1));
+ // Channels should still be equal, because the pattern and the effect set are equivalent.
+ assertThat(channel1).isEqualTo(channel2);
+
+ channel1.setVibrationEffect(effect);
+ channel2.setVibrationPattern(pattern);
+ assertThat(channel1).isNotEqualTo(channel2);
+ }
+
+ @Test
+ public void testSetVibrationPattern_flagOn_setsEffect() throws Exception {
+ mSetFlagsRule.enableFlags(Flags.FLAG_NOTIFICATION_CHANNEL_VIBRATION_EFFECT_API);
+ NotificationChannel channel = new NotificationChannel("id", "name", 3);
+ long[] pattern = new long[] {1, 2, 3};
+
+ channel.setVibrationPattern(pattern);
+
+ assertThat(channel.getVibrationEffect())
+ .isEqualTo(VibrationEffect.createWaveform(pattern, -1));
+ }
+
+ @Test
+ public void testSetVibrationPattern_flagNotOn_doesNotSetEffect() throws Exception {
+ mSetFlagsRule.disableFlags(Flags.FLAG_NOTIFICATION_CHANNEL_VIBRATION_EFFECT_API);
+ NotificationChannel channel = new NotificationChannel("id", "name", 3);
+
+ channel.setVibrationPattern(new long[] {1, 2, 3});
+
+ assertNull(channel.getVibrationEffect());
+ }
+
+ /** Backs up a given channel to an XML, and returns the channel read from the XML. */
+ private NotificationChannel backUpAndRestore(NotificationChannel channel) throws Exception {
+ TypedXmlSerializer serializer = Xml.newFastSerializer();
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ serializer.setOutput(new BufferedOutputStream(baos), "utf-8");
+ serializer.startDocument(null, true);
+
+ channel.writeXmlForBackup(serializer, mContext);
+ serializer.endDocument();
+ serializer.flush();
+
+ TypedXmlPullParser parser = Xml.newFastPullParser();
+ byte[] byteArray = baos.toByteArray();
+ parser.setInput(new BufferedInputStream(new ByteArrayInputStream(byteArray)), null);
+ parser.nextTag();
+
+ NotificationChannel restoredChannel =
+ new NotificationChannel("default_id", "default_name", 3);
+ restoredChannel.populateFromXmlForRestore(parser, true, mContext);
+
+ return restoredChannel;
+ }
+
+ private NotificationChannel writeToAndReadFromParcel(NotificationChannel channel) {
+ Parcel parcel = Parcel.obtain();
+ channel.writeToParcel(parcel, 0);
+ parcel.setDataPosition(0);
+ return NotificationChannel.CREATOR.createFromParcel(parcel);
}
}
diff --git a/core/tests/coretests/src/android/app/QueuedWorkTest.java b/core/tests/coretests/src/android/app/QueuedWorkTest.java
new file mode 100644
index 0000000..7021187
--- /dev/null
+++ b/core/tests/coretests/src/android/app/QueuedWorkTest.java
@@ -0,0 +1,180 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.platform.test.annotations.Presubmit;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.Semaphore;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+@Presubmit
+@SmallTest
+public class QueuedWorkTest {
+
+ private QueuedWork mQueuedWork;
+ private AtomicInteger mCounter;
+
+ private class AddToCounter implements Runnable {
+ private final int mDelta;
+
+ public AddToCounter(int delta) {
+ mDelta = delta;
+ }
+
+ @Override
+ public void run() {
+ mCounter.addAndGet(mDelta);
+ }
+ }
+
+ private class IncrementCounter extends AddToCounter {
+ public IncrementCounter() {
+ super(1);
+ }
+ }
+
+ @Before
+ public void setup() {
+ mQueuedWork = new QueuedWork();
+ mCounter = new AtomicInteger(0);
+ }
+
+ @After
+ public void teardown() {
+ mQueuedWork.waitToFinish();
+ QueuedWork.resetHandler();
+ }
+
+ @Test
+ public void testQueueThenWait() {
+ mQueuedWork.queue(new IncrementCounter(), false);
+ mQueuedWork.waitToFinish();
+ assertThat(mCounter.get()).isEqualTo(1);
+ assertThat(mQueuedWork.hasPendingWork()).isFalse();
+ }
+
+ @Test
+ public void testQueueWithDelayThenWait() {
+ mQueuedWork.queue(new IncrementCounter(), true);
+ mQueuedWork.waitToFinish();
+ assertThat(mCounter.get()).isEqualTo(1);
+ assertThat(mQueuedWork.hasPendingWork()).isFalse();
+ }
+
+ @Test
+ public void testWorkHappensNotOnCallerThread() {
+ AtomicBoolean childThreadStarted = new AtomicBoolean(false);
+ InheritableThreadLocal<Boolean> setTrueInChild =
+ new InheritableThreadLocal<Boolean>() {
+ @Override
+ protected Boolean initialValue() {
+ return false;
+ }
+
+ @Override
+ protected Boolean childValue(Boolean parentValue) {
+ childThreadStarted.set(true);
+ return true;
+ }
+ };
+
+ // Enqueue work to force a worker thread to be created
+ setTrueInChild.get();
+ assertThat(childThreadStarted.get()).isFalse();
+ mQueuedWork.queue(() -> setTrueInChild.get(), false);
+ mQueuedWork.waitToFinish();
+ assertThat(childThreadStarted.get()).isTrue();
+ }
+
+ @Test
+ public void testWaitToFinishDoesNotCreateThread() {
+ InheritableThreadLocal<Boolean> throwInChild =
+ new InheritableThreadLocal<Boolean>() {
+ @Override
+ protected Boolean initialValue() {
+ return false;
+ }
+
+ @Override
+ protected Boolean childValue(Boolean parentValue) {
+ throw new RuntimeException("New thread should not be started!");
+ }
+ };
+
+ try {
+ throwInChild.get();
+ // Intentionally don't enqueue work.
+ mQueuedWork.waitToFinish();
+ throwInChild.get();
+ // If a worker thread was unnecessarily started, we will have crashed.
+ } finally {
+ throwInChild.remove();
+ }
+ }
+
+ @Test
+ public void testFinisher() {
+ mQueuedWork.addFinisher(new AddToCounter(3));
+ mQueuedWork.addFinisher(new AddToCounter(7));
+ mQueuedWork.queue(new IncrementCounter(), false);
+ mQueuedWork.waitToFinish();
+ // The queued task and the two finishers all ran
+ assertThat(mCounter.get()).isEqualTo(1 + 3 + 7);
+ }
+
+ @Test
+ public void testRemoveFinisher() {
+ Runnable addThree = new AddToCounter(3);
+ Runnable addSeven = new AddToCounter(7);
+ mQueuedWork.addFinisher(addThree);
+ mQueuedWork.addFinisher(addSeven);
+ mQueuedWork.removeFinisher(addThree);
+ mQueuedWork.queue(new IncrementCounter(), false);
+ mQueuedWork.waitToFinish();
+ // The queued task and the two finishers all ran
+ assertThat(mCounter.get()).isEqualTo(1 + 7);
+ }
+
+ @Test
+ public void testHasPendingWork() {
+ Semaphore releaser = new Semaphore(0);
+ mQueuedWork.queue(
+ () -> {
+ try {
+ releaser.acquire();
+ } catch (InterruptedException e) {
+ throw new RuntimeException(e);
+ }
+ }, false);
+ assertThat(mQueuedWork.hasPendingWork()).isTrue();
+ releaser.release();
+ mQueuedWork.waitToFinish();
+ assertThat(mQueuedWork.hasPendingWork()).isFalse();
+ }
+}
\ No newline at end of file
diff --git a/core/tests/coretests/src/android/app/backup/BackupRestoreEventLoggerTest.java b/core/tests/coretests/src/android/app/backup/BackupRestoreEventLoggerTest.java
index 6e1c580..0aefef2 100644
--- a/core/tests/coretests/src/android/app/backup/BackupRestoreEventLoggerTest.java
+++ b/core/tests/coretests/src/android/app/backup/BackupRestoreEventLoggerTest.java
@@ -26,10 +26,14 @@
import android.app.backup.BackupRestoreEventLogger.DataTypeResult;
import android.os.Parcel;
import android.platform.test.annotations.Presubmit;
+import android.platform.test.flag.junit.SetFlagsRule;
import androidx.test.runner.AndroidJUnit4;
+import com.android.server.backup.Flags;
+
import org.junit.Before;
+import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -43,7 +47,9 @@
@Presubmit
@RunWith(AndroidJUnit4.class)
public class BackupRestoreEventLoggerTest {
- private static final int DATA_TYPES_ALLOWED = 15;
+ private static final int DATA_TYPES_ALLOWED_AFTER_FLAG = 150;
+
+ private static final int DATA_TYPES_ALLOWED_BEFORE_FLAG = 15;
private static final String DATA_TYPE_1 = "data_type_1";
private static final String DATA_TYPE_2 = "data_type_2";
@@ -55,6 +61,9 @@
private BackupRestoreEventLogger mLogger;
private MessageDigest mHashDigest;
+ @Rule
+ public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+
@Before
public void setUp() throws Exception {
mHashDigest = MessageDigest.getInstance("SHA-256");
@@ -83,10 +92,11 @@
}
@Test
- public void testBackupLogger_onlyAcceptsAllowedNumberOfDataTypes() {
+ public void testBackupLogger_datatypeLimitFlagOff_onlyAcceptsAllowedNumberOfDataTypes() {
+ mSetFlagsRule.disableFlags(Flags.FLAG_ENABLE_INCREASE_DATATYPES_FOR_AGENT_LOGGING);
mLogger = new BackupRestoreEventLogger(BACKUP);
- for (int i = 0; i < DATA_TYPES_ALLOWED; i++) {
+ for (int i = 0; i < DATA_TYPES_ALLOWED_BEFORE_FLAG; i++) {
String dataType = DATA_TYPE_1 + i;
mLogger.logItemsBackedUp(dataType, /* count */ 5);
mLogger.logItemsBackupFailed(dataType, /* count */ 5, /* error */ null);
@@ -103,10 +113,53 @@
}
@Test
- public void testRestoreLogger_onlyAcceptsAllowedNumberOfDataTypes() {
+ public void testRestoreLogger_datatypeLimitFlagOff_onlyAcceptsAllowedNumberOfDataTypes() {
+ mSetFlagsRule.disableFlags(Flags.FLAG_ENABLE_INCREASE_DATATYPES_FOR_AGENT_LOGGING);
mLogger = new BackupRestoreEventLogger(RESTORE);
- for (int i = 0; i < DATA_TYPES_ALLOWED; i++) {
+ for (int i = 0; i < DATA_TYPES_ALLOWED_BEFORE_FLAG; i++) {
+ String dataType = DATA_TYPE_1 + i;
+ mLogger.logItemsRestored(dataType, /* count */ 5);
+ mLogger.logItemsRestoreFailed(dataType, /* count */ 5, /* error */ null);
+ mLogger.logRestoreMetadata(dataType, METADATA_1);
+
+ assertThat(getResultForDataTypeIfPresent(mLogger, dataType)).isNotEqualTo(
+ Optional.empty());
+ }
+
+ mLogger.logItemsRestored(DATA_TYPE_2, /* count */ 5);
+ mLogger.logItemsRestoreFailed(DATA_TYPE_2, /* count */ 5, /* error */ null);
+ mLogger.logRestoreMetadata(DATA_TYPE_2, METADATA_1);
+ assertThat(getResultForDataTypeIfPresent(mLogger, DATA_TYPE_2)).isEqualTo(Optional.empty());
+ }
+
+ @Test
+ public void testBackupLogger_datatypeLimitFlagOn_onlyAcceptsAllowedNumberOfDataTypes() {
+ mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_INCREASE_DATATYPES_FOR_AGENT_LOGGING);
+ mLogger = new BackupRestoreEventLogger(BACKUP);
+
+ for (int i = 0; i < DATA_TYPES_ALLOWED_AFTER_FLAG; i++) {
+ String dataType = DATA_TYPE_1 + i;
+ mLogger.logItemsBackedUp(dataType, /* count */ 5);
+ mLogger.logItemsBackupFailed(dataType, /* count */ 5, /* error */ null);
+ mLogger.logBackupMetadata(dataType, METADATA_1);
+
+ assertThat(getResultForDataTypeIfPresent(mLogger, dataType)).isNotEqualTo(
+ Optional.empty());
+ }
+
+ mLogger.logItemsBackedUp(DATA_TYPE_2, /* count */ 5);
+ mLogger.logItemsBackupFailed(DATA_TYPE_2, /* count */ 5, /* error */ null);
+ mLogger.logRestoreMetadata(DATA_TYPE_2, METADATA_1);
+ assertThat(getResultForDataTypeIfPresent(mLogger, DATA_TYPE_2)).isEqualTo(Optional.empty());
+ }
+
+ @Test
+ public void testRestoreLogger_datatypeLimitFlagOn_onlyAcceptsAllowedNumberOfDataTypes() {
+ mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_INCREASE_DATATYPES_FOR_AGENT_LOGGING);
+ mLogger = new BackupRestoreEventLogger(RESTORE);
+
+ for (int i = 0; i < DATA_TYPES_ALLOWED_AFTER_FLAG; i++) {
String dataType = DATA_TYPE_1 + i;
mLogger.logItemsRestored(dataType, /* count */ 5);
mLogger.logItemsRestoreFailed(dataType, /* count */ 5, /* error */ null);
diff --git a/core/tests/coretests/src/android/app/servertransaction/ObjectPoolTests.java b/core/tests/coretests/src/android/app/servertransaction/ObjectPoolTests.java
index a796a0f..c6447be 100644
--- a/core/tests/coretests/src/android/app/servertransaction/ObjectPoolTests.java
+++ b/core/tests/coretests/src/android/app/servertransaction/ObjectPoolTests.java
@@ -123,6 +123,7 @@
final IBinder shareableActivityToken = new Binder();
final int deviceId = 3;
final IBinder taskFragmentToken = new Binder();
+ final IBinder initialCallerInfoAccessToken = new Binder();
testRecycle(() -> new LaunchActivityItemBuilder(
activityToken, intent, activityInfo)
@@ -140,6 +141,7 @@
.setShareableActivityToken(shareableActivityToken)
.setTaskFragmentToken(taskFragmentToken)
.setDeviceId(deviceId)
+ .setInitialCallerInfoAccessToken(initialCallerInfoAccessToken)
.build());
}
diff --git a/core/tests/coretests/src/android/app/servertransaction/TestUtils.java b/core/tests/coretests/src/android/app/servertransaction/TestUtils.java
index 3823033..d641659 100644
--- a/core/tests/coretests/src/android/app/servertransaction/TestUtils.java
+++ b/core/tests/coretests/src/android/app/servertransaction/TestUtils.java
@@ -132,6 +132,8 @@
private boolean mLaunchedFromBubble;
@Nullable
private IBinder mTaskFragmentToken;
+ @Nullable
+ private IBinder mInitialCallerInfoAccessToken;
LaunchActivityItemBuilder(@NonNull IBinder activityToken, @NonNull Intent intent,
@NonNull ActivityInfo info) {
@@ -251,13 +253,21 @@
}
@NonNull
+ LaunchActivityItemBuilder setInitialCallerInfoAccessToken(
+ @Nullable IBinder initialCallerInfoAccessToken) {
+ mInitialCallerInfoAccessToken = initialCallerInfoAccessToken;
+ return this;
+ }
+
+ @NonNull
LaunchActivityItem build() {
return LaunchActivityItem.obtain(mActivityToken, mIntent, mIdent, mInfo,
mCurConfig, mOverrideConfig, mDeviceId, mReferrer, mVoiceInteractor,
mProcState, mState, mPersistentState, mPendingResults, mPendingNewIntents,
mActivityOptions != null ? mActivityOptions.getSceneTransitionInfo() : null,
mIsForward, mProfilerInfo, mAssistToken, null /* activityClientController */,
- mShareableActivityToken, mLaunchedFromBubble, mTaskFragmentToken);
+ mShareableActivityToken, mLaunchedFromBubble, mTaskFragmentToken,
+ mInitialCallerInfoAccessToken);
}
}
}
diff --git a/core/tests/coretests/src/android/app/servertransaction/TransactionParcelTests.java b/core/tests/coretests/src/android/app/servertransaction/TransactionParcelTests.java
index 952cdd9..508c6b2 100644
--- a/core/tests/coretests/src/android/app/servertransaction/TransactionParcelTests.java
+++ b/core/tests/coretests/src/android/app/servertransaction/TransactionParcelTests.java
@@ -189,6 +189,7 @@
.setAssistToken(new Binder())
.setShareableActivityToken(new Binder())
.setTaskFragmentToken(new Binder())
+ .setInitialCallerInfoAccessToken(new Binder())
.build();
writeAndPrepareForReading(item);
diff --git a/core/tests/coretests/src/android/database/sqlite/SQLiteRawStatementTest.java b/core/tests/coretests/src/android/database/sqlite/SQLiteRawStatementTest.java
index 36bb8e5..548b8ec 100644
--- a/core/tests/coretests/src/android/database/sqlite/SQLiteRawStatementTest.java
+++ b/core/tests/coretests/src/android/database/sqlite/SQLiteRawStatementTest.java
@@ -104,7 +104,9 @@
private void createComplexDatabase() {
mDatabase.beginTransaction();
try {
- mDatabase.execSQL("CREATE TABLE t1 (i int, d double, t text);");
+ // Column "l" is used to test the long variants. The underlying sqlite type is int,
+ // which is the same as a java long.
+ mDatabase.execSQL("CREATE TABLE t1 (i int, d double, t text, l int);");
mDatabase.setTransactionSuccessful();
} finally {
mDatabase.endTransaction();
@@ -115,7 +117,7 @@
* A three-value insert for the complex database.
*/
private String createComplexInsert() {
- return "INSERT INTO t1 (i, d, t) VALUES (?1, ?2, ?3)";
+ return "INSERT INTO t1 (i, d, t, l) VALUES (?1, ?2, ?3, ?4)";
}
/**
@@ -209,22 +211,25 @@
mDatabase.beginTransaction();
try {
try (SQLiteRawStatement s = mDatabase.createRawStatement(createComplexInsert())) {
- for (int i = 0; i < 9; i++) {
- int vi = i * 3;
- double vd = i * 2.5;
- String vt = String.format("text%02dvalue", i);
+ for (int row = 0; row < 9; row++) {
+ int vi = row * 3;
+ double vd = row * 2.5;
+ String vt = String.format("text%02dvalue", row);
+ long vl = Long.MAX_VALUE - row;
s.bindInt(1, vi);
s.bindDouble(2, vd);
s.bindText(3, vt);
+ s.bindLong(4, vl);
boolean r = s.step();
// No row is returned by this query.
assertFalse(r);
s.reset();
}
- // The last row has a null double and a null text.
+ // The last row has a null double, null text, and null long.
s.bindInt(1, 20);
s.bindNull(2);
s.bindNull(3);
+ s.bindNull(4);
assertFalse(s.step());
s.reset();
}
@@ -248,19 +253,31 @@
mDatabase.endTransaction();
}
- // Verify that the element created with i == 3 is correct.
+ // Verify that the element created with row == 3 is correct.
mDatabase.beginTransactionReadOnly();
try {
- final String query = "SELECT i, d, t FROM t1 WHERE t = 'text03value'";
+ final String query = "SELECT i, d, t, l FROM t1 WHERE t = 'text03value'";
try (SQLiteRawStatement s = mDatabase.createRawStatement(query)) {
assertTrue(s.step());
- assertEquals(3, s.getResultColumnCount());
+ assertEquals(4, s.getResultColumnCount());
int vi = s.getColumnInt(0);
double vd = s.getColumnDouble(1);
String vt = s.getColumnText(2);
- assertEquals(3 * 3, vi);
- assertEquals(2.5 * 3, vd, 0.1);
+ long vl = s.getColumnLong(3);
+ // The query extracted the third generated row.
+ final int row = 3;
+ assertEquals(3 * row, vi);
+ assertEquals(2.5 * row, vd, 0.1);
assertEquals("text03value", vt);
+ assertEquals(Long.MAX_VALUE - row, vl);
+
+ // Verify the column types. Remember that sqlite integers are the same as Java
+ // long, so the integer and long columns have type INTEGER.
+ assertEquals(SQLiteRawStatement.SQLITE_DATA_TYPE_INTEGER, s.getColumnType(0));
+ assertEquals(SQLiteRawStatement.SQLITE_DATA_TYPE_FLOAT, s.getColumnType(1));
+ assertEquals(SQLiteRawStatement.SQLITE_DATA_TYPE_TEXT, s.getColumnType(2));
+ assertEquals(SQLiteRawStatement.SQLITE_DATA_TYPE_INTEGER, s.getColumnType(3));
+
// No more rows.
assertFalse(s.step());
}
@@ -268,15 +285,24 @@
mDatabase.endTransaction();
}
+ // Verify that null columns are returned properly.
mDatabase.beginTransactionReadOnly();
try {
- final String query = "SELECT i, d, t FROM t1 WHERE i == 20";
+ final String query = "SELECT i, d, t, l FROM t1 WHERE i == 20";
try (SQLiteRawStatement s = mDatabase.createRawStatement(query)) {
assertTrue(s.step());
- assertEquals(3, s.getResultColumnCount());
+ assertEquals(4, s.getResultColumnCount());
assertEquals(20, s.getColumnInt(0));
assertEquals(0.0, s.getColumnDouble(1), 0.01);
assertEquals(null, s.getColumnText(2));
+ assertEquals(0, s.getColumnLong(3));
+
+ // Verify the column types.
+ assertEquals(SQLiteRawStatement.SQLITE_DATA_TYPE_INTEGER, s.getColumnType(0));
+ assertEquals(SQLiteRawStatement.SQLITE_DATA_TYPE_NULL, s.getColumnType(1));
+ assertEquals(SQLiteRawStatement.SQLITE_DATA_TYPE_NULL, s.getColumnType(2));
+ assertEquals(SQLiteRawStatement.SQLITE_DATA_TYPE_NULL, s.getColumnType(3));
+
// No more rows.
assertFalse(s.step());
}
@@ -495,6 +521,8 @@
// Fetch the entire reference array.
s.bindInt(1, 1);
assertTrue(s.step());
+ assertEquals(SQLiteRawStatement.SQLITE_DATA_TYPE_BLOB, s.getColumnType(0));
+
byte[] a = s.getColumnBlob(0);
assertTrue(Arrays.equals(src, a));
s.reset();
diff --git a/core/tests/coretests/src/android/ddm/OWNERS b/core/tests/coretests/src/android/ddm/OWNERS
deleted file mode 100644
index c8be191..0000000
--- a/core/tests/coretests/src/android/ddm/OWNERS
+++ /dev/null
@@ -1 +0,0 @@
-michschn@google.com
diff --git a/core/tests/coretests/src/android/graphics/drawable/IconTest.java b/core/tests/coretests/src/android/graphics/drawable/IconTest.java
index 49ed3a8..950925f 100644
--- a/core/tests/coretests/src/android/graphics/drawable/IconTest.java
+++ b/core/tests/coretests/src/android/graphics/drawable/IconTest.java
@@ -18,8 +18,13 @@
import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
import android.app.IUriGrantsManager;
import android.content.ContentProvider;
+import android.content.Context;
import android.content.Intent;
import android.content.pm.ParceledListSlice;
import android.graphics.Bitmap;
@@ -32,13 +37,15 @@
import android.os.IBinder;
import android.os.Parcel;
import android.os.RemoteException;
-import android.test.AndroidTestCase;
import android.util.Log;
-import androidx.test.filters.SmallTest;
+import androidx.test.core.app.ApplicationProvider;
import com.android.frameworks.coretests.R;
+import com.google.testing.junit.testparameterinjector.TestParameter;
+import com.google.testing.junit.testparameterinjector.TestParameterInjector;
+
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileNotFoundException;
@@ -46,13 +53,29 @@
import java.util.ArrayList;
import java.util.Arrays;
-public class IconTest extends AndroidTestCase {
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(TestParameterInjector.class)
+public class IconTest {
public static final String TAG = IconTest.class.getSimpleName();
+ private Context mContext;
+
public static void L(String s, Object... parts) {
Log.d(TAG, (parts.length == 0) ? s : String.format(s, parts));
}
- @SmallTest
+ private Context getContext() {
+ return mContext;
+ }
+
+ @Before
+ public void setup() {
+ mContext = ApplicationProvider.getApplicationContext();
+ }
+
+ @Test
public void testWithBitmap() throws Exception {
final Bitmap bm1 = Bitmap.createBitmap(100, 200, Bitmap.Config.ARGB_8888);
final Bitmap bm2 = Bitmap.createBitmap(100, 200, Bitmap.Config.RGB_565);
@@ -119,7 +142,7 @@
}
}
- @SmallTest
+ @Test
public void testScaleDownIfNecessary() throws Exception {
final Bitmap bm = Bitmap.createBitmap(4321, 78, Bitmap.Config.ARGB_8888);
final Icon ic = Icon.createWithBitmap(bm);
@@ -132,7 +155,7 @@
assertThat(ic.getBitmap().getHeight()).isLessThan(21);
}
- @SmallTest
+ @Test
public void testWithAdaptiveBitmap() throws Exception {
final Bitmap bm1 = Bitmap.createBitmap(150, 150, Bitmap.Config.ARGB_8888);
@@ -166,7 +189,7 @@
}
}
- @SmallTest
+ @Test
public void testWithBitmapResource() throws Exception {
final Bitmap res1 = ((BitmapDrawable) getContext().getDrawable(R.drawable.landscape))
.getBitmap();
@@ -193,7 +216,7 @@
* Icon resource test that ensures we can load and draw non-bitmaps. (In this case,
* stat_sys_adb is assumed, and asserted, to be a vector drawable.)
*/
- @SmallTest
+ @Test
public void testWithStatSysAdbResource() throws Exception {
// establish reference bitmap
final float dp = getContext().getResources().getDisplayMetrics().density;
@@ -244,7 +267,7 @@
}
}
- @SmallTest
+ @Test
public void testWithFile() throws Exception {
final Bitmap bit1 = ((BitmapDrawable) getContext().getDrawable(R.drawable.landscape))
.getBitmap();
@@ -268,7 +291,55 @@
}
}
- @SmallTest
+ @Test
+ public void testWithAdaptiveIconResource_useMonochrome() throws Exception {
+ final int colorMono = ((ColorDrawable) getContext().getDrawable(
+ android.R.color.system_accent2_800)).getColor();
+ final Icon im1 = Icon.createWithResourceAdaptiveDrawable(getContext().getPackageName(),
+ R.drawable.adaptiveicon_drawable, true, 0.0f);
+ final Drawable draw1 = im1.loadDrawable(mContext);
+ assertThat(draw1 instanceof InsetDrawable).isTrue();
+ ColorDrawable colorDrawable = (ColorDrawable) ((DrawableWrapper) draw1).getDrawable();
+ assertThat(colorDrawable.getColor()).isEqualTo(colorMono);
+ }
+
+ @Test
+ public void testWithAdaptiveIconResource_dontUseMonochrome() throws Exception {
+ final int colorMono = ((ColorDrawable) getContext().getDrawable(
+ android.R.color.system_accent2_800)).getColor();
+ final int colorFg = ((ColorDrawable) getContext().getDrawable(
+ android.R.color.black)).getColor();
+ final int colorBg = ((ColorDrawable) getContext().getDrawable(
+ android.R.color.white)).getColor();
+
+ final Icon im1 = Icon.createWithResourceAdaptiveDrawable(getContext().getPackageName(),
+ R.drawable.adaptiveicon_drawable, false , 0.0f);
+ final Drawable draw1 = im1.loadDrawable(mContext);
+ assertThat(draw1 instanceof AdaptiveIconDrawable).isTrue();
+ ColorDrawable colorDrawableMono = (ColorDrawable) ((AdaptiveIconDrawable) draw1)
+ .getMonochrome();
+ assertThat(colorDrawableMono.getColor()).isEqualTo(colorMono);
+ ColorDrawable colorDrawableFg = (ColorDrawable) ((AdaptiveIconDrawable) draw1)
+ .getForeground();
+ assertThat(colorDrawableFg.getColor()).isEqualTo(colorFg);
+ ColorDrawable colorDrawableBg = (ColorDrawable) ((AdaptiveIconDrawable) draw1)
+ .getBackground();
+ assertThat(colorDrawableBg.getColor()).isEqualTo(colorBg);
+ }
+
+ @Test
+ public void testAdaptiveIconResource_sameAs(@TestParameter boolean useMonochrome)
+ throws Exception {
+ final Icon im1 = Icon.createWithResourceAdaptiveDrawable(getContext().getPackageName(),
+ R.drawable.adaptiveicon_drawable, useMonochrome, 1.0f);
+ final Parcel parcel = Parcel.obtain();
+ im1.writeToParcel(parcel, 0);
+ parcel.setDataPosition(0);
+ final Icon im2 = Icon.CREATOR.createFromParcel(parcel);
+ assertThat(im1.sameAs(im2)).isTrue();
+ }
+
+ @Test
public void testAsync() throws Exception {
final Bitmap bit1 = ((BitmapDrawable) getContext().getDrawable(R.drawable.landscape))
.getBitmap();
@@ -311,7 +382,7 @@
L(TAG, "asyncTest: done");
}
- @SmallTest
+ @Test
public void testParcel() throws Exception {
final Bitmap originalbits = ((BitmapDrawable) getContext().getDrawable(R.drawable.landscape))
.getBitmap();
@@ -391,7 +462,7 @@
return (int) Math.sqrt(maxNumPixels / aspRatio);
}
- @SmallTest
+ @Test
public void testScaleDownMaxSizeWithBitmap() throws Exception {
final int bmpWidth = 13_000;
final int bmpHeight = 10_000;
@@ -408,7 +479,7 @@
assertThat(drawable.getIntrinsicHeight()).isEqualTo(maxHeight);
}
- @SmallTest
+ @Test
public void testScaleDownMaxSizeWithAdaptiveBitmap() throws Exception {
final int bmpWidth = 20_000;
final int bmpHeight = 10_000;
@@ -427,7 +498,7 @@
assertThat(drawable.getIntrinsicHeight()).isEqualTo(maxHeight);
}
- @SmallTest
+ @Test
public void testScaleDownMaxSizeWithResource() throws Exception {
final Icon ic = Icon.createWithResource(getContext(), R.drawable.test_too_big);
final BitmapDrawable drawable = (BitmapDrawable) ic.loadDrawable(mContext);
@@ -435,7 +506,7 @@
assertThat(drawable.getBitmap().getByteCount()).isAtMost(RecordingCanvas.MAX_BITMAP_SIZE);
}
- @SmallTest
+ @Test
public void testScaleDownMaxSizeWithFile() throws Exception {
final Bitmap bit1 = ((BitmapDrawable) getContext().getDrawable(R.drawable.test_too_big))
.getBitmap();
@@ -450,7 +521,7 @@
assertThat(drawable.getBitmap().getByteCount()).isAtMost(RecordingCanvas.MAX_BITMAP_SIZE);
}
- @SmallTest
+ @Test
public void testScaleDownMaxSizeWithData() throws Exception {
final int bmpBpp = 4;
final Bitmap originalBits = ((BitmapDrawable) getContext().getDrawable(
@@ -465,7 +536,7 @@
assertThat(drawable.getBitmap().getByteCount()).isAtMost(RecordingCanvas.MAX_BITMAP_SIZE);
}
- @SmallTest
+ @Test
public void testLoadSafeDrawable_loadSuccessful() throws FileNotFoundException {
int uid = 12345;
String packageName = "test_pkg";
@@ -509,7 +580,7 @@
}
}
- @SmallTest
+ @Test
public void testLoadSafeDrawable_grantRejected_nullDrawable() throws FileNotFoundException {
int uid = 12345;
String packageName = "test_pkg";
diff --git a/core/tests/coretests/src/android/os/HandlerThreadTest.java b/core/tests/coretests/src/android/os/HandlerThreadTest.java
index 0bac1c7..3237812 100644
--- a/core/tests/coretests/src/android/os/HandlerThreadTest.java
+++ b/core/tests/coretests/src/android/os/HandlerThreadTest.java
@@ -28,15 +28,20 @@
import androidx.test.filters.MediumTest;
import androidx.test.runner.AndroidJUnit4;
+import org.junit.Assume;
import org.junit.Rule;
import org.junit.Test;
+import org.junit.rules.ExpectedException;
import org.junit.runner.RunWith;
@RunWith(AndroidJUnit4.class)
public class HandlerThreadTest {
private static final int TEST_WHAT = 1;
- @Rule
+ @Rule(order = 1)
+ public ExpectedException mThrown = ExpectedException.none();
+
+ @Rule(order = 2)
public final RavenwoodRule mRavenwood = new RavenwoodRule();
private boolean mGotMessage = false;
@@ -112,4 +117,28 @@
assertTrue(mGotMessage);
assertEquals(TEST_WHAT, mGotMessageWhat);
}
+
+ /**
+ * Confirm that a background handler thread throwing an exception during a test results in a
+ * test failure being reported.
+ */
+ @Test
+ public void testUncaughtExceptionFails() throws Exception {
+ // For the moment we can only test Ravenwood; on a physical device uncaught exceptions
+ // are detected, but reported as test failures at a higher level where we can't inspect
+ Assume.assumeTrue(false); // TODO: re-enable
+ mThrown.expect(IllegalStateException.class);
+
+ final HandlerThread thread = new HandlerThread("HandlerThreadTest");
+ thread.start();
+ thread.getThreadHandler().post(() -> {
+ throw new IllegalStateException();
+ });
+
+ // Wait until we've drained past the message above, then terminate test without throwing
+ // directly; the test harness should notice and report the uncaught exception
+ while (!thread.getThreadHandler().getLooper().getQueue().isIdle()) {
+ SystemClock.sleep(10);
+ }
+ }
}
diff --git a/core/tests/coretests/src/android/os/PerformanceHintManagerTest.java b/core/tests/coretests/src/android/os/PerformanceHintManagerTest.java
index a28bb69..2bd5631 100644
--- a/core/tests/coretests/src/android/os/PerformanceHintManagerTest.java
+++ b/core/tests/coretests/src/android/os/PerformanceHintManagerTest.java
@@ -219,6 +219,22 @@
workDuration.setActualGpuDurationNanos(6);
s.reportActualWorkDuration(workDuration);
}
+ {
+ WorkDuration workDuration = new WorkDuration();
+ workDuration.setWorkPeriodStartTimestampNanos(1);
+ workDuration.setActualTotalDurationNanos(14);
+ workDuration.setActualCpuDurationNanos(0);
+ workDuration.setActualGpuDurationNanos(6);
+ s.reportActualWorkDuration(workDuration);
+ }
+ {
+ WorkDuration workDuration = new WorkDuration();
+ workDuration.setWorkPeriodStartTimestampNanos(1);
+ workDuration.setActualTotalDurationNanos(14);
+ workDuration.setActualCpuDurationNanos(7);
+ workDuration.setActualGpuDurationNanos(0);
+ s.reportActualWorkDuration(workDuration);
+ }
}
@Test
@@ -242,7 +258,7 @@
s.reportActualWorkDuration(new WorkDuration(1, 12, -1, 6, 1));
});
assertThrows(IllegalArgumentException.class, () -> {
- s.reportActualWorkDuration(new WorkDuration(1, 12, 0, 6, 1));
+ s.reportActualWorkDuration(new WorkDuration(1, 12, 0, 0, 1));
});
assertThrows(IllegalArgumentException.class, () -> {
s.reportActualWorkDuration(new WorkDuration(1, 12, 8, -1, 1));
diff --git a/core/tests/coretests/src/android/os/VibrationAttributesTest.java b/core/tests/coretests/src/android/os/VibrationAttributesTest.java
new file mode 100644
index 0000000..f5a81c5
--- /dev/null
+++ b/core/tests/coretests/src/android/os/VibrationAttributesTest.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2024 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.os;
+
+import static org.junit.Assert.assertEquals;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+public class VibrationAttributesTest {
+ @Test
+ public void testSimple() throws Exception {
+ final VibrationAttributes attr = new VibrationAttributes.Builder()
+ .setCategory(VibrationAttributes.CATEGORY_KEYBOARD)
+ .setUsage(VibrationAttributes.USAGE_ALARM)
+ .build();
+
+ assertEquals(VibrationAttributes.CATEGORY_KEYBOARD, attr.getCategory());
+ assertEquals(VibrationAttributes.USAGE_ALARM, attr.getUsage());
+ }
+}
diff --git a/core/tests/coretests/src/android/text/TextLineJustificationTest.kt b/core/tests/coretests/src/android/text/TextLineJustificationTest.kt
index a525615..c18f35f 100644
--- a/core/tests/coretests/src/android/text/TextLineJustificationTest.kt
+++ b/core/tests/coretests/src/android/text/TextLineJustificationTest.kt
@@ -126,4 +126,4 @@
TextLine.recycle(tl)
}
-}
\ No newline at end of file
+}
diff --git a/core/tests/coretests/src/android/text/TextLineLetterSpacingTest.kt b/core/tests/coretests/src/android/text/TextLineLetterSpacingTest.kt
index 27869bb..71980c1 100644
--- a/core/tests/coretests/src/android/text/TextLineLetterSpacingTest.kt
+++ b/core/tests/coretests/src/android/text/TextLineLetterSpacingTest.kt
@@ -21,7 +21,7 @@
import android.platform.test.flag.junit.DeviceFlagsValueProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
-import com.android.text.flags.Flags.FLAG_INTER_CHARACTER_JUSTIFICATION
+import com.android.text.flags.Flags.FLAG_LETTER_SPACING_JUSTIFICATION
import com.google.common.truth.Truth.assertThat
import org.junit.Rule
import org.junit.Test
@@ -40,7 +40,7 @@
@JvmField
val mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule()
- @RequiresFlagsEnabled(FLAG_INTER_CHARACTER_JUSTIFICATION)
+ @RequiresFlagsEnabled(FLAG_LETTER_SPACING_JUSTIFICATION)
@Test
fun calculateRunFlagTest() {
// Only one Bidi run
@@ -84,7 +84,7 @@
.isEqualTo(LEFT_EDGE)
}
- @RequiresFlagsEnabled(FLAG_INTER_CHARACTER_JUSTIFICATION)
+ @RequiresFlagsEnabled(FLAG_LETTER_SPACING_JUSTIFICATION)
@Test
fun resolveRunFlagForSubSequenceTest() {
val runStart = 5
@@ -221,4 +221,4 @@
MIDDLE_OF_LINE, true, runStart, runEnd, runStart + 1, runEnd - 1))
.isEqualTo(MIDDLE_OF_LINE)
}
-}
\ No newline at end of file
+}
diff --git a/core/tests/coretests/src/android/tracing/perfetto/DataSourceTest.java b/core/tests/coretests/src/android/tracing/perfetto/DataSourceTest.java
index bd2f36f..d57f1fc 100644
--- a/core/tests/coretests/src/android/tracing/perfetto/DataSourceTest.java
+++ b/core/tests/coretests/src/android/tracing/perfetto/DataSourceTest.java
@@ -658,6 +658,52 @@
Truth.assertThat(matchingPackets).hasSize(1);
}
+ @Test
+ public void canTraceOnFlush() throws InvalidProtocolBufferException, InterruptedException {
+ final int singleIntValue = 101;
+ sInstanceProvider = (ds, idx, config) ->
+ new TestDataSource.TestDataSourceInstance(
+ ds,
+ idx,
+ (args) -> {},
+ (args) -> sTestDataSource.trace(ctx -> {
+ final ProtoOutputStream protoOutputStream = ctx.newTracePacket();
+ long forTestingToken = protoOutputStream.start(FOR_TESTING);
+ long payloadToken = protoOutputStream.start(PAYLOAD);
+ protoOutputStream.write(SINGLE_INT, singleIntValue);
+ protoOutputStream.end(payloadToken);
+ protoOutputStream.end(forTestingToken);
+
+ ctx.flush();
+ }),
+ (args) -> {}
+ );
+
+ final TraceMonitor traceMonitor = PerfettoTraceMonitor.newBuilder()
+ .enableCustomTrace(PerfettoConfig.DataSourceConfig.newBuilder()
+ .setName(sTestDataSource.name).build()).build();
+
+ try {
+ traceMonitor.start();
+ } finally {
+ traceMonitor.stop(mWriter);
+ }
+
+ final ResultReader reader = new ResultReader(mWriter.write(), mTraceConfig);
+ final byte[] rawProtoFromFile = reader.readBytes(TraceType.PERFETTO, Tag.ALL);
+ assert rawProtoFromFile != null;
+ final perfetto.protos.TraceOuterClass.Trace trace = perfetto.protos.TraceOuterClass.Trace
+ .parseFrom(rawProtoFromFile);
+
+ Truth.assertThat(trace.getPacketCount()).isGreaterThan(0);
+ final List<TracePacketOuterClass.TracePacket> tracePackets = trace.getPacketList()
+ .stream().filter(TracePacketOuterClass.TracePacket::hasForTesting).toList();
+ final List<TracePacketOuterClass.TracePacket> matchingPackets = tracePackets.stream()
+ .filter(it -> it.getForTesting().getPayload().getSingleInt()
+ == singleIntValue).toList();
+ Truth.assertThat(matchingPackets).hasSize(1);
+ }
+
interface RunnableCreator {
Runnable create(int state, AtomicInteger stateOut);
}
diff --git a/core/tests/coretests/src/android/ddm/DdmHandleViewDebugTest.java b/core/tests/coretests/src/android/view/ViewDebugTest.java
similarity index 97%
rename from core/tests/coretests/src/android/ddm/DdmHandleViewDebugTest.java
rename to core/tests/coretests/src/android/view/ViewDebugTest.java
index 7248983..4522842 100644
--- a/core/tests/coretests/src/android/ddm/DdmHandleViewDebugTest.java
+++ b/core/tests/coretests/src/android/view/ViewDebugTest.java
@@ -14,17 +14,17 @@
* limitations under the License.
*/
-package android.ddm;
+package android.view;
-import static android.ddm.DdmHandleViewDebug.deserializeMethodParameters;
-import static android.ddm.DdmHandleViewDebug.serializeReturnValue;
+import static android.view.ViewDebug.deserializeMethodParameters;
+import static android.view.ViewDebug.serializeReturnValue;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThrows;
-import android.ddm.DdmHandleViewDebug.ViewMethodInvocationSerializationException;
import android.platform.test.annotations.Presubmit;
+import android.view.ViewDebug.ViewMethodInvocationSerializationException;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
@@ -39,7 +39,7 @@
@RunWith(AndroidJUnit4.class)
@SmallTest
@Presubmit
-public final class DdmHandleViewDebugTest {
+public final class ViewDebugTest {
// true
private static final byte[] SERIALIZED_BOOLEAN_TRUE = {0x00, 0x5A, 1};
diff --git a/core/tests/coretests/src/android/view/ViewRootImplTest.java b/core/tests/coretests/src/android/view/ViewRootImplTest.java
index cf3eb12..52e996c 100644
--- a/core/tests/coretests/src/android/view/ViewRootImplTest.java
+++ b/core/tests/coretests/src/android/view/ViewRootImplTest.java
@@ -18,7 +18,9 @@
import static android.view.accessibility.Flags.FLAG_FORCE_INVERT_COLOR;
import static android.view.flags.Flags.FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY;
+import static android.view.flags.Flags.FLAG_TOOLKIT_FRAME_RATE_BY_SIZE_READ_ONLY;
import static android.view.Surface.FRAME_RATE_CATEGORY_HIGH;
+import static android.view.Surface.FRAME_RATE_CATEGORY_HIGH_HINT;
import static android.view.Surface.FRAME_RATE_CATEGORY_LOW;
import static android.view.Surface.FRAME_RATE_CATEGORY_NORMAL;
import static android.view.Surface.FRAME_RATE_CATEGORY_NO_PREFERENCE;
@@ -475,8 +477,9 @@
* Also, mIsFrameRateBoosting should be true when the visibility becomes visible
*/
@Test
- @RequiresFlagsEnabled(FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY)
- public void votePreferredFrameRate_voteFrameRateCategory_visibility() {
+ @RequiresFlagsEnabled({FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY,
+ FLAG_TOOLKIT_FRAME_RATE_BY_SIZE_READ_ONLY})
+ public void votePreferredFrameRate_voteFrameRateCategory_visibility_bySize() {
View view = new View(sContext);
attachViewToWindow(view);
ViewRootImpl viewRootImpl = view.getViewRootImpl();
@@ -507,8 +510,9 @@
* <7%: FRAME_RATE_CATEGORY_LOW
*/
@Test
- @RequiresFlagsEnabled(FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY)
- public void votePreferredFrameRate_voteFrameRateCategory_smallSize() {
+ @RequiresFlagsEnabled({FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY,
+ FLAG_TOOLKIT_FRAME_RATE_BY_SIZE_READ_ONLY})
+ public void votePreferredFrameRate_voteFrameRateCategory_smallSize_bySize() {
View view = new View(sContext);
WindowManager.LayoutParams wmlp = new WindowManager.LayoutParams(TYPE_APPLICATION_OVERLAY);
wmlp.token = new Binder(); // Set a fake token to bypass 'is your activity running' check
@@ -534,8 +538,9 @@
* >=7% : FRAME_RATE_CATEGORY_NORMAL
*/
@Test
- @RequiresFlagsEnabled(FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY)
- public void votePreferredFrameRate_voteFrameRateCategory_normalSize() {
+ @RequiresFlagsEnabled({FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY,
+ FLAG_TOOLKIT_FRAME_RATE_BY_SIZE_READ_ONLY})
+ public void votePreferredFrameRate_voteFrameRateCategory_normalSize_bySize() {
View view = new View(sContext);
WindowManager.LayoutParams wmlp = new WindowManager.LayoutParams(TYPE_APPLICATION_OVERLAY);
wmlp.token = new Binder(); // Set a fake token to bypass 'is your activity running' check
@@ -559,6 +564,96 @@
}
/**
+ * Test the value of the frame rate cateogry based on the visibility of a view
+ * Invsible: FRAME_RATE_CATEGORY_NO_PREFERENCE
+ * Visible: FRAME_RATE_CATEGORY_HIGH
+ * Also, mIsFrameRateBoosting should be true when the visibility becomes visible
+ */
+ @Test
+ @RequiresFlagsEnabled(FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY)
+ public void votePreferredFrameRate_voteFrameRateCategory_visibility_defaultHigh() {
+ View view = new View(sContext);
+ attachViewToWindow(view);
+ ViewRootImpl viewRootImpl = view.getViewRootImpl();
+ sInstrumentation.runOnMainSync(() -> {
+ view.setVisibility(View.INVISIBLE);
+ view.invalidate();
+ assertEquals(viewRootImpl.getPreferredFrameRateCategory(),
+ FRAME_RATE_CATEGORY_NO_PREFERENCE);
+ });
+ sInstrumentation.waitForIdleSync();
+
+ sInstrumentation.runOnMainSync(() -> {
+ view.setVisibility(View.VISIBLE);
+ view.invalidate();
+ assertEquals(viewRootImpl.getPreferredFrameRateCategory(),
+ FRAME_RATE_CATEGORY_HIGH);
+ });
+ sInstrumentation.waitForIdleSync();
+
+ sInstrumentation.runOnMainSync(() -> {
+ assertEquals(viewRootImpl.getIsFrameRateBoosting(), true);
+ });
+ }
+
+ /**
+ * Test the value of the frame rate cateogry based on the size of a view.
+ * The current threshold value is 7% of the screen size
+ * <7%: FRAME_RATE_CATEGORY_NORMAL
+ */
+ @Test
+ @RequiresFlagsEnabled(FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY)
+ public void votePreferredFrameRate_voteFrameRateCategory_smallSize_defaultHigh() {
+ View view = new View(sContext);
+ WindowManager.LayoutParams wmlp = new WindowManager.LayoutParams(TYPE_APPLICATION_OVERLAY);
+ wmlp.token = new Binder(); // Set a fake token to bypass 'is your activity running' check
+ wmlp.width = 1;
+ wmlp.height = 1;
+
+ sInstrumentation.runOnMainSync(() -> {
+ WindowManager wm = sContext.getSystemService(WindowManager.class);
+ wm.addView(view, wmlp);
+ });
+ sInstrumentation.waitForIdleSync();
+
+ ViewRootImpl viewRootImpl = view.getViewRootImpl();
+ sInstrumentation.runOnMainSync(() -> {
+ view.invalidate();
+ assertEquals(viewRootImpl.getPreferredFrameRateCategory(), FRAME_RATE_CATEGORY_NORMAL);
+ });
+ }
+
+ /**
+ * Test the value of the frame rate cateogry based on the size of a view.
+ * The current threshold value is 7% of the screen size
+ * >=7% : FRAME_RATE_CATEGORY_HIGH
+ */
+ @Test
+ @RequiresFlagsEnabled(FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY)
+ public void votePreferredFrameRate_voteFrameRateCategory_normalSize_defaultHigh() {
+ View view = new View(sContext);
+ WindowManager.LayoutParams wmlp = new WindowManager.LayoutParams(TYPE_APPLICATION_OVERLAY);
+ wmlp.token = new Binder(); // Set a fake token to bypass 'is your activity running' check
+
+ sInstrumentation.runOnMainSync(() -> {
+ WindowManager wm = sContext.getSystemService(WindowManager.class);
+ Display display = wm.getDefaultDisplay();
+ DisplayMetrics metrics = new DisplayMetrics();
+ display.getMetrics(metrics);
+ wmlp.width = (int) (metrics.widthPixels * 0.9);
+ wmlp.height = (int) (metrics.heightPixels * 0.9);
+ wm.addView(view, wmlp);
+ });
+ sInstrumentation.waitForIdleSync();
+
+ ViewRootImpl viewRootImpl = view.getViewRootImpl();
+ sInstrumentation.runOnMainSync(() -> {
+ view.invalidate();
+ assertEquals(viewRootImpl.getPreferredFrameRateCategory(), FRAME_RATE_CATEGORY_HIGH);
+ });
+ }
+
+ /**
* Test how values of the frame rate cateogry are aggregated.
* It should take the max value among all of the voted categories per frame.
*/
@@ -575,8 +670,13 @@
assertEquals(viewRootImpl.getPreferredFrameRateCategory(), FRAME_RATE_CATEGORY_LOW);
viewRootImpl.votePreferredFrameRateCategory(FRAME_RATE_CATEGORY_NORMAL);
assertEquals(viewRootImpl.getPreferredFrameRateCategory(), FRAME_RATE_CATEGORY_NORMAL);
+ viewRootImpl.votePreferredFrameRateCategory(FRAME_RATE_CATEGORY_HIGH_HINT);
+ assertEquals(viewRootImpl.getPreferredFrameRateCategory(),
+ FRAME_RATE_CATEGORY_HIGH_HINT);
viewRootImpl.votePreferredFrameRateCategory(FRAME_RATE_CATEGORY_HIGH);
assertEquals(viewRootImpl.getPreferredFrameRateCategory(), FRAME_RATE_CATEGORY_HIGH);
+ viewRootImpl.votePreferredFrameRateCategory(FRAME_RATE_CATEGORY_HIGH_HINT);
+ assertEquals(viewRootImpl.getPreferredFrameRateCategory(), FRAME_RATE_CATEGORY_HIGH);
viewRootImpl.votePreferredFrameRateCategory(FRAME_RATE_CATEGORY_NORMAL);
assertEquals(viewRootImpl.getPreferredFrameRateCategory(), FRAME_RATE_CATEGORY_HIGH);
viewRootImpl.votePreferredFrameRateCategory(FRAME_RATE_CATEGORY_LOW);
@@ -701,6 +801,88 @@
});
}
+ /**
+ * Test votePreferredFrameRate_voteFrameRateTimeOut
+ * If no frame rate is voted in 100 milliseconds, the value of
+ * mPreferredFrameRate should be set to 0.
+ */
+ @Test
+ @RequiresFlagsEnabled(FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY)
+ public void votePreferredFrameRate_voteFrameRateTimeOut() throws InterruptedException {
+ final long delay = 200L;
+
+ View view = new View(sContext);
+ attachViewToWindow(view);
+ sInstrumentation.waitForIdleSync();
+ ViewRootImpl viewRootImpl = view.getViewRootImpl();
+
+ sInstrumentation.runOnMainSync(() -> {
+ assertEquals(viewRootImpl.getPreferredFrameRate(), 0, 0.1);
+ viewRootImpl.votePreferredFrameRate(24);
+ assertEquals(viewRootImpl.getPreferredFrameRate(), 24, 0.1);
+ view.invalidate();
+ assertEquals(viewRootImpl.getPreferredFrameRate(), 24, 0.1);
+ });
+
+ Thread.sleep(delay);
+ assertEquals(viewRootImpl.getPreferredFrameRate(), 0, 0.1);
+ }
+
+ /**
+ * Test the logic of infrequent layer:
+ * - NORMAL for infrequent update: FT2-FT1 > 100 && FT3-FT2 > 100.
+ * - HIGH/NORMAL based on size for frequent update: (FT3-FT2) + (FT2 - FT1) < 100.
+ * - otherwise, use the previous category value.
+ */
+ @Test
+ @RequiresFlagsEnabled(FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY)
+ public void votePreferredFrameRate_infrequentLayer_defaultHigh() throws InterruptedException {
+ final long delay = 200L;
+
+ View view = new View(sContext);
+ WindowManager.LayoutParams wmlp = new WindowManager.LayoutParams(TYPE_APPLICATION_OVERLAY);
+ wmlp.token = new Binder(); // Set a fake token to bypass 'is your activity running' check
+
+ sInstrumentation.runOnMainSync(() -> {
+ WindowManager wm = sContext.getSystemService(WindowManager.class);
+ Display display = wm.getDefaultDisplay();
+ DisplayMetrics metrics = new DisplayMetrics();
+ display.getMetrics(metrics);
+ wmlp.width = (int) (metrics.widthPixels * 0.9);
+ wmlp.height = (int) (metrics.heightPixels * 0.9);
+ wm.addView(view, wmlp);
+ });
+ sInstrumentation.waitForIdleSync();
+
+ ViewRootImpl viewRootImpl = view.getViewRootImpl();
+
+ // Frequent update
+ sInstrumentation.runOnMainSync(() -> {
+ assertEquals(viewRootImpl.getPreferredFrameRateCategory(),
+ FRAME_RATE_CATEGORY_NO_PREFERENCE);
+ view.invalidate();
+ assertEquals(viewRootImpl.getPreferredFrameRateCategory(), FRAME_RATE_CATEGORY_HIGH);
+ view.invalidate();
+ assertEquals(viewRootImpl.getPreferredFrameRateCategory(), FRAME_RATE_CATEGORY_HIGH);
+ view.invalidate();
+ assertEquals(viewRootImpl.getPreferredFrameRateCategory(), FRAME_RATE_CATEGORY_HIGH);
+ });
+
+ // In transistion from frequent update to infrequent update
+ Thread.sleep(delay);
+ sInstrumentation.runOnMainSync(() -> {
+ view.invalidate();
+ assertEquals(viewRootImpl.getPreferredFrameRateCategory(), FRAME_RATE_CATEGORY_HIGH);
+ });
+
+ // Infrequent update
+ Thread.sleep(delay);
+ sInstrumentation.runOnMainSync(() -> {
+ view.invalidate();
+ assertEquals(viewRootImpl.getPreferredFrameRateCategory(), FRAME_RATE_CATEGORY_NORMAL);
+ });
+ }
+
@Test
public void forceInvertOffDarkThemeOff_forceDarkModeDisabled() {
mSetFlagsRule.enableFlags(FLAG_FORCE_INVERT_COLOR);
diff --git a/core/tests/coretests/src/android/widget/RemoteViewsTest.java b/core/tests/coretests/src/android/widget/RemoteViewsTest.java
index 5862711..3d5494d 100644
--- a/core/tests/coretests/src/android/widget/RemoteViewsTest.java
+++ b/core/tests/coretests/src/android/widget/RemoteViewsTest.java
@@ -422,19 +422,9 @@
if (!drawDataParcel()) {
return;
}
- final RemoteViews.DrawInstructions drawInstructions = getDrawInstructions();
- final RemoteViews rv = new RemoteViews(drawInstructions);
- final View view = rv.apply(mContext, mContainer);
- assertTrue(view instanceof RemoteCanvas);
- assertEquals(drawInstructions, view.getTag());
- }
-
- @Test
- public void remoteCanvasWiresClickHandlers() {
- if (!drawDataParcel()) {
- return;
- }
- final RemoteViews.DrawInstructions drawInstructions = getDrawInstructions();
+ final byte[] bytes = new byte[] {'h', 'e', 'l', 'l', 'o'};
+ final RemoteViews.DrawInstructions drawInstructions =
+ new RemoteViews.DrawInstructions.Builder(Collections.singletonList(bytes)).build();
final RemoteViews rv = new RemoteViews(drawInstructions);
final PendingIntent pi = PendingIntent.getActivity(mContext, 0,
new Intent(Intent.ACTION_VIEW), PendingIntent.FLAG_IMMUTABLE);
@@ -443,15 +433,7 @@
rv.setPendingIntentTemplate(viewId, pi);
rv.setOnClickFillInIntent(viewId, i);
final View view = rv.apply(mContext, mContainer);
- assertTrue(view instanceof RemoteCanvas);
- RemoteCanvas target = (RemoteCanvas) view;
- assertEquals(1, target.getCallbacks().size());
- assertNotNull(target.getCallbacks().get(viewId));
- }
-
- private RemoteViews.DrawInstructions getDrawInstructions() {
- final byte[] bytes = new byte[] {'h', 'e', 'l', 'l', 'o'};
- return new RemoteViews.DrawInstructions.Builder(Collections.singletonList(bytes)).build();
+ assertEquals(drawInstructions, view.getTag());
}
private RemoteViews createViewChained(int depth, String... texts) {
diff --git a/core/tests/coretests/src/android/window/WindowOnBackInvokedDispatcherTest.java b/core/tests/coretests/src/android/window/WindowOnBackInvokedDispatcherTest.java
index 52ff0d4..a709d7b 100644
--- a/core/tests/coretests/src/android/window/WindowOnBackInvokedDispatcherTest.java
+++ b/core/tests/coretests/src/android/window/WindowOnBackInvokedDispatcherTest.java
@@ -20,7 +20,6 @@
import static android.window.OnBackInvokedDispatcher.PRIORITY_OVERLAY;
import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNull;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.isNull;
import static org.mockito.Mockito.atLeast;
@@ -359,7 +358,7 @@
}
@Test
- public void onDetachFromWindow_cancelsBackAnimation() throws RemoteException {
+ public void onDetachFromWindow_cancelCallbackAndIgnoreOnBackInvoked() throws RemoteException {
mDispatcher.registerOnBackInvokedCallback(PRIORITY_DEFAULT, mCallback1);
OnBackInvokedCallbackInfo callbackInfo = assertSetCallbackInfo();
@@ -369,12 +368,13 @@
waitForIdle();
verify(mCallback1).onBackStarted(any(BackEvent.class));
- // This should trigger mCallback1.onBackCancelled() and unset the callback in WM
+ // This should trigger mCallback1.onBackCancelled()
mDispatcher.detachFromWindow();
+ // This should be ignored by mCallback1
+ callbackInfo.getCallback().onBackInvoked();
- OnBackInvokedCallbackInfo callbackInfo1 = assertSetCallbackInfo();
- assertNull(callbackInfo1);
waitForIdle();
+ verify(mCallback1, never()).onBackInvoked();
verify(mCallback1).onBackCancelled();
}
}
diff --git a/core/tests/coretests/src/com/android/internal/accessibility/AccessibilityShortcutControllerTest.java b/core/tests/coretests/src/com/android/internal/accessibility/AccessibilityShortcutControllerTest.java
index 145aa60d..75b0d4a 100644
--- a/core/tests/coretests/src/com/android/internal/accessibility/AccessibilityShortcutControllerTest.java
+++ b/core/tests/coretests/src/com/android/internal/accessibility/AccessibilityShortcutControllerTest.java
@@ -21,6 +21,7 @@
import static android.provider.Settings.Secure.ACCESSIBILITY_SHORTCUT_ON_LOCK_SCREEN;
import static android.provider.Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_SERVICE;
import static android.provider.Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES;
+import static android.view.accessibility.AccessibilityManager.ACCESSIBILITY_SHORTCUT_KEY;
import static com.android.internal.accessibility.AccessibilityShortcutController.ACCESSIBILITY_HEARING_AIDS_COMPONENT_NAME;
import static com.android.internal.accessibility.AccessibilityShortcutController.COLOR_INVERSION_COMPONENT_NAME;
@@ -82,7 +83,6 @@
import com.android.internal.R;
import com.android.internal.accessibility.AccessibilityShortcutController.FrameworkObjectProvider;
-import com.android.internal.accessibility.common.ShortcutConstants;
import com.android.internal.util.test.FakeSettingsProvider;
import org.junit.AfterClass;
@@ -726,14 +726,14 @@
private void configureNoShortcutService() throws Exception {
when(mAccessibilityManagerService
- .getAccessibilityShortcutTargets(ShortcutConstants.UserShortcutType.HARDWARE))
+ .getAccessibilityShortcutTargets(ACCESSIBILITY_SHORTCUT_KEY))
.thenReturn(Collections.emptyList());
Settings.Secure.putString(mContentResolver, ACCESSIBILITY_SHORTCUT_TARGET_SERVICE, "");
}
private void configureValidShortcutService() throws Exception {
when(mAccessibilityManagerService
- .getAccessibilityShortcutTargets(ShortcutConstants.UserShortcutType.HARDWARE))
+ .getAccessibilityShortcutTargets(ACCESSIBILITY_SHORTCUT_KEY))
.thenReturn(Collections.singletonList(SERVICE_NAME_STRING));
Settings.Secure.putString(
mContentResolver, ACCESSIBILITY_SHORTCUT_TARGET_SERVICE, SERVICE_NAME_STRING);
@@ -744,7 +744,7 @@
(ComponentName) AccessibilityShortcutController.getFrameworkShortcutFeaturesMap()
.keySet().toArray()[0];
when(mAccessibilityManagerService
- .getAccessibilityShortcutTargets(ShortcutConstants.UserShortcutType.HARDWARE))
+ .getAccessibilityShortcutTargets(ACCESSIBILITY_SHORTCUT_KEY))
.thenReturn(Collections.singletonList(featureComponentName.flattenToString()));
Settings.Secure.putString(mContentResolver, ACCESSIBILITY_SHORTCUT_TARGET_SERVICE,
featureComponentName.flattenToString());
@@ -806,7 +806,7 @@
private void configureDefaultAccessibilityService() throws Exception {
when(mAccessibilityManagerService
- .getAccessibilityShortcutTargets(ShortcutConstants.UserShortcutType.HARDWARE))
+ .getAccessibilityShortcutTargets(ACCESSIBILITY_SHORTCUT_KEY))
.thenReturn(Collections.singletonList(SERVICE_NAME_STRING));
when(mResources.getString(R.string.config_defaultAccessibilityService)).thenReturn(
diff --git a/core/tests/coretests/src/com/android/internal/accessibility/dialog/InvisibleToggleAccessibilityServiceTargetTest.java b/core/tests/coretests/src/com/android/internal/accessibility/dialog/InvisibleToggleAccessibilityServiceTargetTest.java
index 2ea044c..69b6a9b7a 100644
--- a/core/tests/coretests/src/com/android/internal/accessibility/dialog/InvisibleToggleAccessibilityServiceTargetTest.java
+++ b/core/tests/coretests/src/com/android/internal/accessibility/dialog/InvisibleToggleAccessibilityServiceTargetTest.java
@@ -39,7 +39,6 @@
import androidx.test.runner.AndroidJUnit4;
import com.android.internal.accessibility.TestUtils;
-import com.android.internal.accessibility.common.ShortcutConstants;
import com.android.internal.util.test.FakeSettingsProvider;
import com.android.internal.util.test.FakeSettingsProviderRule;
@@ -100,7 +99,7 @@
mSut = new InvisibleToggleAccessibilityServiceTarget(
mContextSpy,
- ShortcutConstants.UserShortcutType.HARDWARE, accessibilityServiceInfo);
+ AccessibilityManager.ACCESSIBILITY_SHORTCUT_KEY, accessibilityServiceInfo);
}
@Test
diff --git a/core/tests/coretests/src/com/android/internal/policy/PhoneWindowTest.java b/core/tests/coretests/src/com/android/internal/policy/PhoneWindowTest.java
index de55b07..3df3b9d2 100644
--- a/core/tests/coretests/src/com/android/internal/policy/PhoneWindowTest.java
+++ b/core/tests/coretests/src/com/android/internal/policy/PhoneWindowTest.java
@@ -20,6 +20,7 @@
import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT;
import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER;
import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
+import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_EDGE_TO_EDGE_ENFORCED;
import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertThat;
@@ -63,7 +64,8 @@
createPhoneWindowWithTheme(R.style.LayoutInDisplayCutoutModeUnset);
installDecor();
- if (mPhoneWindow.mEdgeToEdgeEnforced && !mPhoneWindow.isFloating()) {
+ if ((mPhoneWindow.getAttributes().privateFlags & PRIVATE_FLAG_EDGE_TO_EDGE_ENFORCED) != 0
+ && !mPhoneWindow.isFloating()) {
assertThat(mPhoneWindow.getAttributes().layoutInDisplayCutoutMode,
is(LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS));
} else {
diff --git a/core/tests/coretests/src/com/android/internal/widget/NotificationOptimizedLinearLayoutComparisonTest.java b/core/tests/coretests/src/com/android/internal/widget/NotificationOptimizedLinearLayoutComparisonTest.java
new file mode 100644
index 0000000..08333ec
--- /dev/null
+++ b/core/tests/coretests/src/com/android/internal/widget/NotificationOptimizedLinearLayoutComparisonTest.java
@@ -0,0 +1,330 @@
+/*
+ * Copyright (C) 2024 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.widget;
+
+
+import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
+import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
+
+import static org.junit.Assert.assertEquals;
+
+import android.content.Context;
+import android.platform.test.annotations.EnableFlags;
+import android.platform.test.annotations.Presubmit;
+import android.platform.test.flag.junit.SetFlagsRule;
+import android.view.Gravity;
+import android.view.View;
+import android.view.View.MeasureSpec;
+import android.view.ViewGroup;
+import android.widget.LinearLayout;
+import android.widget.flags.Flags;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.LargeTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.google.common.truth.Expect;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Tests the consistency of {@link NotificationOptimizedLinearLayout}'s onMeasure and onLayout
+ * implementations with the behavior of the standard Android LinearLayout.
+ */
+@LargeTest
+@RunWith(AndroidJUnit4.class)
+@EnableFlags(Flags.FLAG_NOTIF_LINEARLAYOUT_OPTIMIZED)
+@Presubmit
+public class NotificationOptimizedLinearLayoutComparisonTest {
+
+ @Rule
+ public final Expect mExpect = Expect.create();
+
+ private static final int[] ORIENTATIONS = {LinearLayout.VERTICAL, LinearLayout.HORIZONTAL};
+ private static final int EXACT_SPEC = MeasureSpec.makeMeasureSpec(500,
+ MeasureSpec.EXACTLY);
+ private static final int AT_MOST_SPEC = MeasureSpec.makeMeasureSpec(500,
+ MeasureSpec.AT_MOST);
+
+ private static final int[] MEASURE_SPECS = {EXACT_SPEC, AT_MOST_SPEC};
+
+ private static final int[] GRAVITIES =
+ {Gravity.NO_GRAVITY, Gravity.TOP, Gravity.LEFT, Gravity.CENTER};
+
+ private static final int[] LAYOUT_PARAMS = {MATCH_PARENT, WRAP_CONTENT, 0, 50};
+ private static final int[] CHILD_WEIGHTS = {0, 1};
+
+ @Rule
+ public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+
+ private Context mContext;
+
+ @Before
+ public void before() {
+ mContext = InstrumentationRegistry.getTargetContext();
+ }
+
+ @Test
+ public void test() throws Throwable {
+ for (int orientation : ORIENTATIONS) {
+ for (int widthSpec : MEASURE_SPECS) {
+ for (int heightSpec : MEASURE_SPECS) {
+ for (int firstChildGravity : GRAVITIES) {
+ for (int secondChildGravity : GRAVITIES) {
+ for (int firstChildLayoutWidth : LAYOUT_PARAMS) {
+ for (int firstChildLayoutHeight : LAYOUT_PARAMS) {
+ for (int secondChildLayoutWidth : LAYOUT_PARAMS) {
+ for (int secondChildLayoutHeight : LAYOUT_PARAMS) {
+ for (int firstChildWeight : CHILD_WEIGHTS) {
+ for (int secondChildWeight : CHILD_WEIGHTS) {
+ executeTest(/*testSpec =*/createTestSpec(
+ orientation,
+ widthSpec, heightSpec,
+ firstChildLayoutWidth,
+ firstChildLayoutHeight,
+ secondChildLayoutWidth,
+ secondChildLayoutHeight,
+ firstChildGravity,
+ secondChildGravity,
+ firstChildWeight,
+ secondChildWeight));
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ private void executeTest(TestSpec testSpec) {
+ // GIVEN
+ final List<View> controlChildren =
+ new ArrayList<>();
+ final List<View> testChildren =
+ new ArrayList<>();
+
+ controlChildren.add(
+ buildChildView(
+ testSpec.mFirstChildLayoutWidth,
+ testSpec.mFirstChildLayoutHeight,
+ testSpec.mFirstChildGravity,
+ testSpec.mFirstChildWeight));
+ controlChildren.add(
+ buildChildView(
+ testSpec.mSecondChildLayoutWidth,
+ testSpec.mSecondChildLayoutHeight,
+ testSpec.mSecondChildGravity,
+ testSpec.mSecondChildWeight));
+
+ testChildren.add(
+ buildChildView(
+ testSpec.mFirstChildLayoutWidth,
+ testSpec.mFirstChildLayoutHeight,
+ testSpec.mFirstChildGravity,
+ testSpec.mFirstChildWeight));
+ testChildren.add(
+ buildChildView(
+ testSpec.mSecondChildLayoutWidth,
+ testSpec.mSecondChildLayoutHeight,
+ testSpec.mSecondChildGravity,
+ testSpec.mSecondChildWeight));
+
+ final LinearLayout controlContainer = buildLayout(false,
+ testSpec.mOrientation,
+ controlChildren);
+
+ final LinearLayout testContainer = buildLayout(true,
+ testSpec.mOrientation,
+ testChildren);
+
+ // WHEN
+ controlContainer.measure(testSpec.mWidthSpec, testSpec.mHeightSpec);
+ testContainer.measure(testSpec.mWidthSpec, testSpec.mHeightSpec);
+ controlContainer.layout(0, 0, 1000, 1000);
+ testContainer.layout(0, 0, 1000, 1000);
+ // THEN
+ assertLayoutsEqual("Test Case:" + testSpec, controlContainer, testContainer);
+ }
+
+ private static class TestSpec {
+ private final int mOrientation;
+ private final int mWidthSpec;
+ private final int mHeightSpec;
+ private final int mFirstChildLayoutWidth;
+ private final int mFirstChildLayoutHeight;
+ private final int mSecondChildLayoutWidth;
+ private final int mSecondChildLayoutHeight;
+ private final int mFirstChildGravity;
+ private final int mSecondChildGravity;
+ private final int mFirstChildWeight;
+ private final int mSecondChildWeight;
+
+ TestSpec(
+ int orientation,
+ int widthSpec,
+ int heightSpec,
+ int firstChildLayoutWidth,
+ int firstChildLayoutHeight,
+ int secondChildLayoutWidth,
+ int secondChildLayoutHeight,
+ int firstChildGravity,
+ int secondChildGravity,
+ int firstChildWeight,
+ int secondChildWeight) {
+ mOrientation = orientation;
+ mWidthSpec = widthSpec;
+ mHeightSpec = heightSpec;
+ mFirstChildLayoutWidth = firstChildLayoutWidth;
+ mFirstChildLayoutHeight = firstChildLayoutHeight;
+ mSecondChildLayoutWidth = secondChildLayoutWidth;
+ mSecondChildLayoutHeight = secondChildLayoutHeight;
+ mFirstChildGravity = firstChildGravity;
+ mSecondChildGravity = secondChildGravity;
+ mFirstChildWeight = firstChildWeight;
+ mSecondChildWeight = secondChildWeight;
+ }
+
+ @Override
+ public String toString() {
+ return "TestSpec{"
+ + "mOrientation=" + orientationToString(mOrientation)
+ + ", mWidthSpec=" + MeasureSpec.toString(mWidthSpec)
+ + ", mHeightSpec=" + MeasureSpec.toString(mHeightSpec)
+ + ", mFirstChildLayoutWidth=" + sizeToString(mFirstChildLayoutWidth)
+ + ", mFirstChildLayoutHeight=" + sizeToString(mFirstChildLayoutHeight)
+ + ", mSecondChildLayoutWidth=" + sizeToString(mSecondChildLayoutWidth)
+ + ", mSecondChildLayoutHeight=" + sizeToString(mSecondChildLayoutHeight)
+ + ", mFirstChildGravity=" + mFirstChildGravity
+ + ", mSecondChildGravity=" + mSecondChildGravity
+ + ", mFirstChildWeight=" + mFirstChildWeight
+ + ", mSecondChildWeight=" + mSecondChildWeight
+ + '}';
+ }
+
+ private String orientationToString(int orientation) {
+ if (orientation == LinearLayout.VERTICAL) {
+ return "vertical";
+ } else if (orientation == LinearLayout.HORIZONTAL) {
+ return "horizontal";
+ }
+ throw new IllegalArgumentException();
+ }
+
+ private String sizeToString(int size) {
+ if (size == WRAP_CONTENT) {
+ return "wrap-content";
+ }
+ if (size == MATCH_PARENT) {
+ return "match-parent";
+ }
+ return String.valueOf(size);
+ }
+ }
+
+ private LinearLayout buildLayout(boolean isNotificationOptimized,
+ @LinearLayout.OrientationMode int orientation, List<View> children) {
+ final LinearLayout linearLayout;
+ if (isNotificationOptimized) {
+ linearLayout = new NotificationOptimizedLinearLayout(mContext);
+ } else {
+ linearLayout = new LinearLayout(mContext);
+ }
+ linearLayout.setOrientation(orientation);
+ for (int i = 0; i < children.size(); i++) {
+ linearLayout.addView(children.get(i));
+ }
+ return linearLayout;
+ }
+
+ private void assertLayoutsEqual(String testCase, View controlView, View testView) {
+ mExpect.withMessage("MeasuredWidths are not equal. Test Case:" + testCase)
+ .that(testView.getMeasuredWidth()).isEqualTo(controlView.getMeasuredWidth());
+ mExpect.withMessage("MeasuredHeights are not equal. Test Case:" + testCase)
+ .that(testView.getMeasuredHeight()).isEqualTo(controlView.getMeasuredHeight());
+ mExpect.withMessage("Left Positions are not equal. Test Case:" + testCase)
+ .that(testView.getLeft()).isEqualTo(controlView.getLeft());
+ mExpect.withMessage("Top Positions are not equal. Test Case:" + testCase)
+ .that(testView.getTop()).isEqualTo(controlView.getTop());
+ if (controlView instanceof ViewGroup && testView instanceof ViewGroup) {
+ final ViewGroup controlGroup = (ViewGroup) controlView;
+ final ViewGroup testGroup = (ViewGroup) testView;
+ // Test and Control Views should be identical by hierarchy for the comparison.
+ // That's why mExpect is not used here for assertion.
+ assertEquals(controlGroup.getChildCount(), testGroup.getChildCount());
+
+ for (int i = 0; i < controlGroup.getChildCount(); i++) {
+ View controlChild = controlGroup.getChildAt(i);
+ View testChild = testGroup.getChildAt(i);
+
+ assertLayoutsEqual(testCase, controlChild, testChild);
+ }
+ }
+ }
+
+ private static class TestView extends View {
+ TestView(Context context) {
+ super(context);
+ }
+
+ @Override
+ public int getBaseline() {
+ return 5;
+ }
+ }
+
+
+ private TestSpec createTestSpec(int orientation,
+ int widthSpec, int heightSpec,
+ int firstChildLayoutWidth, int firstChildLayoutHeight, int secondChildLayoutWidth,
+ int secondChildLayoutHeight, int firstChildGravity, int secondChildGravity,
+ int firstChildWeight, int secondChildWeight) {
+
+ return new TestSpec(
+ orientation,
+ widthSpec, heightSpec,
+ firstChildLayoutWidth,
+ firstChildLayoutHeight,
+ secondChildLayoutWidth,
+ secondChildLayoutHeight,
+ firstChildGravity,
+ secondChildGravity,
+ firstChildWeight,
+ secondChildWeight);
+ }
+
+ private View buildChildView(int childLayoutWidth, int childLayoutHeight,
+ int childGravity, int childWeight) {
+ final View childView = new TestView(mContext);
+ // Set desired size using LayoutParams
+ LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(childLayoutWidth,
+ childLayoutHeight, childWeight);
+ params.gravity = childGravity;
+ childView.setLayoutParams(params);
+ return childView;
+ }
+}
diff --git a/core/tests/mockingcoretests/src/android/app/activity/ActivityThreadClientTest.java b/core/tests/mockingcoretests/src/android/app/activity/ActivityThreadClientTest.java
index 3e0e36d..39cb616 100644
--- a/core/tests/mockingcoretests/src/android/app/activity/ActivityThreadClientTest.java
+++ b/core/tests/mockingcoretests/src/android/app/activity/ActivityThreadClientTest.java
@@ -353,7 +353,8 @@
null /* pendingResults */, null /* pendingNewIntents */,
null /* activityOptions */, true /* isForward */, null /* profilerInfo */,
mThread /* client */, null /* asssitToken */, null /* shareableActivityToken */,
- false /* launchedFromBubble */, null /* taskfragmentToken */);
+ false /* launchedFromBubble */, null /* taskfragmentToken */,
+ null /* initialCallerInfoAccessToken */);
}
@Override
diff --git a/data/etc/Android.bp b/data/etc/Android.bp
index ade20d2..1fd1003 100644
--- a/data/etc/Android.bp
+++ b/data/etc/Android.bp
@@ -66,6 +66,12 @@
src: "preinstalled-packages-strict-signature.xml",
}
+prebuilt_etc {
+ name: "enhanced-confirmation.xml",
+ sub_dir: "sysconfig",
+ src: "enhanced-confirmation.xml",
+}
+
// Privapp permission whitelist files
prebuilt_etc {
diff --git a/data/etc/enhanced-confirmation.xml b/data/etc/enhanced-confirmation.xml
new file mode 100644
index 0000000..3b1867c
--- /dev/null
+++ b/data/etc/enhanced-confirmation.xml
@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2024 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<!--
+This XML defines an allowlist of packages that should be exempt from ECM (Enhanced Confirmation
+Mode).
+
+Example usage:
+
+ <enhanced-confirmation-trusted-package
+ package="com.example.app"
+ sha256-cert-digest="E9:7A:BC:2C:D1:CA:8D:58:6A:57:0B:8C:F8:60:AA:D2:8D:13:30:2A:FB:C9:00:2C:5D:53:B2:6C:09:A4:85:A0"/>
+
+ ...
+
+ <enhanced-confirmation-trusted-installer
+ package="com.example.installer"
+ sha256-cert-digest="E9:7A:BC:2C:D1:CA:8D:58:6A:57:0B:8C:F8:60:AA:D2:8D:13:30:2A:FB:C9:00:2C:5D:53:B2:6C:09:A4:85:A0"/>
+
+ ...
+
+The "enhanced-confirmation-trusted-package" entry shown above indicates that "com.example.app"
+should be considered a "trusted package". A "trusted package" will be exempt from ECM restrictions.
+
+The "enhanced-confirmation-trusted-installer" entry shown above indicates that
+"com.example.installer" should be considered a "trusted installer". A "trusted installer", and all
+packages that it installs, will be exempt from ECM restrictions. (There are some exceptions to this.
+For example, a trusted installer, at the time of installing an app, can opt the app back in to ECM
+restrictions by setting the app's package source to PackageInstaller.PACKAGE_SOURCE_DOWNLOADED_FILE
+or PackageInstaller.PACKAGE_SOURCE_LOCAL_FILE.)
+
+In either case:
+
+- The "package" XML attribute refers to the app's package name.
+- The "sha256-cert-digest" XML attribute refers to the SHA-256 hash of an app signing certificate.
+
+For any entry to successfully apply to a package, both XML attributes must be present, and must
+match the package. That is, the package name must match the "package" attribute, and the app must be
+signed by the signing certificate identified by the "sha256-cert-digest" attribute..
+-->
+
+<config></config>
diff --git a/data/etc/platform.xml b/data/etc/platform.xml
index 13d38d2..9d1e507 100644
--- a/data/etc/platform.xml
+++ b/data/etc/platform.xml
@@ -124,6 +124,10 @@
<group gid="security_log_writer" />
</permission>
+ <permission name="android.permission.MANAGE_VIRTUAL_MACHINE">
+ <group gid="virtualmachine" />
+ </permission>
+
<!-- These are permissions that were mapped to gids but we need
to keep them here until an upgrade from L to the current
version is to be supported. These permissions are built-in
diff --git a/data/etc/preinstalled-packages-platform.xml b/data/etc/preinstalled-packages-platform.xml
index bf60944..7823277 100644
--- a/data/etc/preinstalled-packages-platform.xml
+++ b/data/etc/preinstalled-packages-platform.xml
@@ -108,6 +108,7 @@
<install-in user-type="FULL" />
<install-in user-type="PROFILE" />
<do-not-install-in user-type="android.os.usertype.profile.CLONE" />
+ <do-not-install-in user-type="android.os.usertype.profile.PRIVATE" />
</install-in-user-type>
<!-- Settings (Settings app) -->
diff --git a/data/etc/privapp-permissions-platform.xml b/data/etc/privapp-permissions-platform.xml
index d4c2c2b..0baaff0 100644
--- a/data/etc/privapp-permissions-platform.xml
+++ b/data/etc/privapp-permissions-platform.xml
@@ -185,6 +185,8 @@
<permission name="android.permission.READ_COMPAT_CHANGE_CONFIG"/>
<permission name="android.permission.UWB_PRIVILEGED"/>
<permission name="android.permission.READ_SYSTEM_GRAMMATICAL_GENDER"/>
+ <permission name="android.permission.UPDATE_CONFIG"/>
+ <permission name="android.permission.SEND_SAFETY_CENTER_UPDATE" />
</privapp-permissions>
<privapp-permissions package="com.android.providers.calendar">
diff --git a/graphics/java/android/graphics/Paint.java b/graphics/java/android/graphics/Paint.java
index ae61a2d..df95a91 100644
--- a/graphics/java/android/graphics/Paint.java
+++ b/graphics/java/android/graphics/Paint.java
@@ -17,7 +17,7 @@
package android.graphics;
import static com.android.text.flags.Flags.FLAG_FIX_LINE_HEIGHT_FOR_LOCALE;
-import static com.android.text.flags.Flags.FLAG_INTER_CHARACTER_JUSTIFICATION;
+import static com.android.text.flags.Flags.FLAG_LETTER_SPACING_JUSTIFICATION;
import android.annotation.ColorInt;
import android.annotation.ColorLong;
@@ -293,7 +293,7 @@
* {@link Paint#measureText(CharSequence, int, int)}. The non-run based APIs works as both
* {@link #TEXT_RUN_FLAG_LEFT_EDGE} and {@link #TEXT_RUN_FLAG_RIGHT_EDGE} are specified.
*/
- @FlaggedApi(FLAG_INTER_CHARACTER_JUSTIFICATION)
+ @FlaggedApi(FLAG_LETTER_SPACING_JUSTIFICATION)
public static final int TEXT_RUN_FLAG_LEFT_EDGE = 0x2000;
@@ -324,7 +324,7 @@
* {@link Paint#measureText(CharSequence, int, int)}. The non-run based APIs works as both
* {@link #TEXT_RUN_FLAG_LEFT_EDGE} and {@link #TEXT_RUN_FLAG_RIGHT_EDGE} are specified.
*/
- @FlaggedApi(FLAG_INTER_CHARACTER_JUSTIFICATION)
+ @FlaggedApi(FLAG_LETTER_SPACING_JUSTIFICATION)
public static final int TEXT_RUN_FLAG_RIGHT_EDGE = 0x4000;
// These flags are always set on a new/reset paint, even if flags 0 is passed.
diff --git a/graphics/java/android/graphics/SurfaceTexture.java b/graphics/java/android/graphics/SurfaceTexture.java
index dd82fed..50b167e 100644
--- a/graphics/java/android/graphics/SurfaceTexture.java
+++ b/graphics/java/android/graphics/SurfaceTexture.java
@@ -489,7 +489,7 @@
@Surface.ChangeFrameRateStrategy int changeFrameRateStrategy) {
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "postOnSetFrameRateEventFromNative");
try {
- if (Flags.toolkitSetFrameRate()) {
+ if (Flags.toolkitSetFrameRateReadOnly()) {
SurfaceTexture st = weakSelf.get();
if (st != null) {
Handler handler = st.mOnSetFrameRateHandler;
diff --git a/graphics/java/android/graphics/drawable/Icon.java b/graphics/java/android/graphics/drawable/Icon.java
index 45e29a8..f359025 100644
--- a/graphics/java/android/graphics/drawable/Icon.java
+++ b/graphics/java/android/graphics/drawable/Icon.java
@@ -154,6 +154,12 @@
// TYPE_DATA: data offset
private int mInt2;
+ // TYPE_RESOURCE: use the monochrome drawable from an AdaptiveIconDrawable
+ private boolean mUseMonochrome = false;
+
+ // TYPE_RESOURCE: wrap the monochrome drawable in an InsetDrawable with the specified inset
+ private float mInsetScale = 0.0f;
+
/**
* Gets the type of the icon provided.
* <p>
@@ -368,10 +374,34 @@
result.setTintList(mTintList);
result.setTintBlendMode(mBlendMode);
}
+
+ if (mUseMonochrome) {
+ return crateMonochromeDrawable(result, mInsetScale);
+ }
+
return result;
}
/**
+ * Gets the monochrome drawable from an {@link AdaptiveIconDrawable}.
+ *
+ * @param drawable An {@link AdaptiveIconDrawable}
+ * @return Adjusted (wrapped in {@link InsetDrawable}) monochrome drawable
+ * from an {@link AdaptiveIconDrawable}.
+ * Or the original drawable if no monochrome layer exists.
+ */
+ private static Drawable crateMonochromeDrawable(Drawable drawable, float inset) {
+ if (drawable instanceof AdaptiveIconDrawable) {
+ Drawable monochromeDrawable = ((AdaptiveIconDrawable) drawable).getMonochrome();
+ // wrap with negative inset => scale icon (inspired from BaseIconFactory)
+ if (monochromeDrawable != null) {
+ return new InsetDrawable(monochromeDrawable, inset);
+ }
+ }
+ return drawable;
+ }
+
+ /**
* Resizes image if size too large for Canvas to draw
* @param bitmap Bitmap to be resized if size > {@link RecordingCanvas.MAX_BITMAP_SIZE}
* @return resized bitmap
@@ -693,7 +723,9 @@
&& Arrays.equals(getDataBytes(), otherIcon.getDataBytes());
case TYPE_RESOURCE:
return getResId() == otherIcon.getResId()
- && Objects.equals(getResPackage(), otherIcon.getResPackage());
+ && Objects.equals(getResPackage(), otherIcon.getResPackage())
+ && mUseMonochrome == otherIcon.mUseMonochrome
+ && mInsetScale == otherIcon.mInsetScale;
case TYPE_URI:
case TYPE_URI_ADAPTIVE_BITMAP:
return Objects.equals(getUriString(), otherIcon.getUriString());
@@ -748,6 +780,26 @@
}
/**
+ * Create an Icon pointing to a drawable resource.
+ * @param resPackage Name of the package containing the resource in question
+ * @param resId ID of the drawable resource
+ * @param useMonochrome if this icon should use the monochrome res from the adaptive drawable
+ * @hide
+ */
+ public static @NonNull Icon createWithResourceAdaptiveDrawable(@NonNull String resPackage,
+ @DrawableRes int resId, boolean useMonochrome, float inset) {
+ if (resPackage == null) {
+ throw new IllegalArgumentException("Resource package name must not be null.");
+ }
+ final Icon rep = new Icon(TYPE_RESOURCE);
+ rep.mInt1 = resId;
+ rep.mUseMonochrome = useMonochrome;
+ rep.mInsetScale = inset;
+ rep.mString1 = resPackage;
+ return rep;
+ }
+
+ /**
* Create an Icon pointing to a bitmap in memory.
* @param bits A valid {@link android.graphics.Bitmap} object
*/
@@ -986,6 +1038,8 @@
final int resId = in.readInt();
mString1 = pkg;
mInt1 = resId;
+ mUseMonochrome = in.readBoolean();
+ mInsetScale = in.readFloat();
break;
case TYPE_DATA:
final int len = in.readInt();
@@ -1027,6 +1081,8 @@
case TYPE_RESOURCE:
dest.writeString(getResPackage());
dest.writeInt(getResId());
+ dest.writeBoolean(mUseMonochrome);
+ dest.writeFloat(mInsetScale);
break;
case TYPE_DATA:
dest.writeInt(getDataLength());
diff --git a/graphics/java/android/graphics/text/LineBreaker.java b/graphics/java/android/graphics/text/LineBreaker.java
index 9707126..0e9f29d 100644
--- a/graphics/java/android/graphics/text/LineBreaker.java
+++ b/graphics/java/android/graphics/text/LineBreaker.java
@@ -17,7 +17,7 @@
package android.graphics.text;
import static com.android.text.flags.Flags.FLAG_USE_BOUNDS_FOR_WIDTH;
-import static com.android.text.flags.Flags.FLAG_INTER_CHARACTER_JUSTIFICATION;
+import static com.android.text.flags.Flags.FLAG_LETTER_SPACING_JUSTIFICATION;
import android.annotation.FlaggedApi;
import android.annotation.FloatRange;
@@ -183,7 +183,7 @@
/**
* Value for justification mode indicating the text is justified by stretching letter spacing.
*/
- @FlaggedApi(FLAG_INTER_CHARACTER_JUSTIFICATION)
+ @FlaggedApi(FLAG_LETTER_SPACING_JUSTIFICATION)
public static final int JUSTIFICATION_MODE_INTER_CHARACTER = 2;
/**
diff --git a/keystore/java/android/security/AndroidKeyStoreMaintenance.java b/keystore/java/android/security/AndroidKeyStoreMaintenance.java
index 2beb434..2430e8d 100644
--- a/keystore/java/android/security/AndroidKeyStoreMaintenance.java
+++ b/keystore/java/android/security/AndroidKeyStoreMaintenance.java
@@ -18,6 +18,7 @@
import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.os.RemoteException;
import android.os.ServiceManager;
import android.os.ServiceSpecificException;
import android.os.StrictMode;
@@ -218,4 +219,28 @@
return SYSTEM_ERROR;
}
}
+
+ /**
+ * Returns the list of Application UIDs that have auth-bound keys that are bound to
+ * the given SID. This enables warning the user when they are about to invalidate
+ * a SID (for example, removing the LSKF).
+ *
+ * @param userId - The ID of the user the SID is associated with.
+ * @param userSecureId - The SID in question.
+ *
+ * @return A list of app UIDs.
+ */
+ public static long[] getAllAppUidsAffectedBySid(int userId, long userSecureId)
+ throws KeyStoreException {
+ StrictMode.noteDiskWrite();
+ try {
+ return getService().getAppUidsAffectedBySid(userId, userSecureId);
+ } catch (RemoteException | NullPointerException e) {
+ throw new KeyStoreException(SYSTEM_ERROR,
+ "Failure to connect to Keystore while trying to get apps affected by SID.");
+ } catch (ServiceSpecificException e) {
+ throw new KeyStoreException(e.errorCode,
+ "Keystore error while trying to get apps affected by SID.");
+ }
+ }
}
diff --git a/libs/WindowManager/Shell/res/values/config.xml b/libs/WindowManager/Shell/res/values/config.xml
index 9854e58..a80afe2 100644
--- a/libs/WindowManager/Shell/res/values/config.xml
+++ b/libs/WindowManager/Shell/res/values/config.xml
@@ -45,9 +45,6 @@
<!-- Allow PIP to resize to a slightly bigger state upon touch/showing the menu -->
<bool name="config_pipEnableResizeForMenu">true</bool>
- <!-- Allow PIP to resize via dragging the corner of PiP. -->
- <bool name="config_pipEnableDragCornerResize">false</bool>
-
<!-- PiP minimum size, which is a % based off the shorter side of display width and height -->
<fraction name="config_pipShortestEdgePercent">40%</fraction>
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossActivityBackAnimation.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossActivityBackAnimation.java
index 160f922..55982dc 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossActivityBackAnimation.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossActivityBackAnimation.java
@@ -310,12 +310,16 @@
float top = mapRange(progress, mEnteringStartRect.top, mStartTaskRect.top);
float width = mapRange(progress, mEnteringStartRect.width(), mStartTaskRect.width());
float height = mapRange(progress, mEnteringStartRect.height(), mStartTaskRect.height());
- float alpha = mapRange(progress, mEnteringProgress, 1.0f);
-
+ float alpha = mapRange(progress, getPreCommitEnteringAlpha(), 1.0f);
mEnteringRect.set(left, top, left + width, top + height);
applyTransform(mEnteringTarget.leash, mEnteringRect, alpha);
}
+ private float getPreCommitEnteringAlpha() {
+ return Math.max(smoothstep(ENTER_ALPHA_THRESHOLD, 0.7f, mEnteringProgress),
+ MIN_WINDOW_ALPHA);
+ }
+
private float getEnteringProgress() {
return mEnteringProgress * SCALE_FACTOR;
}
@@ -325,9 +329,7 @@
if (mEnteringTarget != null && mEnteringTarget.leash != null) {
transformWithProgress(
mEnteringProgress,
- Math.max(
- smoothstep(ENTER_ALPHA_THRESHOLD, 0.7f, mEnteringProgress),
- MIN_WINDOW_ALPHA), /* alpha */
+ getPreCommitEnteringAlpha(),
mEnteringTarget.leash,
mEnteringRect,
-mWindowXShift,
@@ -336,6 +338,11 @@
}
}
+ private float getPreCommitLeavingAlpha() {
+ return Math.max(1 - smoothstep(0, ENTER_ALPHA_THRESHOLD, mLeavingProgress),
+ MIN_WINDOW_ALPHA);
+ }
+
private float getLeavingProgress() {
return mLeavingProgress * SCALE_FACTOR;
}
@@ -345,9 +352,7 @@
if (mClosingTarget != null && mClosingTarget.leash != null) {
transformWithProgress(
mLeavingProgress,
- Math.max(
- 1 - smoothstep(0, ENTER_ALPHA_THRESHOLD, mLeavingProgress),
- MIN_WINDOW_ALPHA),
+ getPreCommitLeavingAlpha(),
mClosingTarget.leash,
mClosingRect,
0,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java
index f32f030..7a4ad0a 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java
@@ -19,6 +19,7 @@
import static android.os.AsyncTask.Status.FINISHED;
import static com.android.internal.annotations.VisibleForTesting.Visibility.PRIVATE;
+import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_BUBBLES;
import android.annotation.DimenRes;
import android.annotation.Hide;
@@ -47,6 +48,7 @@
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.logging.InstanceId;
+import com.android.internal.protolog.common.ProtoLog;
import com.android.launcher3.icons.BubbleIconFactory;
import com.android.wm.shell.bubbles.bar.BubbleBarExpandedView;
import com.android.wm.shell.bubbles.bar.BubbleBarLayerView;
@@ -465,7 +467,8 @@
/**
* Call when all the views should be removed/cleaned up.
*/
- void cleanupViews() {
+ public void cleanupViews() {
+ ProtoLog.d(WM_SHELL_BUBBLES, "Bubble#cleanupViews=%s", getKey());
cleanupViews(true);
}
@@ -909,9 +912,8 @@
if (uid != -1) {
intent.putExtra(Settings.EXTRA_APP_UID, uid);
}
- intent.addFlags(Intent.FLAG_ACTIVITY_MULTIPLE_TASK);
+ intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
- intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
return intent;
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
index aea3ca1..0aa8959 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
@@ -23,7 +23,6 @@
import static android.view.View.VISIBLE;
import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
-import static com.android.wm.shell.bubbles.BubbleDebugConfig.DEBUG_BUBBLE_CONTROLLER;
import static com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_BUBBLES;
import static com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_WITH_CLASS_NAME;
import static com.android.wm.shell.bubbles.Bubbles.DISMISS_BLOCKED;
@@ -259,7 +258,7 @@
/** One handed mode controller to register transition listener. */
private final Optional<OneHandedController> mOneHandedOptional;
/** Drag and drop controller to register listener for onDragStarted. */
- private final Optional<DragAndDropController> mDragAndDropController;
+ private final DragAndDropController mDragAndDropController;
/** Used to send bubble events to launcher. */
private Bubbles.BubbleStateListener mBubbleStateListener;
@@ -285,7 +284,7 @@
BubblePositioner positioner,
DisplayController displayController,
Optional<OneHandedController> oneHandedOptional,
- Optional<DragAndDropController> dragAndDropController,
+ DragAndDropController dragAndDropController,
@ShellMainThread ShellExecutor mainExecutor,
@ShellMainThread Handler mainHandler,
@ShellBackgroundThread ShellExecutor bgExecutor,
@@ -435,6 +434,9 @@
boolean homeTaskVisible, boolean clearedTask, boolean wasVisible) {
for (Bubble b : mBubbleData.getBubbles()) {
if (task.taskId == b.getTaskId()) {
+ ProtoLog.d(WM_SHELL_BUBBLES,
+ "onActivityRestartAttempt - taskId=%d selecting matching bubble=%s",
+ task.taskId, b.getKey());
mBubbleData.setSelectedBubble(b);
mBubbleData.setExpanded(true);
return;
@@ -442,6 +444,9 @@
}
for (Bubble b : mBubbleData.getOverflowBubbles()) {
if (task.taskId == b.getTaskId()) {
+ ProtoLog.d(WM_SHELL_BUBBLES, "onActivityRestartAttempt - taskId=%d "
+ + "selecting matching overflow bubble=%s",
+ task.taskId, b.getKey());
promoteBubbleFromOverflow(b);
mBubbleData.setExpanded(true);
return;
@@ -463,7 +468,7 @@
});
mOneHandedOptional.ifPresent(this::registerOneHandedState);
- mDragAndDropController.ifPresent(controller -> controller.addListener(this::collapseStack));
+ mDragAndDropController.addListener(this::collapseStack);
// Clear out any persisted bubbles on disk that no longer have a valid user.
List<UserInfo> users = mUserManager.getAliveUsers();
@@ -581,10 +586,15 @@
// Hide the stack temporarily if the status bar has been made invisible, and the stack
// is collapsed. An expanded stack should remain visible until collapsed.
mStackView.setTemporarilyInvisible(!visible && !isStackExpanded());
+ ProtoLog.d(WM_SHELL_BUBBLES, "onStatusBarVisibilityChanged=%b stackExpanded=%b",
+ visible, isStackExpanded());
}
}
private void onZenStateChanged() {
+ if (hasBubbles()) {
+ ProtoLog.d(WM_SHELL_BUBBLES, "onZenStateChanged");
+ }
for (Bubble b : mBubbleData.getBubbles()) {
b.setShowDot(b.showInShade());
}
@@ -593,9 +603,10 @@
@VisibleForTesting
public void onStatusBarStateChanged(boolean isShade) {
boolean didChange = mIsStatusBarShade != isShade;
- if (DEBUG_BUBBLE_CONTROLLER) {
- Log.d(TAG, "onStatusBarStateChanged isShade=" + isShade + " didChange=" + didChange);
- }
+ ProtoLog.d(WM_SHELL_BUBBLES, "onStatusBarStateChanged "
+ + "isShade=%b didChange=%b mNotifEntryToExpandOnShadeUnlock=%s",
+ isShade, didChange, (mNotifEntryToExpandOnShadeUnlock != null
+ ? mNotifEntryToExpandOnShadeUnlock.getKey() : "null"));
mIsStatusBarShade = isShade;
if (!mIsStatusBarShade && didChange) {
// Only collapse stack on change
@@ -611,6 +622,8 @@
@VisibleForTesting
public void onBubbleMetadataFlagChanged(Bubble bubble) {
+ ProtoLog.d(WM_SHELL_BUBBLES, "onBubbleMetadataFlagChanged=%s flags=%d",
+ bubble.getKey(), bubble.getFlags());
// Make sure NoMan knows suppression state so that anyone querying it can tell.
try {
mBarService.onBubbleMetadataFlagChanged(bubble.getKey(), bubble.getFlags());
@@ -623,6 +636,8 @@
/** Called when the current user changes. */
@VisibleForTesting
public void onUserChanged(int newUserId) {
+ ProtoLog.d(WM_SHELL_BUBBLES, "onUserChanged currentUser=%d newUser=%d",
+ mCurrentUserId, newUserId);
saveBubbles(mCurrentUserId);
mCurrentUserId = newUserId;
@@ -730,7 +745,7 @@
// window to show this in, but we use a separate code path.
// TODO(b/273312602): consider foldables where we do need a stack view when folded
if (mLayerView == null) {
- mLayerView = new BubbleBarLayerView(mContext, this);
+ mLayerView = new BubbleBarLayerView(mContext, this, mBubbleData);
mLayerView.setUnBubbleConversationCallback(mSysuiProxy::onUnbubbleConversation);
}
} else {
@@ -825,6 +840,7 @@
*/
void updateWindowFlagsForBackpress(boolean interceptBack) {
if (mAddedToWindowManager) {
+ ProtoLog.d(WM_SHELL_BUBBLES, "updateFlagsForBackPress interceptBack=%b", interceptBack);
mWmLayoutParams.flags = interceptBack
? 0
: WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
@@ -903,6 +919,9 @@
if (mStackView != null) {
mStackView.setVisibility(INVISIBLE);
removeFromWindowManagerMaybe();
+ } else if (mLayerView != null) {
+ mLayerView.setVisibility(INVISIBLE);
+ removeFromWindowManagerMaybe();
}
}
@@ -1014,8 +1033,9 @@
}
private void onNotificationPanelExpandedChanged(boolean expanded) {
- ProtoLog.d(WM_SHELL_BUBBLES, "onNotificationPanelExpandedChanged: expanded=%b", expanded);
if (mStackView != null && mStackView.isExpanded()) {
+ ProtoLog.d(WM_SHELL_BUBBLES,
+ "onNotificationPanelExpandedChanged expanded=%b", expanded);
if (expanded) {
mStackView.stopMonitoringSwipeUpGesture();
} else {
@@ -1096,6 +1116,7 @@
/** Promote the provided bubble from the overflow view. */
public void promoteBubbleFromOverflow(Bubble bubble) {
mLogger.log(bubble, BubbleLogger.Event.BUBBLE_OVERFLOW_REMOVE_BACK_TO_STACK);
+ ProtoLog.d(WM_SHELL_BUBBLES, "promoteBubbleFromOverflow=%s", bubble.getKey());
bubble.setInflateSynchronously(mInflateSynchronously);
bubble.setShouldAutoExpand(true);
bubble.markAsAccessedAt(System.currentTimeMillis());
@@ -1211,11 +1232,8 @@
// Skip update, but store it in user bubbles so it gets restored after user switch
mSavedUserBubbleData.get(bubbleUserId, new UserBubbleData()).add(notif.getKey(),
true /* shownInShade */);
- if (DEBUG_BUBBLE_CONTROLLER) {
- Log.d(TAG,
- "Ignore update to bubble for not active user. Bubble userId=" + bubbleUserId
- + " current userId=" + mCurrentUserId);
- }
+ Log.w(TAG, "updateBubble, ignore update for non-active user=" + bubbleUserId
+ + " currentUser=" + mCurrentUserId);
}
}
@@ -1252,7 +1270,9 @@
}
String appBubbleKey = Bubble.getAppBubbleKeyForApp(intent.getPackage(), user);
- Log.i(TAG, "showOrHideAppBubble, with key: " + appBubbleKey);
+ Log.i(TAG, "showOrHideAppBubble, key= " + appBubbleKey + " stackVisibility= "
+ + (mStackView != null ? mStackView.getVisibility() : " null ")
+ + " statusBarShade=" + mIsStatusBarShade);
PackageManager packageManager = getPackageManagerForUser(mContext, user.getIdentifier());
if (!isResizableActivity(intent, packageManager, appBubbleKey)) return;
@@ -1386,6 +1406,13 @@
removeFromWindowManagerMaybe();
mLayerView = null;
mStackView = null;
+
+ if (!mBubbleData.hasBubbles()) {
+ // if there are no bubbles, don't create the stack or layer views. they will be created
+ // later when the first bubble is added.
+ return;
+ }
+
ensureBubbleViewsAndWindowCreated();
// inflate bubble views
@@ -1714,8 +1741,11 @@
@Override
public void removeBubble(Bubble removedBubble) {
if (mLayerView != null) {
- // TODO: need to check if there's something that needs to happen here, e.g. if
- // the currently selected & expanded bubble is removed?
+ mLayerView.removeBubble(removedBubble);
+ if (!mBubbleData.hasBubbles() && !isStackExpanded()) {
+ mLayerView.setVisibility(INVISIBLE);
+ removeFromWindowManagerMaybe();
+ }
}
}
@@ -1770,18 +1800,19 @@
@Override
public void applyUpdate(BubbleData.Update update) {
- if (DEBUG_BUBBLE_CONTROLLER) {
- Log.d(TAG, "applyUpdate:" + " bubbleAdded=" + (update.addedBubble != null)
- + " bubbleRemoved="
- + (update.removedBubbles != null && update.removedBubbles.size() > 0)
- + " bubbleUpdated=" + (update.updatedBubble != null)
- + " orderChanged=" + update.orderChanged
- + " expandedChanged=" + update.expandedChanged
- + " selectionChanged=" + update.selectionChanged
- + " suppressed=" + (update.suppressedBubble != null)
- + " unsuppressed=" + (update.unsuppressedBubble != null)
- + " shouldShowEducation=" + update.shouldShowEducation);
- }
+ ProtoLog.d(WM_SHELL_BUBBLES, "mBubbleDataListener#applyUpdate:"
+ + " added=%s removed=%b updated=%s orderChanged=%b expansionChanged=%b"
+ + " expanded=%b selectionChanged=%b selected=%s"
+ + " suppressed=%s unsupressed=%s shouldShowEducation=%b",
+ update.addedBubble != null ? update.addedBubble.getKey() : "null",
+ update.removedBubbles.isEmpty(),
+ update.updatedBubble != null ? update.updatedBubble.getKey() : "null",
+ update.orderChanged, update.expandedChanged, update.expanded,
+ update.selectionChanged,
+ update.selectedBubble != null ? update.selectedBubble.getKey() : "null",
+ update.suppressedBubble != null ? update.suppressedBubble.getKey() : "null",
+ update.unsuppressedBubble != null ? update.unsuppressedBubble.getKey() : "null",
+ update.shouldShowEducation);
ensureBubbleViewsAndWindowCreated();
@@ -1975,7 +2006,8 @@
if (mStackView == null && mLayerView == null) {
return;
}
-
+ ProtoLog.v(WM_SHELL_BUBBLES, "updateBubbleViews mIsStatusBarShade=%s hasBubbles=%s",
+ mIsStatusBarShade, hasBubbles());
if (!mIsStatusBarShade) {
// Bubbles don't appear when the device is locked.
if (mStackView != null) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java
index 127c7e8..6d3f0c3 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java
@@ -15,12 +15,10 @@
*/
package com.android.wm.shell.bubbles;
-import static com.android.internal.annotations.VisibleForTesting.Visibility.PACKAGE;
import static com.android.internal.annotations.VisibleForTesting.Visibility.PRIVATE;
-import static com.android.wm.shell.bubbles.Bubble.KEY_APP_BUBBLE;
-import static com.android.wm.shell.bubbles.BubbleDebugConfig.DEBUG_BUBBLE_DATA;
import static com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_BUBBLES;
import static com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_WITH_CLASS_NAME;
+import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_BUBBLES;
import android.annotation.NonNull;
import android.app.PendingIntent;
@@ -37,10 +35,12 @@
import androidx.annotation.Nullable;
import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.protolog.common.ProtoLog;
import com.android.internal.util.FrameworkStatsLog;
import com.android.launcher3.icons.BubbleIconFactory;
import com.android.wm.shell.R;
import com.android.wm.shell.bubbles.Bubbles.DismissReason;
+import com.android.wm.shell.bubbles.bar.BubbleBarLayerView;
import com.android.wm.shell.common.bubbles.BubbleBarUpdate;
import com.android.wm.shell.common.bubbles.RemovedBubble;
@@ -333,9 +333,6 @@
}
public void setExpanded(boolean expanded) {
- if (DEBUG_BUBBLE_DATA) {
- Log.d(TAG, "setExpanded: " + expanded);
- }
setExpandedInternal(expanded);
dispatchPendingChanges();
}
@@ -347,9 +344,8 @@
* updated to have the correct state.
*/
public void setSelectedBubbleFromLauncher(BubbleViewProvider bubble) {
- if (DEBUG_BUBBLE_DATA) {
- Log.d(TAG, "setSelectedBubbleFromLauncher: " + bubble);
- }
+ ProtoLog.d(WM_SHELL_BUBBLES, "setSelectedBubbleFromLauncher=%s",
+ (bubble != null ? bubble.getKey() : "null"));
mExpanded = true;
if (Objects.equals(bubble, mSelectedBubble)) {
return;
@@ -370,9 +366,6 @@
}
public void setSelectedBubble(BubbleViewProvider bubble) {
- if (DEBUG_BUBBLE_DATA) {
- Log.d(TAG, "setSelectedBubble: " + bubble);
- }
setSelectedBubbleInternal(bubble);
dispatchPendingChanges();
}
@@ -427,15 +420,16 @@
/**
* When this method is called it is expected that all info in the bubble has completed loading.
* @see Bubble#inflate(BubbleViewInfoTask.Callback, Context, BubbleController, BubbleStackView,
- * BubbleIconFactory, boolean)
+ * BubbleBarLayerView, BubbleIconFactory, boolean)
*/
void notificationEntryUpdated(Bubble bubble, boolean suppressFlyout, boolean showInShade) {
- if (DEBUG_BUBBLE_DATA) {
- Log.d(TAG, "notificationEntryUpdated: " + bubble);
- }
mPendingBubbles.remove(bubble.getKey()); // No longer pending once we're here
Bubble prevBubble = getBubbleInStackWithKey(bubble.getKey());
suppressFlyout |= !bubble.isTextChanged();
+ ProtoLog.d(WM_SHELL_BUBBLES,
+ "notifEntryUpdated=%s prevBubble=%b suppressFlyout=%b showInShade=%b autoExpand=%b",
+ bubble.getKey(), (prevBubble != null), suppressFlyout, showInShade,
+ bubble.shouldAutoExpand());
if (prevBubble == null) {
// Create a new bubble
@@ -483,9 +477,6 @@
* Dismisses the bubble with the matching key, if it exists.
*/
public void dismissBubbleWithKey(String key, @DismissReason int reason) {
- if (DEBUG_BUBBLE_DATA) {
- Log.d(TAG, "notificationEntryRemoved: key=" + key + " reason=" + reason);
- }
doRemove(key, reason);
dispatchPendingChanges();
}
@@ -603,9 +594,7 @@
}
private void doAdd(Bubble bubble) {
- if (DEBUG_BUBBLE_DATA) {
- Log.d(TAG, "doAdd: " + bubble);
- }
+ ProtoLog.d(WM_SHELL_BUBBLES, "doAdd=%s", bubble.getKey());
mBubbles.add(0, bubble);
mStateChange.addedBubble = bubble;
// Adding the first bubble doesn't change the order
@@ -634,9 +623,7 @@
}
private void doUpdate(Bubble bubble, boolean reorder) {
- if (DEBUG_BUBBLE_DATA) {
- Log.d(TAG, "doUpdate: " + bubble);
- }
+ ProtoLog.d(WM_SHELL_BUBBLES, "BubbleData - doUpdate=%s", bubble.getKey());
mStateChange.updatedBubble = bubble;
if (!isExpanded() && reorder) {
int prevPos = mBubbles.indexOf(bubble);
@@ -663,9 +650,6 @@
}
private void doRemove(String key, @DismissReason int reason) {
- if (DEBUG_BUBBLE_DATA || (key != null && key.contains(KEY_APP_BUBBLE))) {
- Log.d(TAG, "doRemove: " + key + " reason: " + reason);
- }
// If it was pending remove it
if (mPendingBubbles.containsKey(key)) {
mPendingBubbles.remove(key);
@@ -686,9 +670,7 @@
&& shouldRemoveHiddenBubble) {
Bubble b = getOverflowBubbleWithKey(key);
- if (DEBUG_BUBBLE_DATA) {
- Log.d(TAG, "Cancel overflow bubble: " + b);
- }
+ ProtoLog.d(WM_SHELL_BUBBLES, "doRemove - cancel overflow bubble=%s", key);
if (b != null) {
b.stopInflation();
}
@@ -699,9 +681,7 @@
}
if (hasSuppressedBubbleWithKey(key) && shouldRemoveHiddenBubble) {
Bubble b = getSuppressedBubbleWithKey(key);
- if (DEBUG_BUBBLE_DATA) {
- Log.d(TAG, "Cancel suppressed bubble: " + b);
- }
+ ProtoLog.d(WM_SHELL_BUBBLES, "doRemove - cancel suppressed bubble=%s", key);
if (b != null) {
mSuppressedBubbles.remove(b.getLocusId());
b.stopInflation();
@@ -711,6 +691,7 @@
return;
}
Bubble bubbleToRemove = mBubbles.get(indexToRemove);
+ ProtoLog.d(WM_SHELL_BUBBLES, "doRemove=%s", bubbleToRemove.getKey());
bubbleToRemove.stopInflation();
overflowBubble(reason, bubbleToRemove);
@@ -744,17 +725,12 @@
}
// Move selection to the new bubble at the same position.
int newIndex = Math.min(indexOfSelected, mBubbles.size() - 1);
- if (DEBUG_BUBBLE_DATA) {
- Log.d(TAG, "setNewSelectedIndex: " + indexOfSelected);
- }
BubbleViewProvider newSelected = mBubbles.get(newIndex);
setSelectedBubbleInternal(newSelected);
}
private void doSuppress(Bubble bubble) {
- if (DEBUG_BUBBLE_DATA) {
- Log.d(TAG, "doSuppressed: " + bubble);
- }
+ ProtoLog.d(WM_SHELL_BUBBLES, "doSuppress=%s", bubble.getKey());
mStateChange.suppressedBubble = bubble;
bubble.setSuppressBubble(true);
@@ -777,9 +753,7 @@
}
private void doUnsuppress(Bubble bubble) {
- if (DEBUG_BUBBLE_DATA) {
- Log.d(TAG, "doUnsuppressed: " + bubble);
- }
+ ProtoLog.d(WM_SHELL_BUBBLES, "doUnsuppress=%s", bubble.getKey());
bubble.setSuppressBubble(false);
mStateChange.unsuppressedBubble = bubble;
mBubbles.add(bubble);
@@ -801,9 +775,7 @@
|| reason == Bubbles.DISMISS_RELOAD_FROM_DISK)) {
return;
}
- if (DEBUG_BUBBLE_DATA) {
- Log.d(TAG, "Overflowing: " + bubble);
- }
+ ProtoLog.d(WM_SHELL_BUBBLES, "overflowBubble=%s", bubble.getKey());
mLogger.logOverflowAdd(bubble, reason);
mOverflowBubbles.remove(bubble);
mOverflowBubbles.add(0, bubble);
@@ -812,9 +784,7 @@
if (mOverflowBubbles.size() == mMaxOverflowBubbles + 1) {
// Remove oldest bubble.
Bubble oldest = mOverflowBubbles.get(mOverflowBubbles.size() - 1);
- if (DEBUG_BUBBLE_DATA) {
- Log.d(TAG, "Overflow full. Remove: " + oldest);
- }
+ ProtoLog.d(WM_SHELL_BUBBLES, "overflow full, remove=%s", oldest.getKey());
mStateChange.bubbleRemoved(oldest, Bubbles.DISMISS_OVERFLOW_MAX_REACHED);
mLogger.log(bubble, BubbleLogger.Event.BUBBLE_OVERFLOW_REMOVE_MAX_REACHED);
mOverflowBubbles.remove(oldest);
@@ -823,9 +793,7 @@
}
public void dismissAll(@DismissReason int reason) {
- if (DEBUG_BUBBLE_DATA) {
- Log.d(TAG, "dismissAll: reason=" + reason);
- }
+ ProtoLog.d(WM_SHELL_BUBBLES, "dismissAll reason=%d", reason);
if (mBubbles.isEmpty() && mSuppressedBubbles.isEmpty()) {
return;
}
@@ -851,9 +819,10 @@
* @param visible whether the task with the locusId is visible or not.
*/
public void onLocusVisibilityChanged(int taskId, LocusId locusId, boolean visible) {
- if (DEBUG_BUBBLE_DATA) {
- Log.d(TAG, "onLocusVisibilityChanged: " + locusId + " visible=" + visible);
- }
+ if (locusId == null) return;
+
+ ProtoLog.d(WM_SHELL_BUBBLES, "onLocusVisibilityChanged=%s visible=%b taskId=%d",
+ locusId.getId(), visible, taskId);
Bubble matchingBubble = getBubbleInStackWithLocusId(locusId);
// Don't add the locus if it's from a bubble'd activity, we only suppress for non-bubbled.
@@ -910,9 +879,8 @@
* @param bubble the new selected bubble
*/
private void setSelectedBubbleInternal(@Nullable BubbleViewProvider bubble) {
- if (DEBUG_BUBBLE_DATA) {
- Log.d(TAG, "setSelectedBubbleInternal: " + bubble);
- }
+ ProtoLog.d(WM_SHELL_BUBBLES, "setSelectedBubbleInternal=%s",
+ (bubble != null ? bubble.getKey() : "null"));
if (Objects.equals(bubble, mSelectedBubble)) {
return;
}
@@ -969,12 +937,10 @@
* @param shouldExpand the new requested state
*/
private void setExpandedInternal(boolean shouldExpand) {
- if (DEBUG_BUBBLE_DATA) {
- Log.d(TAG, "setExpandedInternal: shouldExpand=" + shouldExpand);
- }
if (mExpanded == shouldExpand) {
return;
}
+ ProtoLog.d(WM_SHELL_BUBBLES, "setExpandedInternal=%b", shouldExpand);
if (shouldExpand) {
if (mBubbles.isEmpty() && !mShowingOverflow) {
Log.e(TAG, "Attempt to expand stack when empty!");
@@ -1026,9 +992,6 @@
* @return true if the position of any bubbles changed as a result
*/
private boolean repackAll() {
- if (DEBUG_BUBBLE_DATA) {
- Log.d(TAG, "repackAll()");
- }
if (mBubbles.isEmpty()) {
return false;
}
@@ -1069,7 +1032,6 @@
/**
* The set of bubbles in row.
*/
- @VisibleForTesting(visibility = PACKAGE)
public List<Bubble> getBubbles() {
return Collections.unmodifiableList(mBubbles);
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleDebugConfig.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleDebugConfig.java
index f56b171..f1a68e2 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleDebugConfig.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleDebugConfig.java
@@ -37,17 +37,7 @@
// Default log tag for the Bubbles package.
public static final String TAG_BUBBLES = "Bubbles";
-
- static final boolean DEBUG_BUBBLE_CONTROLLER = false;
- static final boolean DEBUG_BUBBLE_DATA = false;
- static final boolean DEBUG_BUBBLE_STACK_VIEW = false;
- static final boolean DEBUG_BUBBLE_EXPANDED_VIEW = false;
- static final boolean DEBUG_EXPERIMENTS = true;
- static final boolean DEBUG_OVERFLOW = false;
public static final boolean DEBUG_USER_EDUCATION = false;
- static final boolean DEBUG_POSITIONER = false;
- public static final boolean DEBUG_COLLAPSE_ANIMATOR = false;
- public static boolean DEBUG_EXPANDED_VIEW_DRAGGING = false;
private static final boolean FORCE_SHOW_USER_EDUCATION = false;
private static final String FORCE_SHOW_USER_EDUCATION_SETTING =
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java
index efc4d8b..088660e 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java
@@ -23,10 +23,10 @@
import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
-import static com.android.wm.shell.bubbles.BubbleDebugConfig.DEBUG_BUBBLE_EXPANDED_VIEW;
import static com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_BUBBLES;
import static com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_WITH_CLASS_NAME;
import static com.android.wm.shell.bubbles.BubblePositioner.MAX_HEIGHT;
+import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_BUBBLES;
import android.annotation.NonNull;
import android.annotation.SuppressLint;
@@ -67,6 +67,7 @@
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.policy.ScreenDecorationsUtils;
+import com.android.internal.protolog.common.ProtoLog;
import com.android.wm.shell.R;
import com.android.wm.shell.common.AlphaOptimizedButton;
import com.android.wm.shell.common.TriangleShape;
@@ -199,13 +200,9 @@
@Override
public void onInitialized() {
- if (DEBUG_BUBBLE_EXPANDED_VIEW) {
- Log.d(TAG, "onInitialized: destroyed=" + mDestroyed
- + " initialized=" + mInitialized
- + " bubble=" + getBubbleKey());
- }
-
if (mDestroyed || mInitialized) {
+ ProtoLog.d(WM_SHELL_BUBBLES, "onInitialized: destroyed=%b initialized=%b bubble=%s",
+ mDestroyed, mInitialized, getBubbleKey());
return;
}
@@ -216,10 +213,8 @@
// TODO: I notice inconsistencies in lifecycle
// Post to keep the lifecycle normal
post(() -> {
- if (DEBUG_BUBBLE_EXPANDED_VIEW) {
- Log.d(TAG, "onInitialized: calling startActivity, bubble="
- + getBubbleKey());
- }
+ ProtoLog.d(WM_SHELL_BUBBLES, "onInitialized: calling startActivity, bubble=%s",
+ getBubbleKey());
try {
Rect launchBounds = new Rect();
mTaskView.getBoundsOnScreen(launchBounds);
@@ -279,10 +274,8 @@
@Override
public void onTaskCreated(int taskId, ComponentName name) {
- if (DEBUG_BUBBLE_EXPANDED_VIEW) {
- Log.d(TAG, "onTaskCreated: taskId=" + taskId
- + " bubble=" + getBubbleKey());
- }
+ ProtoLog.d(WM_SHELL_BUBBLES, "onTaskCreated: taskId=%d bubble=%s",
+ taskId, getBubbleKey());
// The taskId is saved to use for removeTask, preventing appearance in recent tasks.
mTaskId = taskId;
@@ -298,15 +291,15 @@
@Override
public void onTaskVisibilityChanged(int taskId, boolean visible) {
+ ProtoLog.d(WM_SHELL_BUBBLES, "onTaskVisibilityChanged=%b bubble=%s taskId=%d",
+ visible, getBubbleKey(), taskId);
setContentVisibility(visible);
}
@Override
public void onTaskRemovalStarted(int taskId) {
- if (DEBUG_BUBBLE_EXPANDED_VIEW) {
- Log.d(TAG, "onTaskRemovalStarted: taskId=" + taskId
- + " bubble=" + getBubbleKey());
- }
+ ProtoLog.d(WM_SHELL_BUBBLES, "onTaskRemovalStarted: taskId=%d bubble=%s",
+ taskId, getBubbleKey());
if (mBubble != null) {
mController.removeBubble(mBubble.getKey(), Bubbles.DISMISS_TASK_FINISHED);
}
@@ -644,9 +637,6 @@
super.onDetachedFromWindow();
mImeVisible = false;
mNeedsNewHeight = false;
- if (DEBUG_BUBBLE_EXPANDED_VIEW) {
- Log.d(TAG, "onDetachedFromWindow: bubble=" + getBubbleKey());
- }
}
/**
@@ -805,10 +795,6 @@
* and setting {@code false} actually means rendering the contents in transparent.
*/
public void setContentVisibility(boolean visibility) {
- if (DEBUG_BUBBLE_EXPANDED_VIEW) {
- Log.d(TAG, "setContentVisibility: visibility=" + visibility
- + " bubble=" + getBubbleKey());
- }
mIsContentVisible = visibility;
if (mTaskView != null && !mIsAnimating) {
mTaskView.setAlpha(visibility ? 1f : 0f);
@@ -867,9 +853,6 @@
* Sets the bubble used to populate this view.
*/
void update(Bubble bubble) {
- if (DEBUG_BUBBLE_EXPANDED_VIEW) {
- Log.d(TAG, "update: bubble=" + bubble);
- }
if (mStackView == null) {
Log.w(TAG, "Stack is null for bubble: " + bubble);
return;
@@ -958,11 +941,6 @@
}
mNeedsNewHeight = false;
}
- if (DEBUG_BUBBLE_EXPANDED_VIEW) {
- Log.d(TAG, "updateHeight: bubble=" + getBubbleKey()
- + " height=" + height
- + " mNeedsNewHeight=" + mNeedsNewHeight);
- }
}
}
@@ -974,10 +952,6 @@
* waiting for layout.
*/
public void updateView(int[] containerLocationOnScreen) {
- if (DEBUG_BUBBLE_EXPANDED_VIEW) {
- Log.d(TAG, "updateView: bubble="
- + getBubbleKey());
- }
mExpandedViewContainerLocation = containerLocationOnScreen;
updateHeight();
if (mTaskView != null
@@ -1103,9 +1077,6 @@
/** Hide the task view. */
public void cleanUpExpandedState() {
- if (DEBUG_BUBBLE_EXPANDED_VIEW) {
- Log.d(TAG, "cleanUpExpandedState: bubble=" + getBubbleKey() + " task=" + mTaskId);
- }
if (mTaskView != null) {
mTaskView.setVisibility(GONE);
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleOverflowContainerView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleOverflowContainerView.java
index 9655470..70cdc82 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleOverflowContainerView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleOverflowContainerView.java
@@ -16,9 +16,9 @@
package com.android.wm.shell.bubbles;
-import static com.android.wm.shell.bubbles.BubbleDebugConfig.DEBUG_OVERFLOW;
import static com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_BUBBLES;
import static com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_WITH_CLASS_NAME;
+import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_BUBBLES;
import android.annotation.NonNull;
import android.content.Context;
@@ -28,7 +28,6 @@
import android.graphics.Color;
import android.graphics.Rect;
import android.util.AttributeSet;
-import android.util.Log;
import android.util.TypedValue;
import android.view.KeyEvent;
import android.view.LayoutInflater;
@@ -43,6 +42,7 @@
import androidx.recyclerview.widget.GridLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
+import com.android.internal.protolog.common.ProtoLog;
import com.android.internal.util.ContrastColorUtil;
import com.android.wm.shell.R;
@@ -245,9 +245,6 @@
Bubble toRemove = update.removedOverflowBubble;
if (toRemove != null) {
- if (DEBUG_OVERFLOW) {
- Log.d(TAG, "remove: " + toRemove);
- }
toRemove.cleanupViews();
final int indexToRemove = mOverflowBubbles.indexOf(toRemove);
mOverflowBubbles.remove(toRemove);
@@ -257,9 +254,6 @@
Bubble toAdd = update.addedOverflowBubble;
if (toAdd != null) {
final int indexToAdd = mOverflowBubbles.indexOf(toAdd);
- if (DEBUG_OVERFLOW) {
- Log.d(TAG, "add: " + toAdd + " prevIndex: " + indexToAdd);
- }
if (indexToAdd > 0) {
mOverflowBubbles.remove(toAdd);
mOverflowBubbles.add(0, toAdd);
@@ -272,10 +266,9 @@
updateEmptyStateVisibility();
- if (DEBUG_OVERFLOW) {
- Log.d(TAG, BubbleDebugConfig.formatBubblesString(
- mController.getOverflowBubbles(), null));
- }
+ ProtoLog.d(WM_SHELL_BUBBLES, "Apply overflow update, added=%s removed=%s",
+ (toAdd != null ? toAdd.getKey() : "null"),
+ (toRemove != null ? toRemove.getKey() : "null"));
}
};
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java
index d62c86c..c03b6f8 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java
@@ -16,18 +16,20 @@
package com.android.wm.shell.bubbles;
+import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_BUBBLES;
+
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Insets;
import android.graphics.PointF;
import android.graphics.Rect;
import android.graphics.RectF;
-import android.util.Log;
import android.view.Surface;
import android.view.WindowManager;
import androidx.annotation.VisibleForTesting;
+import com.android.internal.protolog.common.ProtoLog;
import com.android.launcher3.icons.IconNormalizer;
import com.android.wm.shell.R;
@@ -36,9 +38,6 @@
* placement and positioning calculations to refer to.
*/
public class BubblePositioner {
- private static final String TAG = BubbleDebugConfig.TAG_WITH_CLASS_NAME
- ? "BubblePositioner"
- : BubbleDebugConfig.TAG_BUBBLES;
/** The screen edge the bubble stack is pinned to */
public enum StackPinnedEdge {
@@ -110,16 +109,12 @@
*/
public void update(DeviceConfig deviceConfig) {
mDeviceConfig = deviceConfig;
-
- if (BubbleDebugConfig.DEBUG_POSITIONER) {
- Log.w(TAG, "update positioner:"
- + " rotation: " + mRotation
- + " insets: " + deviceConfig.getInsets()
- + " isLargeScreen: " + deviceConfig.isLargeScreen()
- + " isSmallTablet: " + deviceConfig.isSmallTablet()
- + " showingInBubbleBar: " + mShowingInBubbleBar
- + " bounds: " + deviceConfig.getWindowBounds());
- }
+ ProtoLog.d(WM_SHELL_BUBBLES, "update positioner: "
+ + "rotation=%d insets=%s largeScreen=%b "
+ + "smallTablet=%b isBubbleBar=%b bounds=%s",
+ mRotation, deviceConfig.getInsets(), deviceConfig.isLargeScreen(),
+ deviceConfig.isSmallTablet(), mShowingInBubbleBar,
+ deviceConfig.getWindowBounds());
updateInternal(mRotation, deviceConfig.getInsets(), deviceConfig.getWindowBounds());
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java
index 9facef3..c5bc9eb 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java
@@ -21,7 +21,6 @@
import static com.android.wm.shell.animation.Interpolators.ALPHA_IN;
import static com.android.wm.shell.animation.Interpolators.ALPHA_OUT;
-import static com.android.wm.shell.bubbles.BubbleDebugConfig.DEBUG_BUBBLE_STACK_VIEW;
import static com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_BUBBLES;
import static com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_WITH_CLASS_NAME;
import static com.android.wm.shell.bubbles.BubblePositioner.NUM_VISIBLE_WHEN_RESTING;
@@ -1310,13 +1309,9 @@
final boolean seen = getPrefBoolean(ManageEducationView.PREF_MANAGED_EDUCATION);
final boolean shouldShow = (!seen || BubbleDebugConfig.forceShowUserEducation(mContext))
&& mExpandedBubble != null && mExpandedBubble.getExpandedView() != null;
- if (BubbleDebugConfig.DEBUG_USER_EDUCATION) {
- Log.d(TAG, "Show manage edu: " + shouldShow);
- }
+ ProtoLog.d(WM_SHELL_BUBBLES, "Show manage edu=%b", shouldShow);
if (shouldShow && BubbleDebugConfig.neverShowUserEducation(mContext)) {
- if (BubbleDebugConfig.DEBUG_USER_EDUCATION) {
- Log.d(TAG, "Want to show manage edu, but it is forced hidden");
- }
+ Log.w(TAG, "Want to show manage edu, but it is forced hidden");
return false;
}
return shouldShow;
@@ -1360,13 +1355,9 @@
}
final boolean seen = getPrefBoolean(StackEducationView.PREF_STACK_EDUCATION);
final boolean shouldShow = !seen || BubbleDebugConfig.forceShowUserEducation(mContext);
- if (BubbleDebugConfig.DEBUG_USER_EDUCATION) {
- Log.d(TAG, "Show stack edu: " + shouldShow);
- }
+ ProtoLog.d(WM_SHELL_BUBBLES, "Show stack edu=%b", shouldShow);
if (shouldShow && BubbleDebugConfig.neverShowUserEducation(mContext)) {
- if (BubbleDebugConfig.DEBUG_USER_EDUCATION) {
- Log.d(TAG, "Want to show stack edu, but it is forced hidden");
- }
+ Log.w(TAG, "Want to show stack edu, but it is forced hidden");
return false;
}
return shouldShow;
@@ -1513,7 +1504,7 @@
mBubbleSize = mPositioner.getBubbleSize();
for (Bubble b : mBubbleData.getBubbles()) {
if (b.getIconView() == null) {
- Log.d(TAG, "Display size changed. Icon null: " + b);
+ Log.w(TAG, "Display size changed. Icon null: " + b);
continue;
}
b.getIconView().setLayoutParams(new LayoutParams(mBubbleSize, mBubbleSize));
@@ -1819,10 +1810,6 @@
// via BubbleData.Listener
@SuppressLint("ClickableViewAccessibility")
void addBubble(Bubble bubble) {
- if (DEBUG_BUBBLE_STACK_VIEW) {
- Log.d(TAG, "addBubble: " + bubble);
- }
-
final boolean firstBubble = getBubbleCount() == 0;
if (firstBubble && shouldShowStackEdu()) {
@@ -1868,9 +1855,6 @@
// via BubbleData.Listener
void removeBubble(Bubble bubble) {
- if (DEBUG_BUBBLE_STACK_VIEW) {
- Log.d(TAG, "removeBubble: " + bubble);
- }
if (isExpanded() && getBubbleCount() == 1) {
mRemovingLastBubbleWhileExpanded = true;
// We're expanded while the last bubble is being removed. Let the scrim animate away
@@ -1917,7 +1901,7 @@
bubble.cleanupViews();
logBubbleEvent(bubble, FrameworkStatsLog.BUBBLE_UICHANGED__ACTION__DISMISSED);
} else {
- Log.d(TAG, "was asked to remove Bubble, but didn't find the view! " + bubble);
+ Log.w(TAG, "was asked to remove Bubble, but didn't find the view! " + bubble);
}
}
@@ -1985,10 +1969,6 @@
*/
// via BubbleData.Listener
public void setSelectedBubble(@Nullable BubbleViewProvider bubbleToSelect) {
- if (DEBUG_BUBBLE_STACK_VIEW) {
- Log.d(TAG, "setSelectedBubble: " + bubbleToSelect);
- }
-
if (bubbleToSelect == null) {
mBubbleData.setShowingOverflow(false);
return;
@@ -2081,10 +2061,6 @@
*/
// via BubbleData.Listener
public void setExpanded(boolean shouldExpand) {
- if (DEBUG_BUBBLE_STACK_VIEW) {
- Log.d(TAG, "setExpanded: " + shouldExpand);
- }
-
if (!shouldExpand) {
// If we're collapsing, release the animating-out surface immediately since we have no
// need for it, and this ensures it cannot remain visible as we collapse.
@@ -2126,7 +2102,6 @@
* Monitor for swipe up gesture that is used to collapse expanded view
*/
void startMonitoringSwipeUpGesture() {
- ProtoLog.d(WM_SHELL_BUBBLES, "startMonitoringSwipeUpGesture");
stopMonitoringSwipeUpGestureInternal();
if (isGestureNavEnabled()) {
@@ -2174,7 +2149,6 @@
* Stop monitoring for swipe up gesture
*/
void stopMonitoringSwipeUpGesture() {
- ProtoLog.d(WM_SHELL_BUBBLES, "stopMonitoringSwipeUpGesture");
stopMonitoringSwipeUpGestureInternal();
}
@@ -2202,9 +2176,6 @@
}
void setBubbleSuppressed(Bubble bubble, boolean suppressed) {
- if (DEBUG_BUBBLE_STACK_VIEW) {
- Log.d(TAG, "setBubbleSuppressed: suppressed=" + suppressed + " bubble=" + bubble);
- }
if (suppressed) {
int index = getBubbleIndex(bubble);
mBubbleContainer.removeViewAt(index);
@@ -2339,6 +2310,8 @@
}
private void animateExpansion() {
+ ProtoLog.d(WM_SHELL_BUBBLES, "animateExpansion, expandedBubble=%s",
+ mExpandedBubble != null ? mExpandedBubble.getKey() : "null");
cancelDelayedExpandCollapseSwitchAnimations();
final boolean showVertically = mPositioner.showBubblesVertically();
mIsExpanded = true;
@@ -2465,7 +2438,7 @@
private void animateCollapse() {
cancelDelayedExpandCollapseSwitchAnimations();
-
+ ProtoLog.d(WM_SHELL_BUBBLES, "animateCollapse");
if (isManageEduVisible()) {
mManageEduView.hide();
}
@@ -2508,11 +2481,6 @@
mManageEduView.hide();
}
- if (DEBUG_BUBBLE_STACK_VIEW) {
- Log.d(TAG, "animateCollapse");
- Log.d(TAG, BubbleDebugConfig.formatBubblesString(getBubblesOnScreen(),
- mExpandedBubble));
- }
updateZOrder();
updateBadges(true /* setBadgeForCollapsedStack */);
afterExpandedViewAnimation();
@@ -2612,6 +2580,18 @@
mExpandedViewTemporarilyHidden = false;
mIsBubbleSwitchAnimating = false;
mExpandedViewContainer.setAnimationMatrix(null);
+
+ // When a bubble is being dragged, the expanded view is temporarily hidden.
+ // If the motion ends with dismissing the bubble, with multiple bubbles in
+ // the stack, we'll end up here to switch to the new bubble. However, the
+ // expanded view animation might not actually set the z ordering for the
+ // expanded view correctly, because the view may still be temporarily
+ // hidden. So set it again here.
+ BubbleExpandedView bev = mExpandedBubble.getExpandedView();
+ if (bev != null) {
+ mExpandedBubble.getExpandedView().setSurfaceZOrderedOnTop(false);
+ mExpandedBubble.getExpandedView().setAnimating(false);
+ }
})
.start();
}, 25);
@@ -2691,7 +2671,7 @@
if (!mExpandedBubble.getExpandedView().isUsingMaxHeight()) {
mExpandedViewContainer.animate().translationY(newExpandedViewTop);
}
- List<Animator> animList = new ArrayList();
+ List<Animator> animList = new ArrayList<>();
for (int i = 0; i < mBubbleContainer.getChildCount(); i++) {
View child = mBubbleContainer.getChildAt(i);
float transY = mPositioner.getExpandedBubbleXY(i, getState()).y;
@@ -2894,7 +2874,7 @@
if (!shouldShowFlyout(bubble)) {
return;
}
-
+ ProtoLog.d(WM_SHELL_BUBBLES, "animateFlyout=%s", bubble.getKey());
mFlyoutDragDeltaX = 0f;
clearFlyoutOnHide();
mAfterFlyoutHidden = () -> {
@@ -3036,6 +3016,9 @@
@VisibleForTesting
public void showManageMenu(boolean show) {
if ((mManageMenu.getVisibility() == VISIBLE) == show) return;
+ ProtoLog.d(WM_SHELL_BUBBLES, "showManageMenu=%b for bubble=%s",
+ show, (mExpandedBubble != null ? mExpandedBubble.getKey() : "null"));
+
mShowingManage = show;
// This should not happen, since the manage menu is only visible when there's an expanded
@@ -3167,10 +3150,6 @@
}
private void updateExpandedBubble() {
- if (DEBUG_BUBBLE_STACK_VIEW) {
- Log.d(TAG, "updateExpandedBubble()");
- }
-
mExpandedViewContainer.removeAllViews();
if (mIsExpanded && mExpandedBubble != null
&& mExpandedBubble.getExpandedView() != null) {
@@ -3318,9 +3297,6 @@
}
private void updateExpandedView() {
- if (DEBUG_BUBBLE_STACK_VIEW) {
- Log.d(TAG, "updateExpandedView: mIsExpanded=" + mIsExpanded);
- }
boolean isOverflowExpanded = mExpandedBubble != null
&& BubbleOverflow.KEY.equals(mExpandedBubble.getKey());
int[] paddings = mPositioner.getExpandedViewContainerPadding(
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewHelper.java
index 5fc67d7..530ec5a 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewHelper.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewHelper.java
@@ -20,7 +20,7 @@
import static android.content.Intent.FLAG_ACTIVITY_MULTIPLE_TASK;
import static android.content.Intent.FLAG_ACTIVITY_NEW_DOCUMENT;
-import static com.android.wm.shell.bubbles.BubbleDebugConfig.DEBUG_BUBBLE_EXPANDED_VIEW;
+import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_BUBBLES;
import android.app.ActivityOptions;
import android.app.ActivityTaskManager;
@@ -31,9 +31,11 @@
import android.graphics.Rect;
import android.util.Log;
import android.view.View;
+import android.view.ViewGroup;
import androidx.annotation.Nullable;
+import com.android.internal.protolog.common.ProtoLog;
import com.android.wm.shell.taskview.TaskView;
/**
@@ -78,11 +80,8 @@
@Override
public void onInitialized() {
- if (DEBUG_BUBBLE_EXPANDED_VIEW) {
- Log.d(TAG, "onInitialized: destroyed=" + mDestroyed
- + " initialized=" + mInitialized
- + " bubble=" + getBubbleKey());
- }
+ ProtoLog.d(WM_SHELL_BUBBLES, "onInitialized: destroyed=%b initialized=%b bubble=%s",
+ mDestroyed, mInitialized, getBubbleKey());
if (mDestroyed || mInitialized) {
return;
@@ -98,10 +97,8 @@
// TODO: I notice inconsistencies in lifecycle
// Post to keep the lifecycle normal
mParentView.post(() -> {
- if (DEBUG_BUBBLE_EXPANDED_VIEW) {
- Log.d(TAG, "onInitialized: calling startActivity, bubble="
- + getBubbleKey());
- }
+ ProtoLog.d(WM_SHELL_BUBBLES, "onInitialized: calling startActivity, bubble=%s",
+ getBubbleKey());
try {
options.setTaskAlwaysOnTop(true);
options.setLaunchedFromBubble(true);
@@ -158,10 +155,8 @@
@Override
public void onTaskCreated(int taskId, ComponentName name) {
- if (DEBUG_BUBBLE_EXPANDED_VIEW) {
- Log.d(TAG, "onTaskCreated: taskId=" + taskId
- + " bubble=" + getBubbleKey());
- }
+ ProtoLog.d(WM_SHELL_BUBBLES, "onTaskCreated: taskId=%d bubble=%s",
+ taskId, getBubbleKey());
// The taskId is saved to use for removeTask, preventing appearance in recent tasks.
mTaskId = taskId;
@@ -177,15 +172,14 @@
@Override
public void onTaskRemovalStarted(int taskId) {
- if (DEBUG_BUBBLE_EXPANDED_VIEW) {
- Log.d(TAG, "onTaskRemovalStarted: taskId=" + taskId
- + " bubble=" + getBubbleKey());
- }
+ ProtoLog.d(WM_SHELL_BUBBLES, "onTaskRemovalStarted: taskId=%d bubble=%s",
+ taskId, getBubbleKey());
if (mBubble != null) {
mController.removeBubble(mBubble.getKey(), Bubbles.DISMISS_TASK_FINISHED);
}
if (mTaskView != null) {
mTaskView.release();
+ ((ViewGroup) mParentView).removeView(mTaskView);
mTaskView = null;
}
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/ExpandedViewAnimationControllerImpl.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/ExpandedViewAnimationControllerImpl.java
index 845dca3..e43609f 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/ExpandedViewAnimationControllerImpl.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/ExpandedViewAnimationControllerImpl.java
@@ -15,14 +15,11 @@
*/
package com.android.wm.shell.bubbles.animation;
-import static com.android.wm.shell.bubbles.BubbleDebugConfig.DEBUG_COLLAPSE_ANIMATOR;
-import static com.android.wm.shell.bubbles.BubbleDebugConfig.DEBUG_EXPANDED_VIEW_DRAGGING;
-import static com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_BUBBLES;
-import static com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_WITH_CLASS_NAME;
import static com.android.wm.shell.bubbles.BubbleExpandedView.BACKGROUND_ALPHA;
import static com.android.wm.shell.bubbles.BubbleExpandedView.BOTTOM_CLIP_PROPERTY;
import static com.android.wm.shell.bubbles.BubbleExpandedView.CONTENT_ALPHA;
import static com.android.wm.shell.bubbles.BubbleExpandedView.MANAGE_BUTTON_ALPHA;
+import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_BUBBLES;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
@@ -31,7 +28,6 @@
import android.animation.ValueAnimator;
import android.annotation.SuppressLint;
import android.content.Context;
-import android.util.Log;
import android.view.HapticFeedbackConstants;
import android.view.ViewConfiguration;
@@ -41,6 +37,7 @@
import androidx.dynamicanimation.animation.SpringAnimation;
import androidx.dynamicanimation.animation.SpringForce;
+import com.android.internal.protolog.common.ProtoLog;
import com.android.wm.shell.animation.FlingAnimationUtils;
import com.android.wm.shell.animation.Interpolators;
import com.android.wm.shell.bubbles.BubbleExpandedView;
@@ -55,8 +52,6 @@
*/
public class ExpandedViewAnimationControllerImpl implements ExpandedViewAnimationController {
- private static final String TAG = TAG_WITH_CLASS_NAME ? "ExpandedViewAnimCtrl" : TAG_BUBBLES;
-
private static final float COLLAPSE_THRESHOLD = 0.02f;
private static final int COLLAPSE_DURATION_MS = 250;
@@ -121,9 +116,6 @@
@Override
public void setExpandedView(BubbleExpandedView expandedView) {
if (mExpandedView != null) {
- if (DEBUG_COLLAPSE_ANIMATOR) {
- Log.d(TAG, "updating expandedView, resetting previous");
- }
if (mCollapseAnimation != null) {
mCollapseAnimation.cancel();
}
@@ -140,17 +132,14 @@
if (mExpandedView != null) {
mDraggedAmount = OverScroll.dampedScroll(distance, mExpandedView.getContentHeight());
- if (DEBUG_COLLAPSE_ANIMATOR && DEBUG_EXPANDED_VIEW_DRAGGING) {
- Log.d(TAG, "updateDrag: distance=" + distance + " dragged=" + mDraggedAmount);
- }
+ ProtoLog.d(WM_SHELL_BUBBLES,
+ "updateDrag: distance=%f dragged=%d", distance, mDraggedAmount);
setCollapsedAmount(mDraggedAmount);
if (!mNotifiedAboutThreshold && isPastCollapseThreshold()) {
mNotifiedAboutThreshold = true;
- if (DEBUG_COLLAPSE_ANIMATOR) {
- Log.d(TAG, "notifying over collapse threshold");
- }
+ ProtoLog.d(WM_SHELL_BUBBLES, "notifying over collapse threshold");
vibrateIfEnabled();
}
}
@@ -172,45 +161,35 @@
if (mSwipeDownVelocity > mMinFlingVelocity) {
// Swipe velocity is positive and over fling velocity.
// This is a swipe down, always reset to expanded state, regardless of dragged amount.
- if (DEBUG_COLLAPSE_ANIMATOR) {
- Log.d(TAG,
- "not collapsing expanded view, swipe down velocity: " + mSwipeDownVelocity
- + " minV: " + mMinFlingVelocity);
- }
+ ProtoLog.d(WM_SHELL_BUBBLES,
+ "not collapsing expanded view, swipe down velocity=%f minV=%d",
+ mSwipeDownVelocity, mMinFlingVelocity);
return false;
}
if (mSwipeUpVelocity > mMinFlingVelocity) {
// Swiping up and over fling velocity, collapse the view.
- if (DEBUG_COLLAPSE_ANIMATOR) {
- Log.d(TAG,
- "collapse expanded view, swipe up velocity: " + mSwipeUpVelocity + " minV: "
- + mMinFlingVelocity);
- }
+ ProtoLog.d(WM_SHELL_BUBBLES,
+ "collapse expanded view, swipe up velocity=%f minV=%d",
+ mSwipeUpVelocity, mMinFlingVelocity);
return true;
}
if (isPastCollapseThreshold()) {
- if (DEBUG_COLLAPSE_ANIMATOR) {
- Log.d(TAG, "collapse expanded view, past threshold, dragged: " + mDraggedAmount);
- }
+ ProtoLog.d(WM_SHELL_BUBBLES,
+ "collapse expanded view, past threshold, dragged=%d", mDraggedAmount);
return true;
}
- if (DEBUG_COLLAPSE_ANIMATOR) {
- Log.d(TAG, "not collapsing expanded view");
- }
+ ProtoLog.d(WM_SHELL_BUBBLES, "not collapsing expanded view");
return false;
}
@Override
public void animateCollapse(Runnable startStackCollapse, Runnable after) {
- if (DEBUG_COLLAPSE_ANIMATOR) {
- Log.d(TAG,
- "expandedView animate collapse swipeVel=" + mSwipeUpVelocity + " minFlingVel="
- + mMinFlingVelocity);
- }
+ ProtoLog.d(WM_SHELL_BUBBLES, "expandedView animate collapse swipeVel=%f minFlingVel=%d",
+ mSwipeUpVelocity, mMinFlingVelocity);
if (mExpandedView != null) {
// Mark it as animating immediately to avoid updates to the view before animation starts
mExpandedView.setAnimating(true);
@@ -243,9 +222,7 @@
@Override
public void animateBackToExpanded() {
- if (DEBUG_COLLAPSE_ANIMATOR) {
- Log.d(TAG, "expandedView animate back to expanded");
- }
+ ProtoLog.d(WM_SHELL_BUBBLES, "expandedView animate back to expanded");
BubbleExpandedView expandedView = mExpandedView;
if (expandedView == null) {
return;
@@ -298,9 +275,7 @@
@Override
public void reset() {
- if (DEBUG_COLLAPSE_ANIMATOR) {
- Log.d(TAG, "reset expandedView collapsed state");
- }
+ ProtoLog.d(WM_SHELL_BUBBLES, "reset expandedView collapsed state");
if (mExpandedView == null) {
return;
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java
index 00d683e..73a9cf4 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java
@@ -266,13 +266,8 @@
mListener.onBackPressed();
}
- /** Cleans up task view, should be called when the bubble is no longer active. */
+ /** Cleans up the expanded view, should be called when the bubble is no longer active. */
public void cleanUpExpandedState() {
- if (mBubbleTaskViewHelper != null) {
- if (mTaskView != null) {
- removeView(mTaskView);
- }
- }
mMenuViewController.hideMenu(false /* animated */);
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java
index bd8ce80..62f2726 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java
@@ -34,7 +34,9 @@
import android.widget.FrameLayout;
import com.android.wm.shell.R;
+import com.android.wm.shell.bubbles.Bubble;
import com.android.wm.shell.bubbles.BubbleController;
+import com.android.wm.shell.bubbles.BubbleData;
import com.android.wm.shell.bubbles.BubbleOverflow;
import com.android.wm.shell.bubbles.BubblePositioner;
import com.android.wm.shell.bubbles.BubbleViewProvider;
@@ -61,6 +63,7 @@
private static final float SCRIM_ALPHA = 0.2f;
private final BubbleController mBubbleController;
+ private final BubbleData mBubbleData;
private final BubblePositioner mPositioner;
private final BubbleBarAnimationHelper mAnimationHelper;
private final BubbleEducationViewController mEducationViewController;
@@ -85,9 +88,10 @@
private TouchDelegate mHandleTouchDelegate;
private final Rect mHandleTouchBounds = new Rect();
- public BubbleBarLayerView(Context context, BubbleController controller) {
+ public BubbleBarLayerView(Context context, BubbleController controller, BubbleData bubbleData) {
super(context);
mBubbleController = controller;
+ mBubbleData = bubbleData;
mPositioner = mBubbleController.getPositioner();
mAnimationHelper = new BubbleBarAnimationHelper(context,
@@ -236,15 +240,47 @@
showScrim(true);
}
+ /** Removes the given {@code bubble}. */
+ public void removeBubble(Bubble bubble) {
+ if (mBubbleData.getBubbles().isEmpty()) {
+ // we're removing the last bubble. collapse the expanded view and cleanup bubble views
+ // at the end.
+ collapse(bubble::cleanupViews);
+ } else {
+ bubble.cleanupViews();
+ }
+ }
+
/** Collapses any showing expanded view */
public void collapse() {
+ collapse(/* endAction= */ null);
+ }
+
+ /**
+ * Collapses any showing expanded view.
+ *
+ * @param endAction an action to run and the end of the collapse animation.
+ */
+ public void collapse(@Nullable Runnable endAction) {
+ if (!mIsExpanded) {
+ return;
+ }
mIsExpanded = false;
final BubbleBarExpandedView viewToRemove = mExpandedView;
mEducationViewController.hideEducation(/* animated = */ true);
+ Runnable runnable = () -> {
+ removeView(viewToRemove);
+ if (endAction != null) {
+ endAction.run();
+ }
+ if (mBubbleData.getBubbles().isEmpty()) {
+ mBubbleController.onAllBubblesAnimatedOut();
+ }
+ };
if (mDragController != null && mDragController.isStuckToDismiss()) {
- mAnimationHelper.animateDismiss(() -> removeView(viewToRemove));
+ mAnimationHelper.animateDismiss(runnable);
} else {
- mAnimationHelper.animateCollapse(() -> removeView(viewToRemove));
+ mAnimationHelper.animateCollapse(runnable);
}
mBubbleController.getSysuiProxy().onStackExpandChanged(false);
mExpandedView = null;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/properties/ProdBubbleProperties.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/properties/ProdBubbleProperties.kt
index e1dea3b..33b61b1 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/properties/ProdBubbleProperties.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/properties/ProdBubbleProperties.kt
@@ -17,18 +17,19 @@
package com.android.wm.shell.bubbles.properties
import android.os.SystemProperties
+import com.android.wm.shell.Flags
/** Provides bubble properties in production. */
object ProdBubbleProperties : BubbleProperties {
- // TODO(b/256873975) Should use proper flag when available to shell/launcher
- private var _isBubbleBarEnabled =
+ private var _isBubbleBarEnabled = Flags.enableBubbleBar() ||
SystemProperties.getBoolean("persist.wm.debug.bubble_bar", false)
override val isBubbleBarEnabled
get() = _isBubbleBarEnabled
override fun refresh() {
- _isBubbleBarEnabled = SystemProperties.getBoolean("persist.wm.debug.bubble_bar", false)
+ _isBubbleBarEnabled = Flags.enableBubbleBar() ||
+ SystemProperties.getBoolean("persist.wm.debug.bubble_bar", false)
}
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipBoundsState.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipBoundsState.java
index 8b6c7b6..68d26da 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipBoundsState.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipBoundsState.java
@@ -140,7 +140,7 @@
// spec takes the aspect ratio of the bounds into account, so both width and height
// scale by the same factor.
addPipExclusionBoundsChangeCallback((bounds) -> {
- mBoundsScale = Math.min((float) bounds.width() / mMaxSize.x, 1.0f);
+ updateBoundsScale();
});
}
@@ -152,6 +152,11 @@
mSizeSpecSource.onConfigurationChanged();
}
+ /** Update the bounds scale percentage value. */
+ public void updateBoundsScale() {
+ mBoundsScale = Math.min((float) mBounds.width() / mMaxSize.x, 1.0f);
+ }
+
private void reloadResources() {
mStashOffset = mContext.getResources().getDimensionPixelSize(R.dimen.pip_stash_offset);
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/TvWMShellModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/TvWMShellModule.java
index b52a118..d4ed017 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/TvWMShellModule.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/TvWMShellModule.java
@@ -83,7 +83,6 @@
DisplayController displayController,
DisplayImeController displayImeController,
DisplayInsetsController displayInsetsController,
- Optional<DragAndDropController> dragAndDropController,
Transitions transitions,
TransactionPool transactionPool,
IconProvider iconProvider,
@@ -94,8 +93,8 @@
SystemWindows systemWindows) {
return new TvSplitScreenController(context, shellInit, shellCommandHandler, shellController,
shellTaskOrganizer, syncQueue, rootTDAOrganizer, displayController,
- displayImeController, displayInsetsController, dragAndDropController, transitions,
- transactionPool, iconProvider, recentTasks, launchAdjacentController, mainExecutor,
- mainHandler, systemWindows);
+ displayImeController, displayInsetsController, transitions, transactionPool,
+ iconProvider, recentTasks, launchAdjacentController, mainExecutor, mainHandler,
+ systemWindows);
}
}
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 fc97c798..0d6a852 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
@@ -78,7 +78,6 @@
import com.android.wm.shell.desktopmode.DesktopTasksController;
import com.android.wm.shell.displayareahelper.DisplayAreaHelper;
import com.android.wm.shell.displayareahelper.DisplayAreaHelperController;
-import com.android.wm.shell.draganddrop.DragAndDropController;
import com.android.wm.shell.freeform.FreeformComponents;
import com.android.wm.shell.fullscreen.FullscreenTaskListener;
import com.android.wm.shell.hidedisplaycutout.HideDisplayCutoutController;
@@ -203,20 +202,6 @@
@WMSingleton
@Provides
- static Optional<DragAndDropController> provideDragAndDropController(Context context,
- ShellInit shellInit,
- ShellController shellController,
- ShellCommandHandler shellCommandHandler,
- DisplayController displayController,
- UiEventLogger uiEventLogger,
- IconProvider iconProvider,
- @ShellMainThread ShellExecutor mainExecutor) {
- return Optional.ofNullable(DragAndDropController.create(context, shellInit, shellController,
- shellCommandHandler, displayController, uiEventLogger, iconProvider, mainExecutor));
- }
-
- @WMSingleton
- @Provides
static ShellTaskOrganizer provideShellTaskOrganizer(
Context context,
ShellInit shellInit,
@@ -911,7 +896,6 @@
DisplayController displayController,
DisplayImeController displayImeController,
DisplayInsetsController displayInsetsController,
- Optional<DragAndDropController> dragAndDropControllerOptional,
ShellTaskOrganizer shellTaskOrganizer,
Optional<BubbleController> bubblesOptional,
Optional<SplitScreenController> splitScreenOptional,
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 36f06e8..ead5ad2 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
@@ -172,7 +172,7 @@
BubblePositioner positioner,
DisplayController displayController,
@DynamicOverride Optional<OneHandedController> oneHandedOptional,
- Optional<DragAndDropController> dragAndDropController,
+ DragAndDropController dragAndDropController,
@ShellMainThread ShellExecutor mainExecutor,
@ShellMainThread Handler mainHandler,
@ShellBackgroundThread ShellExecutor bgExecutor,
@@ -338,7 +338,7 @@
DisplayController displayController,
DisplayImeController displayImeController,
DisplayInsetsController displayInsetsController,
- Optional<DragAndDropController> dragAndDropController,
+ DragAndDropController dragAndDropController,
Transitions transitions,
TransactionPool transactionPool,
IconProvider iconProvider,
@@ -553,6 +553,24 @@
}
//
+ // Drag and drop
+ //
+
+ @WMSingleton
+ @Provides
+ static DragAndDropController provideDragAndDropController(Context context,
+ ShellInit shellInit,
+ ShellController shellController,
+ ShellCommandHandler shellCommandHandler,
+ DisplayController displayController,
+ UiEventLogger uiEventLogger,
+ IconProvider iconProvider,
+ @ShellMainThread ShellExecutor mainExecutor) {
+ return new DragAndDropController(context, shellInit, shellController,
+ shellCommandHandler, displayController, uiEventLogger, iconProvider, mainExecutor);
+ }
+
+ //
// Misc
//
@@ -562,6 +580,7 @@
@ShellCreateTriggerOverride
@Provides
static Object provideIndependentShellComponentsToCreate(
+ DragAndDropController dragAndDropController,
DefaultMixedHandler defaultMixedHandler) {
return new Object();
}
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 f82212d..7c8fcbb 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
@@ -248,6 +248,12 @@
// Check if count changed
if (prevCount != newCount) {
+ KtProtoLog.d(
+ WM_SHELL_DESKTOP_MODE,
+ "DesktopTaskRepo: visibleTaskCount has changed from %d to %d",
+ prevCount,
+ newCount
+ )
notifyVisibleTaskListeners(displayId, newCount)
}
}
@@ -262,6 +268,11 @@
* Get number of tasks that are marked as visible on given [displayId]
*/
fun getVisibleTaskCount(displayId: Int): Int {
+ KtProtoLog.d(
+ WM_SHELL_DESKTOP_MODE,
+ "DesktopTaskRepo: visibleTaskCount= %d",
+ displayData[displayId]?.visibleTasks?.size ?: 0
+ )
return displayData[displayId]?.visibleTasks?.size ?: 0
}
@@ -290,6 +301,10 @@
taskId
)
freeformTasksInZOrder.remove(taskId)
+ KtProtoLog.d(
+ WM_SHELL_DESKTOP_MODE,
+ "DesktopTaskRepo: remaining freeform tasks: " + freeformTasksInZOrder.toDumpString()
+ )
}
/**
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropController.java
index fdfb6f3..269c369 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropController.java
@@ -105,25 +105,7 @@
void onDragStarted();
}
- /**
- * Creates {@link DragAndDropController}. Returns {@code null} if the feature is disabled.
- */
- public static DragAndDropController create(Context context,
- ShellInit shellInit,
- ShellController shellController,
- ShellCommandHandler shellCommandHandler,
- DisplayController displayController,
- UiEventLogger uiEventLogger,
- IconProvider iconProvider,
- ShellExecutor mainExecutor) {
- if (!context.getResources().getBoolean(R.bool.config_enableShellDragDrop)) {
- return null;
- }
- return new DragAndDropController(context, shellInit, shellController, shellCommandHandler,
- displayController, uiEventLogger, iconProvider, mainExecutor);
- }
-
- DragAndDropController(Context context,
+ public DragAndDropController(Context context,
ShellInit shellInit,
ShellController shellController,
ShellCommandHandler shellCommandHandler,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java
index 52a06e0..9f73f1b 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java
@@ -1891,6 +1891,9 @@
}
private void removeContentOverlay(SurfaceControl surface, Runnable callback) {
+ ProtoLog.w(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+ "removeContentOverlay: %s, state=%s, surface=%s",
+ mTaskInfo, mPipTransitionState, surface);
if (mPipOverlay != null) {
if (mPipOverlay != surface) {
ProtoLog.w(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java
index 896ca96..e018ecc 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java
@@ -286,12 +286,6 @@
// For transition that we don't animate, but contains the PIP leash, we need to update the
// PIP surface, otherwise it will be reset after the transition.
if (currentPipTaskChange != null) {
- // Set the "end" bounds of pip. The default setup uses the start bounds. Since this is
- // changing the *finish*Transaction, we need to use the end bounds. This will also
- // make sure that the fade-in animation (below) uses the end bounds as well.
- if (!currentPipTaskChange.getEndAbsBounds().isEmpty()) {
- mPipBoundsState.setBounds(currentPipTaskChange.getEndAbsBounds());
- }
updatePipForUnhandledTransition(currentPipTaskChange, startTransaction,
finishTransaction);
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java
index c5a0102..05d4f53 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java
@@ -244,6 +244,9 @@
// The same rotation may have been set by auto PiP-able or fixed rotation. So notify
// the change with fromRotation=false to apply the rotated destination bounds from
// PipTaskOrganizer#onMovementBoundsChanged.
+ // We need to update the bounds scale in case this was from fixed rotation, as the
+ // current proportion was computed using the previous orientation max size and is wrong.
+ mPipBoundsState.updateBoundsScale();
updateMovementBounds(null, false /* fromRotation */,
false /* fromImeAdjustment */, false /* fromShelfAdjustment */, t);
return;
@@ -797,21 +800,15 @@
mPipBoundsAlgorithm.getMovementBounds(postChangeBounds),
mPipBoundsState.getStashedState());
- // Scale PiP on density dpi change, so it appears to be the same size physically.
- final boolean densityDpiChanged =
- mPipDisplayLayoutState.getDisplayLayout().densityDpi() != 0
- && (mPipDisplayLayoutState.getDisplayLayout().densityDpi()
- != layout.densityDpi());
- if (densityDpiChanged) {
- final float scale = (float) layout.densityDpi()
- / mPipDisplayLayoutState.getDisplayLayout().densityDpi();
- postChangeBounds.set(0, 0,
- (int) (postChangeBounds.width() * scale),
- (int) (postChangeBounds.height() * scale));
- }
-
updateDisplayLayout.run();
+ // Resize the PiP bounds to be at the same scale relative to the new size spec. For
+ // example, if PiP was resized to 90% of the maximum size on the previous layout,
+ // make sure it is 90% of the new maximum size spec.
+ postChangeBounds.set(0, 0,
+ (int) (mPipBoundsState.getMaxSize().x * mPipBoundsState.getBoundsScale()),
+ (int) (mPipBoundsState.getMaxSize().y * mPipBoundsState.getBoundsScale()));
+
// Calculate the PiP bounds in the new orientation based on same fraction along the
// rotated movement bounds.
final Rect postChangeMovementBounds = mPipBoundsAlgorithm.getMovementBounds(
@@ -827,6 +824,10 @@
mPipBoundsState.setHasUserResizedPip(true);
mTouchHandler.setUserResizeBounds(postChangeBounds);
+ final boolean densityDpiChanged =
+ mPipDisplayLayoutState.getDisplayLayout().densityDpi() != 0
+ && (mPipDisplayLayoutState.getDisplayLayout().densityDpi()
+ != layout.densityDpi());
if (densityDpiChanged) {
// Using PipMotionHelper#movePip directly here may cause race condition since
// the app content in PiP mode may or may not be updated for the new density dpi.
@@ -1146,6 +1147,11 @@
// Update the display layout
mPipDisplayLayoutState.rotateTo(toRotation);
+ mTouchHandler.updateMinMaxSize(mPipBoundsState.getAspectRatio());
+
+ postChangeStackBounds.set(0, 0,
+ (int) (mPipBoundsState.getMaxSize().x * mPipBoundsState.getBoundsScale()),
+ (int) (mPipBoundsState.getMaxSize().y * mPipBoundsState.getBoundsScale()));
// Calculate the stack bounds in the new orientation based on same fraction along the
// rotated movement bounds.
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipResizeGestureHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipResizeGestureHandler.java
index f175775..5f9195a3 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipResizeGestureHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipResizeGestureHandler.java
@@ -15,19 +15,13 @@
*/
package com.android.wm.shell.pip.phone;
-import static com.android.internal.policy.TaskResizingAlgorithm.CTRL_BOTTOM;
-import static com.android.internal.policy.TaskResizingAlgorithm.CTRL_LEFT;
import static com.android.internal.policy.TaskResizingAlgorithm.CTRL_NONE;
-import static com.android.internal.policy.TaskResizingAlgorithm.CTRL_RIGHT;
-import static com.android.internal.policy.TaskResizingAlgorithm.CTRL_TOP;
-import static com.android.wm.shell.pip.phone.PipMenuView.ANIM_TYPE_NONE;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Point;
import android.graphics.PointF;
import android.graphics.Rect;
-import android.graphics.Region;
import android.hardware.input.InputManager;
import android.os.Looper;
import android.view.BatchedInputEventReceiver;
@@ -41,7 +35,6 @@
import androidx.annotation.VisibleForTesting;
-import com.android.internal.policy.TaskResizingAlgorithm;
import com.android.wm.shell.R;
import com.android.wm.shell.common.ShellExecutor;
import com.android.wm.shell.common.pip.PipBoundsAlgorithm;
@@ -53,7 +46,6 @@
import java.io.PrintWriter;
import java.util.function.Consumer;
-import java.util.function.Function;
/**
* Helper on top of PipTouchHandler that handles inputs OUTSIDE of the PIP window, which is used to
@@ -77,7 +69,6 @@
private final PipPinchResizingAlgorithm mPinchResizingAlgorithm;
private final int mDisplayId;
private final ShellExecutor mMainExecutor;
- private final Region mTmpRegion = new Region();
private final PointF mDownPoint = new PointF();
private final PointF mDownSecondPoint = new PointF();
@@ -88,24 +79,15 @@
private final Rect mLastResizeBounds = new Rect();
private final Rect mUserResizeBounds = new Rect();
private final Rect mDownBounds = new Rect();
- private final Rect mDragCornerSize = new Rect();
- private final Rect mTmpTopLeftCorner = new Rect();
- private final Rect mTmpTopRightCorner = new Rect();
- private final Rect mTmpBottomLeftCorner = new Rect();
- private final Rect mTmpBottomRightCorner = new Rect();
- private final Rect mDisplayBounds = new Rect();
- private final Function<Rect, Rect> mMovementBoundsSupplier;
private final Runnable mUpdateMovementBoundsRunnable;
private final Consumer<Rect> mUpdateResizeBoundsCallback;
- private int mDelta;
private float mTouchSlop;
private boolean mAllowGesture;
private boolean mIsAttached;
private boolean mIsEnabled;
private boolean mEnablePinchResize;
- private boolean mEnableDragCornerResize;
private boolean mIsSysUiStateValid;
private boolean mThresholdCrossed;
private boolean mOngoingPinchToResize = false;
@@ -123,7 +105,7 @@
PipBoundsState pipBoundsState, PipMotionHelper motionHelper,
PipTouchState pipTouchState, PipTaskOrganizer pipTaskOrganizer,
PipDismissTargetHandler pipDismissTargetHandler,
- Function<Rect, Rect> movementBoundsSupplier, Runnable updateMovementBoundsRunnable,
+ Runnable updateMovementBoundsRunnable,
PipUiEventLogger pipUiEventLogger, PhonePipMenuController menuActivityController,
ShellExecutor mainExecutor) {
mContext = context;
@@ -135,7 +117,6 @@
mPipTouchState = pipTouchState;
mPipTaskOrganizer = pipTaskOrganizer;
mPipDismissTargetHandler = pipDismissTargetHandler;
- mMovementBoundsSupplier = movementBoundsSupplier;
mUpdateMovementBoundsRunnable = updateMovementBoundsRunnable;
mPhonePipMenuController = menuActivityController;
mPipUiEventLogger = pipUiEventLogger;
@@ -171,20 +152,9 @@
}
private void reloadResources() {
- final Resources res = mContext.getResources();
- mDelta = res.getDimensionPixelSize(R.dimen.pip_resize_edge_size);
- mEnableDragCornerResize = res.getBoolean(R.bool.config_pipEnableDragCornerResize);
mTouchSlop = ViewConfiguration.get(mContext).getScaledTouchSlop();
}
- private void resetDragCorners() {
- mDragCornerSize.set(0, 0, mDelta, mDelta);
- mTmpTopLeftCorner.set(mDragCornerSize);
- mTmpTopRightCorner.set(mDragCornerSize);
- mTmpBottomLeftCorner.set(mDragCornerSize);
- mTmpBottomRightCorner.set(mDragCornerSize);
- }
-
private void disposeInputChannel() {
if (mInputEventReceiver != null) {
mInputEventReceiver.dispose();
@@ -232,7 +202,7 @@
@VisibleForTesting
void onInputEvent(InputEvent ev) {
- if (!mEnableDragCornerResize && !mEnablePinchResize) {
+ if (!mEnablePinchResize) {
// No need to handle anything if neither form of resizing is enabled.
return;
}
@@ -260,8 +230,6 @@
if (mEnablePinchResize && mOngoingPinchToResize) {
onPinchResize(mv);
- } else if (mEnableDragCornerResize) {
- onDragCornerResize(mv);
}
}
}
@@ -273,48 +241,6 @@
return mCtrlType != CTRL_NONE || mOngoingPinchToResize;
}
- /**
- * Check whether the current x,y coordinate is within the region in which drag-resize should
- * start.
- * This consists of 4 small squares on the 4 corners of the PIP window, a quarter of which
- * overlaps with the PIP window while the rest goes outside of the PIP window.
- * _ _ _ _
- * |_|_|_________|_|_|
- * |_|_| |_|_|
- * | PIP |
- * | WINDOW |
- * _|_ _|_
- * |_|_|_________|_|_|
- * |_|_| |_|_|
- */
- public boolean isWithinDragResizeRegion(int x, int y) {
- if (!mEnableDragCornerResize) {
- return false;
- }
-
- final Rect currentPipBounds = mPipBoundsState.getBounds();
- if (currentPipBounds == null) {
- return false;
- }
- resetDragCorners();
- mTmpTopLeftCorner.offset(currentPipBounds.left - mDelta / 2,
- currentPipBounds.top - mDelta / 2);
- mTmpTopRightCorner.offset(currentPipBounds.right - mDelta / 2,
- currentPipBounds.top - mDelta / 2);
- mTmpBottomLeftCorner.offset(currentPipBounds.left - mDelta / 2,
- currentPipBounds.bottom - mDelta / 2);
- mTmpBottomRightCorner.offset(currentPipBounds.right - mDelta / 2,
- currentPipBounds.bottom - mDelta / 2);
-
- mTmpRegion.setEmpty();
- mTmpRegion.op(mTmpTopLeftCorner, Region.Op.UNION);
- mTmpRegion.op(mTmpTopRightCorner, Region.Op.UNION);
- mTmpRegion.op(mTmpBottomLeftCorner, Region.Op.UNION);
- mTmpRegion.op(mTmpBottomRightCorner, Region.Op.UNION);
-
- return mTmpRegion.contains(x, y);
- }
-
public boolean isUsingPinchToZoom() {
return mEnablePinchResize;
}
@@ -325,62 +251,17 @@
public boolean willStartResizeGesture(MotionEvent ev) {
if (isInValidSysUiState()) {
- switch (ev.getActionMasked()) {
- case MotionEvent.ACTION_DOWN:
- if (isWithinDragResizeRegion((int) ev.getRawX(), (int) ev.getRawY())) {
- return true;
- }
- break;
-
- case MotionEvent.ACTION_POINTER_DOWN:
- if (mEnablePinchResize && ev.getPointerCount() == 2) {
- onPinchResize(ev);
- mOngoingPinchToResize = mAllowGesture;
- return mAllowGesture;
- }
- break;
-
- default:
- break;
+ if (ev.getActionMasked() == MotionEvent.ACTION_POINTER_DOWN) {
+ if (mEnablePinchResize && ev.getPointerCount() == 2) {
+ onPinchResize(ev);
+ mOngoingPinchToResize = mAllowGesture;
+ return mAllowGesture;
+ }
}
}
return false;
}
- private void setCtrlType(int x, int y) {
- final Rect currentPipBounds = mPipBoundsState.getBounds();
-
- Rect movementBounds = mMovementBoundsSupplier.apply(currentPipBounds);
-
- mDisplayBounds.set(movementBounds.left,
- movementBounds.top,
- movementBounds.right + currentPipBounds.width(),
- movementBounds.bottom + currentPipBounds.height());
-
- if (mTmpTopLeftCorner.contains(x, y) && currentPipBounds.top != mDisplayBounds.top
- && currentPipBounds.left != mDisplayBounds.left) {
- mCtrlType |= CTRL_LEFT;
- mCtrlType |= CTRL_TOP;
- }
- if (mTmpTopRightCorner.contains(x, y) && currentPipBounds.top != mDisplayBounds.top
- && currentPipBounds.right != mDisplayBounds.right) {
- mCtrlType |= CTRL_RIGHT;
- mCtrlType |= CTRL_TOP;
- }
- if (mTmpBottomRightCorner.contains(x, y)
- && currentPipBounds.bottom != mDisplayBounds.bottom
- && currentPipBounds.right != mDisplayBounds.right) {
- mCtrlType |= CTRL_RIGHT;
- mCtrlType |= CTRL_BOTTOM;
- }
- if (mTmpBottomLeftCorner.contains(x, y)
- && currentPipBounds.bottom != mDisplayBounds.bottom
- && currentPipBounds.left != mDisplayBounds.left) {
- mCtrlType |= CTRL_LEFT;
- mCtrlType |= CTRL_BOTTOM;
- }
- }
-
private boolean isInValidSysUiState() {
return mIsSysUiStateValid;
}
@@ -457,59 +338,6 @@
}
}
- private void onDragCornerResize(MotionEvent ev) {
- int action = ev.getActionMasked();
- float x = ev.getX();
- float y = ev.getY() - mOhmOffset;
- if (action == MotionEvent.ACTION_DOWN) {
- mLastResizeBounds.setEmpty();
- mAllowGesture = isInValidSysUiState() && isWithinDragResizeRegion((int) x, (int) y);
- if (mAllowGesture) {
- setCtrlType((int) x, (int) y);
- mDownPoint.set(x, y);
- mDownBounds.set(mPipBoundsState.getBounds());
- }
- } else if (mAllowGesture) {
- switch (action) {
- case MotionEvent.ACTION_POINTER_DOWN:
- // We do not support multi touch for resizing via drag
- mAllowGesture = false;
- break;
- case MotionEvent.ACTION_MOVE:
- // Capture inputs
- if (!mThresholdCrossed
- && Math.hypot(x - mDownPoint.x, y - mDownPoint.y) > mTouchSlop) {
- mThresholdCrossed = true;
- // Reset the down to begin resizing from this point
- mDownPoint.set(x, y);
- mInputMonitor.pilferPointers();
- }
- if (mThresholdCrossed) {
- if (mPhonePipMenuController.isMenuVisible()) {
- mPhonePipMenuController.hideMenu(ANIM_TYPE_NONE,
- false /* resize */);
- }
- final Rect currentPipBounds = mPipBoundsState.getBounds();
- mLastResizeBounds.set(TaskResizingAlgorithm.resizeDrag(x, y,
- mDownPoint.x, mDownPoint.y, currentPipBounds, mCtrlType, mMinSize.x,
- mMinSize.y, mMaxSize, true,
- mDownBounds.width() > mDownBounds.height()));
- mPipBoundsAlgorithm.transformBoundsToAspectRatio(mLastResizeBounds,
- mPipBoundsState.getAspectRatio(), false /* useCurrentMinEdgeSize */,
- true /* useCurrentSize */);
- mPipTaskOrganizer.scheduleUserResizePip(mDownBounds, mLastResizeBounds,
- null);
- mPipBoundsState.setHasUserResizedPip(true);
- }
- break;
- case MotionEvent.ACTION_UP:
- case MotionEvent.ACTION_CANCEL:
- finishResize();
- break;
- }
- }
- }
-
private void snapToMovementBoundsEdge(Rect bounds, Rect movementBounds) {
final int leftEdge = bounds.left;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipTouchHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipTouchHandler.java
index 81705e2..11c356d 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipTouchHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipTouchHandler.java
@@ -212,7 +212,7 @@
mPipResizeGestureHandler =
new PipResizeGestureHandler(context, pipBoundsAlgorithm, pipBoundsState,
mMotionHelper, mTouchState, pipTaskOrganizer, mPipDismissTargetHandler,
- this::getMovementBounds, this::updateMovementBounds, pipUiEventLogger,
+ this::updateMovementBounds, pipUiEventLogger,
menuController, mainExecutor);
mConnection = new PipAccessibilityInteractionConnection(mContext, pipBoundsState,
mMotionHelper, pipTaskOrganizer, mPipBoundsAlgorithm.getSnapAlgorithm(),
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 70cb2fc..1b124c2 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
@@ -184,7 +184,7 @@
private final DisplayController mDisplayController;
private final DisplayImeController mDisplayImeController;
private final DisplayInsetsController mDisplayInsetsController;
- private final Optional<DragAndDropController> mDragAndDropController;
+ private final DragAndDropController mDragAndDropController;
private final Transitions mTransitions;
private final TransactionPool mTransactionPool;
private final IconProvider mIconProvider;
@@ -214,7 +214,7 @@
DisplayController displayController,
DisplayImeController displayImeController,
DisplayInsetsController displayInsetsController,
- Optional<DragAndDropController> dragAndDropController,
+ DragAndDropController dragAndDropController,
Transitions transitions,
TransactionPool transactionPool,
IconProvider iconProvider,
@@ -290,7 +290,7 @@
mDisplayController = displayController;
mDisplayImeController = displayImeController;
mDisplayInsetsController = displayInsetsController;
- mDragAndDropController = Optional.of(dragAndDropController);
+ mDragAndDropController = dragAndDropController;
mTransitions = transitions;
mTransactionPool = transactionPool;
mIconProvider = iconProvider;
@@ -328,7 +328,9 @@
// TODO: Multi-display
mStageCoordinator = createStageCoordinator();
}
- mDragAndDropController.ifPresent(controller -> controller.setSplitScreenController(this));
+ if (mDragAndDropController != null) {
+ mDragAndDropController.setSplitScreenController(this);
+ }
mWindowDecorViewModel.ifPresent(viewModel -> viewModel.setSplitScreenController(this));
mDesktopTasksController.ifPresent(controller -> controller.setSplitScreenController(this));
}
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 e8894a83..b60e361 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
@@ -451,6 +451,8 @@
mPendingResize.onConsumed(aborted);
mPendingResize = null;
}
+
+ // TODO: handle transition consumed for active remote handler
}
void onFinish(WindowContainerTransaction wct) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/tv/TvSplitScreenController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/tv/TvSplitScreenController.java
index c101425..aec4d11 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/tv/TvSplitScreenController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/tv/TvSplitScreenController.java
@@ -74,7 +74,6 @@
DisplayController displayController,
DisplayImeController displayImeController,
DisplayInsetsController displayInsetsController,
- Optional<DragAndDropController> dragAndDropController,
Transitions transitions,
TransactionPool transactionPool,
IconProvider iconProvider,
@@ -85,7 +84,7 @@
SystemWindows systemWindows) {
super(context, shellInit, shellCommandHandler, shellController, shellTaskOrganizer,
syncQueue, rootTDAOrganizer, displayController, displayImeController,
- displayInsetsController, dragAndDropController, transitions, transactionPool,
+ displayInsetsController, null, transitions, transactionPool,
iconProvider, recentTasks, launchAdjacentController, Optional.empty(),
Optional.empty(), mainExecutor);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashScreenExitAnimationUtils.java b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashScreenExitAnimationUtils.java
index e86b62d..6325c68 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashScreenExitAnimationUtils.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashScreenExitAnimationUtils.java
@@ -390,7 +390,7 @@
SurfaceControl firstWindowSurface, ViewGroup splashScreenView,
TransactionPool transactionPool, Rect firstWindowFrame,
int mainWindowShiftLength, float roundedCornerRadius) {
- mFromYDelta = fromYDelta - roundedCornerRadius;
+ mFromYDelta = fromYDelta - Math.max(firstWindowFrame.top, roundedCornerRadius);
mToYDelta = toYDelta;
mOccludeHoleView = occludeHoleView;
mApplier = new SyncRtSurfaceTransactionApplier(occludeHoleView);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenContentDrawer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenContentDrawer.java
index a666e20..bfb60c0 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenContentDrawer.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenContentDrawer.java
@@ -253,7 +253,7 @@
: taskInfo.topActivityInfo;
params.layoutInDisplayCutoutMode = a.getInt(
R.styleable.Window_windowLayoutInDisplayCutoutMode,
- PhoneWindow.isEdgeToEdgeEnforced(activityInfo.applicationInfo, false /* local */)
+ PhoneWindow.isEdgeToEdgeEnforced(activityInfo.applicationInfo, false /* local */, a)
? WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS
: params.layoutInDisplayCutoutMode);
params.windowAnimations = a.getResourceId(R.styleable.Window_windowAnimationStyle, 0);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/OneShotRemoteHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/OneShotRemoteHandler.java
index 4355ed2..94519a0 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/OneShotRemoteHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/OneShotRemoteHandler.java
@@ -74,6 +74,9 @@
@Override
public void onTransitionFinished(WindowContainerTransaction wct,
SurfaceControl.Transaction sct) {
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS,
+ "Finished one-shot remote transition %s for (#%d).", mRemote,
+ info.getDebugId());
if (mRemote.asBinder() != null) {
mRemote.asBinder().unlinkToDeath(remoteDied, 0 /* flags */);
}
@@ -82,8 +85,8 @@
}
mMainExecutor.execute(() -> {
finishCallback.onTransitionFinished(wct);
+ mRemote = null;
});
- mRemote = null;
}
};
Transitions.setRunningRemoteTransitionDelegate(mRemote.getAppThread());
@@ -115,17 +118,24 @@
public void mergeAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info,
@NonNull SurfaceControl.Transaction t, @NonNull IBinder mergeTarget,
@NonNull Transitions.TransitionFinishCallback finishCallback) {
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "Merging registered One-shot remote"
+ + " transition %s for (#%d).", mRemote, info.getDebugId());
IRemoteTransitionFinishedCallback cb = new IRemoteTransitionFinishedCallback.Stub() {
@Override
public void onTransitionFinished(WindowContainerTransaction wct,
SurfaceControl.Transaction sct) {
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS,
+ "Finished merging one-shot remote transition %s for (#%d).", mRemote,
+ info.getDebugId());
// We have merged, since we sent the transaction over binder, the one in this
// process won't be cleared if the remote applied it. We don't actually know if the
// remote applied the transaction, but applying twice will break surfaceflinger
// so just assume the worst-case and clear the local transaction.
t.clear();
- mMainExecutor.execute(() -> finishCallback.onTransitionFinished(wct));
- mRemote = null;
+ mMainExecutor.execute(() -> {
+ finishCallback.onTransitionFinished(wct);
+ mRemote = null;
+ });
}
};
try {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/tracing/OWNERS b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/tracing/OWNERS
new file mode 100644
index 0000000..2ff4d90
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/tracing/OWNERS
@@ -0,0 +1,4 @@
+# WM shell transition tracing owners
+# Bug component: 1157642
+natanieljr@google.com
+pablogamito@google.com
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/tracing/PerfettoTransitionTracer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/tracing/PerfettoTransitionTracer.java
index 99df6a3..fa331af 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/tracing/PerfettoTransitionTracer.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/tracing/PerfettoTransitionTracer.java
@@ -18,16 +18,16 @@
import android.internal.perfetto.protos.PerfettoTrace;
import android.os.SystemClock;
-import android.tracing.perfetto.DataSourceInstance;
+import android.os.Trace;
import android.tracing.perfetto.DataSourceParams;
import android.tracing.perfetto.InitArguments;
import android.tracing.perfetto.Producer;
-import android.tracing.perfetto.TracingContext;
import android.tracing.transition.TransitionDataSource;
import android.util.proto.ProtoOutputStream;
import com.android.wm.shell.transition.Transitions;
+import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
@@ -40,6 +40,7 @@
mActiveTraces::incrementAndGet,
this::onFlush,
mActiveTraces::decrementAndGet);
+ private final Map<String, Integer> mHandlerMapping = new HashMap<>();
public PerfettoTransitionTracer() {
Producer.init(InitArguments.DEFAULTS);
@@ -58,8 +59,17 @@
return;
}
+ Trace.traceBegin(Trace.TRACE_TAG_WINDOW_MANAGER, "logDispatched");
+ try {
+ doLogDispatched(transitionId, handler);
+ } finally {
+ Trace.traceEnd(Trace.TRACE_TAG_WINDOW_MANAGER);
+ }
+ }
+
+ private void doLogDispatched(int transitionId, Transitions.TransitionHandler handler) {
mDataSource.trace(ctx -> {
- final int handlerId = getHandlerId(handler, ctx);
+ final int handlerId = getHandlerId(handler);
final ProtoOutputStream os = ctx.newTracePacket();
final long token = os.start(PerfettoTrace.TracePacket.SHELL_TRANSITION);
@@ -71,17 +81,16 @@
});
}
- private static int getHandlerId(Transitions.TransitionHandler handler,
- TracingContext<DataSourceInstance, TransitionDataSource.TlsState, Void> ctx) {
- final Map<String, Integer> handlerMapping =
- ctx.getCustomTlsState().handlerMapping;
+ private int getHandlerId(Transitions.TransitionHandler handler) {
final int handlerId;
- if (handlerMapping.containsKey(handler.getClass().getName())) {
- handlerId = handlerMapping.get(handler.getClass().getName());
- } else {
- // + 1 to avoid 0 ids which can be confused with missing value when dumped to proto
- handlerId = handlerMapping.size() + 1;
- handlerMapping.put(handler.getClass().getName(), handlerId);
+ synchronized (mHandlerMapping) {
+ if (mHandlerMapping.containsKey(handler.getClass().getName())) {
+ handlerId = mHandlerMapping.get(handler.getClass().getName());
+ } else {
+ // + 1 to avoid 0 ids which can be confused with missing value when dumped to proto
+ handlerId = mHandlerMapping.size() + 1;
+ mHandlerMapping.put(handler.getClass().getName(), handlerId);
+ }
}
return handlerId;
}
@@ -97,6 +106,15 @@
return;
}
+ Trace.traceBegin(Trace.TRACE_TAG_WINDOW_MANAGER, "logMergeRequested");
+ try {
+ doLogMergeRequested(mergeRequestedTransitionId, playingTransitionId);
+ } finally {
+ Trace.traceEnd(Trace.TRACE_TAG_WINDOW_MANAGER);
+ }
+ }
+
+ private void doLogMergeRequested(int mergeRequestedTransitionId, int playingTransitionId) {
mDataSource.trace(ctx -> {
final ProtoOutputStream os = ctx.newTracePacket();
final long token = os.start(PerfettoTrace.TracePacket.SHELL_TRANSITION);
@@ -120,10 +138,19 @@
return;
}
+ Trace.traceBegin(Trace.TRACE_TAG_WINDOW_MANAGER, "logMerged");
+ try {
+ doLogMerged(mergedTransitionId, playingTransitionId);
+ } finally {
+ Trace.traceEnd(Trace.TRACE_TAG_WINDOW_MANAGER);
+ }
+ }
+
+ private void doLogMerged(int mergeRequestedTransitionId, int playingTransitionId) {
mDataSource.trace(ctx -> {
final ProtoOutputStream os = ctx.newTracePacket();
final long token = os.start(PerfettoTrace.TracePacket.SHELL_TRANSITION);
- os.write(PerfettoTrace.ShellTransition.ID, mergedTransitionId);
+ os.write(PerfettoTrace.ShellTransition.ID, mergeRequestedTransitionId);
os.write(PerfettoTrace.ShellTransition.MERGE_TIME_NS,
SystemClock.elapsedRealtimeNanos());
os.write(PerfettoTrace.ShellTransition.MERGE_TARGET, playingTransitionId);
@@ -142,6 +169,15 @@
return;
}
+ Trace.traceBegin(Trace.TRACE_TAG_WINDOW_MANAGER, "logAborted");
+ try {
+ doLogAborted(transitionId);
+ } finally {
+ Trace.traceEnd(Trace.TRACE_TAG_WINDOW_MANAGER);
+ }
+ }
+
+ private void doLogAborted(int transitionId) {
mDataSource.trace(ctx -> {
final ProtoOutputStream os = ctx.newTracePacket();
final long token = os.start(PerfettoTrace.TracePacket.SHELL_TRANSITION);
@@ -160,13 +196,18 @@
mDataSource.trace(ctx -> {
final ProtoOutputStream os = ctx.newTracePacket();
- final Map<String, Integer> handlerMapping = ctx.getCustomTlsState().handlerMapping;
- for (String handler : handlerMapping.keySet()) {
- final long token = os.start(PerfettoTrace.TracePacket.SHELL_HANDLER_MAPPINGS);
- os.write(PerfettoTrace.ShellHandlerMapping.ID, handlerMapping.get(handler));
+ final long mappingsToken = os.start(PerfettoTrace.TracePacket.SHELL_HANDLER_MAPPINGS);
+ for (Map.Entry<String, Integer> entry : mHandlerMapping.entrySet()) {
+ final String handler = entry.getKey();
+ final int handlerId = entry.getValue();
+
+ final long mappingEntryToken = os.start(PerfettoTrace.ShellHandlerMappings.MAPPING);
+ os.write(PerfettoTrace.ShellHandlerMapping.ID, handlerId);
os.write(PerfettoTrace.ShellHandlerMapping.NAME, handler);
- os.end(token);
+ os.end(mappingEntryToken);
+
}
+ os.end(mappingsToken);
ctx.flush();
});
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
index a8b39c41..891eea0 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
@@ -888,7 +888,10 @@
private DesktopModeWindowDecoration getFocusedDecor() {
final int size = mWindowDecorByTaskId.size();
DesktopModeWindowDecoration focusedDecor = null;
- for (int i = 0; i < size; i++) {
+ // TODO(b/323251951): We need to iterate this in reverse to avoid potentially getting
+ // a decor for a closed task. This is a short term fix while the core issue is addressed,
+ // which involves refactoring the window decor lifecycle to be visibility based.
+ for (int i = size - 1; i >= 0; i--) {
final DesktopModeWindowDecoration decor = mWindowDecorByTaskId.valueAt(i);
if (decor != null && decor.isFocused()) {
focusedDecor = decor;
diff --git a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/EnterPipToOtherOrientation.kt b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/EnterPipToOtherOrientation.kt
index 8207b85..07cd682 100644
--- a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/EnterPipToOtherOrientation.kt
+++ b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/EnterPipToOtherOrientation.kt
@@ -86,6 +86,8 @@
.withNavOrTaskBarVisible()
.withStatusBarVisible()
.waitForAndVerify()
+
+ pipApp.tapPipToShowMenu(wmHelper)
}
}
@@ -194,6 +196,16 @@
}
}
+ @Postsubmit
+ @Test
+ fun menuOverlayMatchesTaskSurface() {
+ flicker.assertLayersEnd {
+ val pipAppRegion = visibleRegion(pipApp)
+ val pipMenuRegion = visibleRegion(ComponentNameMatcher.PIP_MENU_OVERLAY)
+ pipAppRegion.coversExactly(pipMenuRegion.region)
+ }
+ }
+
/** {@inheritDoc} */
@FlakyTest(bugId = 267424412)
@Test
diff --git a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/apps/MapsEnterPipTest.kt b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/apps/MapsEnterPipTest.kt
index e272958..4c2eff3 100644
--- a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/apps/MapsEnterPipTest.kt
+++ b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/apps/MapsEnterPipTest.kt
@@ -137,6 +137,16 @@
override val thisTransition: FlickerBuilder.() -> Unit = { transitions { tapl.goHome() } }
+ /** Checks [standardAppHelper] layer remains visible throughout the animation */
+ @Postsubmit
+ @Test
+ override fun pipAppLayerAlwaysVisible() {
+ // For Maps the transition goes through the UI mode change that adds a snapshot overlay so
+ // we assert only start/end layers matching the app instead.
+ flicker.assertLayersStart { this.isVisible(standardAppHelper.packageNameMatcher) }
+ flicker.assertLayersEnd { this.isVisible(standardAppHelper.packageNameMatcher) }
+ }
+
@Postsubmit
@Test
override fun focusChanges() {
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleViewInfoTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleViewInfoTest.kt
index 75965d6..1668e37 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleViewInfoTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleViewInfoTest.kt
@@ -143,7 +143,7 @@
bubbleController,
mainExecutor
)
- bubbleBarLayerView = BubbleBarLayerView(context, bubbleController)
+ bubbleBarLayerView = BubbleBarLayerView(context, bubbleController, bubbleData)
}
@Test
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipResizeGestureHandlerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipResizeGestureHandlerTest.java
index 9719ba8..cc726cb 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipResizeGestureHandlerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipResizeGestureHandlerTest.java
@@ -121,7 +121,7 @@
mPipResizeGestureHandler = new PipResizeGestureHandler(mContext, pipBoundsAlgorithm,
mPipBoundsState, motionHelper, mPipTouchState, mPipTaskOrganizer,
mPipDismissTargetHandler,
- (Rect bounds) -> new Rect(), () -> {}, mPipUiEventLogger, mPhonePipMenuController,
+ () -> {}, mPipUiEventLogger, mPhonePipMenuController,
mMainExecutor) {
@Override
public void pilferPointers() {
diff --git a/libs/hwui/FeatureFlags.h b/libs/hwui/FeatureFlags.h
index 6ebfc63..ac75c07 100644
--- a/libs/hwui/FeatureFlags.h
+++ b/libs/hwui/FeatureFlags.h
@@ -41,9 +41,9 @@
#endif // __ANDROID__
}
-inline bool inter_character_justification() {
+inline bool letter_spacing_justification() {
#ifdef __ANDROID__
- return com_android_text_flags_inter_character_justification();
+ return com_android_text_flags_letter_spacing_justification();
#else
return true;
#endif // __ANDROID__
diff --git a/libs/hwui/aconfig/hwui_flags.aconfig b/libs/hwui/aconfig/hwui_flags.aconfig
index 72ddecc..3d7e559 100644
--- a/libs/hwui/aconfig/hwui_flags.aconfig
+++ b/libs/hwui/aconfig/hwui_flags.aconfig
@@ -1,6 +1,13 @@
package: "com.android.graphics.hwui.flags"
flag {
+ name: "clip_shader"
+ namespace: "core_graphics"
+ description: "API for canvas shader clipping operations"
+ bug: "280116960"
+}
+
+flag {
name: "matrix_44"
namespace: "core_graphics"
description: "API for 4x4 matrix and related canvas functions"
diff --git a/libs/hwui/hwui/MinikinUtils.cpp b/libs/hwui/hwui/MinikinUtils.cpp
index 5613369..d66d7f8 100644
--- a/libs/hwui/hwui/MinikinUtils.cpp
+++ b/libs/hwui/hwui/MinikinUtils.cpp
@@ -72,7 +72,7 @@
const minikin::Range contextRange(contextStart, contextStart + contextCount);
const minikin::StartHyphenEdit startHyphen = paint->getStartHyphenEdit();
const minikin::EndHyphenEdit endHyphen = paint->getEndHyphenEdit();
- const minikin::RunFlag minikinRunFlag = text_feature::inter_character_justification()
+ const minikin::RunFlag minikinRunFlag = text_feature::letter_spacing_justification()
? paint->getRunFlag()
: minikin::RunFlag::NONE;
@@ -105,7 +105,7 @@
const minikin::Range range(start, start + count);
const minikin::StartHyphenEdit startHyphen = paint->getStartHyphenEdit();
const minikin::EndHyphenEdit endHyphen = paint->getEndHyphenEdit();
- const minikin::RunFlag minikinRunFlag = text_feature::inter_character_justification()
+ const minikin::RunFlag minikinRunFlag = text_feature::letter_spacing_justification()
? paint->getRunFlag()
: minikin::RunFlag::NONE;
diff --git a/libs/hwui/jni/android_graphics_Canvas.cpp b/libs/hwui/jni/android_graphics_Canvas.cpp
index 295f4dc..e5bdeee 100644
--- a/libs/hwui/jni/android_graphics_Canvas.cpp
+++ b/libs/hwui/jni/android_graphics_Canvas.cpp
@@ -763,41 +763,41 @@
}; // namespace CanvasJNI
static const JNINativeMethod gMethods[] = {
- {"nGetNativeFinalizer", "()J", (void*) CanvasJNI::getNativeFinalizer},
- {"nFreeCaches", "()V", (void*) CanvasJNI::freeCaches},
- {"nFreeTextLayoutCaches", "()V", (void*) CanvasJNI::freeTextLayoutCaches},
- {"nSetCompatibilityVersion", "(I)V", (void*) CanvasJNI::setCompatibilityVersion},
+ {"nGetNativeFinalizer", "()J", (void*)CanvasJNI::getNativeFinalizer},
+ {"nFreeCaches", "()V", (void*)CanvasJNI::freeCaches},
+ {"nFreeTextLayoutCaches", "()V", (void*)CanvasJNI::freeTextLayoutCaches},
+ {"nSetCompatibilityVersion", "(I)V", (void*)CanvasJNI::setCompatibilityVersion},
- // ------------ @FastNative ----------------
- {"nInitRaster", "(J)J", (void*) CanvasJNI::initRaster},
- {"nSetBitmap", "(JJ)V", (void*) CanvasJNI::setBitmap},
- {"nGetClipBounds","(JLandroid/graphics/Rect;)Z", (void*) CanvasJNI::getClipBounds},
+ // ------------ @FastNative ----------------
+ {"nInitRaster", "(J)J", (void*)CanvasJNI::initRaster},
+ {"nSetBitmap", "(JJ)V", (void*)CanvasJNI::setBitmap},
+ {"nGetClipBounds", "(JLandroid/graphics/Rect;)Z", (void*)CanvasJNI::getClipBounds},
- // ------------ @CriticalNative ----------------
- {"nIsOpaque","(J)Z", (void*) CanvasJNI::isOpaque},
- {"nGetWidth","(J)I", (void*) CanvasJNI::getWidth},
- {"nGetHeight","(J)I", (void*) CanvasJNI::getHeight},
- {"nSave","(JI)I", (void*) CanvasJNI::save},
- {"nSaveLayer","(JFFFFJ)I", (void*) CanvasJNI::saveLayer},
- {"nSaveLayerAlpha","(JFFFFI)I", (void*) CanvasJNI::saveLayerAlpha},
- {"nSaveUnclippedLayer","(JIIII)I", (void*) CanvasJNI::saveUnclippedLayer},
- {"nRestoreUnclippedLayer","(JIJ)V", (void*) CanvasJNI::restoreUnclippedLayer},
- {"nGetSaveCount","(J)I", (void*) CanvasJNI::getSaveCount},
- {"nRestore","(J)Z", (void*) CanvasJNI::restore},
- {"nRestoreToCount","(JI)V", (void*) CanvasJNI::restoreToCount},
- {"nGetMatrix", "(JJ)V", (void*)CanvasJNI::getMatrix},
- {"nSetMatrix","(JJ)V", (void*) CanvasJNI::setMatrix},
- {"nConcat","(JJ)V", (void*) CanvasJNI::concat},
- {"nConcat","(J[F)V", (void*) CanvasJNI::concat44},
- {"nRotate","(JF)V", (void*) CanvasJNI::rotate},
- {"nScale","(JFF)V", (void*) CanvasJNI::scale},
- {"nSkew","(JFF)V", (void*) CanvasJNI::skew},
- {"nTranslate","(JFF)V", (void*) CanvasJNI::translate},
- {"nQuickReject","(JJ)Z", (void*) CanvasJNI::quickRejectPath},
- {"nQuickReject","(JFFFF)Z", (void*)CanvasJNI::quickRejectRect},
- {"nClipRect","(JFFFFI)Z", (void*) CanvasJNI::clipRect},
- {"nClipPath","(JJI)Z", (void*) CanvasJNI::clipPath},
- {"nSetDrawFilter", "(JJ)V", (void*) CanvasJNI::setPaintFilter},
+ // ------------ @CriticalNative ----------------
+ {"nIsOpaque", "(J)Z", (void*)CanvasJNI::isOpaque},
+ {"nGetWidth", "(J)I", (void*)CanvasJNI::getWidth},
+ {"nGetHeight", "(J)I", (void*)CanvasJNI::getHeight},
+ {"nSave", "(JI)I", (void*)CanvasJNI::save},
+ {"nSaveLayer", "(JFFFFJ)I", (void*)CanvasJNI::saveLayer},
+ {"nSaveLayerAlpha", "(JFFFFI)I", (void*)CanvasJNI::saveLayerAlpha},
+ {"nSaveUnclippedLayer", "(JIIII)I", (void*)CanvasJNI::saveUnclippedLayer},
+ {"nRestoreUnclippedLayer", "(JIJ)V", (void*)CanvasJNI::restoreUnclippedLayer},
+ {"nGetSaveCount", "(J)I", (void*)CanvasJNI::getSaveCount},
+ {"nRestore", "(J)Z", (void*)CanvasJNI::restore},
+ {"nRestoreToCount", "(JI)V", (void*)CanvasJNI::restoreToCount},
+ {"nGetMatrix", "(JJ)V", (void*)CanvasJNI::getMatrix},
+ {"nSetMatrix", "(JJ)V", (void*)CanvasJNI::setMatrix},
+ {"nConcat", "(JJ)V", (void*)CanvasJNI::concat},
+ {"nConcat", "(J[F)V", (void*)CanvasJNI::concat44},
+ {"nRotate", "(JF)V", (void*)CanvasJNI::rotate},
+ {"nScale", "(JFF)V", (void*)CanvasJNI::scale},
+ {"nSkew", "(JFF)V", (void*)CanvasJNI::skew},
+ {"nTranslate", "(JFF)V", (void*)CanvasJNI::translate},
+ {"nQuickReject", "(JJ)Z", (void*)CanvasJNI::quickRejectPath},
+ {"nQuickReject", "(JFFFF)Z", (void*)CanvasJNI::quickRejectRect},
+ {"nClipRect", "(JFFFFI)Z", (void*)CanvasJNI::clipRect},
+ {"nClipPath", "(JJI)Z", (void*)CanvasJNI::clipPath},
+ {"nSetDrawFilter", "(JJ)V", (void*)CanvasJNI::setPaintFilter},
};
// If called from Canvas these are regular JNI
diff --git a/location/java/android/location/GnssStatus.java b/location/java/android/location/GnssStatus.java
index 09f40e8..970bfda 100644
--- a/location/java/android/location/GnssStatus.java
+++ b/location/java/android/location/GnssStatus.java
@@ -195,7 +195,7 @@
* <li>SBAS: 120-151, 183-192</li>
* <li>GLONASS: One of: OSN, or FCN+100
* <ul>
- * <li>1-24 as the orbital slot number (OSN) (preferred, if known)</li>
+ * <li>1-25 as the orbital slot number (OSN) (preferred, if known)</li>
* <li>93-106 as the frequency channel number (FCN) (-7 to +6) plus 100.
* i.e. encode FCN of -7 as 93, 0 as 100, and +6 as 106</li>
* </ul></li>
diff --git a/location/java/com/android/internal/location/altitude/GeoidMap.java b/location/java/com/android/internal/location/altitude/GeoidMap.java
index 9bf5689..df9ca97 100644
--- a/location/java/com/android/internal/location/altitude/GeoidMap.java
+++ b/location/java/com/android/internal/location/altitude/GeoidMap.java
@@ -51,6 +51,10 @@
*/
public final class GeoidMap {
+ private static final String GEOID_HEIGHT_PREFIX = "geoid-height";
+
+ private static final String EXPIRATION_DISTANCE_PREFIX = "expiration-distance";
+
private static final Object GEOID_HEIGHT_PARAMS_LOCK = new Object();
private static final Object EXPIRATION_DISTANCE_PARAMS_LOCK = new Object();
@@ -82,8 +86,7 @@
public static MapParamsProto getGeoidHeightParams(@NonNull Context context) throws IOException {
synchronized (GEOID_HEIGHT_PARAMS_LOCK) {
if (sGeoidHeightParams == null) {
- // TODO: b/304375846 - Configure with disk tile prefix once resources are updated.
- sGeoidHeightParams = parseParams(context);
+ sGeoidHeightParams = parseParams(context, GEOID_HEIGHT_PREFIX);
}
return sGeoidHeightParams;
}
@@ -99,17 +102,17 @@
throws IOException {
synchronized (EXPIRATION_DISTANCE_PARAMS_LOCK) {
if (sExpirationDistanceParams == null) {
- // TODO: b/304375846 - Configure with disk tile prefix once resources are updated.
- sExpirationDistanceParams = parseParams(context);
+ sExpirationDistanceParams = parseParams(context, EXPIRATION_DISTANCE_PREFIX);
}
return sExpirationDistanceParams;
}
}
@NonNull
- private static MapParamsProto parseParams(@NonNull Context context) throws IOException {
+ private static MapParamsProto parseParams(@NonNull Context context, @NonNull String prefix)
+ throws IOException {
try (InputStream is = context.getApplicationContext().getAssets().open(
- "geoid_height_map/map-params.pb")) {
+ "geoid_map/" + prefix + "-params.pb")) {
return MapParamsProto.parseFrom(is.readAllBytes());
}
}
@@ -267,7 +270,8 @@
@NonNull
public double[] readGeoidHeights(@NonNull MapParamsProto params, @NonNull Context context,
@NonNull long[] s2CellIds) throws IOException {
- return readMapValues(params, context, s2CellIds, mGeoidHeightCacheTiles);
+ return readMapValues(params, context, s2CellIds, mGeoidHeightCacheTiles,
+ GEOID_HEIGHT_PREFIX);
}
/**
@@ -278,7 +282,8 @@
@NonNull
public double[] readExpirationDistances(@NonNull MapParamsProto params,
@NonNull Context context, @NonNull long[] s2CellIds) throws IOException {
- return readMapValues(params, context, s2CellIds, mExpirationDistanceCacheTiles);
+ return readMapValues(params, context, s2CellIds, mExpirationDistanceCacheTiles,
+ EXPIRATION_DISTANCE_PREFIX);
}
/**
@@ -288,15 +293,16 @@
*/
@NonNull
private static double[] readMapValues(@NonNull MapParamsProto params, @NonNull Context context,
- @NonNull long[] s2CellIds, @NonNull LruCache<Long, S2TileProto> cacheTiles)
- throws IOException {
+ @NonNull long[] s2CellIds, @NonNull LruCache<Long, S2TileProto> cacheTiles,
+ @NonNull String prefix) throws IOException {
validate(params, s2CellIds);
double[] mapValuesMeters = new double[s2CellIds.length];
if (getMapValues(params, cacheTiles::get, s2CellIds, mapValuesMeters)) {
return mapValuesMeters;
}
- TileFunction loadedTiles = loadFromCacheAndDisk(params, context, s2CellIds, cacheTiles);
+ TileFunction loadedTiles = loadFromCacheAndDisk(params, context, s2CellIds, cacheTiles,
+ prefix);
if (getMapValues(params, loadedTiles, s2CellIds, mapValuesMeters)) {
return mapValuesMeters;
}
@@ -338,7 +344,8 @@
@NonNull
private static TileFunction loadFromCacheAndDisk(@NonNull MapParamsProto params,
@NonNull Context context, @NonNull long[] s2CellIds,
- @NonNull LruCache<Long, S2TileProto> cacheTiles) throws IOException {
+ @NonNull LruCache<Long, S2TileProto> cacheTiles, @NonNull String prefix)
+ throws IOException {
int len = s2CellIds.length;
// Enable batch loading by finding all cache keys upfront.
@@ -374,7 +381,7 @@
S2TileProto tile;
try (InputStream is = context.getApplicationContext().getAssets().open(
- "geoid_height_map/tile-" + diskTokens[i] + ".pb")) {
+ "geoid_map/" + prefix + "-disk-tile-" + diskTokens[i] + ".pb")) {
tile = S2TileProto.parseFrom(is.readAllBytes());
}
mergeFromDiskTile(params, tile, cacheKeys, diskTokens, i, loadedTiles, cacheTiles);
diff --git a/media/TEST_MAPPING b/media/TEST_MAPPING
index 4fbe9ee..6ac9695 100644
--- a/media/TEST_MAPPING
+++ b/media/TEST_MAPPING
@@ -40,17 +40,6 @@
},
{
"file_patterns": [
- "[^/]*(Ringtone)[^/]*\\.java"
- ],
- "name": "MediaRingtoneTests",
- "options": [
- {"exclude-annotation": "androidx.test.filters.LargeTest"},
- {"exclude-annotation": "androidx.test.filters.FlakyTest"},
- {"exclude-annotation": "org.junit.Ignore"}
- ]
- },
- {
- "file_patterns": [
"[^/]*(LoudnessCodec)[^/]*\\.java"
],
"name": "LoudnessCodecApiTest",
diff --git a/media/java/android/media/AudioManager.java b/media/java/android/media/AudioManager.java
index b6f1004..8f3f82e 100644
--- a/media/java/android/media/AudioManager.java
+++ b/media/java/android/media/AudioManager.java
@@ -5166,13 +5166,12 @@
* dispatch was successfully sent, or {@link #AUDIOFOCUS_REQUEST_DELAYED} if
* the request was successful but the dispatch of focus change was delayed due to a fade
* operation.
- * @throws NullPointerException if the {@link AudioFocusInfo} or {@link AudioPolicy} or list of
- * other active {@link AudioFocusInfo} are {@code null}.
* @hide
*/
@FlaggedApi(FLAG_ENABLE_FADE_MANAGER_CONFIGURATION)
@SystemApi
@RequiresPermission(Manifest.permission.MODIFY_AUDIO_SETTINGS_PRIVILEGED)
+ @FocusRequestResult
public int dispatchAudioFocusChangeWithFade(@NonNull AudioFocusInfo afi, int focusChange,
@NonNull AudioPolicy ap, @NonNull List<AudioFocusInfo> otherActiveAfis,
@Nullable FadeManagerConfiguration transientFadeMgrConfig) {
diff --git a/media/java/android/media/BluetoothProfileConnectionInfo.java b/media/java/android/media/BluetoothProfileConnectionInfo.java
index e4dc152..0613fc6 100644
--- a/media/java/android/media/BluetoothProfileConnectionInfo.java
+++ b/media/java/android/media/BluetoothProfileConnectionInfo.java
@@ -15,6 +15,9 @@
*/
package android.media;
+import static android.media.audio.Flags.FLAG_SCO_MANAGED_BY_AUDIO;
+
+import android.annotation.FlaggedApi;
import android.annotation.NonNull;
import android.annotation.SystemApi;
import android.bluetooth.BluetoothProfile;
@@ -174,4 +177,13 @@
public boolean isLeOutput() {
return mIsLeOutput;
}
+
+ /**
+ * Factory method for <code>BluetoothProfileConnectionInfo</code> for an HFP device.
+ */
+ @FlaggedApi(FLAG_SCO_MANAGED_BY_AUDIO)
+ public static @NonNull BluetoothProfileConnectionInfo createHfpInfo() {
+ return new BluetoothProfileConnectionInfo(BluetoothProfile.HEADSET, false,
+ -1, false);
+ }
}
diff --git a/media/java/android/media/FadeManagerConfiguration.java b/media/java/android/media/FadeManagerConfiguration.java
index 40b0e3e..4f1a8ee 100644
--- a/media/java/android/media/FadeManagerConfiguration.java
+++ b/media/java/android/media/FadeManagerConfiguration.java
@@ -18,6 +18,7 @@
import static android.media.audiopolicy.Flags.FLAG_ENABLE_FADE_MANAGER_CONFIGURATION;
+import android.annotation.DurationMillisLong;
import android.annotation.FlaggedApi;
import android.annotation.IntDef;
import android.annotation.NonNull;
@@ -112,17 +113,11 @@
*/
public static final int FADE_STATE_ENABLED_DEFAULT = 1;
- /**
- * Defines the enabled state with Automotive specific configurations
- */
- public static final int FADE_STATE_ENABLED_AUTO = 2;
-
/** @hide */
@Retention(RetentionPolicy.SOURCE)
@IntDef(flag = false, prefix = "FADE_STATE", value = {
FADE_STATE_DISABLED,
FADE_STATE_ENABLED_DEFAULT,
- FADE_STATE_ENABLED_AUTO,
})
public @interface FadeStateEnum {}
@@ -143,7 +138,14 @@
* @see #getFadeOutDurationForAudioAttributes(AudioAttributes)
* @see #getFadeInDurationForAudioAttributes(AudioAttributes)
*/
- public static final long DURATION_NOT_SET = 0;
+ public static final @DurationMillisLong long DURATION_NOT_SET = 0;
+
+ /** Defines the default fade out duration */
+ private static final @DurationMillisLong long DEFAULT_FADE_OUT_DURATION_MS = 2_000;
+
+ /** Defines the default fade in duration */
+ private static final @DurationMillisLong long DEFAULT_FADE_IN_DURATION_MS = 1_000;
+
/** Map of Usage to Fade volume shaper configs wrapper */
private final SparseArray<FadeVolumeShaperConfigsWrapper> mUsageToFadeWrapperMap;
/** Map of AudioAttributes to Fade volume shaper configs wrapper */
@@ -161,14 +163,15 @@
/** fade state */
private final @FadeStateEnum int mFadeState;
/** fade out duration from builder - used for creating default fade out volume shaper */
- private final long mFadeOutDurationMillis;
+ private final @DurationMillisLong long mFadeOutDurationMillis;
/** fade in duration from builder - used for creating default fade in volume shaper */
- private final long mFadeInDurationMillis;
+ private final @DurationMillisLong long mFadeInDurationMillis;
/** delay after which the offending players are faded back in */
- private final long mFadeInDelayForOffendersMillis;
+ private final @DurationMillisLong long mFadeInDelayForOffendersMillis;
- private FadeManagerConfiguration(int fadeState, long fadeOutDurationMillis,
- long fadeInDurationMillis, long offendersFadeInDelayMillis,
+ private FadeManagerConfiguration(int fadeState, @DurationMillisLong long fadeOutDurationMillis,
+ @DurationMillisLong long fadeInDurationMillis,
+ @DurationMillisLong long offendersFadeInDelayMillis,
@NonNull SparseArray<FadeVolumeShaperConfigsWrapper> usageToFadeWrapperMap,
@NonNull ArrayMap<AudioAttributes, FadeVolumeShaperConfigsWrapper> attrToFadeWrapperMap,
@NonNull IntArray fadeableUsages, @NonNull IntArray unfadeableContentTypes,
@@ -196,8 +199,6 @@
/**
* Get the fade state
- *
- * @return one of the {@link FadeStateEnum} state
*/
@FadeStateEnum
public int getFadeState() {
@@ -207,7 +208,7 @@
/**
* Get the list of usages that can be faded
*
- * @return list of {@link android.media.AudioAttributes.AttributeUsage} that shall be faded
+ * @return list of {@link android.media.AudioAttributes usages} that shall be faded
* @throws IllegalStateException if the fade state is set to {@link #FADE_STATE_DISABLED}
*/
@NonNull
@@ -217,10 +218,10 @@
}
/**
- * Get the list of {@link android.media.AudioPlaybackConfiguration.PlayerType player types}
- * that cannot be faded
+ * Get the list of {@link android.media.AudioPlaybackConfiguration player types} that can be
+ * faded
*
- * @return list of {@link android.media.AudioPlaybackConfiguration.PlayerType}
+ * @return list of {@link android.media.AudioPlaybackConfiguration player types}
* @throws IllegalStateException if the fade state is set to {@link #FADE_STATE_DISABLED}
*/
@NonNull
@@ -230,10 +231,9 @@
}
/**
- * Get the list of {@link android.media.AudioAttributes.AttributeContentType content types}
- * that cannot be faded
+ * Get the list of {@link android.media.AudioAttributes content types} that can be faded
*
- * @return list of {@link android.media.AudioAttributes.AttributeContentType}
+ * @return list of {@link android.media.AudioAttributes content types}
* @throws IllegalStateExceptionif if the fade state is set to {@link #FADE_STATE_DISABLED}
*/
@NonNull
@@ -267,15 +267,15 @@
}
/**
- * Get the duration used to fade out players with
- * {@link android.media.AudioAttributes.AttributeUsage}
+ * Get the duration used to fade out players with {@link android.media.AudioAttributes usage}
*
- * @param usage the {@link android.media.AudioAttributes.AttributeUsage}
+ * @param usage the {@link android.media.AudioAttributes usage}
* @return duration in milliseconds if set for the usage or {@link #DURATION_NOT_SET} otherwise
* @throws IllegalArgumentException if the usage is invalid
* @throws IllegalStateException if the fade state is set to {@link #FADE_STATE_DISABLED}
*/
- public long getFadeOutDurationForUsage(int usage) {
+ @DurationMillisLong
+ public long getFadeOutDurationForUsage(@AudioAttributes.AttributeUsage int usage) {
ensureFadingIsEnabled();
validateUsage(usage);
return getDurationForVolumeShaperConfig(getVolumeShaperConfigFromWrapper(
@@ -283,15 +283,15 @@
}
/**
- * Get the duration used to fade in players with
- * {@link android.media.AudioAttributes.AttributeUsage}
+ * Get the duration used to fade in players with {@link android.media.AudioAttributes usage}
*
- * @param usage the {@link android.media.AudioAttributes.AttributeUsage}
+ * @param usage the {@link android.media.AudioAttributes usage}
* @return duration in milliseconds if set for the usage or {@link #DURATION_NOT_SET} otherwise
* @throws IllegalArgumentException if the usage is invalid
* @throws IllegalStateException if the fade state is set to {@link #FADE_STATE_DISABLED}
*/
- public long getFadeInDurationForUsage(int usage) {
+ @DurationMillisLong
+ public long getFadeInDurationForUsage(@AudioAttributes.AttributeUsage int usage) {
ensureFadingIsEnabled();
validateUsage(usage);
return getDurationForVolumeShaperConfig(getVolumeShaperConfigFromWrapper(
@@ -300,16 +300,17 @@
/**
* Get the {@link android.media.VolumeShaper.Configuration} used to fade out players with
- * {@link android.media.AudioAttributes.AttributeUsage}
+ * {@link android.media.AudioAttributes usage}
*
- * @param usage the {@link android.media.AudioAttributes.AttributeUsage}
+ * @param usage the {@link android.media.AudioAttributes usage}
* @return {@link android.media.VolumeShaper.Configuration} if set for the usage or
- * {@code null} otherwise
+ * {@code null} otherwise
* @throws IllegalArgumentException if the usage is invalid
* @throws IllegalStateException if the fade state is set to {@link #FADE_STATE_DISABLED}
*/
@Nullable
- public VolumeShaper.Configuration getFadeOutVolumeShaperConfigForUsage(int usage) {
+ public VolumeShaper.Configuration getFadeOutVolumeShaperConfigForUsage(
+ @AudioAttributes.AttributeUsage int usage) {
ensureFadingIsEnabled();
validateUsage(usage);
return getVolumeShaperConfigFromWrapper(mUsageToFadeWrapperMap.get(usage),
@@ -318,16 +319,17 @@
/**
* Get the {@link android.media.VolumeShaper.Configuration} used to fade in players with
- * {@link android.media.AudioAttributes.AttributeUsage}
+ * {@link android.media.AudioAttributes usage}
*
- * @param usage the {@link android.media.AudioAttributes.AttributeUsage} of player
+ * @param usage the {@link android.media.AudioAttributes usage}
* @return {@link android.media.VolumeShaper.Configuration} if set for the usage or
- * {@code null} otherwise
+ * {@code null} otherwise
* @throws IllegalArgumentException if the usage is invalid
* @throws IllegalStateException if the fade state is set to {@link #FADE_STATE_DISABLED}
*/
@Nullable
- public VolumeShaper.Configuration getFadeInVolumeShaperConfigForUsage(int usage) {
+ public VolumeShaper.Configuration getFadeInVolumeShaperConfigForUsage(
+ @AudioAttributes.AttributeUsage int usage) {
ensureFadingIsEnabled();
validateUsage(usage);
return getVolumeShaperConfigFromWrapper(mUsageToFadeWrapperMap.get(usage),
@@ -339,10 +341,11 @@
*
* @param audioAttributes {@link android.media.AudioAttributes}
* @return duration in milliseconds if set for the audio attributes or
- * {@link #DURATION_NOT_SET} otherwise
+ * {@link #DURATION_NOT_SET} otherwise
* @throws NullPointerException if the audio attributes is {@code null}
* @throws IllegalStateException if the fade state is set to {@link #FADE_STATE_DISABLED}
*/
+ @DurationMillisLong
public long getFadeOutDurationForAudioAttributes(@NonNull AudioAttributes audioAttributes) {
ensureFadingIsEnabled();
return getDurationForVolumeShaperConfig(getVolumeShaperConfigFromWrapper(
@@ -354,10 +357,11 @@
*
* @param audioAttributes {@link android.media.AudioAttributes}
* @return duration in milliseconds if set for the audio attributes or
- * {@link #DURATION_NOT_SET} otherwise
+ * {@link #DURATION_NOT_SET} otherwise
* @throws NullPointerException if the audio attributes is {@code null}
* @throws IllegalStateException if the fade state is set to {@link #FADE_STATE_DISABLED}
*/
+ @DurationMillisLong
public long getFadeInDurationForAudioAttributes(@NonNull AudioAttributes audioAttributes) {
ensureFadingIsEnabled();
return getDurationForVolumeShaperConfig(getVolumeShaperConfigFromWrapper(
@@ -370,7 +374,7 @@
*
* @param audioAttributes {@link android.media.AudioAttributes}
* @return {@link android.media.VolumeShaper.Configuration} if set for the audio attribute or
- * {@code null} otherwise
+ * {@code null} otherwise
* @throws NullPointerException if the audio attributes is {@code null}
* @throws IllegalStateException if the fade state is set to {@link #FADE_STATE_DISABLED}
*/
@@ -389,7 +393,7 @@
*
* @param audioAttributes {@link android.media.AudioAttributes}
* @return {@link android.media.VolumeShaper.Configuration} used for fading in if set for the
- * audio attribute or {@code null} otherwise
+ * audio attribute or {@code null} otherwise
* @throws NullPointerException if the audio attributes is {@code null}
* @throws IllegalStateException if the fade state is set to {@link #FADE_STATE_DISABLED}
*/
@@ -407,7 +411,7 @@
* configurations are defined
*
* @return list of {@link android.media.AudioAttributes} with valid volume shaper configs or
- * empty list if none set.
+ * empty list if none set.
*/
@NonNull
public List<AudioAttributes> getAudioAttributesWithVolumeShaperConfigs() {
@@ -417,8 +421,14 @@
/**
* Get the delay after which the offending players are faded back in
*
+ * Players are categorized as offending if they do not honor audio focus state changes. For
+ * example - when an app loses audio focus, it is expected that the app stops any active
+ * player in favor of the app(s) that gained audio focus. However, if the app do not stop the
+ * audio playback, such players are termed as offenders.
+ *
* @return delay in milliseconds
*/
+ @DurationMillisLong
public long getFadeInDelayForOffenders() {
return mFadeInDelayForOffendersMillis;
}
@@ -435,8 +445,9 @@
/**
* Query if the usage is fadeable
*
- * @param usage the {@link android.media.AudioAttributes.AttributeUsage}
- * @return {@code true} if usage is fadeable, {@code false} otherwise
+ * @param usage the {@link android.media.AudioAttributes usage}
+ * @return {@code true} if usage is fadeable, {@code false} when the fade state is set to
+ * {@link #FADE_STATE_DISABLED} or if the usage is not fadeable.
*/
public boolean isUsageFadeable(@AudioAttributes.AttributeUsage int usage) {
if (!isFadeEnabled()) {
@@ -448,9 +459,9 @@
/**
* Query if the content type is unfadeable
*
- * @param contentType the {@link android.media.AudioAttributes.AttributeContentType}
+ * @param contentType the {@link android.media.AudioAttributes content type}
* @return {@code true} if content type is unfadeable or if fade state is set to
- * {@link #FADE_STATE_DISABLED}, {@code false} otherwise
+ * {@link #FADE_STATE_DISABLED}, {@code false} otherwise
*/
public boolean isContentTypeUnfadeable(@AudioAttributes.AttributeContentType int contentType) {
if (!isFadeEnabled()) {
@@ -462,11 +473,11 @@
/**
* Query if the player type is unfadeable
*
- * @param playerType the {@link android.media.AudioPlaybackConfiguration} player type
+ * @param playerType the {@link android.media.AudioPlaybackConfiguration player type}
* @return {@code true} if player type is unfadeable or if fade state is set to
- * {@link #FADE_STATE_DISABLED}, {@code false} otherwise
+ * {@link #FADE_STATE_DISABLED}, {@code false} otherwise
*/
- public boolean isPlayerTypeUnfadeable(int playerType) {
+ public boolean isPlayerTypeUnfadeable(@AudioPlaybackConfiguration.PlayerType int playerType) {
if (!isFadeEnabled()) {
return true;
}
@@ -478,7 +489,7 @@
*
* @param audioAttributes the {@link android.media.AudioAttributes}
* @return {@code true} if audio attributes is unfadeable or if fade state is set to
- * {@link #FADE_STATE_DISABLED}, {@code false} otherwise
+ * {@link #FADE_STATE_DISABLED}, {@code false} otherwise
* @throws NullPointerException if the audio attributes is {@code null}
*/
public boolean isAudioAttributesUnfadeable(@NonNull AudioAttributes audioAttributes) {
@@ -494,7 +505,7 @@
*
* @param uid the uid of application
* @return {@code true} if uid is unfadeable or if fade state is set to
- * {@link #FADE_STATE_DISABLED}, {@code false} otherwise
+ * {@link #FADE_STATE_DISABLED}, {@code false} otherwise
*/
public boolean isUidUnfadeable(int uid) {
if (!isFadeEnabled()) {
@@ -503,6 +514,20 @@
return mUnfadeableUids.contains(uid);
}
+ /**
+ * Returns the default fade out duration (in milliseconds)
+ */
+ public static @DurationMillisLong long getDefaultFadeOutDurationMillis() {
+ return DEFAULT_FADE_OUT_DURATION_MS;
+ }
+
+ /**
+ * Returns the default fade in duration (in milliseconds)
+ */
+ public static @DurationMillisLong long getDefaultFadeInDurationMillis() {
+ return DEFAULT_FADE_IN_DURATION_MS;
+ }
+
@Override
public String toString() {
return "FadeManagerConfiguration { fade state = " + fadeStateToString(mFadeState)
@@ -520,7 +545,7 @@
/**
* Convert fade state into a human-readable string
*
- * @param fadeState one of the fade state in {@link FadeStateEnum}
+ * @param fadeState one of {@link #FADE_STATE_DISABLED} or {@link #FADE_STATE_ENABLED_DEFAULT}
* @return human-readable string
* @hide
*/
@@ -531,8 +556,6 @@
return "FADE_STATE_DISABLED";
case FADE_STATE_ENABLED_DEFAULT:
return "FADE_STATE_ENABLED_DEFAULT";
- case FADE_STATE_ENABLED_AUTO:
- return "FADE_STATE_ENABLED_AUTO";
default:
return "unknown fade state: " + fadeState;
}
@@ -712,9 +735,9 @@
*
* <p><b>Notes:</b>
* <ul>
- * <li>When fade state is set to {@link #FADE_STATE_ENABLED_DEFAULT} or
- * {@link #FADE_STATE_ENABLED_AUTO}, the builder expects at least one valid usage to be
- * set/added. Failure to do so will result in an exception during {@link #build()}</li>
+ * <li>When fade state is set to {@link #FADE_STATE_ENABLED_DEFAULT}, the builder expects at
+ * least one valid usage to be set/added. Failure to do so will result in an exception
+ * during {@link #build()}</li>
* <li>Every usage added to the fadeable list should have corresponding volume shaper
* configs defined. This can be achieved by setting either the duration or volume shaper
* config through {@link #setFadeOutDurationForUsage(int, long)} or
@@ -741,11 +764,6 @@
private static final long IS_FADEABLE_USAGES_FIELD_SET = 1 << 1;
private static final long IS_UNFADEABLE_CONTENT_TYPE_FIELD_SET = 1 << 2;
- /** duration of the fade out curve */
- private static final long DEFAULT_FADE_OUT_DURATION_MS = 2_000;
- /** duration of the fade in curve */
- private static final long DEFAULT_FADE_IN_DURATION_MS = 1_000;
-
/**
* delay after which a faded out player will be faded back in. This will be heard by the
* user only in the case of unmuting players that didn't respect audio focus and didn't
@@ -771,9 +789,10 @@
});
private int mFadeState = FADE_STATE_ENABLED_DEFAULT;
- private long mFadeInDelayForOffendersMillis = DEFAULT_DELAY_FADE_IN_OFFENDERS_MS;
- private long mFadeOutDurationMillis;
- private long mFadeInDurationMillis;
+ private @DurationMillisLong long mFadeInDelayForOffendersMillis =
+ DEFAULT_DELAY_FADE_IN_OFFENDERS_MS;
+ private @DurationMillisLong long mFadeOutDurationMillis;
+ private @DurationMillisLong long mFadeInDurationMillis;
private long mBuilderFieldsSet;
private SparseArray<FadeVolumeShaperConfigsWrapper> mUsageToFadeWrapperMap =
new SparseArray<>();
@@ -787,7 +806,8 @@
private List<AudioAttributes> mUnfadeableAudioAttributes = new ArrayList<>();
/**
- * Constructs a new Builder with default fade out and fade in durations
+ * Constructs a new Builder with {@link #DEFAULT_FADE_OUT_DURATION_MS} and
+ * {@link #DEFAULT_FADE_IN_DURATION_MS} durations.
*/
public Builder() {
mFadeOutDurationMillis = DEFAULT_FADE_OUT_DURATION_MS;
@@ -800,7 +820,8 @@
* @param fadeOutDurationMillis duration in milliseconds used for fading out
* @param fadeInDurationMills duration in milliseconds used for fading in
*/
- public Builder(long fadeOutDurationMillis, long fadeInDurationMills) {
+ public Builder(@DurationMillisLong long fadeOutDurationMillis,
+ @DurationMillisLong long fadeInDurationMills) {
mFadeOutDurationMillis = fadeOutDurationMillis;
mFadeInDurationMillis = fadeInDurationMills;
}
@@ -830,7 +851,8 @@
/**
* Set the overall fade state
*
- * @param state one of the {@link FadeStateEnum} states
+ * @param state one of the {@link #FADE_STATE_DISABLED} or
+ * {@link #FADE_STATE_ENABLED_DEFAULT} states
* @return the same Builder instance
* @throws IllegalArgumentException if the fade state is invalid
* @see #getFadeState()
@@ -844,21 +866,22 @@
/**
* Set the {@link android.media.VolumeShaper.Configuration} used to fade out players with
- * {@link android.media.AudioAttributes.AttributeUsage}
+ * {@link android.media.AudioAttributes usage}
* <p>
* This method accepts {@code null} for volume shaper config to clear a previously set
* configuration (example, if set through
* {@link #Builder(android.media.FadeManagerConfiguration)})
*
- * @param usage the {@link android.media.AudioAttributes.AttributeUsage} of target player
+ * @param usage the {@link android.media.AudioAttributes usage} of target player
* @param fadeOutVShaperConfig the {@link android.media.VolumeShaper.Configuration} used
- * to fade out players with usage
+ * to fade out players with usage
* @return the same Builder instance
* @throws IllegalArgumentException if the usage is invalid
* @see #getFadeOutVolumeShaperConfigForUsage(int)
*/
@NonNull
- public Builder setFadeOutVolumeShaperConfigForUsage(int usage,
+ public Builder setFadeOutVolumeShaperConfigForUsage(
+ @AudioAttributes.AttributeUsage int usage,
@Nullable VolumeShaper.Configuration fadeOutVShaperConfig) {
validateUsage(usage);
getFadeVolShaperConfigWrapperForUsage(usage)
@@ -869,21 +892,22 @@
/**
* Set the {@link android.media.VolumeShaper.Configuration} used to fade in players with
- * {@link android.media.AudioAttributes.AttributeUsage}
+ * {@link android.media.AudioAttributes usage}
* <p>
* This method accepts {@code null} for volume shaper config to clear a previously set
* configuration (example, if set through
* {@link #Builder(android.media.FadeManagerConfiguration)})
*
- * @param usage the {@link android.media.AudioAttributes.AttributeUsage}
+ * @param usage the {@link android.media.AudioAttributes usage}
* @param fadeInVShaperConfig the {@link android.media.VolumeShaper.Configuration} used
- * to fade in players with usage
+ * to fade in players with usage
* @return the same Builder instance
* @throws IllegalArgumentException if the usage is invalid
* @see #getFadeInVolumeShaperConfigForUsage(int)
*/
@NonNull
- public Builder setFadeInVolumeShaperConfigForUsage(int usage,
+ public Builder setFadeInVolumeShaperConfigForUsage(
+ @AudioAttributes.AttributeUsage int usage,
@Nullable VolumeShaper.Configuration fadeInVShaperConfig) {
validateUsage(usage);
getFadeVolShaperConfigWrapperForUsage(usage)
@@ -894,7 +918,7 @@
/**
* Set the duration used for fading out players with
- * {@link android.media.AudioAttributes.AttributeUsage}
+ * {@link android.media.AudioAttributes usage}
* <p>
* A Volume shaper configuration is generated with the provided duration and default
* volume curve definitions. This config is then used to fade out players with given usage.
@@ -904,17 +928,18 @@
* {@link #DURATION_NOT_SET} and sets the corresponding fade out volume shaper config to
* {@code null}
*
- * @param usage the {@link android.media.AudioAttributes.AttributeUsage} of target player
+ * @param usage the {@link android.media.AudioAttributes usage} of target player
* @param fadeOutDurationMillis positive duration in milliseconds or
- * {@link #DURATION_NOT_SET}
+ * {@link #DURATION_NOT_SET}
* @return the same Builder instance
* @throws IllegalArgumentException if the fade out duration is non-positive with the
- * exception of {@link #DURATION_NOT_SET}
+ * exception of {@link #DURATION_NOT_SET}
* @see #setFadeOutVolumeShaperConfigForUsage(int, VolumeShaper.Configuration)
* @see #getFadeOutDurationForUsage(int)
*/
@NonNull
- public Builder setFadeOutDurationForUsage(int usage, long fadeOutDurationMillis) {
+ public Builder setFadeOutDurationForUsage(@AudioAttributes.AttributeUsage int usage,
+ @DurationMillisLong long fadeOutDurationMillis) {
validateUsage(usage);
VolumeShaper.Configuration fadeOutVShaperConfig =
createVolShaperConfigForDuration(fadeOutDurationMillis, /* isFadeIn= */ false);
@@ -924,7 +949,7 @@
/**
* Set the duration used for fading in players with
- * {@link android.media.AudioAttributes.AttributeUsage}
+ * {@link android.media.AudioAttributes usage}
* <p>
* A Volume shaper configuration is generated with the provided duration and default
* volume curve definitions. This config is then used to fade in players with given usage.
@@ -934,17 +959,18 @@
* {@link #DURATION_NOT_SET} and sets the corresponding fade in volume shaper config to
* {@code null}
*
- * @param usage the {@link android.media.AudioAttributes.AttributeUsage} of target player
+ * @param usage the {@link android.media.AudioAttributes usage} of target player
* @param fadeInDurationMillis positive duration in milliseconds or
- * {@link #DURATION_NOT_SET}
+ * {@link #DURATION_NOT_SET}
* @return the same Builder instance
* @throws IllegalArgumentException if the fade in duration is non-positive with the
- * exception of {@link #DURATION_NOT_SET}
+ * exception of {@link #DURATION_NOT_SET}
* @see #setFadeInVolumeShaperConfigForUsage(int, VolumeShaper.Configuration)
* @see #getFadeInDurationForUsage(int)
*/
@NonNull
- public Builder setFadeInDurationForUsage(int usage, long fadeInDurationMillis) {
+ public Builder setFadeInDurationForUsage(@AudioAttributes.AttributeUsage int usage,
+ @DurationMillisLong long fadeInDurationMillis) {
validateUsage(usage);
VolumeShaper.Configuration fadeInVShaperConfig =
createVolShaperConfigForDuration(fadeInDurationMillis, /* isFadeIn= */ true);
@@ -962,9 +988,8 @@
*
* @param audioAttributes the {@link android.media.AudioAttributes}
* @param fadeOutVShaperConfig the {@link android.media.VolumeShaper.Configuration} used to
- * fade out players with audio attribute
+ * fade out players with audio attribute
* @return the same Builder instance
- * @throws NullPointerException if the audio attributes is {@code null}
* @see #getFadeOutVolumeShaperConfigForAudioAttributes(AudioAttributes)
*/
@NonNull
@@ -988,7 +1013,7 @@
*
* @param audioAttributes the {@link android.media.AudioAttributes}
* @param fadeInVShaperConfig the {@link android.media.VolumeShaper.Configuration} used to
- * fade in players with audio attribute
+ * fade in players with audio attribute
* @return the same Builder instance
* @throws NullPointerException if the audio attributes is {@code null}
* @see #getFadeInVolumeShaperConfigForAudioAttributes(AudioAttributes)
@@ -1017,12 +1042,12 @@
* {@code null}
*
* @param audioAttributes the {@link android.media.AudioAttributes} for which the fade out
- * duration will be set/updated/reset
+ * duration will be set/updated/reset
* @param fadeOutDurationMillis positive duration in milliseconds or
- * {@link #DURATION_NOT_SET}
+ * {@link #DURATION_NOT_SET}
* @return the same Builder instance
* @throws IllegalArgumentException if the fade out duration is non-positive with the
- * exception of {@link #DURATION_NOT_SET}
+ * exception of {@link #DURATION_NOT_SET}
* @see #getFadeOutDurationForAudioAttributes(AudioAttributes)
* @see #setFadeOutVolumeShaperConfigForAudioAttributes(AudioAttributes,
* VolumeShaper.Configuration)
@@ -1030,7 +1055,7 @@
@NonNull
public Builder setFadeOutDurationForAudioAttributes(
@NonNull AudioAttributes audioAttributes,
- long fadeOutDurationMillis) {
+ @DurationMillisLong long fadeOutDurationMillis) {
Objects.requireNonNull(audioAttributes, "Audio attribute cannot be null");
VolumeShaper.Configuration fadeOutVShaperConfig =
createVolShaperConfigForDuration(fadeOutDurationMillis, /* isFadeIn= */ false);
@@ -1039,8 +1064,7 @@
}
/**
- * Set the duration used for fading in players of type
- * {@link android.media.AudioAttributes}.
+ * Set the duration used for fading in players of type {@link android.media.AudioAttributes}
* <p>
* A Volume shaper configuration is generated with the provided duration and default
* volume curve definitions. This config is then used to fade in players with given usage.
@@ -1051,19 +1075,19 @@
* {@code null}
*
* @param audioAttributes the {@link android.media.AudioAttributes} for which the fade in
- * duration will be set/updated/reset
+ * duration will be set/updated/reset
* @param fadeInDurationMillis positive duration in milliseconds or
- * {@link #DURATION_NOT_SET}
+ * {@link #DURATION_NOT_SET}
* @return the same Builder instance
* @throws IllegalArgumentException if the fade in duration is non-positive with the
- * exception of {@link #DURATION_NOT_SET}
+ * exception of {@link #DURATION_NOT_SET}
* @see #getFadeInDurationForAudioAttributes(AudioAttributes)
* @see #setFadeInVolumeShaperConfigForAudioAttributes(AudioAttributes,
* VolumeShaper.Configuration)
*/
@NonNull
public Builder setFadeInDurationForAudioAttributes(@NonNull AudioAttributes audioAttributes,
- long fadeInDurationMillis) {
+ @DurationMillisLong long fadeInDurationMillis) {
Objects.requireNonNull(audioAttributes, "Audio attribute cannot be null");
VolumeShaper.Configuration fadeInVShaperConfig =
createVolShaperConfigForDuration(fadeInDurationMillis, /* isFadeIn= */ true);
@@ -1072,22 +1096,18 @@
}
/**
- * Set the list of {@link android.media.AudioAttributes.AttributeUsage} that can be faded
+ * Set the list of {@link android.media.AudioAttributes usage} that can be faded
*
* <p>This is a positive list. Players with matching usage will be considered for fading.
* Usages that are not part of this list will not be faded
*
- * <p>Passing an empty list as input clears the existing list. This can be used to
- * reset the list when using a copy constructor
- *
* <p><b>Warning:</b> When fade state is set to enabled, the builder expects at least one
* usage to be set/added. Failure to do so will result in an exception during
* {@link #build()}
*
- * @param usages List of the {@link android.media.AudioAttributes.AttributeUsage}
+ * @param usages List of the {@link android.media.AudioAttributes usages}
* @return the same Builder instance
* @throws IllegalArgumentException if the usages are invalid
- * @throws NullPointerException if the usage list is {@code null}
* @see #getFadeableUsages()
*/
@NonNull
@@ -1101,9 +1121,9 @@
}
/**
- * Add the {@link android.media.AudioAttributes.AttributeUsage} to the fadeable list
+ * Add the {@link android.media.AudioAttributes usage} to the fadeable list
*
- * @param usage the {@link android.media.AudioAttributes.AttributeUsage}
+ * @param usage the {@link android.media.AudioAttributes usage}
* @return the same Builder instance
* @throws IllegalArgumentException if the usage is invalid
* @see #getFadeableUsages()
@@ -1120,30 +1140,23 @@
}
/**
- * Remove the {@link android.media.AudioAttributes.AttributeUsage} from the fadeable list
- * <p>
- * Players of this usage type will not be faded.
+ * Clears the fadeable {@link android.media.AudioAttributes usage} list
*
- * @param usage the {@link android.media.AudioAttributes.AttributeUsage}
+ * <p>This can be used to reset the list when using a copy constructor
+ *
* @return the same Builder instance
- * @throws IllegalArgumentException if the usage is invalid
* @see #getFadeableUsages()
* @see #setFadeableUsages(List)
*/
@NonNull
- public Builder clearFadeableUsage(@AudioAttributes.AttributeUsage int usage) {
- validateUsage(usage);
+ public Builder clearFadeableUsages() {
setFlag(IS_FADEABLE_USAGES_FIELD_SET);
- int index = mFadeableUsages.indexOf(usage);
- if (index != INVALID_INDEX) {
- mFadeableUsages.remove(index);
- }
+ mFadeableUsages.clear();
return this;
}
/**
- * Set the list of {@link android.media.AudioAttributes.AttributeContentType} that can not
- * be faded
+ * Set the list of {@link android.media.AudioAttributes content type} that can not be faded
*
* <p>This is a negative list. Players with matching content type of this list will not be
* faded. Content types that are not part of this list will be considered for fading.
@@ -1151,10 +1164,9 @@
* <p>Passing an empty list as input clears the existing list. This can be used to
* reset the list when using a copy constructor
*
- * @param contentTypes list of {@link android.media.AudioAttributes.AttributeContentType}
+ * @param contentTypes list of {@link android.media.AudioAttributes content types}
* @return the same Builder instance
* @throws IllegalArgumentException if the content types are invalid
- * @throws NullPointerException if the content type list is {@code null}
* @see #getUnfadeableContentTypes()
*/
@NonNull
@@ -1168,9 +1180,9 @@
}
/**
- * Add the {@link android.media.AudioAttributes.AttributeContentType} to unfadeable list
+ * Add the {@link android.media.AudioAttributes content type} to unfadeable list
*
- * @param contentType the {@link android.media.AudioAttributes.AttributeContentType}
+ * @param contentType the {@link android.media.AudioAttributes content type}
* @return the same Builder instance
* @throws IllegalArgumentException if the content type is invalid
* @see #setUnfadeableContentTypes(List)
@@ -1188,24 +1200,18 @@
}
/**
- * Remove the {@link android.media.AudioAttributes.AttributeContentType} from the
- * unfadeable list
+ * Clears the unfadeable {@link android.media.AudioAttributes content type} list
*
- * @param contentType the {@link android.media.AudioAttributes.AttributeContentType}
+ * <p>This can be used to reset the list when using a copy constructor
+ *
* @return the same Builder instance
- * @throws IllegalArgumentException if the content type is invalid
* @see #setUnfadeableContentTypes(List)
* @see #getUnfadeableContentTypes()
*/
@NonNull
- public Builder clearUnfadeableContentType(
- @AudioAttributes.AttributeContentType int contentType) {
- validateContentType(contentType);
+ public Builder clearUnfadeableContentTypes() {
setFlag(IS_UNFADEABLE_CONTENT_TYPE_FIELD_SET);
- int index = mUnfadeableContentTypes.indexOf(contentType);
- if (index != INVALID_INDEX) {
- mUnfadeableContentTypes.remove(index);
- }
+ mUnfadeableContentTypes.clear();
return this;
}
@@ -1213,14 +1219,10 @@
* Set the uids that cannot be faded
*
* <p>This is a negative list. Players with matching uid of this list will not be faded.
- * Uids that are not part of this list shall be considered for fading
- *
- * <p>Passing an empty list as input clears the existing list. This can be used to
- * reset the list when using a copy constructor
+ * Uids that are not part of this list shall be considered for fading.
*
* @param uids list of uids
* @return the same Builder instance
- * @throws NullPointerException if the uid list is {@code null}
* @see #getUnfadeableUids()
*/
@NonNull
@@ -1248,19 +1250,17 @@
}
/**
- * Remove the uid from unfadeable list
+ * Clears the unfadeable uid list
*
- * @param uid client uid
+ * <p>This can be used to reset the list when using a copy constructor.
+ *
* @return the same Builder instance
* @see #setUnfadeableUids(List)
* @see #getUnfadeableUids()
*/
@NonNull
- public Builder clearUnfadeableUid(int uid) {
- int index = mUnfadeableUids.indexOf(uid);
- if (index != INVALID_INDEX) {
- mUnfadeableUids.remove(index);
- }
+ public Builder clearUnfadeableUids() {
+ mUnfadeableUids.clear();
return this;
}
@@ -1270,24 +1270,19 @@
* <p>This is a negative list. Players with matching audio attributes of this list will not
* be faded. Audio attributes that are not part of this list shall be considered for fading.
*
- * <p>Passing an empty list as input clears any existing list. This can be used to
- * reset the list when using a copy constructor
- *
* <p><b>Note:</b> Be cautious when adding generic audio attributes into this list as it can
- * negatively impact fadeability decision if such an audio attribute and corresponding
- * usage fall into opposing lists.
+ * negatively impact fadeability decision (if such an audio attribute and corresponding
+ * usage fall into opposing lists).
* For example:
* <pre class=prettyprint>
* AudioAttributes.Builder().setUsage(AudioAttributes.USAGE_MEDIA).build() </pre>
* is a generic audio attribute for {@link android.media.AudioAttributes.USAGE_MEDIA}.
- * It is an undefined behavior to have an
- * {@link android.media.AudioAttributes.AttributeUsage} in the fadeable usage list and the
- * corresponding generic {@link android.media.AudioAttributes} in the unfadeable list. Such
- * cases will result in an exception during {@link #build()}
+ * It is an undefined behavior to have an {@link android.media.AudioAttributes usage} in the
+ * fadeable usage list and the corresponding generic {@link android.media.AudioAttributes}
+ * in the unfadeable list. Such cases will result in an exception during {@link #build()}.
*
* @param attrs list of {@link android.media.AudioAttributes}
* @return the same Builder instance
- * @throws NullPointerException if the audio attributes list is {@code null}
* @see #getUnfadeableAudioAttributes()
*/
@NonNull
@@ -1303,7 +1298,6 @@
*
* @param audioAttributes the {@link android.media.AudioAttributes}
* @return the same Builder instance
- * @throws NullPointerException if the audio attributes is {@code null}
* @see #setUnfadeableAudioAttributes(List)
* @see #getUnfadeableAudioAttributes()
*/
@@ -1317,19 +1311,16 @@
}
/**
- * Remove the {@link android.media.AudioAttributes} from the unfadeable list.
+ * Clears the unfadeable {@link android.media.AudioAttributes} list.
*
- * @param audioAttributes the {@link android.media.AudioAttributes}
+ * <p>This can be used to reset the list when using a copy constructor.
+ *
* @return the same Builder instance
- * @throws NullPointerException if the audio attributes is {@code null}
* @see #getUnfadeableAudioAttributes()
*/
@NonNull
- public Builder clearUnfadeableAudioAttributes(@NonNull AudioAttributes audioAttributes) {
- Objects.requireNonNull(audioAttributes, "Audio attributes cannot be null");
- if (mUnfadeableAudioAttributes.contains(audioAttributes)) {
- mUnfadeableAudioAttributes.remove(audioAttributes);
- }
+ public Builder clearUnfadeableAudioAttributes() {
+ mUnfadeableAudioAttributes.clear();
return this;
}
@@ -1345,7 +1336,7 @@
* @see #getFadeInDelayForOffenders()
*/
@NonNull
- public Builder setFadeInDelayForOffenders(long delayMillis) {
+ public Builder setFadeInDelayForOffenders(@DurationMillisLong long delayMillis) {
Preconditions.checkArgument(delayMillis >= 0, "Delay cannot be negative");
mFadeInDelayForOffendersMillis = delayMillis;
return this;
@@ -1469,7 +1460,6 @@
switch(state) {
case FADE_STATE_DISABLED:
case FADE_STATE_ENABLED_DEFAULT:
- case FADE_STATE_ENABLED_AUTO:
break;
default:
throw new IllegalArgumentException("Unknown fade state: " + state);
diff --git a/media/java/android/media/IRingtonePlayer.aidl b/media/java/android/media/IRingtonePlayer.aidl
index 1e57be2..c96a400 100644
--- a/media/java/android/media/IRingtonePlayer.aidl
+++ b/media/java/android/media/IRingtonePlayer.aidl
@@ -21,7 +21,6 @@
import android.net.Uri;
import android.os.ParcelFileDescriptor;
import android.os.UserHandle;
-import android.os.VibrationEffect;
/**
* @hide
@@ -30,23 +29,12 @@
/** Used for Ringtone.java playback */
@UnsupportedAppUsage
oneway void play(IBinder token, in Uri uri, in AudioAttributes aa, float volume, boolean looping);
+ oneway void playWithVolumeShaping(IBinder token, in Uri uri, in AudioAttributes aa,
+ float volume, boolean looping, in @nullable VolumeShaper.Configuration volumeShaperConfig);
oneway void stop(IBinder token);
boolean isPlaying(IBinder token);
-
- // RingtoneV1
- oneway void playWithVolumeShaping(IBinder token, in Uri uri, in AudioAttributes aa,
- float volume, boolean looping, in @nullable VolumeShaper.Configuration volumeShaperConfig);
oneway void setPlaybackProperties(IBinder token, float volume, boolean looping,
- boolean hapticGeneratorEnabled);
-
- // RingtoneV2
- oneway void playRemoteRingtone(IBinder token, in Uri uri, in AudioAttributes aa,
- boolean useExactAudioAttributes, int enabledMedia, in @nullable VibrationEffect ve,
- float volume, boolean looping, boolean hapticGeneratorEnabled,
- in @nullable VolumeShaper.Configuration volumeShaperConfig);
- oneway void setLooping(IBinder token, boolean looping);
- oneway void setVolume(IBinder token, float volume);
- oneway void setHapticGeneratorEnabled(IBinder token, boolean hapticGeneratorEnabled);
+ boolean hapticGeneratorEnabled);
/** Used for Notification sound playback. */
oneway void playAsync(in Uri uri, in UserHandle user, boolean looping, in AudioAttributes aa, float volume);
diff --git a/media/java/android/media/LocalRingtonePlayer.java b/media/java/android/media/LocalRingtonePlayer.java
deleted file mode 100644
index fe7cc3e..0000000
--- a/media/java/android/media/LocalRingtonePlayer.java
+++ /dev/null
@@ -1,408 +0,0 @@
-/*
- * Copyright (C) 2023 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.NonNull;
-import android.annotation.Nullable;
-import android.content.Context;
-import android.content.res.AssetFileDescriptor;
-import android.media.audiofx.HapticGenerator;
-import android.net.Uri;
-import android.os.Trace;
-import android.os.VibrationAttributes;
-import android.os.VibrationEffect;
-import android.os.Vibrator;
-import android.util.Log;
-
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.Objects;
-
-/**
- * Plays a ringtone on the local process.
- * @hide
- */
-public class LocalRingtonePlayer
- implements RingtoneV2.RingtonePlayer, MediaPlayer.OnCompletionListener {
- private static final String TAG = "LocalRingtonePlayer";
-
- // keep references on active Ringtones until stopped or completion listener called.
- private static final ArrayList<LocalRingtonePlayer> sActiveMediaPlayers = new ArrayList<>();
-
- private final MediaPlayer mMediaPlayer;
- private final AudioAttributes mAudioAttributes;
- private final RingtoneV2.RingtonePlayer mVibrationPlayer;
- private final Ringtone.Injectables mInjectables;
- private final AudioManager mAudioManager;
- private final VolumeShaper mVolumeShaper;
- private HapticGenerator mHapticGenerator;
-
- private LocalRingtonePlayer(@NonNull MediaPlayer mediaPlayer,
- @NonNull AudioAttributes audioAttributes, @NonNull Ringtone.Injectables injectables,
- @NonNull AudioManager audioManager, @Nullable HapticGenerator hapticGenerator,
- @Nullable VolumeShaper volumeShaper,
- @Nullable RingtoneV2.RingtonePlayer vibrationPlayer) {
- Objects.requireNonNull(mediaPlayer);
- Objects.requireNonNull(audioAttributes);
- Objects.requireNonNull(injectables);
- Objects.requireNonNull(audioManager);
- mMediaPlayer = mediaPlayer;
- mAudioAttributes = audioAttributes;
- mInjectables = injectables;
- mAudioManager = audioManager;
- mVolumeShaper = volumeShaper;
- mVibrationPlayer = vibrationPlayer;
- mHapticGenerator = hapticGenerator;
- }
-
- /**
- * Creates a {@link LocalRingtonePlayer} for a Uri, returning null if the Uri can't be
- * loaded in the local player.
- */
- @Nullable
- static RingtoneV2.RingtonePlayer create(@NonNull Context context,
- @NonNull AudioManager audioManager, @NonNull Vibrator vibrator,
- @NonNull Uri soundUri,
- @NonNull AudioAttributes audioAttributes,
- boolean isVibrationOnly,
- @Nullable VibrationEffect vibrationEffect,
- @NonNull Ringtone.Injectables injectables,
- @Nullable VolumeShaper.Configuration volumeShaperConfig,
- @Nullable AudioDeviceInfo preferredDevice, boolean initialHapticGeneratorEnabled,
- boolean initialLooping, float initialVolume) {
- Objects.requireNonNull(context);
- Objects.requireNonNull(soundUri);
- Objects.requireNonNull(audioAttributes);
- Trace.beginSection("createLocalMediaPlayer");
- MediaPlayer mediaPlayer = injectables.newMediaPlayer();
- HapticGenerator hapticGenerator = null;
- try {
- mediaPlayer.setDataSource(context, soundUri);
- mediaPlayer.setAudioAttributes(audioAttributes);
- mediaPlayer.setPreferredDevice(preferredDevice);
- mediaPlayer.setLooping(initialLooping);
- mediaPlayer.setVolume(isVibrationOnly ? 0 : initialVolume);
- if (initialHapticGeneratorEnabled) {
- hapticGenerator = injectables.createHapticGenerator(mediaPlayer);
- if (hapticGenerator != null) {
- // In practise, this should always be non-null because the initial value is
- // not true unless it's available.
- hapticGenerator.setEnabled(true);
- vibrationEffect = null; // Don't play the VibrationEffect.
- }
- }
- VolumeShaper volumeShaper = null;
- if (volumeShaperConfig != null) {
- volumeShaper = mediaPlayer.createVolumeShaper(volumeShaperConfig);
- }
- mediaPlayer.prepare();
- if (vibrationEffect != null && !audioAttributes.areHapticChannelsMuted()) {
- if (injectables.hasHapticChannels(mediaPlayer)) {
- // Don't play the Vibration effect if the URI has haptic channels.
- vibrationEffect = null;
- }
- }
- VibrationEffectPlayer vibrationEffectPlayer = (vibrationEffect == null) ? null :
- new VibrationEffectPlayer(
- vibrationEffect, audioAttributes, vibrator, initialLooping);
- if (isVibrationOnly && vibrationEffectPlayer != null) {
- // Abandon the media player now that it's confirmed to not have haptic channels.
- mediaPlayer.release();
- return vibrationEffectPlayer;
- }
- return new LocalRingtonePlayer(mediaPlayer, audioAttributes, injectables, audioManager,
- hapticGenerator, volumeShaper, vibrationEffectPlayer);
- } catch (SecurityException | IOException e) {
- if (hapticGenerator != null) {
- hapticGenerator.release();
- }
- // volume shaper closes with media player
- mediaPlayer.release();
- return null;
- } finally {
- Trace.endSection();
- }
- }
-
- /**
- * Creates a {@link LocalRingtonePlayer} for an externally referenced file descriptor. This is
- * intended for loading a fallback from an internal resource, rather than via a Uri.
- */
- @Nullable
- static LocalRingtonePlayer createForFallback(
- @NonNull AudioManager audioManager, @NonNull Vibrator vibrator,
- @NonNull AssetFileDescriptor afd,
- @NonNull AudioAttributes audioAttributes,
- @Nullable VibrationEffect vibrationEffect,
- @NonNull Ringtone.Injectables injectables,
- @Nullable VolumeShaper.Configuration volumeShaperConfig,
- @Nullable AudioDeviceInfo preferredDevice,
- boolean initialLooping, float initialVolume) {
- // Haptic generator not supported for fallback.
- Objects.requireNonNull(audioManager);
- Objects.requireNonNull(afd);
- Objects.requireNonNull(audioAttributes);
- Trace.beginSection("createFallbackLocalMediaPlayer");
-
- MediaPlayer mediaPlayer = injectables.newMediaPlayer();
- try {
- if (afd.getDeclaredLength() < 0) {
- mediaPlayer.setDataSource(afd.getFileDescriptor());
- } else {
- mediaPlayer.setDataSource(afd.getFileDescriptor(),
- afd.getStartOffset(),
- afd.getDeclaredLength());
- }
- mediaPlayer.setAudioAttributes(audioAttributes);
- mediaPlayer.setPreferredDevice(preferredDevice);
- mediaPlayer.setLooping(initialLooping);
- mediaPlayer.setVolume(initialVolume);
- VolumeShaper volumeShaper = null;
- if (volumeShaperConfig != null) {
- volumeShaper = mediaPlayer.createVolumeShaper(volumeShaperConfig);
- }
- mediaPlayer.prepare();
- if (vibrationEffect != null && !audioAttributes.areHapticChannelsMuted()) {
- if (injectables.hasHapticChannels(mediaPlayer)) {
- // Don't play the Vibration effect if the URI has haptic channels.
- vibrationEffect = null;
- }
- }
- VibrationEffectPlayer vibrationEffectPlayer = (vibrationEffect == null) ? null :
- new VibrationEffectPlayer(
- vibrationEffect, audioAttributes, vibrator, initialLooping);
- return new LocalRingtonePlayer(mediaPlayer, audioAttributes, injectables, audioManager,
- /* hapticGenerator= */ null, volumeShaper, vibrationEffectPlayer);
- } catch (SecurityException | IOException e) {
- Log.e(TAG, "Failed to open fallback ringtone");
- // TODO: vibration-effect-only / no-sound LocalRingtonePlayer.
- mediaPlayer.release();
- return null;
- } finally {
- Trace.endSection();
- }
- }
-
- @Override
- public boolean play() {
- // Play ringtones if stream volume is over 0 or if it is a haptic-only ringtone
- // (typically because ringer mode is vibrate).
- if (mAudioManager.getStreamVolume(AudioAttributes.toLegacyStreamType(mAudioAttributes))
- == 0 && (mAudioAttributes.areHapticChannelsMuted() || !hasHapticChannels())) {
- maybeStartVibration();
- return true; // Successfully played while muted.
- }
- synchronized (sActiveMediaPlayers) {
- // We keep-alive when a mediaplayer is active, since its finalizer would stop the
- // ringtone. This isn't necessary for vibrations in the vibrator service
- // (i.e. maybeStartVibration in the muted case, above).
- sActiveMediaPlayers.add(this);
- }
-
- mMediaPlayer.setOnCompletionListener(this);
- mMediaPlayer.start();
- if (mVolumeShaper != null) {
- mVolumeShaper.apply(VolumeShaper.Operation.PLAY);
- }
- maybeStartVibration();
- return true;
- }
-
- private void maybeStartVibration() {
- if (mVibrationPlayer != null) {
- mVibrationPlayer.play();
- }
- }
-
- @Override
- public boolean isPlaying() {
- return mMediaPlayer.isPlaying();
- }
-
- @Override
- public void stopAndRelease() {
- synchronized (sActiveMediaPlayers) {
- sActiveMediaPlayers.remove(this);
- }
- try {
- mMediaPlayer.stop();
- } finally {
- if (mVibrationPlayer != null) {
- try {
- mVibrationPlayer.stopAndRelease();
- } catch (Exception e) {
- Log.e(TAG, "Exception stopping ringtone vibration", e);
- }
- }
- if (mHapticGenerator != null) {
- mHapticGenerator.release();
- }
- mMediaPlayer.setOnCompletionListener(null);
- mMediaPlayer.reset();
- mMediaPlayer.release();
- }
- }
-
- @Override
- public void setPreferredDevice(@Nullable AudioDeviceInfo audioDeviceInfo) {
- mMediaPlayer.setPreferredDevice(audioDeviceInfo);
- }
-
- @Override
- public void setLooping(boolean looping) {
- boolean wasLooping = mMediaPlayer.isLooping();
- if (wasLooping == looping) {
- return;
- }
- mMediaPlayer.setLooping(looping);
- if (mVibrationPlayer != null) {
- mVibrationPlayer.setLooping(looping);
- }
- }
-
- @Override
- public void setHapticGeneratorEnabled(boolean enabled) {
- if (mVibrationPlayer != null) {
- // Ignore haptic generator changes if a vibration player is present. The decision to
- // use one or the other happens before this object is constructed.
- return;
- }
- if (enabled && mHapticGenerator == null && !hasHapticChannels()) {
- mHapticGenerator = mInjectables.createHapticGenerator(mMediaPlayer);
- }
- if (mHapticGenerator != null) {
- mHapticGenerator.setEnabled(enabled);
- }
- }
-
- @Override
- public void setVolume(float volume) {
- mMediaPlayer.setVolume(volume);
- // no effect on vibration player
- }
-
- @Override
- public boolean hasHapticChannels() {
- return mInjectables.hasHapticChannels(mMediaPlayer);
- }
-
- @Override
- public void onCompletion(MediaPlayer mp) {
- synchronized (sActiveMediaPlayers) {
- sActiveMediaPlayers.remove(this);
- }
- mp.setOnCompletionListener(null); // Help the Java GC: break the refcount cycle.
- // No effect on vibration: either it's looping and this callback only happens when stopped,
- // or it's not looping, in which case the vibration should play to its own completion.
- }
-
- /** A RingtonePlayer that only plays a VibrationEffect. */
- static class VibrationEffectPlayer implements RingtoneV2.RingtonePlayer {
- private static final int VIBRATION_LOOP_DELAY_MS = 200;
- private final VibrationEffect mVibrationEffect;
- private final VibrationAttributes mVibrationAttributes;
- private final Vibrator mVibrator;
- private boolean mIsLooping;
- private boolean mStartedVibration;
-
- VibrationEffectPlayer(@NonNull VibrationEffect vibrationEffect,
- @NonNull AudioAttributes audioAttributes,
- @NonNull Vibrator vibrator, boolean initialLooping) {
- mVibrationEffect = vibrationEffect;
- mVibrationAttributes = new VibrationAttributes.Builder(audioAttributes).build();
- mVibrator = vibrator;
- mIsLooping = initialLooping;
- }
-
- @Override
- public boolean play() {
- if (!mStartedVibration) {
- try {
- // Adjust the vibration effect to loop.
- VibrationEffect loopAdjustedEffect =
- mVibrationEffect.applyRepeatingIndefinitely(
- mIsLooping, VIBRATION_LOOP_DELAY_MS);
- mVibrator.vibrate(loopAdjustedEffect, mVibrationAttributes);
- mStartedVibration = true;
- } catch (Exception e) {
- // Catch exceptions widely, because we don't want to "leak" looping sounds or
- // vibrations if something goes wrong.
- Log.e(TAG, "Problem starting " + (mIsLooping ? "looping " : "") + "vibration "
- + "for ringtone: " + mVibrationEffect, e);
- return false;
- }
- }
- return true;
- }
-
- @Override
- public boolean isPlaying() {
- return mStartedVibration;
- }
-
- @Override
- public void stopAndRelease() {
- if (mStartedVibration) {
- try {
- mVibrator.cancel(mVibrationAttributes.getUsage());
- mStartedVibration = false;
- } catch (Exception e) {
- // Catch exceptions widely, because we don't want to "leak" looping sounds or
- // vibrations if something goes wrong.
- Log.e(TAG, "Problem stopping vibration for ringtone", e);
- }
- }
- }
-
- @Override
- public void setPreferredDevice(AudioDeviceInfo audioDeviceInfo) {
- // no-op
- }
-
- @Override
- public void setLooping(boolean looping) {
- if (looping == mIsLooping) {
- return;
- }
- mIsLooping = looping;
- if (mStartedVibration) {
- if (!mIsLooping) {
- // Was looping, stop looping
- stopAndRelease();
- }
- // Else was not looping, but can't interfere with a running vibration without
- // restarting it, and don't know if it was finished. So do nothing: apps shouldn't
- // toggle looping after calling play anyway.
- }
- }
-
- @Override
- public void setHapticGeneratorEnabled(boolean enabled) {
- // n/a
- }
-
- @Override
- public void setVolume(float volume) {
- // n/a
- }
-
- @Override
- public boolean hasHapticChannels() {
- return false;
- }
- }
-}
diff --git a/media/java/android/media/OWNERS b/media/java/android/media/OWNERS
index 058c5be..49890c1 100644
--- a/media/java/android/media/OWNERS
+++ b/media/java/android/media/OWNERS
@@ -11,6 +11,3 @@
per-file ExifInterface.java,ExifInterfaceUtils.java,IMediaHTTPConnection.aidl,IMediaHTTPService.aidl,JetPlayer.java,MediaDataSource.java,MediaExtractor.java,MediaHTTPConnection.java,MediaHTTPService.java,MediaPlayer.java=set noparent
per-file ExifInterface.java,ExifInterfaceUtils.java,IMediaHTTPConnection.aidl,IMediaHTTPService.aidl,JetPlayer.java,MediaDataSource.java,MediaExtractor.java,MediaHTTPConnection.java,MediaHTTPService.java,MediaPlayer.java=file:platform/frameworks/av:/media/janitors/media_solutions_OWNERS
-
-# Haptics team also works on Ringtone
-per-file *Ringtone* = file:/services/core/java/com/android/server/vibrator/OWNERS
diff --git a/media/java/android/media/Ringtone.java b/media/java/android/media/Ringtone.java
index 8800dc8..e78dc31 100644
--- a/media/java/android/media/Ringtone.java
+++ b/media/java/android/media/Ringtone.java
@@ -16,31 +16,27 @@
package android.media;
-import android.Manifest;
-import android.annotation.IntDef;
-import android.annotation.NonNull;
import android.annotation.Nullable;
import android.compat.annotation.UnsupportedAppUsage;
import android.content.ContentProvider;
import android.content.ContentResolver;
import android.content.Context;
-import android.content.pm.PackageManager;
+import android.content.res.AssetFileDescriptor;
+import android.content.res.Resources.NotFoundException;
import android.database.Cursor;
import android.media.audiofx.HapticGenerator;
import android.net.Uri;
+import android.os.Binder;
+import android.os.Build;
import android.os.RemoteException;
import android.os.Trace;
-import android.os.VibrationEffect;
-import android.os.Vibrator;
import android.provider.MediaStore;
import android.provider.MediaStore.MediaColumns;
import android.provider.Settings;
import android.util.Log;
-
import com.android.internal.annotations.VisibleForTesting;
-
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
+import java.io.IOException;
+import java.util.ArrayList;
/**
* Ringtone provides a quick method for playing a ringtone, notification, or
@@ -53,39 +49,7 @@
*/
public class Ringtone {
private static final String TAG = "Ringtone";
-
- /**
- * The ringtone should only play sound. Any vibration is managed externally.
- * @hide
- */
- public static final int MEDIA_SOUND = 1;
- /**
- * The ringtone should only play vibration. Any sound is managed externally.
- * Requires the {@link android.Manifest.permission#VIBRATE} permission.
- * @hide
- */
- public static final int MEDIA_VIBRATION = 1 << 1;
- /**
- * The ringtone should play sound and vibration.
- * @hide
- */
- public static final int MEDIA_SOUND_AND_VIBRATION = MEDIA_SOUND | MEDIA_VIBRATION;
-
- // This is not a public value, because apps shouldn't enable "all" media - that wouldn't be
- // safe if new media types were added.
- static final int MEDIA_ALL = MEDIA_SOUND | MEDIA_VIBRATION;
-
- /**
- * Declares the types of media that this Ringtone is allowed to play.
- * @hide
- */
- @Retention(RetentionPolicy.SOURCE)
- @IntDef(prefix = "MEDIA_", value = {
- MEDIA_SOUND,
- MEDIA_VIBRATION,
- MEDIA_SOUND_AND_VIBRATION,
- })
- public @interface RingtoneMedia {}
+ private static final boolean LOGD = true;
private static final String[] MEDIA_COLUMNS = new String[] {
MediaStore.Audio.Media._ID,
@@ -95,70 +59,51 @@
private static final String MEDIA_SELECTION = MediaColumns.MIME_TYPE + " LIKE 'audio/%' OR "
+ MediaColumns.MIME_TYPE + " IN ('application/ogg', 'application/x-flac')";
- // Flag-selected ringtone implementation to use.
- private final ApiInterface mApiImpl;
+ // keep references on active Ringtones until stopped or completion listener called.
+ private static final ArrayList<Ringtone> sActiveRingtones = new ArrayList<Ringtone>();
+
+ private final Context mContext;
+ private final AudioManager mAudioManager;
+ private VolumeShaper.Configuration mVolumeShaperConfig;
+ private VolumeShaper mVolumeShaper;
+
+ /**
+ * Flag indicating if we're allowed to fall back to remote playback using
+ * {@link #mRemotePlayer}. Typically this is false when we're the remote
+ * player and there is nobody else to delegate to.
+ */
+ private final boolean mAllowRemote;
+ private final IRingtonePlayer mRemotePlayer;
+ private final Binder mRemoteToken;
+
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ private MediaPlayer mLocalPlayer;
+ private final MyOnCompletionListener mCompletionListener = new MyOnCompletionListener();
+ private HapticGenerator mHapticGenerator;
+
+ @UnsupportedAppUsage
+ private Uri mUri;
+ private String mTitle;
+
+ private AudioAttributes mAudioAttributes = new AudioAttributes.Builder()
+ .setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE)
+ .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
+ .build();
+ private boolean mPreferBuiltinDevice;
+ // playback properties, use synchronized with mPlaybackSettingsLock
+ private boolean mIsLooping = false;
+ private float mVolume = 1.0f;
+ private boolean mHapticGeneratorEnabled = false;
+ private final Object mPlaybackSettingsLock = new Object();
/** {@hide} */
@UnsupportedAppUsage
public Ringtone(Context context, boolean allowRemote) {
- mApiImpl = new RingtoneV1(context, allowRemote);
- }
-
- /**
- * Constructor for legacy V1 initialization paths using non-public APIs on RingtoneV1.
- */
- private Ringtone(RingtoneV1 ringtoneV1) {
- mApiImpl = ringtoneV1;
- }
-
- private Ringtone(Builder builder, @Ringtone.RingtoneMedia int effectiveEnabledMedia,
- @NonNull AudioAttributes effectiveAudioAttributes,
- @Nullable VibrationEffect effectiveVibrationEffect,
- boolean effectiveHapticGeneratorEnabled) {
- mApiImpl = new RingtoneV2(builder.mContext, builder.mInjectables, builder.mAllowRemote,
- effectiveEnabledMedia, builder.mUri, effectiveAudioAttributes,
- builder.mUseExactAudioAttributes, builder.mVolumeShaperConfig,
- builder.mPreferBuiltinDevice, builder.mInitialSoundVolume, builder.mLooping,
- effectiveHapticGeneratorEnabled, effectiveVibrationEffect);
- }
-
- /**
- * Temporary V1 constructor for legacy V1 paths with audio attributes.
- * @hide
- */
- public static Ringtone createV1WithCustomAudioAttributes(
- Context context, AudioAttributes audioAttributes, Uri uri,
- VolumeShaper.Configuration volumeShaperConfig, boolean allowRemote) {
- RingtoneV1 ringtoneV1 = new RingtoneV1(context, allowRemote);
- ringtoneV1.setAudioAttributesField(audioAttributes);
- ringtoneV1.setUri(uri, volumeShaperConfig);
- ringtoneV1.reinitializeActivePlayer();
- return new Ringtone(ringtoneV1);
- }
-
- /**
- * Temporary V1 constructor for legacy V1 paths with stream type.
- * @hide
- */
- public static Ringtone createV1WithCustomStreamType(
- Context context, int streamType, Uri uri,
- VolumeShaper.Configuration volumeShaperConfig) {
- RingtoneV1 ringtoneV1 = new RingtoneV1(context, /* allowRemote= */ true);
- if (streamType >= 0) {
- ringtoneV1.setStreamType(streamType);
- }
- ringtoneV1.setUri(uri, volumeShaperConfig);
- if (!ringtoneV1.reinitializeActivePlayer()) {
- Log.e(TAG, "Failed to open ringtone " + uri);
- return null;
- }
- return new Ringtone(ringtoneV1);
- }
-
- /** @hide */
- @RingtoneMedia
- public int getEnabledMedia() {
- return mApiImpl.getEnabledMedia();
+ mContext = context;
+ mAudioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
+ mAllowRemote = allowRemote;
+ mRemotePlayer = allowRemote ? mAudioManager.getRingtonePlayer() : null;
+ mRemoteToken = allowRemote ? new Binder() : null;
}
/**
@@ -169,7 +114,10 @@
*/
@Deprecated
public void setStreamType(int streamType) {
- mApiImpl.setStreamType(streamType);
+ PlayerBase.deprecateStreamTypeForPlayback(streamType, "Ringtone", "setStreamType()");
+ setAudioAttributes(new AudioAttributes.Builder()
+ .setInternalLegacyStreamType(streamType)
+ .build());
}
/**
@@ -181,7 +129,7 @@
*/
@Deprecated
public int getStreamType() {
- return mApiImpl.getStreamType();
+ return AudioAttributes.toLegacyStreamType(mAudioAttributes);
}
/**
@@ -190,45 +138,54 @@
*/
public void setAudioAttributes(AudioAttributes attributes)
throws IllegalArgumentException {
- mApiImpl.setAudioAttributes(attributes);
+ setAudioAttributesField(attributes);
+ // The audio attributes have to be set before the media player is prepared.
+ // Re-initialize it.
+ setUri(mUri, mVolumeShaperConfig);
+ createLocalMediaPlayer();
}
/**
- * Returns the vibration effect that this ringtone was created with, if vibration is enabled.
- * Otherwise, returns null.
+ * Same as {@link #setAudioAttributes(AudioAttributes)} except this one does not create
+ * the media player.
* @hide
*/
- @Nullable
- public VibrationEffect getVibrationEffect() {
- return mApiImpl.getVibrationEffect();
- }
-
- /** @hide */
- @VisibleForTesting
- public boolean getPreferBuiltinDevice() {
- return mApiImpl.getPreferBuiltinDevice();
- }
-
- /** @hide */
- @VisibleForTesting
- public VolumeShaper.Configuration getVolumeShaperConfig() {
- return mApiImpl.getVolumeShaperConfig();
+ public void setAudioAttributesField(@Nullable AudioAttributes attributes) {
+ if (attributes == null) {
+ throw new IllegalArgumentException("Invalid null AudioAttributes for Ringtone");
+ }
+ mAudioAttributes = attributes;
}
/**
- * Returns whether this player is local only, or can defer to the remote player. The
- * result may differ from the builder if there is no remote player available at all.
- * @hide
+ * Finds the output device of type {@link AudioDeviceInfo#TYPE_BUILTIN_SPEAKER}. This device is
+ * the one on which outgoing audio for SIM calls is played.
+ *
+ * @param audioManager the audio manage.
+ * @return the {@link AudioDeviceInfo} corresponding to the builtin device, or {@code null} if
+ * none can be found.
*/
- @VisibleForTesting
- public boolean isLocalOnly() {
- return mApiImpl.isLocalOnly();
+ private AudioDeviceInfo getBuiltinDevice(AudioManager audioManager) {
+ AudioDeviceInfo[] deviceList = audioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS);
+ for (AudioDeviceInfo device : deviceList) {
+ if (device.getType() == AudioDeviceInfo.TYPE_BUILTIN_SPEAKER) {
+ return device;
+ }
+ }
+ return null;
}
- /** @hide */
- @VisibleForTesting
- public boolean isUsingRemotePlayer() {
- return mApiImpl.isUsingRemotePlayer();
+ /**
+ * Sets the preferred device of the ringtong playback to the built-in device.
+ *
+ * @hide
+ */
+ public boolean preferBuiltinDevice(boolean enable) {
+ mPreferBuiltinDevice = enable;
+ if (mLocalPlayer == null) {
+ return true;
+ }
+ return mLocalPlayer.setPreferredDevice(getBuiltinDevice(mAudioManager));
}
/**
@@ -237,16 +194,76 @@
* false if it did not succeed and can't be tried remotely.
* @hide
*/
- public boolean reinitializeActivePlayer() {
- return mApiImpl.reinitializeActivePlayer();
+ public boolean createLocalMediaPlayer() {
+ Trace.beginSection("createLocalMediaPlayer");
+ if (mUri == null) {
+ Log.e(TAG, "Could not create media player as no URI was provided.");
+ return mAllowRemote && mRemotePlayer != null;
+ }
+ destroyLocalPlayer();
+ // try opening uri locally before delegating to remote player
+ mLocalPlayer = new MediaPlayer();
+ try {
+ mLocalPlayer.setDataSource(mContext, mUri);
+ mLocalPlayer.setAudioAttributes(mAudioAttributes);
+ mLocalPlayer.setPreferredDevice(
+ mPreferBuiltinDevice ? getBuiltinDevice(mAudioManager) : null);
+ synchronized (mPlaybackSettingsLock) {
+ applyPlaybackProperties_sync();
+ }
+ if (mVolumeShaperConfig != null) {
+ mVolumeShaper = mLocalPlayer.createVolumeShaper(mVolumeShaperConfig);
+ }
+ mLocalPlayer.prepare();
+
+ } catch (SecurityException | IOException e) {
+ destroyLocalPlayer();
+ if (!mAllowRemote) {
+ Log.w(TAG, "Remote playback not allowed: " + e);
+ }
+ }
+
+ if (LOGD) {
+ if (mLocalPlayer != null) {
+ Log.d(TAG, "Successfully created local player");
+ } else {
+ Log.d(TAG, "Problem opening; delegating to remote player");
+ }
+ }
+ Trace.endSection();
+ return mLocalPlayer != null || (mAllowRemote && mRemotePlayer != null);
}
/**
* Same as AudioManager.hasHapticChannels except it assumes an already created ringtone.
+ * If the ringtone has not been created, it will load based on URI provided at {@link #setUri}
+ * and if not URI has been set, it will assume no haptic channels are present.
* @hide
*/
public boolean hasHapticChannels() {
- return mApiImpl.hasHapticChannels();
+ // FIXME: support remote player, or internalize haptic channels support and remove entirely.
+ try {
+ android.os.Trace.beginSection("Ringtone.hasHapticChannels");
+ if (mLocalPlayer != null) {
+ for(MediaPlayer.TrackInfo trackInfo : mLocalPlayer.getTrackInfo()) {
+ if (trackInfo.hasHapticChannels()) {
+ return true;
+ }
+ }
+ }
+ } finally {
+ android.os.Trace.endSection();
+ }
+ return false;
+ }
+
+ /**
+ * Returns whether a local player has been created for this ringtone.
+ * @hide
+ */
+ @VisibleForTesting
+ public boolean hasLocalPlayer() {
+ return mLocalPlayer != null;
}
/**
@@ -255,7 +272,7 @@
* {@link #setAudioAttributes(AudioAttributes)} or the default attributes if none were set.
*/
public AudioAttributes getAudioAttributes() {
- return mApiImpl.getAudioAttributes();
+ return mAudioAttributes;
}
/**
@@ -263,7 +280,10 @@
* @param looping whether to loop or not.
*/
public void setLooping(boolean looping) {
- mApiImpl.setLooping(looping);
+ synchronized (mPlaybackSettingsLock) {
+ mIsLooping = looping;
+ applyPlaybackProperties_sync();
+ }
}
/**
@@ -271,7 +291,9 @@
* @return true if this player loops when playing.
*/
public boolean isLooping() {
- return mApiImpl.isLooping();
+ synchronized (mPlaybackSettingsLock) {
+ return mIsLooping;
+ }
}
/**
@@ -280,7 +302,12 @@
* corresponds to no attenuation being applied.
*/
public void setVolume(float volume) {
- mApiImpl.setVolume(volume);
+ synchronized (mPlaybackSettingsLock) {
+ if (volume < 0.0f) { volume = 0.0f; }
+ if (volume > 1.0f) { volume = 1.0f; }
+ mVolume = volume;
+ applyPlaybackProperties_sync();
+ }
}
/**
@@ -288,7 +315,9 @@
* @return a value between 0.0f and 1.0f.
*/
public float getVolume() {
- return mApiImpl.getVolume();
+ synchronized (mPlaybackSettingsLock) {
+ return mVolume;
+ }
}
/**
@@ -299,7 +328,14 @@
* @see android.media.audiofx.HapticGenerator#isAvailable()
*/
public boolean setHapticGeneratorEnabled(boolean enabled) {
- return mApiImpl.setHapticGeneratorEnabled(enabled);
+ if (!HapticGenerator.isAvailable()) {
+ return false;
+ }
+ synchronized (mPlaybackSettingsLock) {
+ mHapticGeneratorEnabled = enabled;
+ applyPlaybackProperties_sync();
+ }
+ return true;
}
/**
@@ -307,7 +343,35 @@
* @return true if the HapticGenerator is enabled.
*/
public boolean isHapticGeneratorEnabled() {
- return mApiImpl.isHapticGeneratorEnabled();
+ synchronized (mPlaybackSettingsLock) {
+ return mHapticGeneratorEnabled;
+ }
+ }
+
+ /**
+ * Must be called synchronized on mPlaybackSettingsLock
+ */
+ private void applyPlaybackProperties_sync() {
+ if (mLocalPlayer != null) {
+ mLocalPlayer.setVolume(mVolume);
+ mLocalPlayer.setLooping(mIsLooping);
+ if (mHapticGenerator == null && mHapticGeneratorEnabled) {
+ mHapticGenerator = HapticGenerator.create(mLocalPlayer.getAudioSessionId());
+ }
+ if (mHapticGenerator != null) {
+ mHapticGenerator.setEnabled(mHapticGeneratorEnabled);
+ }
+ } else if (mAllowRemote && (mRemotePlayer != null)) {
+ try {
+ mRemotePlayer.setPlaybackProperties(
+ mRemoteToken, mVolume, mIsLooping, mHapticGeneratorEnabled);
+ } catch (RemoteException e) {
+ Log.w(TAG, "Problem setting playback properties: ", e);
+ }
+ } else {
+ Log.w(TAG,
+ "Neither local nor remote player available when applying playback properties");
+ }
}
/**
@@ -317,7 +381,8 @@
* @param context A context used for querying.
*/
public String getTitle(Context context) {
- return mApiImpl.getTitle(context);
+ if (mTitle != null) return mTitle;
+ return mTitle = getTitle(context, mUri, true /*followSettingsUri*/, mAllowRemote);
}
/**
@@ -391,24 +456,126 @@
return title;
}
+ /**
+ * Set {@link Uri} to be used for ringtone playback.
+ * {@link IRingtonePlayer}.
+ *
+ * @hide
+ */
+ @UnsupportedAppUsage
+ public void setUri(Uri uri) {
+ setUri(uri, null);
+ }
+
+ /**
+ * @hide
+ */
+ public void setVolumeShaperConfig(@Nullable VolumeShaper.Configuration volumeShaperConfig) {
+ mVolumeShaperConfig = volumeShaperConfig;
+ }
+
+ /**
+ * Set {@link Uri} to be used for ringtone playback. Attempts to open
+ * locally, otherwise will delegate playback to remote
+ * {@link IRingtonePlayer}. Add {@link VolumeShaper} if required.
+ *
+ * @hide
+ */
+ public void setUri(Uri uri, @Nullable VolumeShaper.Configuration volumeShaperConfig) {
+ mVolumeShaperConfig = volumeShaperConfig;
+ mUri = uri;
+ if (mUri == null) {
+ destroyLocalPlayer();
+ }
+ }
+
/** {@hide} */
@UnsupportedAppUsage
public Uri getUri() {
- return mApiImpl.getUri();
+ return mUri;
}
/**
* Plays the ringtone.
*/
public void play() {
- mApiImpl.play();
+ if (mLocalPlayer != null) {
+ // Play ringtones if stream volume is over 0 or if it is a haptic-only ringtone
+ // (typically because ringer mode is vibrate).
+ if (mAudioManager.getStreamVolume(AudioAttributes.toLegacyStreamType(mAudioAttributes))
+ != 0) {
+ startLocalPlayer();
+ } else if (!mAudioAttributes.areHapticChannelsMuted() && hasHapticChannels()) {
+ // is haptic only ringtone
+ startLocalPlayer();
+ }
+ } else if (mAllowRemote && (mRemotePlayer != null) && (mUri != null)) {
+ final Uri canonicalUri = mUri.getCanonicalUri();
+ final boolean looping;
+ final float volume;
+ synchronized (mPlaybackSettingsLock) {
+ looping = mIsLooping;
+ volume = mVolume;
+ }
+ try {
+ mRemotePlayer.playWithVolumeShaping(mRemoteToken, canonicalUri, mAudioAttributes,
+ volume, looping, mVolumeShaperConfig);
+ } catch (RemoteException e) {
+ if (!playFallbackRingtone()) {
+ Log.w(TAG, "Problem playing ringtone: " + e);
+ }
+ }
+ } else {
+ if (!playFallbackRingtone()) {
+ Log.w(TAG, "Neither local nor remote playback available");
+ }
+ }
}
/**
* Stops a playing ringtone.
*/
public void stop() {
- mApiImpl.stop();
+ if (mLocalPlayer != null) {
+ destroyLocalPlayer();
+ } else if (mAllowRemote && (mRemotePlayer != null)) {
+ try {
+ mRemotePlayer.stop(mRemoteToken);
+ } catch (RemoteException e) {
+ Log.w(TAG, "Problem stopping ringtone: " + e);
+ }
+ }
+ }
+
+ private void destroyLocalPlayer() {
+ if (mLocalPlayer != null) {
+ if (mHapticGenerator != null) {
+ mHapticGenerator.release();
+ mHapticGenerator = null;
+ }
+ mLocalPlayer.setOnCompletionListener(null);
+ mLocalPlayer.reset();
+ mLocalPlayer.release();
+ mLocalPlayer = null;
+ mVolumeShaper = null;
+ synchronized (sActiveRingtones) {
+ sActiveRingtones.remove(this);
+ }
+ }
+ }
+
+ private void startLocalPlayer() {
+ if (mLocalPlayer == null) {
+ return;
+ }
+ synchronized (sActiveRingtones) {
+ sActiveRingtones.add(this);
+ }
+ mLocalPlayer.setOnCompletionListener(mCompletionListener);
+ mLocalPlayer.start();
+ if (mVolumeShaper != null) {
+ mVolumeShaper.apply(VolumeShaper.Operation.PLAY);
+ }
}
/**
@@ -417,353 +584,87 @@
* @return True if playing, false otherwise.
*/
public boolean isPlaying() {
- return mApiImpl.isPlaying();
- }
-
- /**
- * Build a {@link Ringtone} to easily play sounds for ringtones, alarms and notifications.
- *
- * TODO: when un-hide, deprecate Ringtone: setAudioAttributes, setLooping,
- * setHapticGeneratorEnabled (no-effect if MEDIA_VIBRATION),
- * static RingtoneManager.getRingtone.
- * @hide
- */
- public static final class Builder {
- private final Context mContext;
- private final int mEnabledMedia;
- private Uri mUri;
- private final AudioAttributes mAudioAttributes;
- private boolean mUseExactAudioAttributes = false;
- // Not a static default since it doesn't really need to be in memory forever.
- private Injectables mInjectables = new Injectables();
- private VolumeShaper.Configuration mVolumeShaperConfig;
- private boolean mPreferBuiltinDevice = false;
- private boolean mAllowRemote = true;
- private boolean mHapticGeneratorEnabled = false;
- private float mInitialSoundVolume = 1.0f;
- private boolean mLooping = false;
- private VibrationEffect mVibrationEffect;
-
- /**
- * Constructs a builder to play the given media types from the mediaUri. If the mediaUri
- * is null (for example, an unset-setting), then fallback logic will dictate what plays.
- *
- * <p>When built, if the ringtone is already known to be a no-op, such as explicitly
- * silent, then the {@link #build} may return null.
- *
- * @param context The context for playing the ringtone.
- * @param enabledMedia Which media to play. Media not included is implicitly muted. Device
- * settings such as volume and vibrate-only may also affect which
- * media is played.
- * @param audioAttributes The attributes to use for playback, which affects the volumes and
- * settings that are applied.
- */
- public Builder(@NonNull Context context, @RingtoneMedia int enabledMedia,
- @NonNull AudioAttributes audioAttributes) {
- mContext = context;
- mEnabledMedia = enabledMedia;
- mAudioAttributes = audioAttributes;
- }
-
- /**
- * Inject test intercepters for static methods.
- * @hide
- */
- @NonNull
- public Builder setInjectables(Injectables injectables) {
- mInjectables = injectables;
- return this;
- }
-
- /**
- * The uri for the ringtone media to play. This is typically a user's preference for the
- * sound. If null, then it is treated as though the user's preference is unset and
- * fallback behavior, such as using the default ringtone setting, are used instead.
- *
- * When sound media is enabled, this is assumed to be a sound URI.
- */
- @NonNull
- public Builder setUri(@Nullable Uri uri) {
- mUri = uri;
- return this;
- }
-
- /**
- * Sets the VibrationEffect to use if vibration is enabled on this ringtone. The caller
- * should use {@link android.os.Vibrator#areVibrationFeaturesSupported} to ensure
- * that the effect is usable on this device, otherwise system defaults will be used.
- *
- * <p>Vibration will only happen if the Builder was created with media type
- * {@link Ringtone#MEDIA_VIBRATION} or {@link Ringtone#MEDIA_SOUND_AND_VIBRATION}, and
- * the application has the {@link android.Manifest.permission#VIBRATE} permission.
- *
- * <p>If the Ringtone is looping when it is played, then the VibrationEffect will be
- * modified to loop. Similarly, if the ringtone is not looping, a repeating
- * VibrationEffect will be modified to be non-repeating when the ringtone is played. Calls
- * to {@link Ringtone#setLooping} after the ringtone has started playing will stop a looping
- * vibration, but has no effect otherwise: specifically it will not restart vibration.
- */
- @NonNull
- public Builder setVibrationEffect(@NonNull VibrationEffect effect) {
- mVibrationEffect = effect;
- return this;
- }
-
- /**
- * Sets whether the resulting ringtone should loop until {@link Ringtone#stop()} is called,
- * or just play once.
- */
- @NonNull
- public Builder setLooping(boolean looping) {
- mLooping = looping;
- return this;
- }
-
- /**
- * Sets the VolumeShaper.Configuration to apply to the ringtone.
- * @hide
- */
- @NonNull
- public Builder setVolumeShaperConfig(
- @Nullable VolumeShaper.Configuration volumeShaperConfig) {
- mVolumeShaperConfig = volumeShaperConfig;
- return this;
- }
-
- /**
- * Whether to enable or disable the haptic generator.
- * @hide
- */
- @NonNull
- public Builder setEnableHapticGenerator(boolean enabled) {
- // Note that this property is mutable (but deprecated) on the Ringtone class itself.
- mHapticGeneratorEnabled = enabled;
- return this;
- }
-
- /**
- * Sets the initial sound volume for the ringtone.
- */
- @NonNull
- public Builder setInitialSoundVolume(float initialSoundVolume) {
- mInitialSoundVolume = initialSoundVolume;
- return this;
- }
-
- /**
- * Sets the preferred device of the ringtone playback to the built-in device. This is
- * only for use by the system server with known-good Uris.
- * @hide
- */
- @NonNull
- public Builder setPreferBuiltinDevice() {
- mPreferBuiltinDevice = true;
- mAllowRemote = false; // Already in system.
- return this;
- }
-
- /**
- * Indicates that {@link AudioAttributes#areHapticChannelsMuted()} on the builder's
- * AudioAttributes should not be overridden. This is used to enable legacy behavior of
- * calling {@link Ringtone#setAudioAttributes} on an already-created ringtone, and can in
- * turn cause vibration during a "sound-only" session or can suppress audio-coupled
- * haptics that would usually take priority (therefore potentially falling back to
- * the VibrationEffect or system defaults).
- *
- * <p>Without this setting, the haptic channels will be automatically muted or not by the
- * Ringtone according to whether vibration is enabled or not.
- *
- * <p>This is for internal-use only. New applications should configure the vibration
- * behavior explicitly with the (TODO: future RingtoneSetting.setVibrationSource).
- * Handling haptic channels outside Ringtone leads to extra loads of the sound uri.
- * @hide
- */
- @NonNull
- public Builder setUseExactAudioAttributes(boolean useExactAttrs) {
- mUseExactAudioAttributes = useExactAttrs;
- return this;
- }
-
- /**
- * Prevent fallback to the remote service. This is primarily intended for use within the
- * remote IRingtonePlayer service itself, to avoid loops.
- * @hide
- */
- @NonNull
- public Builder setLocalOnly() {
- mAllowRemote = false;
- return this;
- }
-
- private boolean isVibrationEnabledAndAvailable() {
- if ((mEnabledMedia & MEDIA_VIBRATION) == 0) {
- return false;
- }
- Vibrator vibrator = mContext.getSystemService(Vibrator.class);
- if (!vibrator.hasVibrator()) {
- return false;
- }
- if (mContext.checkSelfPermission(Manifest.permission.VIBRATE)
- != PackageManager.PERMISSION_GRANTED) {
- Log.w(TAG, "Ringtone requests vibration enabled, but no VIBRATE permission");
- return false;
- }
- return true;
- }
-
- /**
- * Returns the built Ringtone, or null if there was a problem loading the Uri and there
- * are no fallback options available.
- */
- @Nullable
- public Ringtone build() {
- @Ringtone.RingtoneMedia int effectiveEnabledMedia = mEnabledMedia;
- VibrationEffect effectiveVibrationEffect = mVibrationEffect;
-
- // Normalize media to that supported on this SDK level.
- if (effectiveEnabledMedia != (effectiveEnabledMedia & MEDIA_ALL)) {
- Log.e(TAG, "Unsupported media type: " + effectiveEnabledMedia);
- effectiveEnabledMedia = effectiveEnabledMedia & MEDIA_ALL;
- }
- final boolean effectiveHapticGenerator;
- final boolean hapticChannelsSupported;
- AudioAttributes effectiveAudioAttributes = mAudioAttributes;
- final boolean hapticChannelsMuted = mAudioAttributes.areHapticChannelsMuted();
- if (!isVibrationEnabledAndAvailable()) {
- // Vibration isn't active: turn off everything that might cause extra work.
- effectiveEnabledMedia &= ~MEDIA_VIBRATION;
- effectiveHapticGenerator = false;
- effectiveVibrationEffect = null;
- if (!mUseExactAudioAttributes && !hapticChannelsMuted) {
- effectiveAudioAttributes = new AudioAttributes.Builder(effectiveAudioAttributes)
- .setHapticChannelsMuted(true)
- .build();
- }
- } else {
- // Vibration is active.
- effectiveHapticGenerator =
- mHapticGeneratorEnabled && mInjectables.isHapticGeneratorAvailable();
- hapticChannelsSupported = mInjectables.isHapticPlaybackSupported();
- // Haptic channels are preferred if they are available, and not explicitly muted.
- // We won't know if haptic channels are available until loading the media player,
- // and since the media player needs to be reset to change audio attributes, then
- // we proactively enable the channels - it won't matter if they aren't present.
- if (!mUseExactAudioAttributes) {
- boolean shouldBeMuted = effectiveHapticGenerator || !hapticChannelsSupported;
- if (shouldBeMuted != hapticChannelsMuted) {
- effectiveAudioAttributes =
- new AudioAttributes.Builder(effectiveAudioAttributes)
- .setHapticChannelsMuted(shouldBeMuted)
- .build();
- }
- }
- // If no contextual vibration, then try loading the default one for the URI.
- if (mVibrationEffect == null && mUri != null) {
- effectiveVibrationEffect = VibrationEffect.get(mUri, mContext);
- }
- }
+ if (mLocalPlayer != null) {
+ return mLocalPlayer.isPlaying();
+ } else if (mAllowRemote && (mRemotePlayer != null)) {
try {
- Ringtone ringtone = new Ringtone(this, effectiveEnabledMedia,
- effectiveAudioAttributes, effectiveVibrationEffect,
- effectiveHapticGenerator);
- if (ringtone.reinitializeActivePlayer()) {
- return ringtone;
- } else {
- Log.e(TAG, "Failed to open ringtone " + mUri);
- return null;
- }
- } catch (Exception ex) {
- // Catching Exception isn't great, but was done in the old
- // RingtoneManager.getRingtone and hides errors like DocumentsProvider throwing
- // IllegalArgumentException instead of FileNotFoundException, and also robolectric
- // failures when ShadowMediaPlayer wasn't pre-informed of the ringtone.
- Log.e(TAG, "Failed while opening ringtone " + mUri, ex);
- return null;
+ return mRemotePlayer.isPlaying(mRemoteToken);
+ } catch (RemoteException e) {
+ Log.w(TAG, "Problem checking ringtone: " + e);
+ return false;
}
- }
- }
-
- /**
- * Interface for intercepting static methods and constructors, for unit testing only.
- * @hide
- */
- @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
- public static class Injectables {
- /** Intercept {@code new MediaPlayer()}. */
- @NonNull
- public MediaPlayer newMediaPlayer() {
- return new MediaPlayer();
- }
-
- /** Intercept {@link HapticGenerator#isAvailable}. */
- public boolean isHapticGeneratorAvailable() {
- return HapticGenerator.isAvailable();
- }
-
- /**
- * Intercept {@link HapticGenerator#create} using
- * {@link MediaPlayer#getAudioSessionId()} from the given media player.
- */
- @Nullable
- public HapticGenerator createHapticGenerator(@NonNull MediaPlayer mediaPlayer) {
- return HapticGenerator.create(mediaPlayer.getAudioSessionId());
- }
-
- /** Returns the result of {@link AudioManager#isHapticPlaybackSupported()}. */
- public boolean isHapticPlaybackSupported() {
- return AudioManager.isHapticPlaybackSupported();
- }
-
- /**
- * Returns whether the MediaPlayer tracks have haptic channels. This is the same as
- * AudioManager.hasHapticChannels, except it uses an already prepared MediaPlayer to avoid
- * loading the metadata a second time.
- */
- public boolean hasHapticChannels(MediaPlayer mp) {
- try {
- Trace.beginSection("Ringtone.hasHapticChannels");
- for (MediaPlayer.TrackInfo trackInfo : mp.getTrackInfo()) {
- if (trackInfo.hasHapticChannels()) {
- return true;
- }
- }
- } finally {
- Trace.endSection();
- }
+ } else {
+ Log.w(TAG, "Neither local nor remote playback available");
return false;
}
-
}
- /**
- * Interface for alternative Ringtone implementations. See the public Ringtone methods that
- * delegate to these for documentation.
- * @hide
- */
- interface ApiInterface {
- void setStreamType(int streamType);
- int getStreamType();
- void setAudioAttributes(AudioAttributes attributes);
- boolean getPreferBuiltinDevice();
- VolumeShaper.Configuration getVolumeShaperConfig();
- boolean isLocalOnly();
- boolean isUsingRemotePlayer();
- boolean reinitializeActivePlayer();
- boolean hasHapticChannels();
- AudioAttributes getAudioAttributes();
- void setLooping(boolean looping);
- boolean isLooping();
- void setVolume(float volume);
- float getVolume();
- boolean setHapticGeneratorEnabled(boolean enabled);
- boolean isHapticGeneratorEnabled();
- String getTitle(Context context);
- Uri getUri();
- void play();
- void stop();
- boolean isPlaying();
- // V2 future-public methods
- @RingtoneMedia int getEnabledMedia();
- VibrationEffect getVibrationEffect();
+ private boolean playFallbackRingtone() {
+ int streamType = AudioAttributes.toLegacyStreamType(mAudioAttributes);
+ if (mAudioManager.getStreamVolume(streamType) == 0) {
+ return false;
+ }
+ int ringtoneType = RingtoneManager.getDefaultType(mUri);
+ if (ringtoneType != -1 &&
+ RingtoneManager.getActualDefaultRingtoneUri(mContext, ringtoneType) == null) {
+ Log.w(TAG, "not playing fallback for " + mUri);
+ return false;
+ }
+ // Default ringtone, try fallback ringtone.
+ try {
+ AssetFileDescriptor afd = mContext.getResources().openRawResourceFd(
+ com.android.internal.R.raw.fallbackring);
+ if (afd == null) {
+ Log.e(TAG, "Could not load fallback ringtone");
+ return false;
+ }
+ mLocalPlayer = new MediaPlayer();
+ if (afd.getDeclaredLength() < 0) {
+ mLocalPlayer.setDataSource(afd.getFileDescriptor());
+ } else {
+ mLocalPlayer.setDataSource(afd.getFileDescriptor(),
+ afd.getStartOffset(),
+ afd.getDeclaredLength());
+ }
+ mLocalPlayer.setAudioAttributes(mAudioAttributes);
+ synchronized (mPlaybackSettingsLock) {
+ applyPlaybackProperties_sync();
+ }
+ if (mVolumeShaperConfig != null) {
+ mVolumeShaper = mLocalPlayer.createVolumeShaper(mVolumeShaperConfig);
+ }
+ mLocalPlayer.prepare();
+ startLocalPlayer();
+ afd.close();
+ } catch (IOException ioe) {
+ destroyLocalPlayer();
+ Log.e(TAG, "Failed to open fallback ringtone");
+ return false;
+ } catch (NotFoundException nfe) {
+ Log.e(TAG, "Fallback ringtone does not exist");
+ return false;
+ }
+ return true;
+ }
+
+ void setTitle(String title) {
+ mTitle = title;
+ }
+
+ @Override
+ protected void finalize() {
+ if (mLocalPlayer != null) {
+ mLocalPlayer.release();
+ }
+ }
+
+ class MyOnCompletionListener implements MediaPlayer.OnCompletionListener {
+ @Override
+ public void onCompletion(MediaPlayer mp) {
+ synchronized (sActiveRingtones) {
+ sActiveRingtones.remove(Ringtone.this);
+ }
+ mp.setOnCompletionListener(null); // Help the Java GC: break the refcount cycle.
+ }
}
}
diff --git a/media/java/android/media/RingtoneManager.java b/media/java/android/media/RingtoneManager.java
index b5a9ae2..3432b3f 100644
--- a/media/java/android/media/RingtoneManager.java
+++ b/media/java/android/media/RingtoneManager.java
@@ -16,7 +16,7 @@
package android.media;
-import android.annotation.IntDef;
+import android.Manifest;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.RequiresPermission;
@@ -30,27 +30,24 @@
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.Context;
-import android.content.Intent;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.pm.UserInfo;
import android.content.res.AssetFileDescriptor;
import android.database.Cursor;
import android.database.StaleDataException;
import android.net.Uri;
+import android.os.Build;
import android.os.Environment;
import android.os.FileUtils;
import android.os.SystemProperties;
import android.os.UserHandle;
import android.os.UserManager;
-import android.os.vibrator.Flags;
-import android.os.vibrator.persistence.VibrationXmlParser;
import android.provider.BaseColumns;
import android.provider.MediaStore;
import android.provider.MediaStore.Audio.AudioColumns;
import android.provider.MediaStore.MediaColumns;
import android.provider.Settings;
import android.provider.Settings.System;
-import android.text.TextUtils;
import android.util.Log;
import com.android.internal.database.SortCursor;
@@ -61,8 +58,6 @@
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.List;
@@ -122,53 +117,6 @@
public static final String ACTION_RINGTONE_PICKER = "android.intent.action.RINGTONE_PICKER";
/**
- * Given to the ringtone picker as a string that represents the category of ringtone picker that
- * should be used. This value should also be returned once a ringtone is selected.
- * <p>
- * The categories are:
- * <li>{@link #CATEGORY_RINGTONE_PICKER_SOUND}
- * <li>{@link #CATEGORY_RINGTONE_PICKER_VIBRATION}
- * <li>{@link #CATEGORY_RINGTONE_PICKER_RINGTONE}
- * <li>{@link Intent#CATEGORY_DEFAULT}
- *
- * <p> If the category is {@link Intent#CATEGORY_DEFAULT} or absent, then the picker will
- * default to a sound-only ringtone picker.
- *
- * <p> If the selected category was not supported, then the returned category will be null.
- *
- * @hide
- */
- public static final String EXTRA_RINGTONE_PICKER_CATEGORY =
- "android.intent.extra.ringtone.RINGTONE_PICKER_CATEGORY";
-
- /**
- * A sound-only ringtone picker.
- *
- * @hide
- * @see #EXTRA_RINGTONE_PICKER_CATEGORY
- */
- public static final String CATEGORY_RINGTONE_PICKER_SOUND =
- "android.net.category.RINGTONE_PICKER_SOUND";
-
- /**
- * A vibration-only ringtone picker.
- *
- * @hide
- * @see #EXTRA_RINGTONE_PICKER_CATEGORY
- */
- public static final String CATEGORY_RINGTONE_PICKER_VIBRATION =
- "android.net.category.RINGTONE_PICKER_VIBRATION";
-
- /**
- * A combined sound and vibration ringtone picker.
- *
- * @hide
- * @see #EXTRA_RINGTONE_PICKER_CATEGORY
- */
- public static final String CATEGORY_RINGTONE_PICKER_RINGTONE =
- "android.net.category.RINGTONE_PICKER_RINGTONE";
-
- /**
* Given to the ringtone picker as a boolean. Whether to show an item for
* "Default".
*
@@ -209,18 +157,6 @@
*/
public static final String EXTRA_RINGTONE_EXISTING_URI =
"android.intent.extra.ringtone.EXISTING_URI";
-
- /**
- * Similar to #EXTRA_RINGTONE_EXISTING_URI but the {@link Uri} can include both sound and
- * vibration.
- * <p>This can include silent sound/vibration explicitly by setting that part of the URI to
- * null.
- *
- * @hide
- * @see #ACTION_RINGTONE_PICKER
- */
- public static final String EXTRA_RINGTONE_EXISTING_RINGTONE_URI =
- "android.intent.extra.ringtone.RINGTONE_EXISTING_RINGTONE_URI";
/**
* Given to the ringtone picker as a {@link Uri}. The {@link Uri} of the
@@ -273,30 +209,21 @@
*/
public static final String EXTRA_RINGTONE_PICKED_URI =
"android.intent.extra.ringtone.PICKED_URI";
-
- /**
- * Declares the allowed types of media for this RingtoneManager.
- * @hide
- */
- @Retention(RetentionPolicy.SOURCE)
- @IntDef(prefix = "MEDIA_", value = {
- Ringtone.MEDIA_SOUND,
- Ringtone.MEDIA_VIBRATION,
- })
- public @interface MediaType {}
-
+
// Make sure the column ordering and then ..._COLUMN_INDEX are in sync
- private static final String[] MEDIA_AUDIO_COLUMNS = new String[] {
+ private static final String[] INTERNAL_COLUMNS = new String[] {
MediaStore.Audio.Media._ID,
MediaStore.Audio.Media.TITLE,
MediaStore.Audio.Media.TITLE,
MediaStore.Audio.Media.TITLE_KEY,
};
- private static final String[] MEDIA_VIBRATION_COLUMNS = new String[]{
- MediaStore.Files.FileColumns._ID,
- MediaStore.Files.FileColumns.TITLE,
+ private static final String[] MEDIA_COLUMNS = new String[] {
+ MediaStore.Audio.Media._ID,
+ MediaStore.Audio.Media.TITLE,
+ MediaStore.Audio.Media.TITLE,
+ MediaStore.Audio.Media.TITLE_KEY,
};
/**
@@ -324,9 +251,7 @@
private Cursor mCursor;
private int mType = TYPE_RINGTONE;
- @MediaType
- private int mMediaType = Ringtone.MEDIA_SOUND;
-
+
/**
* If a column (item from this list) exists in the Cursor, its value must
* be true (value of 1) for the row to be returned.
@@ -393,41 +318,6 @@
}
/**
- * Sets the media type that will be listed by the RingtoneManager.
- *
- * <p>This method should be called before calling {@link RingtoneManager#getCursor()}.
- *
- * @hide
- */
- public void setMediaType(@MediaType int mediaType) {
- if (mCursor != null) {
- throw new IllegalStateException(
- "Setting media should be done before calling getCursor().");
- }
-
- switch (mediaType) {
- case Ringtone.MEDIA_SOUND:
- case Ringtone.MEDIA_VIBRATION:
- mMediaType = mediaType;
- break;
- default:
- throw new IllegalArgumentException("Unsupported media type " + mediaType);
- }
- }
-
- /**
- * Returns the RingtoneManagers media type.
- *
- * @return the media type.
- * @see #setMediaType
- * @hide
- */
- @MediaType
- public int getMediaType() {
- return mMediaType;
- }
-
- /**
* Sets which type(s) of ringtones will be listed by this.
*
* @param type The type(s), one or more of {@link #TYPE_RINGTONE},
@@ -465,25 +355,6 @@
}
}
- /** @hide */
- @NonNull
- public static AudioAttributes getDefaultAudioAttributes(int ringtoneType) {
- AudioAttributes.Builder builder = new AudioAttributes.Builder();
- switch (ringtoneType) {
- case TYPE_ALARM:
- builder.setUsage(AudioAttributes.USAGE_ALARM);
- break;
- case TYPE_NOTIFICATION:
- builder.setUsage(AudioAttributes.USAGE_NOTIFICATION);
- break;
- default: // ringtone or all
- builder.setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE);
- break;
- }
- builder.setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION);
- return builder.build();
- }
-
/**
* Whether retrieving another {@link Ringtone} will stop playing the
* previously retrieved {@link Ringtone}.
@@ -564,19 +435,19 @@
return mCursor;
}
- ArrayList<Cursor> cursors = new ArrayList<>();
-
- cursors.add(queryMediaStore(/* internal= */ true));
- cursors.add(queryMediaStore(/* internal= */ false));
+ ArrayList<Cursor> ringtoneCursors = new ArrayList<Cursor>();
+ ringtoneCursors.add(getInternalRingtones());
+ ringtoneCursors.add(getMediaRingtones());
if (mIncludeParentRingtones) {
Cursor parentRingtonesCursor = getParentProfileRingtones();
if (parentRingtonesCursor != null) {
- cursors.add(parentRingtonesCursor);
+ ringtoneCursors.add(parentRingtonesCursor);
}
}
- return mCursor = new SortCursor(cursors.toArray(new Cursor[cursors.size()]),
- getSortOrderForMedia(mMediaType));
+
+ return mCursor = new SortCursor(ringtoneCursors.toArray(new Cursor[ringtoneCursors.size()]),
+ MediaStore.Audio.Media.DEFAULT_SORT_ORDER);
}
private Cursor getParentProfileRingtones() {
@@ -588,7 +459,9 @@
// We don't need to re-add the internal ringtones for the work profile since
// they are the same as the personal profile. We just need the external
// ringtones.
- return queryMediaStore(parentContext, /* internal= */ false);
+ final Cursor res = getMediaRingtones(parentContext);
+ return new ExternalRingtonesCursorWrapper(res, ContentProvider.maybeAddUserId(
+ MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, parentInfo.id));
}
}
return null;
@@ -606,32 +479,11 @@
mPreviousRingtone.stop();
}
- Ringtone ringtone;
- Uri positionUri = getRingtoneUri(position);
- if (Flags.hapticsCustomizationRingtoneV2Enabled()) {
- mPreviousRingtone = new Ringtone.Builder(
- mContext, mMediaType, getDefaultAudioAttributes(mType))
- .setUri(positionUri)
- .build();
- } else {
- mPreviousRingtone = createRingtoneV1WithStreamType(mContext, positionUri,
- inferStreamType(), /* volumeShaperConfig= */ null);
- }
+ mPreviousRingtone =
+ getRingtone(mContext, getRingtoneUri(position), inferStreamType(), true);
return mPreviousRingtone;
}
- private static Ringtone createRingtoneV1WithStreamType(
- final Context context, Uri ringtoneUri, int streamType,
- @Nullable VolumeShaper.Configuration volumeShaperConfig) {
- try {
- return Ringtone.createV1WithCustomStreamType(context, streamType, ringtoneUri,
- volumeShaperConfig);
- } catch (Exception ex) {
- Log.e(TAG, "Failed to open ringtone " + ringtoneUri + ": " + ex);
- }
- return null;
- }
-
/**
* Gets a {@link Uri} for the ringtone at the given position in the {@link Cursor}.
*
@@ -783,13 +635,11 @@
*/
public static Uri getValidRingtoneUri(Context context) {
final RingtoneManager rm = new RingtoneManager(context);
-
- Uri uri = getValidRingtoneUriFromCursorAndClose(context,
- rm.queryMediaStore(/* internal= */ true));
+
+ Uri uri = getValidRingtoneUriFromCursorAndClose(context, rm.getInternalRingtones());
if (uri == null) {
- uri = getValidRingtoneUriFromCursorAndClose(context,
- rm.queryMediaStore(/* internal= */ false));
+ uri = getValidRingtoneUriFromCursorAndClose(context, rm.getMediaRingtones());
}
return uri;
@@ -810,26 +660,28 @@
}
}
- private Cursor queryMediaStore(boolean internal) {
- return queryMediaStore(mContext, internal);
+ @UnsupportedAppUsage
+ private Cursor getInternalRingtones() {
+ final Cursor res = query(
+ MediaStore.Audio.Media.INTERNAL_CONTENT_URI, INTERNAL_COLUMNS,
+ constructBooleanTrueWhereClause(mFilterColumns),
+ null, MediaStore.Audio.Media.DEFAULT_SORT_ORDER);
+ return new ExternalRingtonesCursorWrapper(res, MediaStore.Audio.Media.INTERNAL_CONTENT_URI);
}
- private Cursor queryMediaStore(Context context, boolean internal) {
- Uri contentUri = getContentUriForMedia(mMediaType, internal);
- String[] columns =
- mMediaType == Ringtone.MEDIA_VIBRATION ? MEDIA_VIBRATION_COLUMNS
- : MEDIA_AUDIO_COLUMNS;
- String whereClause = getWhereClauseForMedia(mMediaType, mFilterColumns);
- String sortOrder = getSortOrderForMedia(mMediaType);
+ private Cursor getMediaRingtones() {
+ final Cursor res = getMediaRingtones(mContext);
+ return new ExternalRingtonesCursorWrapper(res, MediaStore.Audio.Media.EXTERNAL_CONTENT_URI);
+ }
- Cursor cursor = query(contentUri, columns, whereClause, /* selectionArgs= */ null,
- sortOrder, context);
-
- if (context.getUserId() != mContext.getUserId()) {
- contentUri = ContentProvider.maybeAddUserId(contentUri, context.getUserId());
- }
-
- return new ExternalRingtonesCursorWrapper(cursor, contentUri);
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ private Cursor getMediaRingtones(Context context) {
+ // MediaStore now returns ringtones on other storage devices, even when
+ // we don't have storage or audio permissions
+ return query(
+ MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, MEDIA_COLUMNS,
+ constructBooleanTrueWhereClause(mFilterColumns), null,
+ MediaStore.Audio.Media.DEFAULT_SORT_ORDER, context);
}
private void setFilterColumnsList(int type) {
@@ -848,56 +700,6 @@
columns.add(MediaStore.Audio.AudioColumns.IS_ALARM);
}
}
-
- /**
- * Returns the sort order for the specified media.
- *
- * @param media The RingtoneManager media type.
- * @return The sort order column.
- */
- private static String getSortOrderForMedia(@MediaType int media) {
- return media == Ringtone.MEDIA_VIBRATION ? MediaStore.Files.FileColumns.TITLE
- : MediaStore.Audio.Media.DEFAULT_SORT_ORDER;
- }
-
- /**
- * Returns the content URI based on the specified media and whether it's internal or external
- * storage.
- *
- * @param media The RingtoneManager media type.
- * @param internal Whether it's for internal or external storage.
- * @return The media content URI.
- */
- private static Uri getContentUriForMedia(@MediaType int media, boolean internal) {
- switch (media) {
- case Ringtone.MEDIA_VIBRATION:
- return MediaStore.Files.getContentUri(
- internal ? MediaStore.VOLUME_INTERNAL : MediaStore.VOLUME_EXTERNAL);
- case Ringtone.MEDIA_SOUND:
- return internal ? MediaStore.Audio.Media.INTERNAL_CONTENT_URI
- : MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
- default:
- throw new IllegalArgumentException("Unsupported media type " + media);
- }
- }
-
- /**
- * Constructs a where clause based on the media type. This will be used to find all matching
- * sound or vibration files.
- *
- * @param media The RingtoneManager media type.
- * @param columns The columns that must be true, when media type is {@link Ringtone#MEDIA_SOUND}
- * @return The where clause.
- */
- private static String getWhereClauseForMedia(@MediaType int media, List<String> columns) {
- // TODO(b/296213309): Filtering by ringtone-type isn't supported yet for vibrations.
- if (media == Ringtone.MEDIA_VIBRATION) {
- return TextUtils.formatSimple("(%s='%s')", MediaStore.Files.FileColumns.MIME_TYPE,
- VibrationXmlParser.APPLICATION_VIBRATION_XML_MIME_TYPE);
- }
-
- return constructBooleanTrueWhereClause(columns);
- }
/**
* Constructs a where clause that consists of at least one column being 1
@@ -927,6 +729,14 @@
return sb.toString();
}
+
+ private Cursor query(Uri uri,
+ String[] projection,
+ String selection,
+ String[] selectionArgs,
+ String sortOrder) {
+ return query(uri, projection, selection, selectionArgs, sortOrder, mContext);
+ }
private Cursor query(Uri uri,
String[] projection,
@@ -954,14 +764,40 @@
* @return A {@link Ringtone} for the given URI, or null.
*/
public static Ringtone getRingtone(final Context context, Uri ringtoneUri) {
- if (Flags.hapticsCustomizationRingtoneV2Enabled()) {
- return new Ringtone.Builder(
- context, Ringtone.MEDIA_SOUND, getDefaultAudioAttributes(-1))
- .setUri(ringtoneUri)
- .build();
- } else {
- return createRingtoneV1WithStreamType(context, ringtoneUri, -1, null);
- }
+ // Don't set the stream type
+ return getRingtone(context, ringtoneUri, -1, true);
+ }
+
+ /**
+ * Returns a {@link Ringtone} with {@link VolumeShaper} if required for a given sound URI.
+ * <p>
+ * If the given URI cannot be opened for any reason, this method will
+ * attempt to fallback on another sound. If it cannot find any, it will
+ * return null.
+ *
+ * @param context A context used to query.
+ * @param ringtoneUri The {@link Uri} of a sound or ringtone.
+ * @param volumeShaperConfig config for volume shaper of the ringtone if applied.
+ * @return A {@link Ringtone} for the given URI, or null.
+ *
+ * @hide
+ */
+ public static Ringtone getRingtone(
+ final Context context, Uri ringtoneUri,
+ @Nullable VolumeShaper.Configuration volumeShaperConfig) {
+ // Don't set the stream type
+ return getRingtone(context, ringtoneUri, -1 /* streamType */, volumeShaperConfig, true);
+ }
+
+ /**
+ * @hide
+ */
+ public static Ringtone getRingtone(final Context context, Uri ringtoneUri,
+ @Nullable VolumeShaper.Configuration volumeShaperConfig,
+ boolean createLocalMediaPlayer) {
+ // Don't set the stream type
+ return getRingtone(context, ringtoneUri, -1 /* streamType */, volumeShaperConfig,
+ createLocalMediaPlayer);
}
/**
@@ -970,23 +806,64 @@
public static Ringtone getRingtone(final Context context, Uri ringtoneUri,
@Nullable VolumeShaper.Configuration volumeShaperConfig,
AudioAttributes audioAttributes) {
- // TODO: move caller(s) away from this method: inline the builder call.
- if (Flags.hapticsCustomizationRingtoneV2Enabled()) {
- return new Ringtone.Builder(context, Ringtone.MEDIA_SOUND, audioAttributes)
- .setUri(ringtoneUri)
- .setVolumeShaperConfig(volumeShaperConfig)
- .setUseExactAudioAttributes(true) // May be using audio-coupled via attrs
- .build();
- } else {
- try {
- return Ringtone.createV1WithCustomAudioAttributes(context, audioAttributes,
- ringtoneUri, volumeShaperConfig, /* allowRemote= */ true);
- } catch (Exception ex) {
- // Match broad catching of createRingtoneV1.
- Log.e(TAG, "Failed to open ringtone " + ringtoneUri + ": " + ex);
+ // Don't set the stream type
+ Ringtone ringtone = getRingtone(context, ringtoneUri, -1 /* streamType */,
+ volumeShaperConfig, false);
+ if (ringtone != null) {
+ ringtone.setAudioAttributesField(audioAttributes);
+ if (!ringtone.createLocalMediaPlayer()) {
+ Log.e(TAG, "Failed to open ringtone " + ringtoneUri);
return null;
}
}
+ return ringtone;
+ }
+
+ //FIXME bypass the notion of stream types within the class
+ /**
+ * Returns a {@link Ringtone} for a given sound URI on the given stream
+ * type. Normally, if you change the stream type on the returned
+ * {@link Ringtone}, it will re-create the {@link MediaPlayer}. This is just
+ * an optimized route to avoid that.
+ *
+ * @param streamType The stream type for the ringtone, or -1 if it should
+ * not be set (and the default used instead).
+ * @param createLocalMediaPlayer when true, the ringtone returned will be fully
+ * created otherwise, it will require the caller to create the media player manually
+ * {@link Ringtone#createLocalMediaPlayer()} in order to play the Ringtone.
+ * @see #getRingtone(Context, Uri)
+ */
+ @UnsupportedAppUsage
+ private static Ringtone getRingtone(final Context context, Uri ringtoneUri, int streamType,
+ boolean createLocalMediaPlayer) {
+ return getRingtone(context, ringtoneUri, streamType, null /* volumeShaperConfig */,
+ createLocalMediaPlayer);
+ }
+
+ private static Ringtone getRingtone(final Context context, Uri ringtoneUri, int streamType,
+ @Nullable VolumeShaper.Configuration volumeShaperConfig,
+ boolean createLocalMediaPlayer) {
+ try {
+ final Ringtone r = new Ringtone(context, true);
+ if (streamType >= 0) {
+ //FIXME deprecated call
+ r.setStreamType(streamType);
+ }
+
+ r.setVolumeShaperConfig(volumeShaperConfig);
+ r.setUri(ringtoneUri, volumeShaperConfig);
+ if (createLocalMediaPlayer) {
+ if (!r.createLocalMediaPlayer()) {
+ Log.e(TAG, "Failed to open ringtone " + ringtoneUri);
+ return null;
+ }
+ }
+ return r;
+ } catch (Exception ex) {
+ Log.e(TAG, "Failed to open ringtone " + ringtoneUri + ": " + ex);
+ }
+
+ return null;
}
/**
diff --git a/media/java/android/media/RingtoneV1.java b/media/java/android/media/RingtoneV1.java
deleted file mode 100644
index 3c54d4a..0000000
--- a/media/java/android/media/RingtoneV1.java
+++ /dev/null
@@ -1,614 +0,0 @@
-/*
- * Copyright (C) 2023 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.Nullable;
-import android.compat.annotation.UnsupportedAppUsage;
-import android.content.Context;
-import android.content.res.AssetFileDescriptor;
-import android.content.res.Resources.NotFoundException;
-import android.media.audiofx.HapticGenerator;
-import android.net.Uri;
-import android.os.Binder;
-import android.os.Build;
-import android.os.RemoteException;
-import android.os.Trace;
-import android.os.VibrationEffect;
-import android.provider.MediaStore;
-import android.provider.MediaStore.MediaColumns;
-import android.util.Log;
-
-import com.android.internal.annotations.VisibleForTesting;
-
-import java.io.IOException;
-import java.util.ArrayList;
-
-/**
- * Hosts original Ringtone implementation, retained for flagging large builder+vibration features
- * in RingtoneV2.java. This does not support new features in the V2 builder.
- *
- * Only modified methods are moved here.
- *
- * @hide
- */
-class RingtoneV1 implements Ringtone.ApiInterface {
- private static final String TAG = "RingtoneV1";
- private static final boolean LOGD = true;
-
- private static final String[] MEDIA_COLUMNS = new String[] {
- MediaStore.Audio.Media._ID,
- MediaStore.Audio.Media.TITLE
- };
- /** Selection that limits query results to just audio files */
- private static final String MEDIA_SELECTION = MediaColumns.MIME_TYPE + " LIKE 'audio/%' OR "
- + MediaColumns.MIME_TYPE + " IN ('application/ogg', 'application/x-flac')";
-
- // keep references on active Ringtones until stopped or completion listener called.
- private static final ArrayList<RingtoneV1> sActiveRingtones = new ArrayList<>();
-
- private final Context mContext;
- private final AudioManager mAudioManager;
- private VolumeShaper.Configuration mVolumeShaperConfig;
- private VolumeShaper mVolumeShaper;
-
- /**
- * Flag indicating if we're allowed to fall back to remote playback using
- * {@link #mRemotePlayer}. Typically this is false when we're the remote
- * player and there is nobody else to delegate to.
- */
- private final boolean mAllowRemote;
- private final IRingtonePlayer mRemotePlayer;
- private final Binder mRemoteToken;
-
- @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
- private MediaPlayer mLocalPlayer;
- private final MyOnCompletionListener mCompletionListener = new MyOnCompletionListener();
- private HapticGenerator mHapticGenerator;
-
- @UnsupportedAppUsage
- private Uri mUri;
- private String mTitle;
-
- private AudioAttributes mAudioAttributes = new AudioAttributes.Builder()
- .setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE)
- .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
- .build();
- private boolean mPreferBuiltinDevice;
- // playback properties, use synchronized with mPlaybackSettingsLock
- private boolean mIsLooping = false;
- private float mVolume = 1.0f;
- private boolean mHapticGeneratorEnabled = false;
- private final Object mPlaybackSettingsLock = new Object();
-
- /** {@hide} */
- @UnsupportedAppUsage
- public RingtoneV1(Context context, boolean allowRemote) {
- mContext = context;
- mAudioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
- mAllowRemote = allowRemote;
- mRemotePlayer = allowRemote ? mAudioManager.getRingtonePlayer() : null;
- mRemoteToken = allowRemote ? new Binder() : null;
- }
-
- /**
- * Sets the stream type where this ringtone will be played.
- *
- * @param streamType The stream, see {@link AudioManager}.
- * @deprecated use {@link #setAudioAttributes(AudioAttributes)}
- */
- @Deprecated
- public void setStreamType(int streamType) {
- PlayerBase.deprecateStreamTypeForPlayback(streamType, "Ringtone", "setStreamType()");
- setAudioAttributes(new AudioAttributes.Builder()
- .setInternalLegacyStreamType(streamType)
- .build());
- }
-
- /**
- * Gets the stream type where this ringtone will be played.
- *
- * @return The stream type, see {@link AudioManager}.
- * @deprecated use of stream types is deprecated, see
- * {@link #setAudioAttributes(AudioAttributes)}
- */
- @Deprecated
- public int getStreamType() {
- return AudioAttributes.toLegacyStreamType(mAudioAttributes);
- }
-
- /**
- * Sets the {@link AudioAttributes} for this ringtone.
- * @param attributes the non-null attributes characterizing this ringtone.
- */
- public void setAudioAttributes(AudioAttributes attributes)
- throws IllegalArgumentException {
- setAudioAttributesField(attributes);
- // The audio attributes have to be set before the media player is prepared.
- // Re-initialize it.
- setUri(mUri, mVolumeShaperConfig);
- reinitializeActivePlayer();
- }
-
- /**
- * Same as {@link #setAudioAttributes(AudioAttributes)} except this one does not create
- * the media player.
- * @hide
- */
- public void setAudioAttributesField(@Nullable AudioAttributes attributes) {
- if (attributes == null) {
- throw new IllegalArgumentException("Invalid null AudioAttributes for Ringtone");
- }
- mAudioAttributes = attributes;
- }
-
- /**
- * Finds the output device of type {@link AudioDeviceInfo#TYPE_BUILTIN_SPEAKER}. This device is
- * the one on which outgoing audio for SIM calls is played.
- *
- * @param audioManager the audio manage.
- * @return the {@link AudioDeviceInfo} corresponding to the builtin device, or {@code null} if
- * none can be found.
- */
- private AudioDeviceInfo getBuiltinDevice(AudioManager audioManager) {
- AudioDeviceInfo[] deviceList = audioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS);
- for (AudioDeviceInfo device : deviceList) {
- if (device.getType() == AudioDeviceInfo.TYPE_BUILTIN_SPEAKER) {
- return device;
- }
- }
- return null;
- }
-
- /**
- * Sets the preferred device of the ringtong playback to the built-in device.
- *
- * @hide
- */
- public boolean preferBuiltinDevice(boolean enable) {
- mPreferBuiltinDevice = enable;
- if (mLocalPlayer == null) {
- return true;
- }
- return mLocalPlayer.setPreferredDevice(getBuiltinDevice(mAudioManager));
- }
-
- /**
- * Creates a local media player for the ringtone using currently set attributes.
- * @return true if media player creation succeeded or is deferred,
- * false if it did not succeed and can't be tried remotely.
- * @hide
- */
- public boolean reinitializeActivePlayer() {
- Trace.beginSection("reinitializeActivePlayer");
- if (mUri == null) {
- Log.e(TAG, "Could not create media player as no URI was provided.");
- return mAllowRemote && mRemotePlayer != null;
- }
- destroyLocalPlayer();
- // try opening uri locally before delegating to remote player
- mLocalPlayer = new MediaPlayer();
- try {
- mLocalPlayer.setDataSource(mContext, mUri);
- mLocalPlayer.setAudioAttributes(mAudioAttributes);
- mLocalPlayer.setPreferredDevice(
- mPreferBuiltinDevice ? getBuiltinDevice(mAudioManager) : null);
- synchronized (mPlaybackSettingsLock) {
- applyPlaybackProperties_sync();
- }
- if (mVolumeShaperConfig != null) {
- mVolumeShaper = mLocalPlayer.createVolumeShaper(mVolumeShaperConfig);
- }
- mLocalPlayer.prepare();
-
- } catch (SecurityException | IOException e) {
- destroyLocalPlayer();
- if (!mAllowRemote) {
- Log.w(TAG, "Remote playback not allowed: " + e);
- }
- }
-
- if (LOGD) {
- if (mLocalPlayer != null) {
- Log.d(TAG, "Successfully created local player");
- } else {
- Log.d(TAG, "Problem opening; delegating to remote player");
- }
- }
- Trace.endSection();
- return mLocalPlayer != null || (mAllowRemote && mRemotePlayer != null);
- }
-
- /**
- * Same as AudioManager.hasHapticChannels except it assumes an already created ringtone.
- * If the ringtone has not been created, it will load based on URI provided at {@link #setUri}
- * and if not URI has been set, it will assume no haptic channels are present.
- * @hide
- */
- public boolean hasHapticChannels() {
- // FIXME: support remote player, or internalize haptic channels support and remove entirely.
- try {
- android.os.Trace.beginSection("Ringtone.hasHapticChannels");
- if (mLocalPlayer != null) {
- for(MediaPlayer.TrackInfo trackInfo : mLocalPlayer.getTrackInfo()) {
- if (trackInfo.hasHapticChannels()) {
- return true;
- }
- }
- }
- } finally {
- android.os.Trace.endSection();
- }
- return false;
- }
-
- /**
- * Returns whether a local player has been created for this ringtone.
- * @hide
- */
- @VisibleForTesting
- public boolean hasLocalPlayer() {
- return mLocalPlayer != null;
- }
-
- public @Ringtone.RingtoneMedia int getEnabledMedia() {
- return Ringtone.MEDIA_SOUND; // RingtoneV2 only
- }
-
- public VibrationEffect getVibrationEffect() {
- return null; // RingtoneV2 only
- }
-
- /**
- * Returns the {@link AudioAttributes} used by this object.
- * @return the {@link AudioAttributes} that were set with
- * {@link #setAudioAttributes(AudioAttributes)} or the default attributes if none were set.
- */
- public AudioAttributes getAudioAttributes() {
- return mAudioAttributes;
- }
-
- /**
- * Sets the player to be looping or non-looping.
- * @param looping whether to loop or not.
- */
- public void setLooping(boolean looping) {
- synchronized (mPlaybackSettingsLock) {
- mIsLooping = looping;
- applyPlaybackProperties_sync();
- }
- }
-
- /**
- * Returns whether the looping mode was enabled on this player.
- * @return true if this player loops when playing.
- */
- public boolean isLooping() {
- synchronized (mPlaybackSettingsLock) {
- return mIsLooping;
- }
- }
-
- /**
- * Sets the volume on this player.
- * @param volume a raw scalar in range 0.0 to 1.0, where 0.0 mutes this player, and 1.0
- * corresponds to no attenuation being applied.
- */
- public void setVolume(float volume) {
- synchronized (mPlaybackSettingsLock) {
- if (volume < 0.0f) { volume = 0.0f; }
- if (volume > 1.0f) { volume = 1.0f; }
- mVolume = volume;
- applyPlaybackProperties_sync();
- }
- }
-
- /**
- * Returns the volume scalar set on this player.
- * @return a value between 0.0f and 1.0f.
- */
- public float getVolume() {
- synchronized (mPlaybackSettingsLock) {
- return mVolume;
- }
- }
-
- /**
- * Enable or disable the {@link android.media.audiofx.HapticGenerator} effect. The effect can
- * only be enabled on devices that support the effect.
- *
- * @return true if the HapticGenerator effect is successfully enabled. Otherwise, return false.
- * @see android.media.audiofx.HapticGenerator#isAvailable()
- */
- public boolean setHapticGeneratorEnabled(boolean enabled) {
- if (!HapticGenerator.isAvailable()) {
- return false;
- }
- synchronized (mPlaybackSettingsLock) {
- mHapticGeneratorEnabled = enabled;
- applyPlaybackProperties_sync();
- }
- return true;
- }
-
- /**
- * Return whether the {@link android.media.audiofx.HapticGenerator} effect is enabled or not.
- * @return true if the HapticGenerator is enabled.
- */
- public boolean isHapticGeneratorEnabled() {
- synchronized (mPlaybackSettingsLock) {
- return mHapticGeneratorEnabled;
- }
- }
-
- /**
- * Must be called synchronized on mPlaybackSettingsLock
- */
- private void applyPlaybackProperties_sync() {
- if (mLocalPlayer != null) {
- mLocalPlayer.setVolume(mVolume);
- mLocalPlayer.setLooping(mIsLooping);
- if (mHapticGenerator == null && mHapticGeneratorEnabled) {
- mHapticGenerator = HapticGenerator.create(mLocalPlayer.getAudioSessionId());
- }
- if (mHapticGenerator != null) {
- mHapticGenerator.setEnabled(mHapticGeneratorEnabled);
- }
- } else if (mAllowRemote && (mRemotePlayer != null)) {
- try {
- mRemotePlayer.setPlaybackProperties(
- mRemoteToken, mVolume, mIsLooping, mHapticGeneratorEnabled);
- } catch (RemoteException e) {
- Log.w(TAG, "Problem setting playback properties: ", e);
- }
- } else {
- Log.w(TAG,
- "Neither local nor remote player available when applying playback properties");
- }
- }
-
- /**
- * Returns a human-presentable title for ringtone. Looks in media
- * content provider. If not in either, uses the filename
- *
- * @param context A context used for querying.
- */
- public String getTitle(Context context) {
- if (mTitle != null) return mTitle;
- return mTitle = Ringtone.getTitle(context, mUri, true /*followSettingsUri*/, mAllowRemote);
- }
-
- /**
- * Set {@link Uri} to be used for ringtone playback.
- * {@link IRingtonePlayer}.
- *
- * @hide
- */
- @UnsupportedAppUsage
- public void setUri(Uri uri) {
- setUri(uri, null);
- }
-
- /**
- * @hide
- */
- public void setVolumeShaperConfig(@Nullable VolumeShaper.Configuration volumeShaperConfig) {
- mVolumeShaperConfig = volumeShaperConfig;
- }
-
- /**
- * Set {@link Uri} to be used for ringtone playback. Attempts to open
- * locally, otherwise will delegate playback to remote
- * {@link IRingtonePlayer}. Add {@link VolumeShaper} if required.
- *
- * @hide
- */
- public void setUri(Uri uri, @Nullable VolumeShaper.Configuration volumeShaperConfig) {
- mVolumeShaperConfig = volumeShaperConfig;
- mUri = uri;
- if (mUri == null) {
- destroyLocalPlayer();
- }
- }
-
- /** {@hide} */
- @UnsupportedAppUsage
- public Uri getUri() {
- return mUri;
- }
-
- /**
- * Plays the ringtone.
- */
- public void play() {
- if (mLocalPlayer != null) {
- // Play ringtones if stream volume is over 0 or if it is a haptic-only ringtone
- // (typically because ringer mode is vibrate).
- if (mAudioManager.getStreamVolume(AudioAttributes.toLegacyStreamType(mAudioAttributes))
- != 0) {
- startLocalPlayer();
- } else if (!mAudioAttributes.areHapticChannelsMuted() && hasHapticChannels()) {
- // is haptic only ringtone
- startLocalPlayer();
- }
- } else if (mAllowRemote && (mRemotePlayer != null) && (mUri != null)) {
- final Uri canonicalUri = mUri.getCanonicalUri();
- final boolean looping;
- final float volume;
- synchronized (mPlaybackSettingsLock) {
- looping = mIsLooping;
- volume = mVolume;
- }
- try {
- mRemotePlayer.playWithVolumeShaping(mRemoteToken, canonicalUri, mAudioAttributes,
- volume, looping, mVolumeShaperConfig);
- } catch (RemoteException e) {
- if (!playFallbackRingtone()) {
- Log.w(TAG, "Problem playing ringtone: " + e);
- }
- }
- } else {
- if (!playFallbackRingtone()) {
- Log.w(TAG, "Neither local nor remote playback available");
- }
- }
- }
-
- /**
- * Stops a playing ringtone.
- */
- public void stop() {
- if (mLocalPlayer != null) {
- destroyLocalPlayer();
- } else if (mAllowRemote && (mRemotePlayer != null)) {
- try {
- mRemotePlayer.stop(mRemoteToken);
- } catch (RemoteException e) {
- Log.w(TAG, "Problem stopping ringtone: " + e);
- }
- }
- }
-
- private void destroyLocalPlayer() {
- if (mLocalPlayer != null) {
- if (mHapticGenerator != null) {
- mHapticGenerator.release();
- mHapticGenerator = null;
- }
- mLocalPlayer.setOnCompletionListener(null);
- mLocalPlayer.reset();
- mLocalPlayer.release();
- mLocalPlayer = null;
- mVolumeShaper = null;
- synchronized (sActiveRingtones) {
- sActiveRingtones.remove(this);
- }
- }
- }
-
- private void startLocalPlayer() {
- if (mLocalPlayer == null) {
- return;
- }
- synchronized (sActiveRingtones) {
- sActiveRingtones.add(this);
- }
- if (LOGD) {
- Log.d(TAG, "Starting ringtone playback");
- }
- mLocalPlayer.setOnCompletionListener(mCompletionListener);
- mLocalPlayer.start();
- if (mVolumeShaper != null) {
- mVolumeShaper.apply(VolumeShaper.Operation.PLAY);
- }
- }
-
- /**
- * Whether this ringtone is currently playing.
- *
- * @return True if playing, false otherwise.
- */
- public boolean isPlaying() {
- if (mLocalPlayer != null) {
- return mLocalPlayer.isPlaying();
- } else if (mAllowRemote && (mRemotePlayer != null)) {
- try {
- return mRemotePlayer.isPlaying(mRemoteToken);
- } catch (RemoteException e) {
- Log.w(TAG, "Problem checking ringtone: " + e);
- return false;
- }
- } else {
- Log.w(TAG, "Neither local nor remote playback available");
- return false;
- }
- }
-
- private boolean playFallbackRingtone() {
- int streamType = AudioAttributes.toLegacyStreamType(mAudioAttributes);
- if (mAudioManager.getStreamVolume(streamType) == 0) {
- return false;
- }
- int ringtoneType = RingtoneManager.getDefaultType(mUri);
- if (ringtoneType != -1 &&
- RingtoneManager.getActualDefaultRingtoneUri(mContext, ringtoneType) == null) {
- Log.w(TAG, "not playing fallback for " + mUri);
- return false;
- }
- // Default ringtone, try fallback ringtone.
- try {
- AssetFileDescriptor afd = mContext.getResources().openRawResourceFd(
- com.android.internal.R.raw.fallbackring);
- if (afd == null) {
- Log.e(TAG, "Could not load fallback ringtone");
- return false;
- }
- mLocalPlayer = new MediaPlayer();
- if (afd.getDeclaredLength() < 0) {
- mLocalPlayer.setDataSource(afd.getFileDescriptor());
- } else {
- mLocalPlayer.setDataSource(afd.getFileDescriptor(),
- afd.getStartOffset(),
- afd.getDeclaredLength());
- }
- mLocalPlayer.setAudioAttributes(mAudioAttributes);
- synchronized (mPlaybackSettingsLock) {
- applyPlaybackProperties_sync();
- }
- if (mVolumeShaperConfig != null) {
- mVolumeShaper = mLocalPlayer.createVolumeShaper(mVolumeShaperConfig);
- }
- mLocalPlayer.prepare();
- startLocalPlayer();
- afd.close();
- } catch (IOException ioe) {
- destroyLocalPlayer();
- Log.e(TAG, "Failed to open fallback ringtone");
- return false;
- } catch (NotFoundException nfe) {
- Log.e(TAG, "Fallback ringtone does not exist");
- return false;
- }
- return true;
- }
-
- public boolean getPreferBuiltinDevice() {
- return mPreferBuiltinDevice;
- }
-
- public VolumeShaper.Configuration getVolumeShaperConfig() {
- return mVolumeShaperConfig;
- }
-
- public boolean isLocalOnly() {
- return mAllowRemote;
- }
-
- public boolean isUsingRemotePlayer() {
- // V2 testing api, but this is the v1 approximation.
- return (mLocalPlayer == null) && mAllowRemote && (mRemotePlayer != null);
- }
-
- class MyOnCompletionListener implements MediaPlayer.OnCompletionListener {
- @Override
- public void onCompletion(MediaPlayer mp) {
- synchronized (sActiveRingtones) {
- sActiveRingtones.remove(RingtoneV1.this);
- }
- mp.setOnCompletionListener(null); // Help the Java GC: break the refcount cycle.
- }
- }
-}
diff --git a/media/java/android/media/RingtoneV2.java b/media/java/android/media/RingtoneV2.java
deleted file mode 100644
index f1a8155..0000000
--- a/media/java/android/media/RingtoneV2.java
+++ /dev/null
@@ -1,690 +0,0 @@
-/*
- * Copyright (C) 2023 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.Nullable;
-import android.compat.annotation.UnsupportedAppUsage;
-import android.content.Context;
-import android.content.res.AssetFileDescriptor;
-import android.content.res.Resources.NotFoundException;
-import android.media.Ringtone.Injectables;
-import android.net.Uri;
-import android.os.Binder;
-import android.os.IBinder;
-import android.os.RemoteException;
-import android.os.Trace;
-import android.os.VibrationEffect;
-import android.os.Vibrator;
-import android.provider.MediaStore;
-import android.provider.MediaStore.MediaColumns;
-import android.util.Log;
-
-import com.android.internal.annotations.VisibleForTesting;
-
-import java.io.IOException;
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-
-/**
- * New Ringtone implementation, supporting vibration as well as sound, and configuration via a
- * builder. During flagged transition, the original implementation is in RingtoneV1.java.
- *
- * Only modified methods are moved here.
- *
- * @hide
- */
-class RingtoneV2 implements Ringtone.ApiInterface {
- private static final String TAG = "RingtoneV2";
-
- /**
- * The ringtone should only play sound. Any vibration is managed externally.
- * @hide
- */
- public static final int MEDIA_SOUND = 1;
- /**
- * The ringtone should only play vibration. Any sound is managed externally.
- * Requires the {@link android.Manifest.permission#VIBRATE} permission.
- * @hide
- */
- public static final int MEDIA_VIBRATION = 1 << 1;
- /**
- * The ringtone should play sound and vibration.
- * @hide
- */
- public static final int MEDIA_SOUND_AND_VIBRATION = MEDIA_SOUND | MEDIA_VIBRATION;
-
- // This is not a public value, because apps shouldn't enable "all" media - that wouldn't be
- // safe if new media types were added.
- static final int MEDIA_ALL = MEDIA_SOUND | MEDIA_VIBRATION;
-
- /**
- * Declares the types of media that this Ringtone is allowed to play.
- * @hide
- */
- @Retention(RetentionPolicy.SOURCE)
- @IntDef(prefix = "MEDIA_", value = {
- MEDIA_SOUND,
- MEDIA_VIBRATION,
- MEDIA_SOUND_AND_VIBRATION,
- })
- public @interface RingtoneMedia {}
-
- private static final String[] MEDIA_COLUMNS = new String[] {
- MediaStore.Audio.Media._ID,
- MediaStore.Audio.Media.TITLE
- };
- /** Selection that limits query results to just audio files */
- private static final String MEDIA_SELECTION = MediaColumns.MIME_TYPE + " LIKE 'audio/%' OR "
- + MediaColumns.MIME_TYPE + " IN ('application/ogg', 'application/x-flac')";
-
- private final Context mContext;
- private final Vibrator mVibrator;
- private final AudioManager mAudioManager;
- private VolumeShaper.Configuration mVolumeShaperConfig;
-
- /**
- * Flag indicating if we're allowed to fall back to remote playback using
- * {@link #mRemoteRingtoneService}. Typically this is false when we're the remote
- * player and there is nobody else to delegate to.
- */
- private final boolean mAllowRemote;
- private final IRingtonePlayer mRemoteRingtoneService;
- private final Injectables mInjectables;
-
- private final int mEnabledMedia;
-
- private final Uri mUri;
- private String mTitle;
-
- private AudioAttributes mAudioAttributes;
- private boolean mUseExactAudioAttributes;
- private boolean mPreferBuiltinDevice;
- private RingtonePlayer mActivePlayer;
- // playback properties, use synchronized with mPlaybackSettingsLock
- private boolean mIsLooping;
- private float mVolume;
- private boolean mHapticGeneratorEnabled;
- private final Object mPlaybackSettingsLock = new Object();
- private final VibrationEffect mVibrationEffect;
-
- /** Only for use by Ringtone constructor */
- RingtoneV2(@NonNull Context context, @NonNull Injectables injectables,
- boolean allowRemote, @Ringtone.RingtoneMedia int enabledMedia,
- @Nullable Uri uri, @NonNull AudioAttributes audioAttributes,
- boolean useExactAudioAttributes,
- @Nullable VolumeShaper.Configuration volumeShaperConfig,
- boolean preferBuiltinDevice, float soundVolume, boolean looping,
- boolean hapticGeneratorEnabled, @Nullable VibrationEffect vibrationEffect) {
- // Context
- mContext = context;
- mInjectables = injectables;
- mVibrator = mContext.getSystemService(Vibrator.class);
- mAudioManager = mContext.getSystemService(AudioManager.class);
- mRemoteRingtoneService = allowRemote ? mAudioManager.getRingtonePlayer() : null;
- mAllowRemote = (mRemoteRingtoneService != null); // Only set if allowed, and present.
-
- // Properties potentially propagated to remote player.
- mEnabledMedia = enabledMedia;
- mUri = uri;
- mAudioAttributes = audioAttributes;
- mUseExactAudioAttributes = useExactAudioAttributes;
- mVolumeShaperConfig = volumeShaperConfig;
- mPreferBuiltinDevice = preferBuiltinDevice; // system-only, not supported for remote play.
- mVolume = soundVolume;
- mIsLooping = looping;
- mHapticGeneratorEnabled = hapticGeneratorEnabled;
- mVibrationEffect = vibrationEffect;
- }
-
- /** @hide */
- @RingtoneMedia
- public int getEnabledMedia() {
- return mEnabledMedia;
- }
-
- /**
- * Sets the stream type where this ringtone will be played.
- *
- * @param streamType The stream, see {@link AudioManager}.
- * @deprecated use {@link #setAudioAttributes(AudioAttributes)}
- */
- @Deprecated
- public void setStreamType(int streamType) {
- setAudioAttributes(
- getAudioAttributesForLegacyStreamType(streamType, "setStreamType()"));
- }
-
- private AudioAttributes getAudioAttributesForLegacyStreamType(int streamType, String originOp) {
- PlayerBase.deprecateStreamTypeForPlayback(streamType, "Ringtone", originOp);
- return new AudioAttributes.Builder()
- .setInternalLegacyStreamType(streamType)
- .build();
- }
-
- /**
- * Gets the stream type where this ringtone will be played.
- *
- * @return The stream type, see {@link AudioManager}.
- * @deprecated use of stream types is deprecated, see
- * {@link #setAudioAttributes(AudioAttributes)}
- */
- @Deprecated
- public int getStreamType() {
- return AudioAttributes.toLegacyStreamType(mAudioAttributes);
- }
-
- /**
- * Sets the {@link AudioAttributes} for this ringtone.
- * @param attributes the non-null attributes characterizing this ringtone.
- */
- public void setAudioAttributes(AudioAttributes attributes)
- throws IllegalArgumentException {
- // TODO: deprecate this method - it will be done with a builder.
- if (attributes == null) {
- throw new IllegalArgumentException("Invalid null AudioAttributes for Ringtone");
- }
- mAudioAttributes = attributes;
- // Setting the audio attributes requires re-initializing the player.
- if (mActivePlayer != null) {
- // The audio attributes have to be set before the media player is prepared.
- // Re-initialize it.
- reinitializeActivePlayer();
- }
- }
-
- /**
- * Returns the vibration effect that this ringtone was created with, if vibration is enabled.
- * Otherwise, returns null.
- * @hide
- */
- @Nullable
- public VibrationEffect getVibrationEffect() {
- return mVibrationEffect;
- }
-
- /** @hide */
- @VisibleForTesting
- public boolean getPreferBuiltinDevice() {
- return mPreferBuiltinDevice;
- }
-
- /** @hide */
- @VisibleForTesting
- public VolumeShaper.Configuration getVolumeShaperConfig() {
- return mVolumeShaperConfig;
- }
-
- /**
- * Returns whether this player is local only, or can defer to the remote player. The
- * result may differ from the builder if there is no remote player available at all.
- * @hide
- */
- @VisibleForTesting
- public boolean isLocalOnly() {
- return !mAllowRemote;
- }
-
- /** @hide */
- @VisibleForTesting
- public boolean isUsingRemotePlayer() {
- return mActivePlayer instanceof RemoteRingtonePlayer;
- }
-
- /**
- * Finds the output device of type {@link AudioDeviceInfo#TYPE_BUILTIN_SPEAKER}. This device is
- * the one on which outgoing audio for SIM calls is played.
- *
- * @param audioManager the audio manage.
- * @return the {@link AudioDeviceInfo} corresponding to the builtin device, or {@code null} if
- * none can be found.
- */
- private AudioDeviceInfo getBuiltinDevice(AudioManager audioManager) {
- AudioDeviceInfo[] deviceList = audioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS);
- for (AudioDeviceInfo device : deviceList) {
- if (device.getType() == AudioDeviceInfo.TYPE_BUILTIN_SPEAKER) {
- return device;
- }
- }
- return null;
- }
-
- /**
- * Creates a local media player for the ringtone using currently set attributes.
- * @return true if media player creation succeeded or is deferred,
- * false if it did not succeed and can't be tried remotely.
- * @hide
- */
- public boolean reinitializeActivePlayer() {
- // Try creating a local media player, or fallback to creating a remote one.
- Trace.beginSection("reinitializeActivePlayer");
- try {
- if (mActivePlayer != null) {
- // This would only happen if calling the deprecated setAudioAttributes after
- // building the Ringtone.
- stopAndReleaseActivePlayer();
- }
-
- boolean vibrationOnly = (mEnabledMedia & MEDIA_ALL) == MEDIA_VIBRATION;
- // Vibration can come from the audio file if using haptic generator or if haptic
- // channels are a possibility.
- boolean maybeAudioVibration = mUri != null && mInjectables.isHapticPlaybackSupported()
- && (mHapticGeneratorEnabled || !mAudioAttributes.areHapticChannelsMuted());
-
- // VibrationEffect only, use the simplified player without checking for haptic channels.
- if (vibrationOnly && !maybeAudioVibration && mVibrationEffect != null) {
- mActivePlayer = new LocalRingtonePlayer.VibrationEffectPlayer(
- mVibrationEffect, mAudioAttributes, mVibrator, mIsLooping);
- return true;
- }
-
- AudioDeviceInfo preferredDevice =
- mPreferBuiltinDevice ? getBuiltinDevice(mAudioManager) : null;
- if (mUri != null) {
- mActivePlayer = LocalRingtonePlayer.create(mContext, mAudioManager, mVibrator, mUri,
- mAudioAttributes, vibrationOnly, mVibrationEffect, mInjectables,
- mVolumeShaperConfig, preferredDevice, mHapticGeneratorEnabled, mIsLooping,
- mVolume);
- } else {
- // Using the remote player won't help play a null Uri. Revert straight to fallback.
- // The vibration-only case was already covered above.
- mActivePlayer = createFallbackRingtonePlayer();
- // Fall through to attempting remote fallback play if null.
- }
-
- if (mActivePlayer == null && mAllowRemote) {
- mActivePlayer = new RemoteRingtonePlayer(mRemoteRingtoneService, mUri,
- mAudioAttributes, mUseExactAudioAttributes, mEnabledMedia, mVibrationEffect,
- mVolumeShaperConfig, mHapticGeneratorEnabled, mIsLooping, mVolume);
- }
-
- return mActivePlayer != null;
- } finally {
- if (mActivePlayer != null) {
- Log.d(TAG, "Initialized ringtone player with " + mActivePlayer.getClass());
- } else {
- Log.d(TAG, "Failed to initialize ringtone player");
- }
- Trace.endSection();
- }
- }
-
- @Nullable
- private LocalRingtonePlayer createFallbackRingtonePlayer() {
- int ringtoneType = RingtoneManager.getDefaultType(mUri);
- if (ringtoneType != -1
- && RingtoneManager.getActualDefaultRingtoneUri(mContext, ringtoneType) == null) {
- Log.w(TAG, "not playing fallback for " + mUri);
- return null;
- }
- // Default ringtone, try fallback ringtone.
- try (AssetFileDescriptor afd = mContext.getResources().openRawResourceFd(
- com.android.internal.R.raw.fallbackring)) {
- if (afd == null) {
- Log.e(TAG, "Could not load fallback ringtone");
- return null;
- }
-
- AudioDeviceInfo preferredDevice =
- mPreferBuiltinDevice ? getBuiltinDevice(mAudioManager) : null;
- return LocalRingtonePlayer.createForFallback(mAudioManager, mVibrator, afd,
- mAudioAttributes, mVibrationEffect, mInjectables, mVolumeShaperConfig,
- preferredDevice, mIsLooping, mVolume);
- } catch (NotFoundException nfe) {
- Log.e(TAG, "Fallback ringtone does not exist");
- return null;
- } catch (IOException e) {
- // As with the above messages, not including much information about the
- // failure so as not to expose details of the fallback ringtone resource.
- Log.e(TAG, "Exception reading fallback ringtone");
- return null;
- }
- }
-
- /**
- * Same as AudioManager.hasHapticChannels except it assumes an already created ringtone.
- * @hide
- */
- public boolean hasHapticChannels() {
- return (mActivePlayer == null) ? false : mActivePlayer.hasHapticChannels();
- }
-
- /**
- * Returns the {@link AudioAttributes} used by this object.
- * @return the {@link AudioAttributes} that were set with
- * {@link #setAudioAttributes(AudioAttributes)} or the default attributes if none were set.
- */
- public AudioAttributes getAudioAttributes() {
- return mAudioAttributes;
- }
-
- /**
- * Sets the player to be looping or non-looping.
- * @param looping whether to loop or not.
- */
- public void setLooping(boolean looping) {
- synchronized (mPlaybackSettingsLock) {
- mIsLooping = looping;
- if (mActivePlayer != null) {
- mActivePlayer.setLooping(looping);
- }
- }
- }
-
- /**
- * Returns whether the looping mode was enabled on this player.
- * @return true if this player loops when playing.
- */
- public boolean isLooping() {
- synchronized (mPlaybackSettingsLock) {
- return mIsLooping;
- }
- }
-
- /**
- * Sets the volume on this player.
- * @param volume a raw scalar in range 0.0 to 1.0, where 0.0 mutes this player, and 1.0
- * corresponds to no attenuation being applied.
- */
- public void setVolume(float volume) {
- // Ignore if sound not enabled.
- if ((mEnabledMedia & MEDIA_SOUND) == 0) {
- return;
- }
- if (volume < 0.0f) {
- volume = 0.0f;
- } else if (volume > 1.0f) {
- volume = 1.0f;
- }
-
- synchronized (mPlaybackSettingsLock) {
- mVolume = volume;
- if (mActivePlayer != null) {
- mActivePlayer.setVolume(volume);
- }
- }
- }
-
- /**
- * Returns the volume scalar set on this player.
- * @return a value between 0.0f and 1.0f.
- */
- public float getVolume() {
- synchronized (mPlaybackSettingsLock) {
- return mVolume;
- }
- }
-
- /**
- * Enable or disable the {@link android.media.audiofx.HapticGenerator} effect. The effect can
- * only be enabled on devices that support the effect.
- *
- * @return true if the HapticGenerator effect is successfully enabled. Otherwise, return false.
- * @see android.media.audiofx.HapticGenerator#isAvailable()
- */
- public boolean setHapticGeneratorEnabled(boolean enabled) {
- if (!mInjectables.isHapticGeneratorAvailable()) {
- return false;
- }
- synchronized (mPlaybackSettingsLock) {
- mHapticGeneratorEnabled = enabled;
- if (mActivePlayer != null) {
- mActivePlayer.setHapticGeneratorEnabled(enabled);
- }
- }
- return true;
- }
-
- /**
- * Return whether the {@link android.media.audiofx.HapticGenerator} effect is enabled or not.
- * @return true if the HapticGenerator is enabled.
- */
- public boolean isHapticGeneratorEnabled() {
- synchronized (mPlaybackSettingsLock) {
- return mHapticGeneratorEnabled;
- }
- }
-
- /**
- * Returns a human-presentable title for ringtone. Looks in media
- * content provider. If not in either, uses the filename
- *
- * @param context A context used for querying.
- */
- public String getTitle(Context context) {
- if (mTitle != null) return mTitle;
- return mTitle = Ringtone.getTitle(context, mUri, true /*followSettingsUri*/, mAllowRemote);
- }
-
-
- /** {@hide} */
- @UnsupportedAppUsage
- public Uri getUri() {
- return mUri;
- }
-
- /**
- * Plays the ringtone.
- */
- public void play() {
- if (mActivePlayer != null) {
- Log.d(TAG, "Starting ringtone playback");
- if (mActivePlayer.play()) {
- return;
- } else {
- // Discard active player: play() is only meant to be called once.
- stopAndReleaseActivePlayer();
- }
- }
- if (!playFallbackRingtone()) {
- Log.w(TAG, "Neither local nor remote playback available");
- }
- }
-
- /**
- * Stops a playing ringtone.
- */
- public void stop() {
- stopAndReleaseActivePlayer();
- }
-
- private void stopAndReleaseActivePlayer() {
- if (mActivePlayer != null) {
- mActivePlayer.stopAndRelease();
- mActivePlayer = null;
- }
- }
-
- /**
- * Whether this ringtone is currently playing.
- *
- * @return True if playing, false otherwise.
- */
- public boolean isPlaying() {
- if (mActivePlayer != null) {
- return mActivePlayer.isPlaying();
- } else {
- Log.w(TAG, "No active ringtone player");
- return false;
- }
- }
-
- /**
- * Fallback during the play stage rather than initialization, typically due to an issue
- * communicating with the remote player.
- */
- private boolean playFallbackRingtone() {
- if (mActivePlayer != null) {
- Log.wtf(TAG, "Playing fallback ringtone with another active player");
- stopAndReleaseActivePlayer();
- }
- int streamType = AudioAttributes.toLegacyStreamType(mAudioAttributes);
- if (mAudioManager.getStreamVolume(streamType) == 0) {
- // TODO: Return true? If volume is off, this is a successful play.
- return false;
- }
- mActivePlayer = createFallbackRingtonePlayer();
- if (mActivePlayer == null) {
- return false; // the create method logs if it returns null.
- } else if (mActivePlayer.play()) {
- return true;
- } else {
- stopAndReleaseActivePlayer();
- return false;
- }
- }
-
- void setTitle(String title) {
- mTitle = title;
- }
-
- /**
- * Play a specific ringtone. This interface is implemented by either local (this process) or
- * proxied-remote playback via AudioManager.getRingtonePlayer, so that the caller
- * (Ringtone class) can just use a single player after the initial creation.
- * @hide
- */
- interface RingtonePlayer {
- /**
- * Start playing the ringtone, returning false if there was a problem that
- * requires falling back to the fallback ringtone resource.
- */
- boolean play();
- boolean isPlaying();
- void stopAndRelease();
-
- // Mutating playback methods.
- void setPreferredDevice(@Nullable AudioDeviceInfo audioDeviceInfo);
- void setLooping(boolean looping);
- void setHapticGeneratorEnabled(boolean enabled);
- void setVolume(float volume);
-
- boolean hasHapticChannels();
- }
-
- /**
- * Remote RingtonePlayer. All operations are delegated via the IRingtonePlayer interface, which
- * should ultimately be backed by a RingtoneLocalPlayer within the system services.
- */
- static class RemoteRingtonePlayer implements RingtonePlayer {
- private final IBinder mRemoteToken = new Binder();
- private final IRingtonePlayer mRemoteRingtoneService;
- private final Uri mCanonicalUri;
- private final int mEnabledMedia;
- private final VibrationEffect mVibrationEffect;
- private final VolumeShaper.Configuration mVolumeShaperConfig;
- private final AudioAttributes mAudioAttributes;
- private final boolean mUseExactAudioAttributes;
- private boolean mIsLooping;
- private float mVolume;
- private boolean mHapticGeneratorEnabled;
-
- RemoteRingtonePlayer(@NonNull IRingtonePlayer remoteRingtoneService,
- @NonNull Uri uri, @NonNull AudioAttributes audioAttributes,
- boolean useExactAudioAttributes,
- @RingtoneMedia int enabledMedia, @Nullable VibrationEffect vibrationEffect,
- @Nullable VolumeShaper.Configuration volumeShaperConfig,
- boolean hapticGeneratorEnabled, boolean initialIsLooping, float initialVolume) {
- mRemoteRingtoneService = remoteRingtoneService;
- mCanonicalUri = (uri == null) ? null : uri.getCanonicalUri();
- mAudioAttributes = audioAttributes;
- mUseExactAudioAttributes = useExactAudioAttributes;
- mEnabledMedia = enabledMedia;
- mVibrationEffect = vibrationEffect;
- mVolumeShaperConfig = volumeShaperConfig;
- mHapticGeneratorEnabled = hapticGeneratorEnabled;
- mIsLooping = initialIsLooping;
- mVolume = initialVolume;
- }
-
- @Override
- public boolean play() {
- try {
- mRemoteRingtoneService.playRemoteRingtone(mRemoteToken, mCanonicalUri,
- mAudioAttributes, mUseExactAudioAttributes, mEnabledMedia, mVibrationEffect,
- mVolume, mIsLooping, mHapticGeneratorEnabled, mVolumeShaperConfig);
- return true;
- } catch (RemoteException e) {
- Log.w(TAG, "Problem playing ringtone: " + e);
- return false;
- }
- }
-
- @Override
- public boolean isPlaying() {
- try {
- return mRemoteRingtoneService.isPlaying(mRemoteToken);
- } catch (RemoteException e) {
- Log.w(TAG, "Problem checking ringtone isPlaying: " + e);
- return false;
- }
- }
-
- @Override
- public void stopAndRelease() {
- try {
- mRemoteRingtoneService.stop(mRemoteToken);
- } catch (RemoteException e) {
- Log.w(TAG, "Problem stopping ringtone: " + e);
- }
- }
-
- @Override
- public void setPreferredDevice(@Nullable AudioDeviceInfo audioDeviceInfo) {
- // un-implemented for remote (but not used outside system).
- }
-
- @Override
- public void setLooping(boolean looping) {
- mIsLooping = looping;
- try {
- mRemoteRingtoneService.setLooping(mRemoteToken, looping);
- } catch (RemoteException e) {
- Log.w(TAG, "Problem setting looping: " + e);
- }
- }
-
- @Override
- public void setHapticGeneratorEnabled(boolean enabled) {
- mHapticGeneratorEnabled = enabled;
- try {
- mRemoteRingtoneService.setHapticGeneratorEnabled(mRemoteToken, enabled);
- } catch (RemoteException e) {
- Log.w(TAG, "Problem setting hapticGeneratorEnabled: " + e);
- }
- }
-
- @Override
- public void setVolume(float volume) {
- mVolume = volume;
- try {
- mRemoteRingtoneService.setVolume(mRemoteToken, volume);
- } catch (RemoteException e) {
- Log.w(TAG, "Problem setting volume: " + e);
- }
- }
-
- @Override
- public boolean hasHapticChannels() {
- // FIXME: support remote player, or internalize haptic channels support and remove
- // entirely.
- return false;
- }
- }
-
-}
diff --git a/media/java/android/media/audiopolicy/AudioProductStrategy.java b/media/java/android/media/audiopolicy/AudioProductStrategy.java
index e8adfaf..48ca4bf 100644
--- a/media/java/android/media/audiopolicy/AudioProductStrategy.java
+++ b/media/java/android/media/audiopolicy/AudioProductStrategy.java
@@ -50,6 +50,14 @@
private static final String TAG = "AudioProductStrategy";
+ /**
+ * The audio flags that will affect product strategy selection.
+ */
+ private static final int AUDIO_FLAGS_AFFECT_STRATEGY_SELECTION =
+ AudioAttributes.FLAG_AUDIBILITY_ENFORCED
+ | AudioAttributes.FLAG_SCO
+ | AudioAttributes.FLAG_BEACON;
+
private final AudioAttributesGroup[] mAudioAttributesGroups;
private final String mName;
/**
@@ -438,8 +446,8 @@
|| (attr.getSystemUsage() == refAttr.getSystemUsage()))
&& ((refAttr.getContentType() == AudioAttributes.CONTENT_TYPE_UNKNOWN)
|| (attr.getContentType() == refAttr.getContentType()))
- && ((refAttr.getAllFlags() == 0)
- || (attr.getAllFlags() != 0
+ && (((refAttr.getAllFlags() & AUDIO_FLAGS_AFFECT_STRATEGY_SELECTION) == 0)
+ || ((attr.getAllFlags() & AUDIO_FLAGS_AFFECT_STRATEGY_SELECTION) != 0
&& (attr.getAllFlags() & refAttr.getAllFlags()) == refAttr.getAllFlags()))
&& ((refFormattedTags.length() == 0) || refFormattedTags.equals(cliFormattedTags));
}
diff --git a/media/java/android/media/browse/MediaBrowserUtils.java b/media/java/android/media/browse/MediaBrowserUtils.java
index 19d9f00..8c008bc 100644
--- a/media/java/android/media/browse/MediaBrowserUtils.java
+++ b/media/java/android/media/browse/MediaBrowserUtils.java
@@ -18,6 +18,9 @@
import android.os.Bundle;
+import java.util.Collections;
+import java.util.List;
+
/**
* @hide
*/
@@ -75,4 +78,29 @@
}
return false;
}
+
+ /**
+ * Returns a paged version of the given {@code list}, using the paging parameters in {@code
+ * options}.
+ */
+ public static List<MediaBrowser.MediaItem> applyPagingOptions(
+ List<MediaBrowser.MediaItem> list, final Bundle options) {
+ if (list == null) {
+ return null;
+ }
+ int page = options.getInt(MediaBrowser.EXTRA_PAGE, -1);
+ int pageSize = options.getInt(MediaBrowser.EXTRA_PAGE_SIZE, -1);
+ if (page == -1 && pageSize == -1) {
+ return list;
+ }
+ int fromIndex = pageSize * page;
+ int toIndex = fromIndex + pageSize;
+ if (page < 0 || pageSize < 1 || fromIndex >= list.size()) {
+ return Collections.EMPTY_LIST;
+ }
+ if (toIndex > list.size()) {
+ toIndex = list.size();
+ }
+ return list.subList(fromIndex, toIndex);
+ }
}
diff --git a/media/java/android/media/tv/BroadcastInfoRequest.java b/media/java/android/media/tv/BroadcastInfoRequest.java
index 694756c..eb980a3 100644
--- a/media/java/android/media/tv/BroadcastInfoRequest.java
+++ b/media/java/android/media/tv/BroadcastInfoRequest.java
@@ -16,9 +16,11 @@
package android.media.tv;
+import android.annotation.FlaggedApi;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.SuppressLint;
+import android.media.tv.flags.Flags;
import android.os.Parcel;
import android.os.Parcelable;
@@ -51,14 +53,14 @@
/**
* Request option: one-way
* <p> With this option, no response is expected after sending the request.
- * @hide
*/
+ @FlaggedApi(Flags.FLAG_TIAF_V_APIS)
public static final int REQUEST_OPTION_ONEWAY = 2;
/**
* Request option: one-shot
* <p> With this option, only one response will be given per request.
- * @hide
*/
+ @FlaggedApi(Flags.FLAG_TIAF_V_APIS)
public static final int REQUEST_OPTION_ONESHOT = 3;
public static final @NonNull Parcelable.Creator<BroadcastInfoRequest> CREATOR =
diff --git a/media/java/android/media/tv/ITvInputManager.aidl b/media/java/android/media/tv/ITvInputManager.aidl
index 84c197d..be93abb 100644
--- a/media/java/android/media/tv/ITvInputManager.aidl
+++ b/media/java/android/media/tv/ITvInputManager.aidl
@@ -111,7 +111,7 @@
void resumeRecording(in IBinder sessionToken, in Bundle params, int userId);
// For playback control
- void startPlayback(in IBinder sessionToken, int userId);
+ void resumePlayback(in IBinder sessionToken, int userId);
void stopPlayback(in IBinder sessionToken, int mode, int userId);
// For broadcast info
diff --git a/media/java/android/media/tv/ITvInputSession.aidl b/media/java/android/media/tv/ITvInputSession.aidl
index 7b20c39..6b247cc 100644
--- a/media/java/android/media/tv/ITvInputSession.aidl
+++ b/media/java/android/media/tv/ITvInputSession.aidl
@@ -63,7 +63,7 @@
void timeShiftSetMode(int mode);
void timeShiftEnablePositionTracking(boolean enable);
- void startPlayback();
+ void resumePlayback();
void stopPlayback(int mode);
// For the recording session
diff --git a/media/java/android/media/tv/ITvInputSessionWrapper.java b/media/java/android/media/tv/ITvInputSessionWrapper.java
index 999e2cf..d145397 100644
--- a/media/java/android/media/tv/ITvInputSessionWrapper.java
+++ b/media/java/android/media/tv/ITvInputSessionWrapper.java
@@ -80,7 +80,7 @@
private static final int DO_SET_TV_MESSAGE_ENABLED = 31;
private static final int DO_NOTIFY_TV_MESSAGE = 32;
private static final int DO_STOP_PLAYBACK = 33;
- private static final int DO_START_PLAYBACK = 34;
+ private static final int DO_RESUME_PLAYBACK = 34;
private static final int DO_SET_VIDEO_FROZEN = 35;
private static final int DO_NOTIFY_AD_SESSION_DATA = 36;
@@ -295,8 +295,8 @@
mTvInputSessionImpl.stopPlayback(msg.arg1);
break;
}
- case DO_START_PLAYBACK: {
- mTvInputSessionImpl.startPlayback();
+ case DO_RESUME_PLAYBACK: {
+ mTvInputSessionImpl.resumePlayback();
break;
}
case DO_SET_VIDEO_FROZEN: {
@@ -523,8 +523,8 @@
}
@Override
- public void startPlayback() {
- mCaller.executeOrSendMessage(mCaller.obtainMessage(DO_START_PLAYBACK));
+ public void resumePlayback() {
+ mCaller.executeOrSendMessage(mCaller.obtainMessage(DO_RESUME_PLAYBACK));
}
diff --git a/media/java/android/media/tv/TvInputManager.java b/media/java/android/media/tv/TvInputManager.java
index 8720bfe..aed3e60e 100644
--- a/media/java/android/media/tv/TvInputManager.java
+++ b/media/java/android/media/tv/TvInputManager.java
@@ -17,6 +17,7 @@
package android.media.tv;
import android.annotation.CallbackExecutor;
+import android.annotation.FlaggedApi;
import android.annotation.IntDef;
import android.annotation.IntRange;
import android.annotation.NonNull;
@@ -35,6 +36,7 @@
import android.media.AudioPresentation;
import android.media.PlaybackParams;
import android.media.tv.ad.TvAdManager;
+import android.media.tv.flags.Flags;
import android.media.tv.interactive.TvInteractiveAppManager;
import android.net.Uri;
import android.os.Binder;
@@ -131,7 +133,8 @@
VIDEO_UNAVAILABLE_REASON_CAS_NEED_ACTIVATION, VIDEO_UNAVAILABLE_REASON_CAS_NEED_PAIRING,
VIDEO_UNAVAILABLE_REASON_CAS_NO_CARD, VIDEO_UNAVAILABLE_REASON_CAS_CARD_MUTE,
VIDEO_UNAVAILABLE_REASON_CAS_CARD_INVALID, VIDEO_UNAVAILABLE_REASON_CAS_BLACKOUT,
- VIDEO_UNAVAILABLE_REASON_CAS_REBOOTING, VIDEO_UNAVAILABLE_REASON_CAS_UNKNOWN})
+ VIDEO_UNAVAILABLE_REASON_CAS_REBOOTING, VIDEO_UNAVAILABLE_REASON_CAS_UNKNOWN,
+ VIDEO_UNAVAILABLE_REASON_STOPPED})
public @interface VideoUnavailableReason {}
/** Indicates that this TV message contains watermarking data */
@@ -344,9 +347,9 @@
/**
* Reason for {@link TvInputService.Session#notifyVideoUnavailable(int)} and
* {@link TvView.TvInputCallback#onVideoUnavailable(String, int)}: Video is unavailable because
- * it has been stopped by stopPlayback.
- * @hide
+ * it has been stopped by {@link TvView#stopPlayback(int)}.
*/
+ @FlaggedApi(Flags.FLAG_TIAF_V_APIS)
public static final int VIDEO_UNAVAILABLE_REASON_STOPPED = 19;
/** @hide */
@@ -553,85 +556,85 @@
/**
* Informs the application that the session has been tuned to the given channel.
*
- * @see TvInputService.Session#notifyTvInputSessionData(String, Bundle)
+ * @see TvInputService.Session#sendTvInputSessionData(String, Bundle)
* @see SESSION_DATA_KEY_CHANNEL_URI
- * @hide
*/
+ @FlaggedApi(Flags.FLAG_ENABLE_AD_SERVICE_FW)
public static final String SESSION_DATA_TYPE_TUNED = "tuned";
/**
* Sends the type and ID of a selected track. This is used to inform the application that a
* specific track is selected.
*
- * @see TvInputService.Session#notifyTvInputSessionData(String, Bundle)
+ * @see TvInputService.Session#sendTvInputSessionData(String, Bundle)
* @see SESSION_DATA_KEY_TRACK_TYPE
* @see SESSION_DATA_KEY_TRACK_ID
- * @hide
*/
+ @FlaggedApi(Flags.FLAG_ENABLE_AD_SERVICE_FW)
public static final String SESSION_DATA_TYPE_TRACK_SELECTED = "track_selected";
/**
* Sends the list of all audio/video/subtitle tracks.
*
- * @see TvInputService.Session#notifyTvInputSessionData(String, Bundle)
+ * @see TvInputService.Session#sendTvInputSessionData(String, Bundle)
* @see SESSION_DATA_KEY_TRACKS
- * @hide
*/
+ @FlaggedApi(Flags.FLAG_ENABLE_AD_SERVICE_FW)
public static final String SESSION_DATA_TYPE_TRACKS_CHANGED = "tracks_changed";
/**
* Informs the application that the video is now available for watching.
*
- * @see TvInputService.Session#notifyTvInputSessionData(String, Bundle)
- * @hide
+ * @see TvInputService.Session#sendTvInputSessionData(String, Bundle)
*/
+ @FlaggedApi(Flags.FLAG_ENABLE_AD_SERVICE_FW)
public static final String SESSION_DATA_TYPE_VIDEO_AVAILABLE = "video_available";
/**
* Informs the application that the video became unavailable for some reason.
*
- * @see TvInputService.Session#notifyTvInputSessionData(String, Bundle)
+ * @see TvInputService.Session#sendTvInputSessionData(String, Bundle)
* @see SESSION_DATA_KEY_VIDEO_UNAVAILABLE_REASON
- * @hide
*/
+ @FlaggedApi(Flags.FLAG_ENABLE_AD_SERVICE_FW)
public static final String SESSION_DATA_TYPE_VIDEO_UNAVAILABLE = "video_unavailable";
/**
* Notifies response for broadcast info.
*
- * @see TvInputService.Session#notifyTvInputSessionData(String, Bundle)
+ * @see TvInputService.Session#sendTvInputSessionData(String, Bundle)
* @see SESSION_DATA_KEY_BROADCAST_INFO_RESPONSE
- * @hide
*/
+ @FlaggedApi(Flags.FLAG_ENABLE_AD_SERVICE_FW)
public static final String SESSION_DATA_TYPE_BROADCAST_INFO_RESPONSE =
"broadcast_info_response";
/**
* Notifies response for advertisement.
*
- * @see TvInputService.Session#notifyTvInputSessionData(String, Bundle)
+ * @see TvInputService.Session#sendTvInputSessionData(String, Bundle)
* @see SESSION_DATA_KEY_AD_RESPONSE
- * @hide
*/
+ @FlaggedApi(Flags.FLAG_ENABLE_AD_SERVICE_FW)
public static final String SESSION_DATA_TYPE_AD_RESPONSE = "ad_response";
/**
* Notifies the advertisement buffer is consumed.
*
- * @see TvInputService.Session#notifyTvInputSessionData(String, Bundle)
+ * @see TvInputService.Session#sendTvInputSessionData(String, Bundle)
* @see SESSION_DATA_KEY_AD_BUFFER
- * @hide
*/
+ @FlaggedApi(Flags.FLAG_ENABLE_AD_SERVICE_FW)
public static final String SESSION_DATA_TYPE_AD_BUFFER_CONSUMED = "ad_buffer_consumed";
/**
* Sends the TV message.
*
- * @see TvInputService.Session#notifyTvInputSessionData(String, Bundle)
+ * @see TvInputService.Session#sendTvInputSessionData(String, Bundle)
* @see TvInputService.Session#notifyTvMessage(int, Bundle)
* @see SESSION_DATA_KEY_TV_MESSAGE_TYPE
- * @hide
*/
+ @FlaggedApi(Flags.FLAG_ENABLE_AD_SERVICE_FW)
public static final String SESSION_DATA_TYPE_TV_MESSAGE = "tv_message";
@@ -654,9 +657,9 @@
*
* <p> Type: android.net.Uri
*
- * @see TvInputService.Session#notifyTvInputSessionData(String, Bundle)
- * @hide
+ * @see TvInputService.Session#sendTvInputSessionData(String, Bundle)
*/
+ @FlaggedApi(Flags.FLAG_ENABLE_AD_SERVICE_FW)
public static final String SESSION_DATA_KEY_CHANNEL_URI = "channel_uri";
/**
@@ -668,9 +671,9 @@
* <p> Type: Integer
*
* @see TvTrackInfo#getType()
- * @see TvInputService.Session#notifyTvInputSessionData(String, Bundle)
- * @hide
+ * @see TvInputService.Session#sendTvInputSessionData(String, Bundle)
*/
+ @FlaggedApi(Flags.FLAG_ENABLE_AD_SERVICE_FW)
public static final String SESSION_DATA_KEY_TRACK_TYPE = "track_type";
/**
@@ -679,9 +682,9 @@
* <p> Type: String
*
* @see TvTrackInfo#getId()
- * @see TvInputService.Session#notifyTvInputSessionData(String, Bundle)
- * @hide
+ * @see TvInputService.Session#sendTvInputSessionData(String, Bundle)
*/
+ @FlaggedApi(Flags.FLAG_ENABLE_AD_SERVICE_FW)
public static final String SESSION_DATA_KEY_TRACK_ID = "track_id";
/**
@@ -689,9 +692,9 @@
*
* <p> Type: {@code java.util.List<android.media.tv.TvTrackInfo> }
*
- * @see TvInputService.Session#notifyTvInputSessionData(String, Bundle)
- * @hide
+ * @see TvInputService.Session#sendTvInputSessionData(String, Bundle)
*/
+ @FlaggedApi(Flags.FLAG_ENABLE_AD_SERVICE_FW)
public static final String SESSION_DATA_KEY_TRACKS = "tracks";
/**
@@ -701,9 +704,9 @@
*
* <p> Type: Integer
*
- * @see TvInputService.Session#notifyTvInputSessionData(String, Bundle)
- * @hide
+ * @see TvInputService.Session#sendTvInputSessionData(String, Bundle)
*/
+ @FlaggedApi(Flags.FLAG_ENABLE_AD_SERVICE_FW)
public static final String SESSION_DATA_KEY_VIDEO_UNAVAILABLE_REASON =
"video_unavailable_reason";
@@ -712,9 +715,9 @@
*
* <p> Type: android.media.tv.BroadcastInfoResponse
*
- * @see TvInputService.Session#notifyTvInputSessionData(String, Bundle)
- * @hide
+ * @see TvInputService.Session#sendTvInputSessionData(String, Bundle)
*/
+ @FlaggedApi(Flags.FLAG_ENABLE_AD_SERVICE_FW)
public static final String SESSION_DATA_KEY_BROADCAST_INFO_RESPONSE = "broadcast_info_response";
/**
@@ -722,9 +725,9 @@
*
* <p> Type: android.media.tv.AdResponse
*
- * @see TvInputService.Session#notifyTvInputSessionData(String, Bundle)
- * @hide
+ * @see TvInputService.Session#sendTvInputSessionData(String, Bundle)
*/
+ @FlaggedApi(Flags.FLAG_ENABLE_AD_SERVICE_FW)
public static final String SESSION_DATA_KEY_AD_RESPONSE = "ad_response";
/**
@@ -732,9 +735,9 @@
*
* <p> Type: android.media.tv.AdBuffer
*
- * @see TvInputService.Session#notifyTvInputSessionData(String, Bundle)
- * @hide
+ * @see TvInputService.Session#sendTvInputSessionData(String, Bundle)
*/
+ @FlaggedApi(Flags.FLAG_ENABLE_AD_SERVICE_FW)
public static final String SESSION_DATA_KEY_AD_BUFFER = "ad_buffer";
/**
@@ -744,9 +747,9 @@
*
* <p> Type: Integer
*
- * @see TvInputService.Session#notifyTvInputSessionData(String, Bundle)
- * @hide
+ * @see TvInputService.Session#sendTvInputSessionData(String, Bundle)
*/
+ @FlaggedApi(Flags.FLAG_ENABLE_AD_SERVICE_FW)
public static final String SESSION_DATA_KEY_TV_MESSAGE_TYPE = "tv_message_type";
@@ -3616,13 +3619,13 @@
}
}
- void startPlayback() {
+ void resumePlayback() {
if (mToken == null) {
Log.w(TAG, "The session has been already released");
return;
}
try {
- mService.startPlayback(mToken, mUserId);
+ mService.resumePlayback(mToken, mUserId);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
diff --git a/media/java/android/media/tv/TvInputService.java b/media/java/android/media/tv/TvInputService.java
index a022b1c..6658918 100644
--- a/media/java/android/media/tv/TvInputService.java
+++ b/media/java/android/media/tv/TvInputService.java
@@ -16,6 +16,7 @@
package android.media.tv;
+import android.annotation.FlaggedApi;
import android.annotation.FloatRange;
import android.annotation.IntDef;
import android.annotation.MainThread;
@@ -35,6 +36,7 @@
import android.media.AudioPresentation;
import android.media.PlaybackParams;
import android.media.tv.ad.TvAdManager;
+import android.media.tv.flags.Flags;
import android.media.tv.interactive.TvInteractiveAppService;
import android.net.Uri;
import android.os.AsyncTask;
@@ -766,12 +768,12 @@
/**
* Informs the application that the video freeze state has been updated.
*
- * When {@code true}, the video is frozen on the last frame but audio playback remains
+ * <p>When {@code true}, the video is frozen on the last frame but audio playback remains
* active.
*
* @param isFrozen Whether or not the video is frozen
- * @hide
*/
+ @FlaggedApi(Flags.FLAG_TIAF_V_APIS)
public void notifyVideoFreezeUpdated(boolean isFrozen) {
executeOrPostRunnableOnMainThread(new Runnable() {
@MainThread
@@ -1260,7 +1262,7 @@
}
/**
- * Notifies data related to this session to corresponding linked
+ * Sends data related to this session to corresponding linked
* {@link android.media.tv.ad.TvAdService} object via TvAdView.
*
* <p>Methods like {@link #notifyBroadcastInfoResponse(BroadcastInfoResponse)} sends the
@@ -1270,21 +1272,21 @@
*
* @param type data type
* @param data the related data values
- * @hide
*/
- public void notifyTvInputSessionData(
+ @FlaggedApi(Flags.FLAG_ENABLE_AD_SERVICE_FW)
+ public void sendTvInputSessionData(
@NonNull @TvInputManager.SessionDataType String type, @NonNull Bundle data) {
executeOrPostRunnableOnMainThread(new Runnable() {
@MainThread
@Override
public void run() {
try {
- if (DEBUG) Log.d(TAG, "notifyTvInputSessionData");
+ if (DEBUG) Log.d(TAG, "sendTvInputSessionData");
if (mSessionCallback != null) {
mSessionCallback.onTvInputSessionData(type, data);
}
} catch (RemoteException e) {
- Log.w(TAG, "error in notifyTvInputSessionData", e);
+ Log.w(TAG, "error in sendTvInputSessionData", e);
}
}
});
@@ -1439,10 +1441,10 @@
*
* @param type the type of the data
* @param data a bundle contains the data received
- * @see android.media.tv.ad.TvAdService.Session#notifyTvAdSessionData(String, Bundle)
+ * @see android.media.tv.ad.TvAdService.Session#sendTvAdSessionData(String, Bundle)
* @see android.media.tv.ad.TvAdView#setTvView(TvView)
- * @hide
*/
+ @FlaggedApi(Flags.FLAG_ENABLE_AD_SERVICE_FW)
public void onTvAdSessionData(
@NonNull @TvAdManager.SessionDataType String type, @NonNull Bundle data) {
}
@@ -1615,20 +1617,20 @@
* <p> Note that this is different form {@link #timeShiftPause()} as should release the
* stream, making it impossible to resume from this position again.
* @param mode
- * @hide
*/
+ @FlaggedApi(Flags.FLAG_TIAF_V_APIS)
public void onStopPlayback(@TvInteractiveAppService.PlaybackCommandStopMode int mode) {
}
/**
- * Starts playback of the Audio, Video, and CC streams.
+ * Resumes playback of the Audio, Video, and CC streams.
*
* <p> Note that this is different form {@link #timeShiftResume()} as this is intended to be
- * used after stopping playback. This is used to restart playback from the current position
+ * used after stopping playback. This is used to resume playback from the current position
* in the live broadcast.
- * @hide
*/
- public void onStartPlayback() {
+ @FlaggedApi(Flags.FLAG_TIAF_V_APIS)
+ public void onResumePlayback() {
}
/**
@@ -2112,10 +2114,10 @@
}
/**
- * Calls {@link #onStartPlayback()}.
+ * Calls {@link #onResumePlayback()}.
*/
- void startPlayback() {
- onStartPlayback();
+ void resumePlayback() {
+ onResumePlayback();
}
/**
diff --git a/media/java/android/media/tv/TvView.java b/media/java/android/media/tv/TvView.java
index cb45661..e604cb7 100644
--- a/media/java/android/media/tv/TvView.java
+++ b/media/java/android/media/tv/TvView.java
@@ -16,6 +16,7 @@
package android.media.tv;
+import android.annotation.FlaggedApi;
import android.annotation.FloatRange;
import android.annotation.NonNull;
import android.annotation.Nullable;
@@ -37,6 +38,7 @@
import android.media.tv.TvInputManager.Session;
import android.media.tv.TvInputManager.Session.FinishedInputEventCallback;
import android.media.tv.TvInputManager.SessionCallback;
+import android.media.tv.flags.Flags;
import android.media.tv.interactive.TvInteractiveAppService;
import android.net.Uri;
import android.os.Bundle;
@@ -650,10 +652,10 @@
* <p>The metadata that will continue to be filtered includes the PSI
* (Program specific information) and SI (Service Information), part of ISO/IEC 13818-1.
*
- * <p> Note that this is different form {@link #timeShiftPause()} as this completely drops
+ * <p> Note that this is different from {@link #timeShiftPause()} as this completely drops
* the stream, making it impossible to resume from this position again.
- * @hide
*/
+ @FlaggedApi(Flags.FLAG_TIAF_V_APIS)
public void stopPlayback(@TvInteractiveAppService.PlaybackCommandStopMode int mode) {
if (mSession != null) {
mSession.stopPlayback(mode);
@@ -661,16 +663,19 @@
}
/**
- * Starts playback of the Audio, Video, and CC streams.
+ * Resumes playback of the Audio, Video, and CC streams.
*
- * <p> Note that this is different form {@link #timeShiftResume()} as this is intended to be
- * used after stopping playback. This is used to restart playback from the current position
- * in the live broadcast.
- * @hide
+ * <p> Note that this is different from {@link #timeShiftResume()} as this is intended to
+ * be used after {@link #stopPlayback(int)} has been called. This is used to resume
+ * playback from the current position in the live broadcast.
+
+ * <p> If this is the first time playback should begin, you will need to use
+ * {@link #tune(String, Uri, Bundle)} to begin playback.
*/
- public void startPlayback() {
+ @FlaggedApi(Flags.FLAG_TIAF_V_APIS)
+ public void resumePlayback() {
if (mSession != null) {
- mSession.startPlayback();
+ mSession.resumePlayback();
}
}
@@ -678,13 +683,15 @@
* Sets whether or not the video is frozen. While the video is frozen, audio playback will
* continue.
*
- * <p> This should be invoked after a {@link TvInteractiveAppService.Session#requestCommand} is
+ * <p>This should be invoked after a {@link TvInteractiveAppService.Session#requestCommand} is
* received with the command to freeze the video.
*
- * <p> This will freeze the video to the last frame when the state is set to {@code true}.
+ * <p>This will freeze the video to the last frame when the state is set to {@code true}.
+ *
+ * @see TvView.TvInputCallback#setVideoFrozen(boolean)
* @param isFrozen whether or not the video is frozen.
- * @hide
*/
+ @FlaggedApi(Flags.FLAG_TIAF_V_APIS)
public void setVideoFrozen(boolean isFrozen) {
if (mSession != null) {
mSession.setVideoFrozen(isFrozen);
@@ -1320,6 +1327,16 @@
public void onTvMessage(@NonNull String inputId,
@TvInputManager.TvMessageType int type, @NonNull Bundle data) {
}
+
+ /**
+ * This is called when the video freeze status is updated.
+ *
+ * @see #setVideoFrozen(boolean)
+ * @param inputId The ID of the TV input bound to this view.
+ * @param isFrozen Whether or not the video is currently frozen on the las
+ */
+ @FlaggedApi(Flags.FLAG_TIAF_V_APIS)
+ public void onVideoFreezeUpdated(@NonNull String inputId, boolean isFrozen) {}
}
/**
@@ -1748,5 +1765,19 @@
mCallback.onTvMessage(mInputId, type, data);
}
}
+
+ @Override
+ public void onVideoFreezeUpdated(Session session, boolean isFrozen) {
+ if (DEBUG) {
+ Log.d(TAG, "onVideoFreezeUpdated(isFrozen=" + isFrozen + ")");
+ }
+ if (this != mSessionCallback) {
+ Log.w(TAG, "onVideoFreezeUpdated - session not created");
+ return;
+ }
+ if (mCallback != null) {
+ mCallback.onVideoFreezeUpdated(mInputId, isFrozen);
+ }
+ }
}
}
diff --git a/media/java/android/media/tv/ad/TvAdManager.java b/media/java/android/media/tv/ad/TvAdManager.java
index 02c0d75..59b10c6 100644
--- a/media/java/android/media/tv/ad/TvAdManager.java
+++ b/media/java/android/media/tv/ad/TvAdManager.java
@@ -68,7 +68,6 @@
* <p>Type: String
*
* @see #sendAppLinkCommand(String, Bundle)
- * @hide
*/
public static final String APP_LINK_KEY_PACKAGE_NAME = "package_name";
@@ -77,7 +76,6 @@
* <p>Type: String
*
* @see #sendAppLinkCommand(String, Bundle)
- * @hide
*/
public static final String APP_LINK_KEY_CLASS_NAME = "class_name";
@@ -86,7 +84,6 @@
* <p>Type: String
*
* @see #sendAppLinkCommand(String, Bundle)
- * @hide
*/
public static final String APP_LINK_KEY_COMMAND_TYPE = "command_type";
@@ -95,7 +92,6 @@
* <p>Type: String
*
* @see #sendAppLinkCommand(String, Bundle)
- * @hide
*/
public static final String APP_LINK_KEY_SERVICE_ID = "service_id";
@@ -104,7 +100,6 @@
* <p>Type: String
*
* @see #sendAppLinkCommand(String, Bundle)
- * @hide
*/
public static final String APP_LINK_KEY_BACK_URI = "back_uri";
@@ -112,7 +107,6 @@
* Broadcast intent action to send app command to TV app.
*
* @see #sendAppLinkCommand(String, Bundle)
- * @hide
*/
public static final String ACTION_APP_LINK_COMMAND =
"android.media.tv.ad.action.APP_LINK_COMMAND";
@@ -123,7 +117,6 @@
*
* @see #sendAppLinkCommand(String, Bundle)
* @see #ACTION_APP_LINK_COMMAND
- * @hide
*/
public static final String INTENT_KEY_TV_INPUT_ID = "tv_input_id";
@@ -134,7 +127,6 @@
* @see #sendAppLinkCommand(String, Bundle)
* @see #ACTION_APP_LINK_COMMAND
* @see TvAdServiceInfo#getId()
- * @hide
*/
public static final String INTENT_KEY_AD_SERVICE_ID = "ad_service_id";
@@ -144,7 +136,6 @@
*
* @see #sendAppLinkCommand(String, Bundle)
* @see #ACTION_APP_LINK_COMMAND
- * @hide
*/
public static final String INTENT_KEY_CHANNEL_URI = "channel_uri";
@@ -155,7 +146,6 @@
*
* @see #sendAppLinkCommand(String, Bundle)
* @see #ACTION_APP_LINK_COMMAND
- * @hide
*/
public static final String INTENT_KEY_COMMAND_TYPE = "command_type";
@@ -171,36 +161,32 @@
/**
* Sends an advertisement request to be processed by the related TV input.
*
- * @see TvAdService.Session#notifyTvAdSessionData(String, Bundle)
+ * @see TvAdService.Session#sendTvAdSessionData(String, Bundle)
* @see SESSION_DATA_KEY_AD_REQUEST
- * @hide
*/
public static final String SESSION_DATA_TYPE_AD_REQUEST = "ad_request";
/**
* Notifies the advertisement buffer is ready.
*
- * @see TvAdService.Session#notifyTvAdSessionData(String, Bundle)
+ * @see TvAdService.Session#sendTvAdSessionData(String, Bundle)
* @see SESSION_DATA_KEY_AD_BUFFER
- * @hide
*/
public static final String SESSION_DATA_TYPE_AD_BUFFER_READY = "ad_buffer_ready";
/**
* Sends request for broadcast info.
*
- * @see TvAdService.Session#notifyTvAdSessionData(String, Bundle)
+ * @see TvAdService.Session#sendTvAdSessionData(String, Bundle)
* @see SESSION_DATA_KEY_BROADCAST_INFO_RESQUEST
- * @hide
*/
public static final String SESSION_DATA_TYPE_BROADCAST_INFO_REQUEST = "broadcast_info_request";
/**
* Removes request for broadcast info.
*
- * @see TvAdService.Session#notifyTvAdSessionData(String, Bundle)
+ * @see TvAdService.Session#sendTvAdSessionData(String, Bundle)
* @see SESSION_DATA_KEY_BROADCAST_INFO_REQUEST_ID
- * @hide
*/
public static final String SESSION_DATA_TYPE_REMOVE_BROADCAST_INFO_REQUEST =
"remove_broadcast_info_request";
@@ -220,8 +206,7 @@
*
* <p> Type: android.media.tv.AdRequest
*
- * @see TvAdService.Session#notifyTvAdSessionData(String, Bundle)
- * @hide
+ * @see TvAdService.Session#sendTvAdSessionData(String, Bundle)
*/
public static final String SESSION_DATA_KEY_AD_REQUEST = "ad_request";
@@ -230,8 +215,7 @@
*
* <p> Type: android.media.tv.AdBuffer
*
- * @see TvAdService.Session#notifyTvAdSessionData(String, Bundle)
- * @hide
+ * @see TvAdService.Session#sendTvAdSessionData(String, Bundle)
*/
public static final String SESSION_DATA_KEY_AD_BUFFER = "ad_buffer";
@@ -240,8 +224,7 @@
*
* <p> Type: android.media.tv.BroadcastInfoRequest
*
- * @see TvAdService.Session#notifyTvAdSessionData(String, Bundle)
- * @hide
+ * @see TvAdService.Session#sendTvAdSessionData(String, Bundle)
*/
public static final String SESSION_DATA_KEY_BROADCAST_INFO_REQUEST = "broadcast_info_request";
@@ -250,8 +233,7 @@
*
* <p> Type: Integer
*
- * @see TvAdService.Session#notifyTvAdSessionData(String, Bundle)
- * @hide
+ * @see TvAdService.Session#sendTvAdSessionData(String, Bundle)
*/
public static final String SESSION_DATA_KEY_REQUEST_ID = "request_id";
@@ -437,11 +419,10 @@
}
/**
- * Returns the complete list of TV AD service on the system.
+ * Returns the complete list of TV AD services on the system.
*
* @return List of {@link TvAdServiceInfo} for each TV AD service that describes its meta
* information.
- * @hide
*/
@NonNull
public List<TvAdServiceInfo> getTvAdServiceList() {
@@ -495,8 +476,17 @@
*
* @param serviceId The ID of TV AD service which the command to be sent to. The ID can be found
* in {@link TvAdServiceInfo#getId()}.
- * @param command The command to be sent.
- * @hide
+ * @param command The command to be sent. The command is a bundle with the following keys:
+ * <ul>
+ * <li>{@link #APP_LINK_KEY_PACKAGE_NAME}: The package name of the app to be
+ * launched.
+ * <li>{@link #APP_LINK_KEY_CLASS_NAME}: The class name of the app to be
+ * launched.
+ * <li>{@link #APP_LINK_KEY_COMMAND_TYPE}: The command type.
+ * <li>{@link #APP_LINK_KEY_SERVICE_ID}: The ID of the TV AD service.
+ * <li>{@link #APP_LINK_KEY_BACK_URI}: The URI to be used to return to the
+ * previous app.
+ * </ul>
*/
public void sendAppLinkCommand(@NonNull String serviceId, @NonNull Bundle command) {
try {
@@ -511,7 +501,6 @@
*
* @param callback A callback used to monitor status of the TV AD services.
* @param executor A {@link Executor} that the status change will be delivered to.
- * @hide
*/
public void registerCallback(
@CallbackExecutor @NonNull Executor executor,
@@ -527,7 +516,6 @@
* Unregisters the existing {@link TvAdServiceCallback}.
*
* @param callback The existing callback to remove.
- * @hide
*/
public void unregisterCallback(@NonNull final TvAdServiceCallback callback) {
Preconditions.checkNotNull(callback);
@@ -1174,8 +1162,7 @@
}
/**
- * Callback used to monitor status of the TV AD service.
- * @hide
+ * Callback used to monitor status of the TV advertisement service.
*/
public abstract static class TvAdServiceCallback {
/**
diff --git a/media/java/android/media/tv/ad/TvAdService.java b/media/java/android/media/tv/ad/TvAdService.java
index 4d8f5c8b..6c8a8fd 100644
--- a/media/java/android/media/tv/ad/TvAdService.java
+++ b/media/java/android/media/tv/ad/TvAdService.java
@@ -17,6 +17,7 @@
package android.media.tv.ad;
import android.annotation.CallSuper;
+import android.annotation.FlaggedApi;
import android.annotation.MainThread;
import android.annotation.NonNull;
import android.annotation.Nullable;
@@ -32,6 +33,7 @@
import android.media.tv.TvInputManager;
import android.media.tv.TvTrackInfo;
import android.media.tv.TvView;
+import android.media.tv.flags.Flags;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Bundle;
@@ -61,8 +63,8 @@
/**
* The TvAdService class represents a TV client-side advertisement service.
- * @hide
*/
+@FlaggedApi(Flags.FLAG_ENABLE_AD_SERVICE_FW)
public abstract class TvAdService extends Service {
private static final boolean DEBUG = false;
private static final String TAG = "TvAdService";
@@ -73,7 +75,6 @@
* Name under which a TvAdService component publishes information about itself. This meta-data
* must reference an XML resource containing an
* <code><{@link android.R.styleable#TvAdService tv-ad-service}></code> tag.
- * @hide
*/
public static final String SERVICE_META_DATA = "android.media.tv.ad.service";
@@ -92,7 +93,7 @@
@Override
@Nullable
- public final IBinder onBind(@NonNull Intent intent) {
+ public final IBinder onBind(@Nullable Intent intent) {
ITvAdService.Stub tvAdServiceBinder = new ITvAdService.Stub() {
@Override
public void registerCallback(ITvAdServiceCallback cb) {
@@ -135,7 +136,6 @@
* Called when app link command is received.
*
* @see TvAdManager#sendAppLinkCommand(String, Bundle)
- * @hide
*/
public void onAppLinkCommand(@NonNull Bundle command) {
}
@@ -198,7 +198,6 @@
*
* @param enable {@code true} if you want to enable the media view. {@code false}
* otherwise.
- * @hide
*/
@CallSuper
public void setMediaViewEnabled(final boolean enable) {
@@ -224,7 +223,6 @@
* Returns {@code true} if media view is enabled, {@code false} otherwise.
*
* @see #setMediaViewEnabled(boolean)
- * @hide
*/
public boolean isMediaViewEnabled() {
return mMediaViewEnabled;
@@ -252,21 +250,18 @@
/**
* Starts TvAdService session.
- * @hide
*/
public void onStartAdService() {
}
/**
* Stops TvAdService session.
- * @hide
*/
public void onStopAdService() {
}
/**
* Resets TvAdService session.
- * @hide
*/
public void onResetAdService() {
}
@@ -398,6 +393,7 @@
* @param data the original bytes to be signed.
*
* @see #onSigningResult(String, byte[])
+ * @hide
*/
@CallSuper
public void requestSigning(@NonNull String signingId, @NonNull String algorithm,
@@ -421,22 +417,22 @@
}
@Override
- public boolean onKeyDown(int keyCode, @NonNull KeyEvent event) {
+ public boolean onKeyDown(int keyCode, @Nullable KeyEvent event) {
return false;
}
@Override
- public boolean onKeyLongPress(int keyCode, @NonNull KeyEvent event) {
+ public boolean onKeyLongPress(int keyCode, @Nullable KeyEvent event) {
return false;
}
@Override
- public boolean onKeyMultiple(int keyCode, int count, @NonNull KeyEvent event) {
+ public boolean onKeyMultiple(int keyCode, int count, @Nullable KeyEvent event) {
return false;
}
@Override
- public boolean onKeyUp(int keyCode, @NonNull KeyEvent event) {
+ public boolean onKeyUp(int keyCode, @Nullable KeyEvent event) {
return false;
}
@@ -484,6 +480,7 @@
* @param top Top position in pixels, relative to the overlay view.
* @param right Right position in pixels, relative to the overlay view.
* @param bottom Bottom position in pixels, relative to the overlay view.
+ *
*/
@CallSuper
public void layoutSurface(final int left, final int top, final int right,
@@ -615,9 +612,8 @@
*
* @param type the type of the data
* @param data a bundle contains the data received
- * @see android.media.tv.TvInputService.Session#notifyTvAdSessionData(String, Bundle)
+ * @see android.media.tv.TvInputService.Session#sendTvInputSessionData(String, Bundle)
* @see android.media.tv.ad.TvAdView#setTvView(TvView)
- * @hide
*/
public void onTvInputSessionData(
@NonNull @TvInputManager.SessionDataType String type, @NonNull Bundle data) {
@@ -633,7 +629,6 @@
*
* @param width The width of the media view, in pixels.
* @param height The height of the media view, in pixels.
- * @hide
*/
public void onMediaViewSizeChanged(@Px int width, @Px int height) {
}
@@ -643,7 +638,6 @@
* implementation can override this method and return its own view.
*
* @return a view attached to the media window. {@code null} if no media view is created.
- * @hide
*/
@Nullable
public View onCreateMediaView() {
@@ -651,27 +645,26 @@
}
/**
- * Notifies data related to this session to corresponding linked
+ * Sends data related to this session to corresponding linked
* {@link android.media.tv.TvInputService} object via TvView.
*
* @param type data type
* @param data the related data values
* @see TvAdView#setTvView(TvView)
- * @hide
*/
- public void notifyTvAdSessionData(
+ public void sendTvAdSessionData(
@NonNull @TvAdManager.SessionDataType String type, @NonNull Bundle data) {
executeOrPostRunnableOnMainThread(new Runnable() {
@MainThread
@Override
public void run() {
try {
- if (DEBUG) Log.d(TAG, "notifyTvAdSessionData");
+ if (DEBUG) Log.d(TAG, "sendTvAdSessionData");
if (mSessionCallback != null) {
mSessionCallback.onTvAdSessionData(type, data);
}
} catch (RemoteException e) {
- Log.w(TAG, "error in notifyTvAdSessionData", e);
+ Log.w(TAG, "error in sendTvAdSessionData", e);
}
}
});
diff --git a/media/java/android/media/tv/ad/TvAdServiceInfo.java b/media/java/android/media/tv/ad/TvAdServiceInfo.java
index 45dc89d..bac14e7 100644
--- a/media/java/android/media/tv/ad/TvAdServiceInfo.java
+++ b/media/java/android/media/tv/ad/TvAdServiceInfo.java
@@ -16,6 +16,7 @@
package android.media.tv.ad;
+import android.annotation.FlaggedApi;
import android.annotation.Nullable;
import android.content.ComponentName;
import android.content.Context;
@@ -26,6 +27,7 @@
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.content.res.XmlResourceParser;
+import android.media.tv.flags.Flags;
import android.os.Parcel;
import android.os.Parcelable;
import android.util.AttributeSet;
@@ -42,8 +44,8 @@
/**
* This class is used to specify meta information of a TV AD service.
- * @hide
*/
+@FlaggedApi(Flags.FLAG_ENABLE_AD_SERVICE_FW)
public final class TvAdServiceInfo implements Parcelable {
private static final boolean DEBUG = false;
private static final String TAG = "TvAdServiceInfo";
@@ -95,6 +97,7 @@
in.readStringList(mTypes);
}
+ @NonNull
public static final Creator<TvAdServiceInfo> CREATOR = new Creator<TvAdServiceInfo>() {
@Override
public TvAdServiceInfo createFromParcel(Parcel in) {
diff --git a/media/java/android/media/tv/ad/TvAdView.java b/media/java/android/media/tv/ad/TvAdView.java
index 604dbd5..ee01468 100644
--- a/media/java/android/media/tv/ad/TvAdView.java
+++ b/media/java/android/media/tv/ad/TvAdView.java
@@ -17,6 +17,7 @@
package android.media.tv.ad;
import android.annotation.CallbackExecutor;
+import android.annotation.FlaggedApi;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.Context;
@@ -29,6 +30,7 @@
import android.media.tv.TvTrackInfo;
import android.media.tv.TvView;
import android.media.tv.ad.TvAdManager.Session.FinishedInputEventCallback;
+import android.media.tv.flags.Flags;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
@@ -48,9 +50,9 @@
import java.util.concurrent.Executor;
/**
- * Displays contents of TV AD services.
- * @hide
+ * Displays contents of TV advertisement services.
*/
+@FlaggedApi(Flags.FLAG_ENABLE_AD_SERVICE_FW)
public class TvAdView extends ViewGroup {
private static final String TAG = "TvAdView";
private static final boolean DEBUG = false;
@@ -158,7 +160,6 @@
* @param tvView the TvView to be linked to this TvAdView via linking of Sessions. {@code null}
* to unlink the TvView.
* @return {@code true} if it's linked successfully; {@code false} otherwise.
- * @hide
*/
public boolean setTvView(@Nullable TvView tvView) {
if (tvView == null) {
@@ -182,14 +183,12 @@
return true;
}
- /** @hide */
@Override
public void onAttachedToWindow() {
super.onAttachedToWindow();
createSessionMediaView();
}
- /** @hide */
@Override
public void onDetachedFromWindow() {
removeSessionMediaView();
@@ -259,7 +258,6 @@
* Resets this TvAdView to release its resources.
*
* <p>It can be reused by call {@link #prepareAdService(String, String)}.
- * @hide
*/
public void reset() {
if (DEBUG) Log.d(TAG, "reset()");
@@ -362,7 +360,6 @@
*
* @param event The input event.
* @return {@code true} if the event was handled by the view, {@code false} otherwise.
- * @hide
*/
public boolean dispatchUnhandledInputEvent(@NonNull InputEvent event) {
if (mOnUnhandledInputEventListener != null) {
@@ -391,7 +388,6 @@
* by the TV AD service.
*
* @param listener The callback to be invoked when the unhandled input event is received.
- * @hide
*/
public void setOnUnhandledInputEventListener(
@NonNull @CallbackExecutor Executor executor,
@@ -406,7 +402,6 @@
*
* @see #setOnUnhandledInputEventListener(Executor, OnUnhandledInputEventListener)
* @see #clearOnUnhandledInputEventListener()
- * @hide
*/
@Nullable
public OnUnhandledInputEventListener getOnUnhandledInputEventListener() {
@@ -415,14 +410,13 @@
/**
* Clears the {@link OnUnhandledInputEventListener}.
- * @hide
*/
public void clearOnUnhandledInputEventListener() {
mOnUnhandledInputEventListener = null;
}
@Override
- public boolean dispatchKeyEvent(@NonNull KeyEvent event) {
+ public boolean dispatchKeyEvent(@Nullable KeyEvent event) {
if (super.dispatchKeyEvent(event)) {
return true;
}
@@ -452,7 +446,6 @@
/**
* Starts the AD service.
- * @hide
*/
public void startAdService() {
if (DEBUG) {
@@ -493,6 +486,7 @@
* Sends current video bounds to related TV AD service.
*
* @param bounds the rectangle area for rendering the current video.
+ * @hide
*/
public void sendCurrentVideoBounds(@NonNull Rect bounds) {
if (DEBUG) {
@@ -508,6 +502,7 @@
*
* @param channelUri The current channel URI; {@code null} if there is no currently tuned
* channel.
+ * @hide
*/
public void sendCurrentChannelUri(@Nullable Uri channelUri) {
if (DEBUG) {
@@ -520,6 +515,7 @@
/**
* Sends track info list to related TV AD service.
+ * @hide
*/
public void sendTrackInfoList(@Nullable List<TvTrackInfo> tracks) {
if (DEBUG) {
@@ -536,6 +532,7 @@
* @param inputId The current TV input ID whose channel is tuned. {@code null} if no channel is
* tuned.
* @see android.media.tv.TvInputInfo
+ * @hide
*/
public void sendCurrentTvInputId(@Nullable String inputId) {
if (DEBUG) {
@@ -577,6 +574,7 @@
* can also be added to the params.
*
* @see #ERROR_KEY_METHOD_NAME
+ * @hide
*/
public void notifyError(@NonNull String errMsg, @NonNull Bundle params) {
if (DEBUG) {
@@ -599,6 +597,7 @@
* {@link TvInputManager#TV_MESSAGE_KEY_RAW_DATA}.
* See {@link TvInputManager#TV_MESSAGE_KEY_SUBTYPE} for more information on
* how to parse this data.
+ * @hide
*/
public void notifyTvMessage(@NonNull @TvInputManager.TvMessageType int type,
@NonNull Bundle data) {
@@ -613,7 +612,6 @@
/**
* Interface definition for a callback to be invoked when the unhandled input event is received.
- * @hide
*/
public interface OnUnhandledInputEventListener {
/**
diff --git a/media/java/android/media/tv/interactive/TvInteractiveAppManager.java b/media/java/android/media/tv/interactive/TvInteractiveAppManager.java
index 498eec6..7cf32ec 100755
--- a/media/java/android/media/tv/interactive/TvInteractiveAppManager.java
+++ b/media/java/android/media/tv/interactive/TvInteractiveAppManager.java
@@ -2634,8 +2634,8 @@
/**
* This is called when
- * {@link TvInteractiveAppService.Session#requestSigning(String, String, String, int, byte[])} is
- * called.
+ * {@link TvInteractiveAppService.Session#requestSigning(String, String, String, int, byte[])}
+ * is called.
*
* @param session A {@link TvInteractiveAppService.Session} associated with this callback.
* @param signingId the ID to identify the request.
@@ -2644,7 +2644,6 @@
* @param host The host of the SSL CLient Authentication Server
* @param port The port of the SSL Client Authentication Server
* @param data the original bytes to be signed.
- * @hide
*/
public void onRequestSigning(
Session session, String signingId, String algorithm, String host,
@@ -2657,7 +2656,6 @@
* @param session A {@link TvInteractiveAppService.Session} associated with this callback.
* @param host the host name of the SSL authentication server.
* @param port the port of the SSL authentication server. E.g., 443
- * @hide
*/
public void onRequestCertificate(Session session, String host, int port) {
}
diff --git a/media/java/android/media/tv/interactive/TvInteractiveAppService.java b/media/java/android/media/tv/interactive/TvInteractiveAppService.java
index 7b6dc38..eba26d4 100755
--- a/media/java/android/media/tv/interactive/TvInteractiveAppService.java
+++ b/media/java/android/media/tv/interactive/TvInteractiveAppService.java
@@ -17,6 +17,7 @@
package android.media.tv.interactive;
import android.annotation.CallSuper;
+import android.annotation.FlaggedApi;
import android.annotation.IntDef;
import android.annotation.MainThread;
import android.annotation.NonNull;
@@ -44,6 +45,7 @@
import android.media.tv.TvRecordingInfo;
import android.media.tv.TvTrackInfo;
import android.media.tv.TvView;
+import android.media.tv.flags.Flags;
import android.media.tv.interactive.TvInteractiveAppView.TvInteractiveAppCallback;
import android.net.Uri;
import android.net.http.SslCertificate;
@@ -740,8 +742,8 @@
* @param host the host name of the SSL authentication server.
* @param port the port of the SSL authentication server. E.g., 443
* @param cert the SSL certificate received.
- * @hide
*/
+ @FlaggedApi(Flags.FLAG_TIAF_V_APIS)
public void onCertificate(@NonNull String host, int port, @NonNull SslCertificate cert) {
}
@@ -894,13 +896,13 @@
}
/**
- * Called when video becomes frozen or unfrozen. Audio playback will continue while
- * video will be frozen to the last frame if {@code true}.
+ * Called when video becomes frozen or unfrozen. Audio playback will continue while video
+ * will be frozen to the last frame if {@code true}.
+ *
* @param isFrozen Whether or not the video is frozen.
- * @hide
*/
- public void onVideoFreezeUpdated(boolean isFrozen) {
- }
+ @FlaggedApi(Flags.FLAG_TIAF_V_APIS)
+ public void onVideoFreezeUpdated(boolean isFrozen) {}
/**
* Called when content is allowed.
@@ -959,12 +961,12 @@
/**
* Called when the TV App sends the selected track info as a response to
- * requestSelectedTrackInfo.
+ * {@link #requestSelectedTrackInfo()}
*
- * @param tracks
- * @hide
+ * @param tracks A list of {@link TvTrackInfo} that are currently selected
*/
- public void onSelectedTrackInfo(List<TvTrackInfo> tracks) {
+ @FlaggedApi(Flags.FLAG_TIAF_V_APIS)
+ public void onSelectedTrackInfo(@NonNull List<TvTrackInfo> tracks) {
}
@Override
@@ -1373,13 +1375,13 @@
}
/**
- * Requests the currently selected {@link TvTrackInfo} from the TV App.
+ * Requests a list of the currently selected {@link TvTrackInfo} from the TV App.
*
* <p> Normally, track info cannot be synchronized until the channel has
- * been changed. This is used when the session of the TIAS is newly
- * created and the normal synchronization has not happened yet.
- * @hide
+ * been changed. This is used when the session of the {@link TvInteractiveAppService}
+ * is newly created and the normal synchronization has not happened yet.
*/
+ @FlaggedApi(Flags.FLAG_TIAF_V_APIS)
@CallSuper
public void requestSelectedTrackInfo() {
executeOrPostRunnableOnMainThread(() -> {
@@ -1664,9 +1666,9 @@
* @see #onSigningResult(String, byte[])
* @see TvInteractiveAppView#createBiInteractiveApp(Uri, Bundle)
* @see TvInteractiveAppView#BI_INTERACTIVE_APP_KEY_ALIAS
- * @hide
*/
@CallSuper
+ @FlaggedApi(Flags.FLAG_TIAF_V_APIS)
public void requestSigning(@NonNull String signingId, @NonNull String algorithm,
@NonNull String host, int port, @NonNull byte[] data) {
executeOrPostRunnableOnMainThread(new Runnable() {
@@ -1693,8 +1695,9 @@
*
* @param host the host name of the SSL authentication server.
* @param port the port of the SSL authentication server. E.g., 443
- * @hide
*/
+ @CallSuper
+ @FlaggedApi(Flags.FLAG_TIAF_V_APIS)
public void requestCertificate(@NonNull String host, int port) {
executeOrPostRunnableOnMainThread(new Runnable() {
@MainThread
diff --git a/media/java/android/media/tv/interactive/TvInteractiveAppView.java b/media/java/android/media/tv/interactive/TvInteractiveAppView.java
index 3b29574..29a3b98 100755
--- a/media/java/android/media/tv/interactive/TvInteractiveAppView.java
+++ b/media/java/android/media/tv/interactive/TvInteractiveAppView.java
@@ -17,6 +17,7 @@
package android.media.tv.interactive;
import android.annotation.CallbackExecutor;
+import android.annotation.FlaggedApi;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.Context;
@@ -30,6 +31,7 @@
import android.media.tv.TvRecordingInfo;
import android.media.tv.TvTrackInfo;
import android.media.tv.TvView;
+import android.media.tv.flags.Flags;
import android.media.tv.interactive.TvInteractiveAppManager.Session;
import android.media.tv.interactive.TvInteractiveAppManager.Session.FinishedInputEventCallback;
import android.media.tv.interactive.TvInteractiveAppManager.SessionCallback;
@@ -585,8 +587,9 @@
/**
* Sends the currently selected track info to the TV Interactive App.
*
- * @hide
+ * @param tracks list of {@link TvTrackInfo} of the currently selected track(s)
*/
+ @FlaggedApi(Flags.FLAG_TIAF_V_APIS)
public void sendSelectedTrackInfo(@Nullable List<TvTrackInfo> tracks) {
if (DEBUG) {
Log.d(TAG, "sendSelectedTrackInfo");
@@ -720,12 +723,12 @@
}
/**
- * Alerts the TV Interactive app that the video freeze state has been updated.
- * If {@code true}, the video is frozen on the last frame while audio playback continues.
+ * Alerts the TV Interactive app that the video freeze state has been updated. If {@code true},
+ * the video is frozen on the last frame while audio playback continues.
*
* @param isFrozen Whether the video is frozen.
- * @hide
*/
+ @FlaggedApi(Flags.FLAG_TIAF_V_APIS)
public void notifyVideoFreezeUpdated(boolean isFrozen) {
if (DEBUG) {
Log.d(TAG, "notifyVideoFreezeUpdated");
@@ -757,12 +760,12 @@
}
/**
- * Send the requested SSL certificate to the TV Interactive App
+ * Sends the requested SSL certificate to the TV Interactive App
* @param host the host name of the SSL authentication server.
* @param port the port of the SSL authentication server. E.g., 443
* @param cert the SSL certificate requested
- * @hide
*/
+ @FlaggedApi(Flags.FLAG_TIAF_V_APIS)
public void sendCertificate(@NonNull String host, int port, @NonNull SslCertificate cert) {
if (DEBUG) {
Log.d(TAG, "sendCertificate");
@@ -1248,8 +1251,8 @@
* called.
*
* @param iAppServiceId The ID of the TV interactive app service bound to this view.
- * @hide
*/
+ @FlaggedApi(Flags.FLAG_TIAF_V_APIS)
public void onRequestSelectedTrackInfo(@NonNull String iAppServiceId) {
}
@@ -1387,6 +1390,37 @@
}
/**
+ * This is called when
+ * {@link TvInteractiveAppService.Session#requestSigning(String, String, String, int, byte[])}
+ * is called.
+ *
+ * @param iAppServiceId The ID of the TV interactive app service bound to this view.
+ * @param signingId the ID to identify the request.
+ * @param algorithm the standard name of the signature algorithm requested, such as
+ * MD5withRSA, SHA256withDSA, etc.
+ * @param host The hostname of the SSL authentication server.
+ * @param port The port of the SSL authentication server.
+ * @param data the original bytes to be signed.
+ */
+ @FlaggedApi(Flags.FLAG_TIAF_V_APIS)
+ public void onRequestSigning(@NonNull String iAppServiceId, @NonNull String signingId,
+ @NonNull String algorithm, @NonNull String host, int port, @NonNull byte[] data) {
+ }
+
+ /**
+ * This is called when
+ * {@link TvInteractiveAppService.Session#requestCertificate(String, int)} is called.
+ *
+ * @param iAppServiceId The ID of the TV interactive app service bound to this view.
+ * @param host The hostname of the SSL authentication server.
+ * @param port The port of the SSL authentication server.
+ */
+ @FlaggedApi(Flags.FLAG_TIAF_V_APIS)
+ public void onRequestCertificate(@NonNull String iAppServiceId, @NonNull String host,
+ int port) {
+ }
+
+ /**
* This is called when {@link TvInteractiveAppService.Session#setTvRecordingInfo(String,
* TvRecordingInfo)} is called.
*
@@ -1954,5 +1988,34 @@
mCallback.onRequestSigning(mIAppServiceId, id, algorithm, alias, data);
}
}
+
+ @Override
+ public void onRequestSigning(
+ Session session, String id, String algorithm, String host, int port, byte[] data) {
+ if (DEBUG) {
+ Log.d(TAG, "onRequestSigning");
+ }
+ if (this != mSessionCallback) {
+ Log.w(TAG, "onRequestSigning - session not created");
+ return;
+ }
+ if (mCallback != null && Flags.tiafVApis()) {
+ mCallback.onRequestSigning(mIAppServiceId, id, algorithm, host, port, data);
+ }
+ }
+
+ @Override
+ public void onRequestCertificate(Session session, String host, int port) {
+ if (DEBUG) {
+ Log.d(TAG, "onRequestCertificate");
+ }
+ if (this != mSessionCallback) {
+ Log.w(TAG, "onRequestCertificate - session not created");
+ return;
+ }
+ if (mCallback != null && Flags.tiafVApis()) {
+ mCallback.onRequestCertificate(mIAppServiceId, host, port);
+ }
+ }
}
}
diff --git a/media/java/android/service/media/MediaBrowserService.java b/media/java/android/service/media/MediaBrowserService.java
index e8ef464..ba7ab9a 100644
--- a/media/java/android/service/media/MediaBrowserService.java
+++ b/media/java/android/service/media/MediaBrowserService.java
@@ -48,7 +48,6 @@
import java.lang.annotation.RetentionPolicy;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
-import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
@@ -728,7 +727,7 @@
List<MediaBrowser.MediaItem> filteredList =
(flag & RESULT_FLAG_OPTION_NOT_HANDLED) != 0
- ? applyOptions(list, options) : list;
+ ? MediaBrowserUtils.applyPagingOptions(list, options) : list;
final ParceledListSlice<MediaBrowser.MediaItem> pls;
if (filteredList == null) {
pls = null;
@@ -762,27 +761,6 @@
}
}
- private List<MediaBrowser.MediaItem> applyOptions(List<MediaBrowser.MediaItem> list,
- final Bundle options) {
- if (list == null) {
- return null;
- }
- int page = options.getInt(MediaBrowser.EXTRA_PAGE, -1);
- int pageSize = options.getInt(MediaBrowser.EXTRA_PAGE_SIZE, -1);
- if (page == -1 && pageSize == -1) {
- return list;
- }
- int fromIndex = pageSize * page;
- int toIndex = fromIndex + pageSize;
- if (page < 0 || pageSize < 1 || fromIndex >= list.size()) {
- return Collections.EMPTY_LIST;
- }
- if (toIndex > list.size()) {
- toIndex = list.size();
- }
- return list.subList(fromIndex, toIndex);
- }
-
private void performLoadItem(String itemId, final ConnectionRecord connection,
final ResultReceiver receiver) {
final Result<MediaBrowser.MediaItem> result =
diff --git a/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/FadeManagerConfigurationUnitTest.java b/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/FadeManagerConfigurationUnitTest.java
index f105ae9..236b1fd 100644
--- a/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/FadeManagerConfigurationUnitTest.java
+++ b/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/FadeManagerConfigurationUnitTest.java
@@ -45,8 +45,10 @@
@RunWith(AndroidJUnit4.class)
@RequiresFlagsEnabled(FLAG_ENABLE_FADE_MANAGER_CONFIGURATION)
public final class FadeManagerConfigurationUnitTest {
- private static final long DEFAULT_FADE_OUT_DURATION_MS = 2_000;
- private static final long DEFAULT_FADE_IN_DURATION_MS = 1_000;
+ private static final long DEFAULT_FADE_OUT_DURATION_MS =
+ FadeManagerConfiguration.getDefaultFadeOutDurationMillis();
+ private static final long DEFAULT_FADE_IN_DURATION_MS =
+ FadeManagerConfiguration.getDefaultFadeInDurationMillis();
private static final long TEST_FADE_OUT_DURATION_MS = 1_500;
private static final long TEST_FADE_IN_DURATION_MS = 750;
private static final int TEST_INVALID_USAGE = -10;
@@ -259,16 +261,6 @@
}
@Test
- public void testSetFadeState_toEnableAuto() {
- final int fadeStateAuto = FadeManagerConfiguration.FADE_STATE_ENABLED_AUTO;
- FadeManagerConfiguration fmc = new FadeManagerConfiguration.Builder()
- .setFadeState(fadeStateAuto).build();
-
- expect.withMessage("Fade state when enabled for audio").that(fmc.getFadeState())
- .isEqualTo(fadeStateAuto);
- }
-
- @Test
public void testSetFadeState_toInvalid_fails() {
IllegalArgumentException thrown = assertThrows(IllegalArgumentException.class, () ->
new FadeManagerConfiguration.Builder()
@@ -310,13 +302,13 @@
}
@Test
- public void testSetFadeVolShaperConfig_withNullVolumeShaper_getsNull() {
+ public void testSetFadeOutVolShaperConfig_withNullVolumeShaper_getsNull() {
FadeManagerConfiguration fmc = new FadeManagerConfiguration.Builder(mFmc)
.setFadeOutVolumeShaperConfigForAudioAttributes(TEST_MEDIA_AUDIO_ATTRIBUTE,
/* VolumeShaper.Configuration= */ null)
.setFadeInVolumeShaperConfigForAudioAttributes(TEST_MEDIA_AUDIO_ATTRIBUTE,
/* VolumeShaper.Configuration= */ null)
- .clearFadeableUsage(AudioAttributes.USAGE_MEDIA).build();
+ .clearFadeableUsages().addFadeableUsage(AudioAttributes.USAGE_MEDIA).build();
expect.withMessage("Fade out volume shaper config set with null value")
.that(fmc.getFadeOutVolumeShaperConfigForAudioAttributes(
@@ -547,31 +539,13 @@
}
@Test
- public void testClearFadeableUsage() {
- final int usageToClear = AudioAttributes.USAGE_MEDIA;
- List<Integer> updatedUsages = new ArrayList<>(mFmc.getFadeableUsages());
- updatedUsages.remove((Integer) usageToClear);
+ public void testClearFadeableUsages() {
+ FadeManagerConfiguration updatedFmc = new FadeManagerConfiguration.Builder(mFmc)
+ .clearFadeableUsages().addFadeableUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION)
+ .build();
- FadeManagerConfiguration updatedFmc = new FadeManagerConfiguration
- .Builder(mFmc).clearFadeableUsage(usageToClear).build();
-
- expect.withMessage("Clear fadeable usage").that(updatedFmc.getFadeableUsages())
- .containsExactlyElementsIn(updatedUsages);
- }
-
- @Test
- public void testClearFadeableUsage_withInvalidUsage_fails() {
- FadeManagerConfiguration.Builder fmcBuilder = new FadeManagerConfiguration.Builder(mFmc);
-
- IllegalArgumentException thrown = assertThrows(IllegalArgumentException.class, () ->
- fmcBuilder.clearFadeableUsage(TEST_INVALID_USAGE)
- );
-
- FadeManagerConfiguration fmc = fmcBuilder.build();
- expect.withMessage("Clear invalid usage").that(thrown).hasMessageThat()
- .contains("Invalid usage");
- expect.withMessage("Fadeable usages").that(fmc.getFadeableUsages())
- .containsExactlyElementsIn(mFmc.getFadeableUsages());
+ expect.withMessage("Clear fadeable usages").that(updatedFmc.getFadeableUsages())
+ .containsExactlyElementsIn(List.of(AudioAttributes.USAGE_VOICE_COMMUNICATION));
}
@Test
@@ -673,7 +647,7 @@
}
@Test
- public void testClearUnfadeableContentType() {
+ public void testClearUnfadeableContentTypes() {
List<Integer> unfadeableContentTypes = new ArrayList<>(Arrays.asList(
AudioAttributes.CONTENT_TYPE_MOVIE,
AudioAttributes.CONTENT_TYPE_SONIFICATION
@@ -682,23 +656,10 @@
FadeManagerConfiguration updatedFmc = new FadeManagerConfiguration.Builder()
.setUnfadeableContentTypes(unfadeableContentTypes)
- .clearUnfadeableContentType(contentTypeToClear).build();
+ .clearUnfadeableContentTypes().build();
- unfadeableContentTypes.remove((Integer) contentTypeToClear);
expect.withMessage("Unfadeable content types").that(updatedFmc.getUnfadeableContentTypes())
- .containsExactlyElementsIn(unfadeableContentTypes);
- }
-
- @Test
- public void testClearUnfadeableContentType_withInvalidContentType_fails() {
- FadeManagerConfiguration.Builder fmcBuilder = new FadeManagerConfiguration.Builder(mFmc);
-
- IllegalArgumentException thrown = assertThrows(IllegalArgumentException.class, () ->
- fmcBuilder.clearUnfadeableContentType(TEST_INVALID_CONTENT_TYPE).build()
- );
-
- expect.withMessage("Invalid content type exception").that(thrown).hasMessageThat()
- .contains("Invalid content type");
+ .isEmpty();
}
@Test
@@ -735,7 +696,7 @@
}
@Test
- public void testClearUnfadebaleUid() {
+ public void testClearUnfadebaleUids() {
final List<Integer> unfadeableUids = List.of(
TEST_UID_1,
TEST_UID_2
@@ -744,10 +705,9 @@
.setUnfadeableUids(unfadeableUids).build();
FadeManagerConfiguration updatedFmc = new FadeManagerConfiguration.Builder(fmc)
- .clearUnfadeableUid(TEST_UID_1).build();
+ .clearUnfadeableUids().build();
- expect.withMessage("Unfadeable uids").that(updatedFmc.getUnfadeableUids())
- .isEqualTo(List.of(TEST_UID_2));
+ expect.withMessage("Unfadeable uids").that(updatedFmc.getUnfadeableUids()).isEmpty();
}
@Test
diff --git a/media/tests/EffectsTest/src/com/android/effectstest/EffectsTest.java b/media/tests/EffectsTest/src/com/android/effectstest/EffectsTest.java
index 70202463..c18a2de 100644
--- a/media/tests/EffectsTest/src/com/android/effectstest/EffectsTest.java
+++ b/media/tests/EffectsTest/src/com/android/effectstest/EffectsTest.java
@@ -41,7 +41,7 @@
public EffectsTest() {
- Log.d(TAG, "contructor");
+ Log.d(TAG, "constructor");
}
@Override
diff --git a/media/tests/MediaFrameworkTest/Android.bp b/media/tests/MediaFrameworkTest/Android.bp
index 7a329bc..1325fc1 100644
--- a/media/tests/MediaFrameworkTest/Android.bp
+++ b/media/tests/MediaFrameworkTest/Android.bp
@@ -22,7 +22,6 @@
"android-ex-camera2",
"android.media.playback.flags-aconfig-java",
"flag-junit",
- "testables",
"testng",
"truth",
],
diff --git a/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/unit/OWNERS b/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/unit/OWNERS
deleted file mode 100644
index 6d5f82c..0000000
--- a/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/unit/OWNERS
+++ /dev/null
@@ -1,2 +0,0 @@
-# Haptics team also works on Ringtone
-per-file *Ringtone* = file:/services/core/java/com/android/server/vibrator/OWNERS
diff --git a/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/unit/RingtoneTest.java b/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/unit/RingtoneTest.java
deleted file mode 100644
index 3c0c684..0000000
--- a/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/unit/RingtoneTest.java
+++ /dev/null
@@ -1,840 +0,0 @@
-/*
- * Copyright (C) 2023 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.mediaframeworktest.unit;
-
-import static android.media.Ringtone.MEDIA_SOUND;
-import static android.media.Ringtone.MEDIA_SOUND_AND_VIBRATION;
-import static android.media.Ringtone.MEDIA_VIBRATION;
-
-import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.truth.Truth.assertWithMessage;
-
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyBoolean;
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.ArgumentMatchers.isNull;
-import static org.mockito.Mockito.doCallRealMethod;
-import static org.mockito.Mockito.doReturn;
-import static org.mockito.Mockito.doThrow;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.spy;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.verifyNoMoreInteractions;
-import static org.mockito.Mockito.verifyZeroInteractions;
-import static org.mockito.Mockito.when;
-
-import android.Manifest;
-import android.content.Context;
-import android.content.pm.PackageManager;
-import android.content.res.AssetFileDescriptor;
-import android.media.AudioAttributes;
-import android.media.AudioManager;
-import android.media.IRingtonePlayer;
-import android.media.MediaPlayer;
-import android.media.Ringtone;
-import android.media.audiofx.HapticGenerator;
-import android.net.Uri;
-import android.os.IBinder;
-import android.os.VibrationAttributes;
-import android.os.VibrationEffect;
-import android.os.Vibrator;
-import android.testing.TestableContext;
-import android.util.ArrayMap;
-import android.util.ArraySet;
-
-import androidx.test.InstrumentationRegistry;
-import androidx.test.runner.AndroidJUnit4;
-
-import com.android.mediaframeworktest.R;
-
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.rules.TestRule;
-import org.junit.runner.Description;
-import org.junit.runner.RunWith;
-import org.junit.runners.model.Statement;
-import org.mockito.ArgumentCaptor;
-import org.mockito.Captor;
-import org.mockito.Mock;
-import org.mockito.Mockito;
-import org.mockito.MockitoAnnotations;
-
-import java.io.FileNotFoundException;
-import java.util.ArrayDeque;
-import java.util.Map;
-import java.util.Queue;
-
-@RunWith(AndroidJUnit4.class)
-public class RingtoneTest {
-
- private static final Uri SOUND_URI = Uri.parse("content://fake-sound-uri");
-
- private static final AudioAttributes RINGTONE_ATTRIBUTES =
- audioAttributes(AudioAttributes.USAGE_NOTIFICATION_RINGTONE);
- private static final AudioAttributes RINGTONE_ATTRIBUTES_WITH_HC =
- new AudioAttributes.Builder(RINGTONE_ATTRIBUTES).setHapticChannelsMuted(false).build();
- private static final VibrationAttributes RINGTONE_VIB_ATTRIBUTES =
- new VibrationAttributes.Builder(RINGTONE_ATTRIBUTES).build();
-
- private static final VibrationEffect VIBRATION_EFFECT =
- VibrationEffect.createWaveform(new long[] { 0, 100, 50, 100}, -1);
- private static final VibrationEffect VIBRATION_EFFECT_REPEATING =
- VibrationEffect.createWaveform(new long[] { 0, 100, 50, 100, 50}, 1);
-
- @Rule
- public final RingtoneInjectablesTrackingTestRule
- mMediaPlayerRule = new RingtoneInjectablesTrackingTestRule();
-
- @Captor private ArgumentCaptor<IBinder> mIBinderCaptor;
- @Mock private IRingtonePlayer mMockRemotePlayer;
- @Mock private Vibrator mMockVibrator;
- private AudioManager mSpyAudioManager;
- private TestableContext mContext;
-
- @Before
- public void setUp() throws Exception {
- MockitoAnnotations.initMocks(this);
- TestableContext testContext =
- new TestableContext(InstrumentationRegistry.getTargetContext(), null);
- testContext.getTestablePermissions().setPermission(Manifest.permission.VIBRATE,
- PackageManager.PERMISSION_GRANTED);
- AudioManager realAudioManager = testContext.getSystemService(AudioManager.class);
- mSpyAudioManager = spy(realAudioManager);
- when(mSpyAudioManager.getRingtonePlayer()).thenReturn(mMockRemotePlayer);
- testContext.addMockSystemService(AudioManager.class, mSpyAudioManager);
- testContext.addMockSystemService(Vibrator.class, mMockVibrator);
-
- mContext = spy(testContext);
- }
-
- @Test
- public void testRingtone_fullLifecycleUsingLocalMediaPlayer() throws Exception {
- MediaPlayer mockMediaPlayer = mMediaPlayerRule.expectLocalMediaPlayer();
- Ringtone ringtone =
- newBuilder(MEDIA_SOUND, RINGTONE_ATTRIBUTES).setUri(SOUND_URI).build();
- assertThat(ringtone).isNotNull();
- assertThat(ringtone.isUsingRemotePlayer()).isFalse();
-
- // Verify all the properties.
- assertThat(ringtone.getEnabledMedia()).isEqualTo(MEDIA_SOUND);
- assertThat(ringtone.getUri()).isEqualTo(SOUND_URI);
- assertThat(ringtone.getAudioAttributes()).isEqualTo(RINGTONE_ATTRIBUTES);
- assertThat(ringtone.getVolume()).isEqualTo(1.0f);
- assertThat(ringtone.isLooping()).isEqualTo(false);
- assertThat(ringtone.isHapticGeneratorEnabled()).isEqualTo(false);
- assertThat(ringtone.getPreferBuiltinDevice()).isFalse();
- assertThat(ringtone.getVolumeShaperConfig()).isNull();
- assertThat(ringtone.isLocalOnly()).isFalse();
-
- // Prepare
- verifyLocalPlayerSetup(mockMediaPlayer, SOUND_URI, RINGTONE_ATTRIBUTES);
- verify(mockMediaPlayer).setVolume(1.0f);
- verify(mockMediaPlayer).setLooping(false);
- verify(mockMediaPlayer).prepare();
-
- // Play
- ringtone.play();
- verifyLocalPlay(mockMediaPlayer);
-
- // Verify dynamic controls.
- ringtone.setVolume(0.8f);
- verify(mockMediaPlayer).setVolume(0.8f);
- when(mockMediaPlayer.isLooping()).thenReturn(false);
- ringtone.setLooping(true);
- verify(mockMediaPlayer).isLooping();
- verify(mockMediaPlayer).setLooping(true);
- HapticGenerator mockHapticGenerator =
- mMediaPlayerRule.expectHapticGenerator(mockMediaPlayer);
- ringtone.setHapticGeneratorEnabled(true);
- verify(mockHapticGenerator).setEnabled(true);
-
- // Release
- ringtone.stop();
- verifyLocalStop(mockMediaPlayer);
-
- // This test is intended to strictly verify all interactions with MediaPlayer in a local
- // playback case. This shouldn't be necessary in other tests that have the same basic
- // setup.
- verifyNoMoreInteractions(mockMediaPlayer);
- verify(mockHapticGenerator).release();
- verifyNoMoreInteractions(mockHapticGenerator);
- verifyZeroInteractions(mMockRemotePlayer);
- verifyZeroInteractions(mMockVibrator);
- }
-
- @Test
- public void testRingtone_localMediaPlayerWithAudioCoupledOverride() throws Exception {
- // Audio coupled playback is enabled in the incoming attributes, plus an instruction
- // to leave the attributes alone. This test verifies that the attributes reach the
- // media player without changing.
- final AudioAttributes audioAttributes = RINGTONE_ATTRIBUTES_WITH_HC;
- MediaPlayer mockMediaPlayer = mMediaPlayerRule.expectLocalMediaPlayer();
- mMediaPlayerRule.setHasHapticChannels(mockMediaPlayer, true);
- Ringtone ringtone =
- newBuilder(MEDIA_SOUND, audioAttributes)
- .setUri(SOUND_URI)
- .setUseExactAudioAttributes(true)
- .build();
- assertThat(ringtone).isNotNull();
- assertThat(ringtone.isUsingRemotePlayer()).isFalse();
-
- // Verify all the properties.
- assertThat(ringtone.getEnabledMedia()).isEqualTo(MEDIA_SOUND);
- assertThat(ringtone.getUri()).isEqualTo(SOUND_URI);
- assertThat(ringtone.getAudioAttributes()).isEqualTo(audioAttributes);
-
- // Prepare
- verifyLocalPlayerSetup(mockMediaPlayer, SOUND_URI, audioAttributes);
- verify(mockMediaPlayer).prepare();
-
- // Play
- ringtone.play();
- verifyLocalPlay(mockMediaPlayer);
-
- // Release
- ringtone.stop();
- verifyLocalStop(mockMediaPlayer);
-
- verifyZeroInteractions(mMockRemotePlayer);
- verifyZeroInteractions(mMockVibrator);
- }
-
- @Test
- public void testRingtone_fullLifecycleUsingRemoteMediaPlayer() throws Exception {
- MediaPlayer mockMediaPlayer = mMediaPlayerRule.expectLocalMediaPlayer();
- setupFileNotFound(mockMediaPlayer, SOUND_URI);
- Ringtone ringtone =
- newBuilder(MEDIA_SOUND, RINGTONE_ATTRIBUTES)
- .setUri(SOUND_URI)
- .build();
- assertThat(ringtone).isNotNull();
- assertThat(ringtone.isUsingRemotePlayer()).isTrue();
-
- // Verify all the properties.
- assertThat(ringtone.getEnabledMedia()).isEqualTo(MEDIA_SOUND);
- assertThat(ringtone.getUri()).isEqualTo(SOUND_URI);
- assertThat(ringtone.getAudioAttributes()).isEqualTo(RINGTONE_ATTRIBUTES);
- assertThat(ringtone.getVolume()).isEqualTo(1.0f);
- assertThat(ringtone.isLooping()).isEqualTo(false);
- assertThat(ringtone.isHapticGeneratorEnabled()).isEqualTo(false);
- assertThat(ringtone.getPreferBuiltinDevice()).isFalse();
- assertThat(ringtone.getVolumeShaperConfig()).isNull();
- assertThat(ringtone.isLocalOnly()).isFalse();
-
- // Initialization did try to create a local media player.
- verify(mockMediaPlayer).setDataSource(mContext, SOUND_URI);
- // setDataSource throws file not found, so nothing else will happen on the local player.
- verify(mockMediaPlayer).release();
-
- // Delegates to remote media player.
- ringtone.play();
- verify(mMockRemotePlayer).playRemoteRingtone(mIBinderCaptor.capture(), eq(SOUND_URI),
- eq(RINGTONE_ATTRIBUTES), eq(false), eq(MEDIA_SOUND), isNull(),
- eq(1.0f), eq(false), eq(false), isNull());
- IBinder remoteToken = mIBinderCaptor.getValue();
-
- // Verify dynamic controls.
- ringtone.setVolume(0.8f);
- verify(mMockRemotePlayer).setVolume(remoteToken, 0.8f);
- ringtone.setLooping(true);
- verify(mMockRemotePlayer).setLooping(remoteToken, true);
- ringtone.setHapticGeneratorEnabled(true);
- verify(mMockRemotePlayer).setHapticGeneratorEnabled(remoteToken, true);
-
- ringtone.stop();
- verify(mMockRemotePlayer).stop(remoteToken);
- verifyNoMoreInteractions(mMockRemotePlayer);
- verifyNoMoreInteractions(mockMediaPlayer);
- verifyZeroInteractions(mMockVibrator);
- }
-
- @Test
- public void testRingtone_localMediaWithVibration() throws Exception {
- MediaPlayer mockMediaPlayer = mMediaPlayerRule.expectLocalMediaPlayer();
- when(mMockVibrator.hasVibrator()).thenReturn(true);
- Ringtone ringtone =
- newBuilder(MEDIA_SOUND_AND_VIBRATION, RINGTONE_ATTRIBUTES)
- .setUri(SOUND_URI)
- .setVibrationEffect(VIBRATION_EFFECT)
- .build();
- assertThat(ringtone).isNotNull();
- assertThat(ringtone.isUsingRemotePlayer()).isFalse();
- verify(mMockVibrator).hasVibrator();
-
- // Verify all the properties.
- assertThat(ringtone.getEnabledMedia()).isEqualTo(MEDIA_SOUND_AND_VIBRATION);
- assertThat(ringtone.getUri()).isEqualTo(SOUND_URI);
- assertThat(ringtone.getVibrationEffect()).isEqualTo(VIBRATION_EFFECT);
-
- // Prepare
- // Uses attributes with haptic channels enabled, but will use the effect when there aren't
- // any present.
- verifyLocalPlayerSetup(mockMediaPlayer, SOUND_URI, RINGTONE_ATTRIBUTES_WITH_HC);
- verify(mockMediaPlayer).setVolume(1.0f);
- verify(mockMediaPlayer).setLooping(false);
- verify(mockMediaPlayer).prepare();
-
- // Play
- ringtone.play();
-
- verifyLocalPlay(mockMediaPlayer);
- verify(mMockVibrator).vibrate(VIBRATION_EFFECT, RINGTONE_VIB_ATTRIBUTES);
-
- // Verify dynamic controls.
- ringtone.setVolume(0.8f);
- verify(mockMediaPlayer).setVolume(0.8f);
-
- // Set looping doesn't affect an already-started vibration.
- when(mockMediaPlayer.isLooping()).thenReturn(false); // Checks original
- ringtone.setLooping(true);
- verify(mockMediaPlayer).isLooping();
- verify(mockMediaPlayer).setLooping(true);
-
- // This is ignored because there's a vibration effect being used.
- ringtone.setHapticGeneratorEnabled(true);
-
- // Release
- ringtone.stop();
- verifyLocalStop(mockMediaPlayer);
- verify(mMockVibrator).cancel(VibrationAttributes.USAGE_RINGTONE);
-
- // This test is intended to strictly verify all interactions with MediaPlayer in a local
- // playback case. This shouldn't be necessary in other tests that have the same basic
- // setup.
- verifyNoMoreInteractions(mockMediaPlayer);
- verifyZeroInteractions(mMockRemotePlayer);
- verifyNoMoreInteractions(mMockVibrator);
- }
-
- @Test
- public void testRingtone_localMediaWithVibrationOnly() throws Exception {
- when(mMockVibrator.hasVibrator()).thenReturn(true);
- Ringtone ringtone =
- newBuilder(MEDIA_VIBRATION, RINGTONE_ATTRIBUTES)
- // TODO: set sound uri too in diff test
- .setVibrationEffect(VIBRATION_EFFECT)
- .build();
- assertThat(ringtone).isNotNull();
- assertThat(ringtone.isUsingRemotePlayer()).isFalse();
- verify(mMockVibrator).hasVibrator();
-
- // Verify all the properties.
- assertThat(ringtone.getEnabledMedia()).isEqualTo(MEDIA_VIBRATION);
- assertThat(ringtone.getUri()).isNull();
- assertThat(ringtone.getVibrationEffect()).isEqualTo(VIBRATION_EFFECT);
-
- // Play
- ringtone.play();
-
- verify(mMockVibrator).vibrate(VIBRATION_EFFECT, RINGTONE_VIB_ATTRIBUTES);
-
- // Verify dynamic controls (no-op without sound)
- ringtone.setVolume(0.8f);
-
- // Set looping doesn't affect an already-started vibration.
- ringtone.setLooping(true);
-
- // This is ignored because there's a vibration effect being used and no sound.
- ringtone.setHapticGeneratorEnabled(true);
-
- // Release
- ringtone.stop();
- verify(mMockVibrator).cancel(VibrationAttributes.USAGE_RINGTONE);
-
- // This test is intended to strictly verify all interactions with MediaPlayer in a local
- // playback case. This shouldn't be necessary in other tests that have the same basic
- // setup.
- verifyZeroInteractions(mMockRemotePlayer);
- verifyNoMoreInteractions(mMockVibrator);
- }
-
- @Test
- public void testRingtone_localMediaWithVibrationOnlyAndSoundUriNoHapticChannels()
- throws Exception {
- // A media player will still be created for vibration-only because the vibration can come
- // from haptic channels on the sound file (although in this case it doesn't).
- MediaPlayer mockMediaPlayer = mMediaPlayerRule.expectLocalMediaPlayer();
- mMediaPlayerRule.setHasHapticChannels(mockMediaPlayer, false);
- when(mMockVibrator.hasVibrator()).thenReturn(true);
- Ringtone ringtone =
- newBuilder(MEDIA_VIBRATION, RINGTONE_ATTRIBUTES)
- .setUri(SOUND_URI)
- .setVibrationEffect(VIBRATION_EFFECT)
- .build();
- assertThat(ringtone).isNotNull();
- assertThat(ringtone.isUsingRemotePlayer()).isFalse();
- verify(mMockVibrator).hasVibrator();
-
- // Verify all the properties.
- assertThat(ringtone.getEnabledMedia()).isEqualTo(MEDIA_VIBRATION);
- assertThat(ringtone.getUri()).isEqualTo(SOUND_URI);
- assertThat(ringtone.getVibrationEffect()).isEqualTo(VIBRATION_EFFECT);
-
- // Prepare
- // Uses attributes with haptic channels enabled, but will abandon the MediaPlayer when it
- // knows there aren't any.
- verifyLocalPlayerSetup(mockMediaPlayer, SOUND_URI, RINGTONE_ATTRIBUTES_WITH_HC);
- verify(mockMediaPlayer).setVolume(0.0f); // Vibration-only: sound muted.
- verify(mockMediaPlayer).setLooping(false);
- verify(mockMediaPlayer).prepare();
- verify(mockMediaPlayer).release(); // abandoned: no haptic channels.
-
- // Play
- ringtone.play();
-
- verify(mMockVibrator).vibrate(VIBRATION_EFFECT, RINGTONE_VIB_ATTRIBUTES);
-
- // Verify dynamic controls (no-op without sound)
- ringtone.setVolume(0.8f);
-
- // Set looping doesn't affect an already-started vibration.
- ringtone.setLooping(true);
-
- // This is ignored because there's a vibration effect being used and no sound.
- ringtone.setHapticGeneratorEnabled(true);
-
- // Release
- ringtone.stop();
- verify(mMockVibrator).cancel(VibrationAttributes.USAGE_RINGTONE);
-
- // This test is intended to strictly verify all interactions with MediaPlayer in a local
- // playback case. This shouldn't be necessary in other tests that have the same basic
- // setup.
- verifyZeroInteractions(mMockRemotePlayer);
- verifyNoMoreInteractions(mMockVibrator);
- verifyNoMoreInteractions(mockMediaPlayer);
- }
-
- @Test
- public void testRingtone_localMediaWithVibrationOnlyAndSoundUriWithHapticChannels()
- throws Exception {
- MediaPlayer mockMediaPlayer = mMediaPlayerRule.expectLocalMediaPlayer();
- when(mMockVibrator.hasVibrator()).thenReturn(true);
- mMediaPlayerRule.setHasHapticChannels(mockMediaPlayer, true);
- Ringtone ringtone =
- newBuilder(MEDIA_VIBRATION, RINGTONE_ATTRIBUTES)
- .setUri(SOUND_URI)
- .setVibrationEffect(VIBRATION_EFFECT)
- .build();
- assertThat(ringtone).isNotNull();
- assertThat(ringtone.isUsingRemotePlayer()).isFalse();
- verify(mMockVibrator).hasVibrator();
-
- // Verify all the properties.
- assertThat(ringtone.getEnabledMedia()).isEqualTo(MEDIA_VIBRATION);
- assertThat(ringtone.getUri()).isEqualTo(SOUND_URI);
- assertThat(ringtone.getVibrationEffect()).isEqualTo(VIBRATION_EFFECT);
-
- // Prepare
- // Uses attributes with haptic channels enabled, but will use the effect when there aren't
- // any present.
- verifyLocalPlayerSetup(mockMediaPlayer, SOUND_URI, RINGTONE_ATTRIBUTES_WITH_HC);
- verify(mockMediaPlayer).setVolume(0.0f); // Vibration-only: sound muted.
- verify(mockMediaPlayer).setLooping(false);
- verify(mockMediaPlayer).prepare();
-
- // Play
- ringtone.play();
- // Vibrator.vibrate isn't called because the vibration comes from the sound.
- verifyLocalPlay(mockMediaPlayer);
-
- // Verify dynamic controls (no-op without sound)
- ringtone.setVolume(0.8f);
-
- when(mockMediaPlayer.isLooping()).thenReturn(false); // Checks original
- ringtone.setLooping(true);
- verify(mockMediaPlayer).isLooping();
- verify(mockMediaPlayer).setLooping(true);
-
- // This is ignored because it's using haptic channels.
- ringtone.setHapticGeneratorEnabled(true);
-
- // Release
- ringtone.stop();
- verifyLocalStop(mockMediaPlayer);
-
- // This test is intended to strictly verify all interactions with MediaPlayer in a local
- // playback case. This shouldn't be necessary in other tests that have the same basic
- // setup.
- verifyZeroInteractions(mMockRemotePlayer);
- verifyZeroInteractions(mMockVibrator);
- }
-
- @Test
- public void testRingtone_localMediaWithVibrationPrefersHapticChannels() throws Exception {
- MediaPlayer mockMediaPlayer = mMediaPlayerRule.expectLocalMediaPlayer();
- mMediaPlayerRule.setHasHapticChannels(mockMediaPlayer, true);
- when(mMockVibrator.hasVibrator()).thenReturn(true);
- Ringtone ringtone =
- newBuilder(MEDIA_SOUND_AND_VIBRATION, RINGTONE_ATTRIBUTES)
- .setUri(SOUND_URI)
- .setVibrationEffect(VIBRATION_EFFECT)
- .build();
- assertThat(ringtone).isNotNull();
- assertThat(ringtone.isUsingRemotePlayer()).isFalse();
- verify(mMockVibrator).hasVibrator();
-
- // Verify all the properties.
- assertThat(ringtone.getEnabledMedia()).isEqualTo(MEDIA_SOUND_AND_VIBRATION);
- assertThat(ringtone.getUri()).isEqualTo(SOUND_URI);
- assertThat(ringtone.getVibrationEffect()).isEqualTo(VIBRATION_EFFECT);
-
- // Prepare
- // The attributes here have haptic channels enabled (unlike above)
- verifyLocalPlayerSetup(mockMediaPlayer, SOUND_URI, RINGTONE_ATTRIBUTES_WITH_HC);
- verify(mockMediaPlayer).prepare();
-
- // Play
- ringtone.play();
- when(mockMediaPlayer.isPlaying()).thenReturn(true);
- verifyLocalPlay(mockMediaPlayer);
-
- // Release
- ringtone.stop();
- verifyLocalStop(mockMediaPlayer);
-
- verifyZeroInteractions(mMockRemotePlayer);
- // Nothing after the initial hasVibrator - it uses audio-coupled.
- verifyNoMoreInteractions(mMockVibrator);
- }
-
- @Test
- public void testRingtone_localMediaWithVibrationButSoundMuted() throws Exception {
- MediaPlayer mockMediaPlayer = mMediaPlayerRule.expectLocalMediaPlayer();
- mMediaPlayerRule.setHasHapticChannels(mockMediaPlayer, false);
- doReturn(0).when(mSpyAudioManager)
- .getStreamVolume(AudioAttributes.toLegacyStreamType(RINGTONE_ATTRIBUTES));
- when(mMockVibrator.hasVibrator()).thenReturn(true);
- Ringtone ringtone =
- newBuilder(MEDIA_SOUND_AND_VIBRATION, RINGTONE_ATTRIBUTES)
- .setUri(SOUND_URI)
- .setVibrationEffect(VIBRATION_EFFECT)
- .build();
- assertThat(ringtone).isNotNull();
- assertThat(ringtone.isUsingRemotePlayer()).isFalse();
- verify(mMockVibrator).hasVibrator();
-
- // Verify all the properties.
- assertThat(ringtone.getEnabledMedia()).isEqualTo(MEDIA_SOUND_AND_VIBRATION);
- assertThat(ringtone.getUri()).isEqualTo(SOUND_URI);
- assertThat(ringtone.getVibrationEffect()).isEqualTo(VIBRATION_EFFECT);
-
- // Prepare
- // The attributes here have haptic channels enabled (unlike above)
- verifyLocalPlayerSetup(mockMediaPlayer, SOUND_URI, RINGTONE_ATTRIBUTES_WITH_HC);
- verify(mockMediaPlayer).prepare();
-
- // Play
- ringtone.play();
- // The media player is never played, because sound is muted.
- verify(mockMediaPlayer, never()).start();
- when(mockMediaPlayer.isPlaying()).thenReturn(true);
- verify(mMockVibrator).vibrate(VIBRATION_EFFECT, RINGTONE_VIB_ATTRIBUTES);
-
- // Release
- ringtone.stop();
- verify(mockMediaPlayer).release();
- verify(mMockVibrator).cancel(VibrationAttributes.USAGE_RINGTONE);
-
- verifyZeroInteractions(mMockRemotePlayer);
- // Nothing after the initial hasVibrator - it uses audio-coupled.
- verifyNoMoreInteractions(mMockVibrator);
- }
-
- @Test
- public void testRingtone_nullMediaOnBuilderUsesFallback() throws Exception {
- AssetFileDescriptor testResourceFd =
- mContext.getResources().openRawResourceFd(R.raw.shortmp3);
- // Ensure it will flow as expected.
- assertThat(testResourceFd).isNotNull();
- assertThat(testResourceFd.getDeclaredLength()).isAtLeast(0);
- mContext.getOrCreateTestableResources()
- .addOverride(com.android.internal.R.raw.fallbackring, testResourceFd);
-
- MediaPlayer mockMediaPlayer = mMediaPlayerRule.expectLocalMediaPlayer();
- Ringtone ringtone = newBuilder(MEDIA_SOUND, RINGTONE_ATTRIBUTES)
- .setUri(null)
- .build();
- assertThat(ringtone).isNotNull();
- assertThat(ringtone.isUsingRemotePlayer()).isFalse();
-
- // Delegates straight to fallback in local player.
- // Prepare
- verifyLocalPlayerFallbackSetup(mockMediaPlayer, testResourceFd, RINGTONE_ATTRIBUTES);
- verify(mockMediaPlayer).setVolume(1.0f);
- verify(mockMediaPlayer).setLooping(false);
- verify(mockMediaPlayer).prepare();
-
- // Play
- ringtone.play();
- verifyLocalPlay(mockMediaPlayer);
-
- // Release
- ringtone.stop();
- verifyLocalStop(mockMediaPlayer);
-
- verifyNoMoreInteractions(mockMediaPlayer);
- verifyNoMoreInteractions(mMockRemotePlayer);
- }
-
- @Test
- public void testRingtone_nullMediaOnBuilderUsesFallbackViaRemote() throws Exception {
- mContext.getOrCreateTestableResources()
- .addOverride(com.android.internal.R.raw.fallbackring, null);
- Ringtone ringtone = newBuilder(MEDIA_SOUND, RINGTONE_ATTRIBUTES)
- .setUri(null)
- .setLooping(true) // distinct from haptic generator, to match plumbing
- .build();
- assertThat(ringtone).isNotNull();
- // Local player fallback fails as the resource isn't found (no media player creation is
- // attempted), and then goes on to create the remote player.
- assertThat(ringtone.isUsingRemotePlayer()).isTrue();
-
- ringtone.play();
- verify(mMockRemotePlayer).playRemoteRingtone(mIBinderCaptor.capture(), isNull(),
- eq(RINGTONE_ATTRIBUTES), eq(false),
- eq(MEDIA_SOUND), isNull(),
- eq(1.0f), eq(true), eq(false), isNull());
- ringtone.stop();
- verify(mMockRemotePlayer).stop(mIBinderCaptor.getValue());
- verifyNoMoreInteractions(mMockRemotePlayer);
- }
-
- @Test
- public void testRingtone_noMediaSetOnBuilderFallbackFailsAndNoRemote() throws Exception {
- mContext.getOrCreateTestableResources()
- .addOverride(com.android.internal.R.raw.fallbackring, null);
- Ringtone ringtone = newBuilder(MEDIA_SOUND, RINGTONE_ATTRIBUTES)
- .setUri(null)
- .setLocalOnly()
- .build();
- // Local player fallback fails as the resource isn't found (no media player creation is
- // attempted), and since there is no local player, the ringtone ends up having nothing to
- // do.
- assertThat(ringtone).isNull();
- }
-
- private Ringtone.Builder newBuilder(@Ringtone.RingtoneMedia int ringtoneMedia,
- AudioAttributes audioAttributes) {
- return new Ringtone.Builder(mContext, ringtoneMedia, audioAttributes)
- .setInjectables(mMediaPlayerRule.injectables);
- }
-
- private static AudioAttributes audioAttributes(int audioUsage) {
- return new AudioAttributes.Builder()
- .setUsage(audioUsage)
- .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
- .build();
- }
-
- /** Makes the mock get some sort of file access problem. */
- private void setupFileNotFound(MediaPlayer mockMediaPlayer, Uri uri) throws Exception {
- doThrow(new FileNotFoundException("Fake file not found"))
- .when(mockMediaPlayer).setDataSource(any(Context.class), eq(uri));
- }
-
- private void verifyLocalPlayerSetup(MediaPlayer mockPlayer, Uri expectedUri,
- AudioAttributes expectedAudioAttributes) throws Exception {
- verify(mockPlayer).setDataSource(mContext, expectedUri);
- verify(mockPlayer).setAudioAttributes(expectedAudioAttributes);
- verify(mockPlayer).setPreferredDevice(null);
- verify(mockPlayer).prepare();
- }
-
- private void verifyLocalPlayerFallbackSetup(MediaPlayer mockPlayer, AssetFileDescriptor afd,
- AudioAttributes expectedAudioAttributes) throws Exception {
- // This is very specific but it's a simple way to test that the test resource matches.
- if (afd.getDeclaredLength() < 0) {
- verify(mockPlayer).setDataSource(afd.getFileDescriptor());
- } else {
- verify(mockPlayer).setDataSource(afd.getFileDescriptor(),
- afd.getStartOffset(),
- afd.getDeclaredLength());
- }
- verify(mockPlayer).setAudioAttributes(expectedAudioAttributes);
- verify(mockPlayer).setPreferredDevice(null);
- verify(mockPlayer).prepare();
- }
-
- private void verifyLocalPlay(MediaPlayer mockMediaPlayer) {
- verify(mockMediaPlayer).setOnCompletionListener(any());
- verify(mockMediaPlayer).start();
- }
-
- private void verifyLocalStop(MediaPlayer mockMediaPlayer) {
- verify(mockMediaPlayer).stop();
- verify(mockMediaPlayer).setOnCompletionListener(isNull());
- verify(mockMediaPlayer).reset();
- verify(mockMediaPlayer).release();
- }
-
- /**
- * This rule ensures that all expected media player creations from the factory do actually
- * occur. The reason for this level of control is that creating a media player is fairly
- * expensive and blocking, so we do want unit tests of this class to "declare" interactions
- * of all created media players.
- *
- * This needs to be a TestRule so that the teardown assertions can be skipped if the test has
- * failed (and media player assertions may just be a distracting side effect). Otherwise, the
- * teardown failures hide the real test ones.
- */
- public static class RingtoneInjectablesTrackingTestRule implements TestRule {
- public Ringtone.Injectables injectables = new TestInjectables();
- public boolean hapticGeneratorAvailable = true;
-
- // Queue of (local) media players, in order of expected creation. Enqueue using
- // expectNewMediaPlayer(), dequeued by the media player factory passed to Ringtone.
- // This queue is asserted to be empty at the end of the test.
- private Queue<MediaPlayer> mMockMediaPlayerQueue = new ArrayDeque<>();
-
- // Similar to media players, but for haptic generator, which also needs releasing.
- private Map<MediaPlayer, HapticGenerator> mMockHapticGeneratorMap = new ArrayMap<>();
-
- // Media players with haptic channels.
- private ArraySet<MediaPlayer> mHapticChannels = new ArraySet<>();
-
- @Override
- public Statement apply(Statement base, Description description) {
- return new Statement() {
- @Override
- public void evaluate() throws Throwable {
- base.evaluate();
- // Only assert if the test didn't fail (base.evaluate() would throw).
- assertWithMessage("Test setup an expectLocalMediaPlayer but it wasn't consumed")
- .that(mMockMediaPlayerQueue).isEmpty();
- // Only assert if the test didn't fail (base.evaluate() would throw).
- assertWithMessage(
- "Test setup an expectLocalHapticGenerator but it wasn't consumed")
- .that(mMockHapticGeneratorMap).isEmpty();
- }
- };
- }
-
- private TestMediaPlayer expectLocalMediaPlayer() {
- TestMediaPlayer mockMediaPlayer = Mockito.mock(TestMediaPlayer.class);
- // Delegate to simulated methods. This means they can be verified but also reflect
- // realistic transitions from the TestMediaPlayer.
- doCallRealMethod().when(mockMediaPlayer).start();
- doCallRealMethod().when(mockMediaPlayer).stop();
- doCallRealMethod().when(mockMediaPlayer).setLooping(anyBoolean());
- when(mockMediaPlayer.isLooping()).thenCallRealMethod();
- when(mockMediaPlayer.isLooping()).thenCallRealMethod();
- mMockMediaPlayerQueue.add(mockMediaPlayer);
- return mockMediaPlayer;
- }
-
- private HapticGenerator expectHapticGenerator(MediaPlayer mockMediaPlayer) {
- HapticGenerator mockHapticGenerator = Mockito.mock(HapticGenerator.class);
- // A test should never want this.
- assertWithMessage("Can't expect a second haptic generator created "
- + "for one media player")
- .that(mMockHapticGeneratorMap.put(mockMediaPlayer, mockHapticGenerator))
- .isNull();
- return mockHapticGenerator;
- }
-
- private void setHasHapticChannels(MediaPlayer mp, boolean hasHapticChannels) {
- if (hasHapticChannels) {
- mHapticChannels.add(mp);
- } else {
- mHapticChannels.remove(mp);
- }
- }
-
- private class TestInjectables extends Ringtone.Injectables {
- @Override
- public MediaPlayer newMediaPlayer() {
- assertWithMessage(
- "Unexpected MediaPlayer creation. Bug or need expectNewMediaPlayer")
- .that(mMockMediaPlayerQueue)
- .isNotEmpty();
- return mMockMediaPlayerQueue.remove();
- }
-
- @Override
- public boolean isHapticGeneratorAvailable() {
- return hapticGeneratorAvailable;
- }
-
- @Override
- public HapticGenerator createHapticGenerator(MediaPlayer mediaPlayer) {
- HapticGenerator mockHapticGenerator = mMockHapticGeneratorMap.remove(mediaPlayer);
- assertWithMessage("Unexpected HapticGenerator creation. "
- + "Bug or need expectHapticGenerator")
- .that(mockHapticGenerator)
- .isNotNull();
- return mockHapticGenerator;
- }
-
- @Override
- public boolean isHapticPlaybackSupported() {
- return true;
- }
-
- @Override
- public boolean hasHapticChannels(MediaPlayer mp) {
- return mHapticChannels.contains(mp);
- }
- }
- }
-
- /**
- * MediaPlayer relies on a native backend and so its necessary to intercept calls from
- * fake usage hitting them.
- *
- * Mocks don't work directly on native calls, but if they're overridden then it does work.
- * Some basic state faking is also done to make the mocks more realistic.
- */
- private static class TestMediaPlayer extends MediaPlayer {
- private boolean mIsPlaying = false;
- private boolean mIsLooping = false;
-
- @Override
- public void start() {
- mIsPlaying = true;
- }
-
- @Override
- public void stop() {
- mIsPlaying = false;
- }
-
- @Override
- public void setLooping(boolean value) {
- mIsLooping = value;
- }
-
- @Override
- public boolean isLooping() {
- return mIsLooping;
- }
-
- @Override
- public boolean isPlaying() {
- return mIsPlaying;
- }
-
- void simulatePlayingFinished() {
- if (!mIsPlaying) {
- throw new IllegalStateException(
- "Attempted to pretend playing finished when not playing");
- }
- mIsPlaying = false;
- }
- }
-}
diff --git a/media/tests/ringtone/Android.bp b/media/tests/ringtone/Android.bp
deleted file mode 100644
index 55b98c4..0000000
--- a/media/tests/ringtone/Android.bp
+++ /dev/null
@@ -1,30 +0,0 @@
-package {
- // See: http://go/android-license-faq
- default_applicable_licenses: ["frameworks_base_license"],
-}
-
-android_test {
- name: "MediaRingtoneTests",
-
- srcs: ["src/**/*.java"],
-
- libs: [
- "android.test.runner",
- "android.test.base",
- ],
-
- static_libs: [
- "androidx.test.rules",
- "testng",
- "androidx.test.ext.truth",
- "frameworks-base-testutils",
- ],
-
- test_suites: [
- "device-tests",
- "automotive-tests",
- ],
-
- platform_apis: true,
- certificate: "platform",
-}
diff --git a/media/tests/ringtone/AndroidManifest.xml b/media/tests/ringtone/AndroidManifest.xml
deleted file mode 100644
index 27eda07..0000000
--- a/media/tests/ringtone/AndroidManifest.xml
+++ /dev/null
@@ -1,41 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright 2023 The Android Open Source Project
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
--->
-
-<manifest xmlns:android="http://schemas.android.com/apk/res/android"
- package="com.android.framework.base.media.ringtone.tests">
-
- <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
- <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
- <uses-permission android:name="android.permission.MANAGE_USERS" />
-
- <application android:debuggable="true">
- <uses-library android:name="android.test.runner" />
-
- <activity android:name="MediaRingtoneTests"
- android:label="Media Ringtone Tests"
- android:exported="true">
- <intent-filter>
- <action android:name="android.intent.action.MAIN"/>
- <category android:name="android.intent.category.LAUNCHER"/>
- </intent-filter>
- </activity>
-
- </application>
-
- <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
- android:targetPackage="com.android.framework.base.media.ringtone.tests"
- android:label="Media Ringtone Tests"/>
-</manifest>
diff --git a/media/tests/ringtone/TEST_MAPPING b/media/tests/ringtone/TEST_MAPPING
deleted file mode 100644
index 6f25c14..0000000
--- a/media/tests/ringtone/TEST_MAPPING
+++ /dev/null
@@ -1,20 +0,0 @@
-{
- "presubmit": [
- {
- "name": "MediaRingtoneTests",
- "options": [
- {"exclude-annotation": "androidx.test.filters.LargeTest"},
- {"exclude-annotation": "androidx.test.filters.FlakyTest"},
- {"exclude-annotation": "org.junit.Ignore"}
- ]
- }
- ],
- "postsubmit": [
- {
- "name": "MediaRingtoneTests",
- "options": [
- {"exclude-annotation": "org.junit.Ignore"}
- ]
- }
- ]
-}
\ No newline at end of file
diff --git a/media/tests/ringtone/res/raw/test_haptic_file.ahv b/media/tests/ringtone/res/raw/test_haptic_file.ahv
deleted file mode 100644
index d6eba1a..0000000
--- a/media/tests/ringtone/res/raw/test_haptic_file.ahv
+++ /dev/null
@@ -1,17 +0,0 @@
-<vibration-effect>
- <waveform-effect>
- <waveform-entry durationMs="63" amplitude="255"/>
- <waveform-entry durationMs="63" amplitude="231"/>
- <waveform-entry durationMs="63" amplitude="208"/>
- <waveform-entry durationMs="63" amplitude="185"/>
- <waveform-entry durationMs="63" amplitude="162"/>
- <waveform-entry durationMs="63" amplitude="139"/>
- <waveform-entry durationMs="63" amplitude="115"/>
- <waveform-entry durationMs="63" amplitude="92"/>
- <waveform-entry durationMs="63" amplitude="69"/>
- <waveform-entry durationMs="63" amplitude="46"/>
- <waveform-entry durationMs="63" amplitude="23"/>
- <waveform-entry durationMs="63" amplitude="0"/>
- <waveform-entry durationMs="1250" amplitude="0"/>
- </waveform-effect>
-</vibration-effect>
diff --git a/media/tests/ringtone/res/raw/test_sound_file.mp3 b/media/tests/ringtone/res/raw/test_sound_file.mp3
deleted file mode 100644
index c1b2fdf..0000000
--- a/media/tests/ringtone/res/raw/test_sound_file.mp3
+++ /dev/null
Binary files differ
diff --git a/media/tests/ringtone/src/com/android/media/RingtoneManagerTest.java b/media/tests/ringtone/src/com/android/media/RingtoneManagerTest.java
deleted file mode 100644
index a92b298..0000000
--- a/media/tests/ringtone/src/com/android/media/RingtoneManagerTest.java
+++ /dev/null
@@ -1,233 +0,0 @@
-/*
- * Copyright 2023 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.media;
-
-import static com.google.android.mms.ContentType.AUDIO_MP3;
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.junit.Assert.assertThrows;
-import static org.junit.Assume.assumeTrue;
-
-import android.content.ContentResolver;
-import android.content.ContentValues;
-import android.content.Context;
-import android.database.Cursor;
-import android.media.Ringtone;
-import android.media.RingtoneManager;
-import android.net.Uri;
-import android.os.Environment;
-import android.os.ParcelFileDescriptor;
-import android.os.SystemClock;
-import android.os.vibrator.persistence.VibrationXmlParser;
-import android.provider.MediaStore;
-import android.text.TextUtils;
-
-import androidx.test.platform.app.InstrumentationRegistry;
-
-import com.android.framework.base.media.ringtone.tests.R;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.Parameterized;
-
-import java.io.FileOutputStream;
-import java.io.InputStream;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.List;
-
-@RunWith(Parameterized.class)
-public class RingtoneManagerTest {
- @RingtoneManager.MediaType
- private final int mMediaType;
- private final List<Uri> mAddedFilesUri;
- private Context mContext;
- private RingtoneManager mRingtoneManager;
- private long mTimestamp;
-
- @Parameterized.Parameters(name = "media = {0}")
- public static Iterable<?> data() {
- return Arrays.asList(Ringtone.MEDIA_SOUND, Ringtone.MEDIA_VIBRATION);
- }
-
- public RingtoneManagerTest(@RingtoneManager.MediaType int mediaType) {
- mMediaType = mediaType;
- mAddedFilesUri = new ArrayList<>();
- }
-
- @Before
- public void setUp() throws Exception {
- mContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
- mTimestamp = SystemClock.uptimeMillis();
- mRingtoneManager = new RingtoneManager(mContext);
- mRingtoneManager.setMediaType(mMediaType);
- }
-
- @After
- public void tearDown() {
- // Clean up media store
- for (Uri fileUri : mAddedFilesUri) {
- mContext.getContentResolver().delete(fileUri, null);
- }
- }
-
- @Test
- public void testSetMediaType_withValidValue_setsMediaCorrectly() {
- mRingtoneManager.setMediaType(mMediaType);
- assertThat(mRingtoneManager.getMediaType()).isEqualTo(mMediaType);
- }
-
- @Test
- public void testSetMediaType_withInvalidValue_throwsException() {
- assertThrows(IllegalArgumentException.class, () -> mRingtoneManager.setMediaType(999));
- }
-
- @Test
- public void testSetMediaType_afterCallingGetCursor_throwsException() {
- mRingtoneManager.getCursor();
- assertThrows(IllegalStateException.class, () -> mRingtoneManager.setMediaType(mMediaType));
- }
-
- @Test
- public void testGetRingtone_ringtoneHasCorrectTitle() throws Exception {
- String fileName = generateUniqueFileName("new_file");
- Ringtone ringtone = addNewRingtoneToMediaStore(mRingtoneManager, fileName);
-
- assertThat(ringtone.getTitle(mContext)).isEqualTo(fileName);
- }
-
- @Test
- public void testGetRingtone_ringtoneCanBePlayedAndStopped() throws Exception {
- //TODO(b/261571543) Remove this assumption once we support playing vibrations.
- assumeTrue(mMediaType == Ringtone.MEDIA_SOUND);
- String fileName = generateUniqueFileName("new_file");
- Ringtone ringtone = addNewRingtoneToMediaStore(mRingtoneManager, fileName);
-
- ringtone.play();
- assertThat(ringtone.isPlaying()).isTrue();
-
- ringtone.stop();
- assertThat(ringtone.isPlaying()).isFalse();
- }
-
- @Test
- public void testGetCursor_withDifferentMedia_returnsCorrectCursor() throws Exception {
- RingtoneManager audioRingtoneManager = new RingtoneManager(mContext);
- String audioFileName = generateUniqueFileName("ringtone");
- addNewRingtoneToMediaStore(audioRingtoneManager, audioFileName);
-
- RingtoneManager vibrationRingtoneManager = new RingtoneManager(mContext);
- vibrationRingtoneManager.setMediaType(Ringtone.MEDIA_VIBRATION);
- String vibrationFileName = generateUniqueFileName("vibration");
- addNewRingtoneToMediaStore(vibrationRingtoneManager, vibrationFileName);
-
- Cursor audioCursor = audioRingtoneManager.getCursor();
- Cursor vibrationCursor = vibrationRingtoneManager.getCursor();
-
- List<String> audioTitles = extractRecordTitles(audioCursor);
- List<String> vibrationTitles = extractRecordTitles(vibrationCursor);
-
- assertThat(audioTitles).contains(audioFileName);
- assertThat(audioTitles).doesNotContain(vibrationFileName);
-
- assertThat(vibrationTitles).contains(vibrationFileName);
- assertThat(vibrationTitles).doesNotContain(audioFileName);
- }
-
- private List<String> extractRecordTitles(Cursor cursor) {
- List<String> titles = new ArrayList<>();
-
- if (cursor.moveToFirst()) {
- do {
- String title = cursor.getString(RingtoneManager.TITLE_COLUMN_INDEX);
- titles.add(title);
- } while (cursor.moveToNext());
- }
-
- return titles;
- }
-
- private Ringtone addNewRingtoneToMediaStore(RingtoneManager ringtoneManager, String fileName)
- throws Exception {
- Uri fileUri = ringtoneManager.getMediaType() == Ringtone.MEDIA_SOUND ? addAudioFile(
- fileName) : addVibrationFile(fileName);
- mAddedFilesUri.add(fileUri);
-
- int ringtonePosition = ringtoneManager.getRingtonePosition(fileUri);
- Ringtone ringtone = ringtoneManager.getRingtone(ringtonePosition);
- // Validate this is the expected ringtone.
- assertThat(ringtone.getUri()).isEqualTo(fileUri);
- return ringtone;
- }
-
- private Uri addAudioFile(String fileName) throws Exception {
- ContentResolver resolver = mContext.getContentResolver();
- ContentValues contentValues = new ContentValues();
- contentValues.put(MediaStore.Audio.Media.DISPLAY_NAME, fileName + ".mp3");
- contentValues.put(MediaStore.Audio.Media.RELATIVE_PATH, Environment.DIRECTORY_RINGTONES);
- contentValues.put(MediaStore.Audio.Media.MIME_TYPE, AUDIO_MP3);
- contentValues.put(MediaStore.Audio.Media.TITLE, fileName);
- contentValues.put(MediaStore.Audio.Media.IS_RINGTONE, 1);
-
- Uri contentUri = resolver.insert(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
- contentValues);
- writeRawDataToFile(resolver, contentUri, R.raw.test_sound_file);
-
- return resolver.canonicalizeOrElse(contentUri);
- }
-
- private Uri addVibrationFile(String fileName) throws Exception {
- ContentResolver resolver = mContext.getContentResolver();
- ContentValues contentValues = new ContentValues();
- contentValues.put(MediaStore.Files.FileColumns.DISPLAY_NAME, fileName + ".ahv");
- contentValues.put(MediaStore.Files.FileColumns.RELATIVE_PATH,
- Environment.DIRECTORY_DOWNLOADS);
- contentValues.put(MediaStore.Files.FileColumns.MIME_TYPE,
- VibrationXmlParser.APPLICATION_VIBRATION_XML_MIME_TYPE);
- contentValues.put(MediaStore.Files.FileColumns.TITLE, fileName);
-
- Uri contentUri = resolver.insert(MediaStore.Files.getContentUri(MediaStore
- .VOLUME_EXTERNAL), contentValues);
- writeRawDataToFile(resolver, contentUri, R.raw.test_haptic_file);
-
- return resolver.canonicalizeOrElse(contentUri);
- }
-
- private void writeRawDataToFile(ContentResolver resolver, Uri contentUri, int rawResource)
- throws Exception {
- try (ParcelFileDescriptor pfd =
- resolver.openFileDescriptor(contentUri, "w", null)) {
- InputStream inputStream = mContext.getResources().openRawResource(rawResource);
- FileOutputStream outputStream = new FileOutputStream(pfd.getFileDescriptor());
- outputStream.write(inputStream.readAllBytes());
-
- inputStream.close();
- outputStream.flush();
- outputStream.close();
-
- } catch (Exception e) {
- throw new Exception("Failed to write data to file", e);
- }
- }
-
- private String generateUniqueFileName(String prefix) {
- return TextUtils.formatSimple("%s_%d", prefix, mTimestamp);
- }
-
-}
diff --git a/native/android/performance_hint.cpp b/native/android/performance_hint.cpp
index c572944..279f9d6 100644
--- a/native/android/performance_hint.cpp
+++ b/native/android/performance_hint.cpp
@@ -484,8 +484,9 @@
WorkDuration& workDuration = *static_cast<WorkDuration*>(workDurationPtr);
VALIDATE_INT(workDuration.workPeriodStartTimestampNanos, > 0)
VALIDATE_INT(workDuration.actualTotalDurationNanos, > 0)
- VALIDATE_INT(workDuration.actualCpuDurationNanos, > 0)
+ VALIDATE_INT(workDuration.actualCpuDurationNanos, >= 0)
VALIDATE_INT(workDuration.actualGpuDurationNanos, >= 0)
+ VALIDATE_INT(workDuration.actualGpuDurationNanos + workDuration.actualCpuDurationNanos, > 0)
return session->reportActualWorkDuration(workDurationPtr);
}
@@ -517,7 +518,7 @@
void AWorkDuration_setActualCpuDurationNanos(AWorkDuration* aWorkDuration,
int64_t actualCpuDurationNanos) {
VALIDATE_PTR(aWorkDuration)
- WARN_INT(actualCpuDurationNanos, > 0)
+ WARN_INT(actualCpuDurationNanos, >= 0)
static_cast<WorkDuration*>(aWorkDuration)->actualCpuDurationNanos = actualCpuDurationNanos;
}
diff --git a/nfc/api/current.txt b/nfc/api/current.txt
index 1046d8e9..845a8f9 100644
--- a/nfc/api/current.txt
+++ b/nfc/api/current.txt
@@ -64,10 +64,8 @@
}
public final class NfcAdapter {
- method @FlaggedApi("android.nfc.nfc_observe_mode") public boolean allowTransaction();
method public void disableForegroundDispatch(android.app.Activity);
method public void disableReaderMode(android.app.Activity);
- method @FlaggedApi("android.nfc.nfc_observe_mode") public boolean disallowTransaction();
method public void enableForegroundDispatch(android.app.Activity, android.app.PendingIntent, android.content.IntentFilter[], String[][]);
method public void enableReaderMode(android.app.Activity, android.nfc.NfcAdapter.ReaderCallback, int, android.os.Bundle);
method public static android.nfc.NfcAdapter getDefaultAdapter(android.content.Context);
@@ -75,6 +73,7 @@
method @FlaggedApi("android.nfc.enable_nfc_charging") @Nullable public android.nfc.WlcListenerDeviceInfo getWlcListenerDeviceInfo();
method public boolean ignore(android.nfc.Tag, int, android.nfc.NfcAdapter.OnTagRemovedListener, android.os.Handler);
method public boolean isEnabled();
+ method @FlaggedApi("android.nfc.nfc_observe_mode") public boolean isObserveModeEnabled();
method @FlaggedApi("android.nfc.nfc_observe_mode") public boolean isObserveModeSupported();
method @FlaggedApi("android.nfc.enable_nfc_reader_option") public boolean isReaderOptionEnabled();
method @FlaggedApi("android.nfc.enable_nfc_reader_option") public boolean isReaderOptionSupported();
@@ -83,6 +82,7 @@
method @FlaggedApi("android.nfc.enable_nfc_charging") public boolean isWlcEnabled();
method @FlaggedApi("android.nfc.enable_nfc_set_discovery_tech") public void resetDiscoveryTechnology(@NonNull android.app.Activity);
method @FlaggedApi("android.nfc.enable_nfc_set_discovery_tech") public void setDiscoveryTechnology(@NonNull android.app.Activity, int, int);
+ method @FlaggedApi("android.nfc.nfc_observe_mode") public boolean setTransactionAllowed(boolean);
field public static final String ACTION_ADAPTER_STATE_CHANGED = "android.nfc.action.ADAPTER_STATE_CHANGED";
field public static final String ACTION_NDEF_DISCOVERED = "android.nfc.action.NDEF_DISCOVERED";
field @RequiresPermission(android.Manifest.permission.NFC_PREFERRED_PAYMENT_INFO) public static final String ACTION_PREFERRED_PAYMENT_CHANGED = "android.nfc.action.PREFERRED_PAYMENT_CHANGED";
@@ -99,12 +99,12 @@
field public static final String EXTRA_SECURE_ELEMENT_NAME = "android.nfc.extra.SECURE_ELEMENT_NAME";
field public static final String EXTRA_TAG = "android.nfc.extra.TAG";
field @FlaggedApi("android.nfc.enable_nfc_set_discovery_tech") public static final int FLAG_LISTEN_DISABLE = 0; // 0x0
- field @FlaggedApi("android.nfc.enable_nfc_set_discovery_tech") public static final int FLAG_LISTEN_KEEP = -1; // 0xffffffff
+ field @FlaggedApi("android.nfc.enable_nfc_set_discovery_tech") public static final int FLAG_LISTEN_KEEP = -2147483648; // 0x80000000
field @FlaggedApi("android.nfc.enable_nfc_set_discovery_tech") public static final int FLAG_LISTEN_NFC_PASSIVE_A = 1; // 0x1
field @FlaggedApi("android.nfc.enable_nfc_set_discovery_tech") public static final int FLAG_LISTEN_NFC_PASSIVE_B = 2; // 0x2
field @FlaggedApi("android.nfc.enable_nfc_set_discovery_tech") public static final int FLAG_LISTEN_NFC_PASSIVE_F = 4; // 0x4
field @FlaggedApi("android.nfc.enable_nfc_set_discovery_tech") public static final int FLAG_READER_DISABLE = 0; // 0x0
- field @FlaggedApi("android.nfc.enable_nfc_set_discovery_tech") public static final int FLAG_READER_KEEP = -1; // 0xffffffff
+ field @FlaggedApi("android.nfc.enable_nfc_set_discovery_tech") public static final int FLAG_READER_KEEP = -2147483648; // 0x80000000
field public static final int FLAG_READER_NFC_A = 1; // 0x1
field public static final int FLAG_READER_NFC_B = 2; // 0x2
field public static final int FLAG_READER_NFC_BARCODE = 16; // 0x10
@@ -232,13 +232,13 @@
method public final void sendResponseApdu(byte[]);
field public static final int DEACTIVATION_DESELECTED = 1; // 0x1
field public static final int DEACTIVATION_LINK_LOSS = 0; // 0x0
- field @FlaggedApi("android.nfc.nfc_read_polling_loop") public static final String POLLING_LOOP_DATA_KEY = "android.nfc.cardemulation.DATA";
- field @FlaggedApi("android.nfc.nfc_read_polling_loop") public static final String POLLING_LOOP_GAIN_KEY = "android.nfc.cardemulation.GAIN";
- field @FlaggedApi("android.nfc.nfc_read_polling_loop") public static final String POLLING_LOOP_TIMESTAMP_KEY = "android.nfc.cardemulation.TIMESTAMP";
+ field @FlaggedApi("android.nfc.nfc_read_polling_loop") public static final String KEY_POLLING_LOOP_DATA = "android.nfc.cardemulation.DATA";
+ field @FlaggedApi("android.nfc.nfc_read_polling_loop") public static final String KEY_POLLING_LOOP_GAIN = "android.nfc.cardemulation.GAIN";
+ field @FlaggedApi("android.nfc.nfc_read_polling_loop") public static final String KEY_POLLING_LOOP_TIMESTAMP = "android.nfc.cardemulation.TIMESTAMP";
+ field @FlaggedApi("android.nfc.nfc_read_polling_loop") public static final String KEY_POLLING_LOOP_TYPE = "android.nfc.cardemulation.TYPE";
field @FlaggedApi("android.nfc.nfc_read_polling_loop") public static final char POLLING_LOOP_TYPE_A = 65; // 0x0041 'A'
field @FlaggedApi("android.nfc.nfc_read_polling_loop") public static final char POLLING_LOOP_TYPE_B = 66; // 0x0042 'B'
field @FlaggedApi("android.nfc.nfc_read_polling_loop") public static final char POLLING_LOOP_TYPE_F = 70; // 0x0046 'F'
- field @FlaggedApi("android.nfc.nfc_read_polling_loop") public static final String POLLING_LOOP_TYPE_KEY = "android.nfc.cardemulation.TYPE";
field @FlaggedApi("android.nfc.nfc_read_polling_loop") public static final char POLLING_LOOP_TYPE_OFF = 88; // 0x0058 'X'
field @FlaggedApi("android.nfc.nfc_read_polling_loop") public static final char POLLING_LOOP_TYPE_ON = 79; // 0x004f 'O'
field @FlaggedApi("android.nfc.nfc_read_polling_loop") public static final char POLLING_LOOP_TYPE_UNKNOWN = 85; // 0x0055 'U'
diff --git a/nfc/api/system-current.txt b/nfc/api/system-current.txt
index f8b640c7..dd2e174 100644
--- a/nfc/api/system-current.txt
+++ b/nfc/api/system-current.txt
@@ -14,15 +14,23 @@
method @RequiresPermission(android.Manifest.permission.NFC_SET_CONTROLLER_ALWAYS_ON) public boolean isControllerAlwaysOnSupported();
method @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public boolean isTagIntentAppPreferenceSupported();
method @RequiresPermission(android.Manifest.permission.NFC_SET_CONTROLLER_ALWAYS_ON) public void registerControllerAlwaysOnListener(@NonNull java.util.concurrent.Executor, @NonNull android.nfc.NfcAdapter.ControllerAlwaysOnListener);
+ method @FlaggedApi("android.nfc.nfc_vendor_cmd") @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public void registerNfcVendorNciCallback(@NonNull java.util.concurrent.Executor, @NonNull android.nfc.NfcAdapter.NfcVendorNciCallback);
method @FlaggedApi("android.nfc.enable_nfc_charging") public void registerWlcStateListener(@NonNull java.util.concurrent.Executor, @NonNull android.nfc.NfcAdapter.WlcStateListener);
method @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public boolean removeNfcUnlockHandler(android.nfc.NfcAdapter.NfcUnlockHandler);
+ method @FlaggedApi("android.nfc.nfc_vendor_cmd") @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public int sendVendorNciMessage(int, @IntRange(from=0, to=15) int, @IntRange(from=0) int, @NonNull byte[]);
method @RequiresPermission(android.Manifest.permission.NFC_SET_CONTROLLER_ALWAYS_ON) public boolean setControllerAlwaysOn(boolean);
method @FlaggedApi("android.nfc.enable_nfc_mainline") @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public void setReaderModePollingEnabled(boolean);
method @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public int setTagIntentAppPreferenceForUser(int, @NonNull String, boolean);
method @FlaggedApi("android.nfc.enable_nfc_charging") @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public boolean setWlcEnabled(boolean);
method @RequiresPermission(android.Manifest.permission.NFC_SET_CONTROLLER_ALWAYS_ON) public void unregisterControllerAlwaysOnListener(@NonNull android.nfc.NfcAdapter.ControllerAlwaysOnListener);
+ method @FlaggedApi("android.nfc.nfc_vendor_cmd") @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public void unregisterNfcVendorNciCallback(@NonNull android.nfc.NfcAdapter.NfcVendorNciCallback);
method @FlaggedApi("android.nfc.enable_nfc_charging") public void unregisterWlcStateListener(@NonNull android.nfc.NfcAdapter.WlcStateListener);
field @FlaggedApi("android.nfc.enable_nfc_mainline") public static final String ACTION_REQUIRE_UNLOCK_FOR_NFC = "android.nfc.action.REQUIRE_UNLOCK_FOR_NFC";
+ field @FlaggedApi("android.nfc.nfc_vendor_cmd") public static final int MESSAGE_TYPE_COMMAND = 1; // 0x1
+ field @FlaggedApi("android.nfc.nfc_vendor_cmd") public static final int SEND_VENDOR_NCI_STATUS_FAILED = 3; // 0x3
+ field @FlaggedApi("android.nfc.nfc_vendor_cmd") public static final int SEND_VENDOR_NCI_STATUS_MESSAGE_CORRUPTED = 2; // 0x2
+ field @FlaggedApi("android.nfc.nfc_vendor_cmd") public static final int SEND_VENDOR_NCI_STATUS_REJECTED = 1; // 0x1
+ field @FlaggedApi("android.nfc.nfc_vendor_cmd") public static final int SEND_VENDOR_NCI_STATUS_SUCCESS = 0; // 0x0
field public static final int TAG_INTENT_APP_PREF_RESULT_PACKAGE_NOT_FOUND = -1; // 0xffffffff
field public static final int TAG_INTENT_APP_PREF_RESULT_SUCCESS = 0; // 0x0
field public static final int TAG_INTENT_APP_PREF_RESULT_UNAVAILABLE = -2; // 0xfffffffe
@@ -36,6 +44,11 @@
method public boolean onUnlockAttempted(android.nfc.Tag);
}
+ @FlaggedApi("android.nfc.nfc_vendor_cmd") public static interface NfcAdapter.NfcVendorNciCallback {
+ method @FlaggedApi("android.nfc.nfc_vendor_cmd") public void onVendorNciNotification(@IntRange(from=9, to=15) int, int, @NonNull byte[]);
+ method @FlaggedApi("android.nfc.nfc_vendor_cmd") public void onVendorNciResponse(@IntRange(from=0, to=15) int, int, @NonNull byte[]);
+ }
+
@FlaggedApi("android.nfc.enable_nfc_charging") public static interface NfcAdapter.WlcStateListener {
method public void onWlcStateChanged(@NonNull android.nfc.WlcListenerDeviceInfo);
}
diff --git a/nfc/java/android/nfc/INfcAdapter.aidl b/nfc/java/android/nfc/INfcAdapter.aidl
index 63c3414..c444740 100644
--- a/nfc/java/android/nfc/INfcAdapter.aidl
+++ b/nfc/java/android/nfc/INfcAdapter.aidl
@@ -24,6 +24,7 @@
import android.nfc.IAppCallback;
import android.nfc.INfcAdapterExtras;
import android.nfc.INfcControllerAlwaysOnListener;
+import android.nfc.INfcVendorNciCallback;
import android.nfc.INfcTag;
import android.nfc.INfcCardEmulation;
import android.nfc.INfcFCardEmulation;
@@ -87,6 +88,7 @@
@JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS)")
boolean enableReaderOption(boolean enable);
boolean isObserveModeSupported();
+ boolean isObserveModeEnabled();
boolean setObserveMode(boolean enabled);
@JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS)")
@@ -100,4 +102,7 @@
void notifyPollingLoop(in Bundle frame);
void notifyHceDeactivated();
+ int sendVendorNciMessage(int mt, int gid, int oid, in byte[] payload);
+ void registerVendorExtensionCallback(in INfcVendorNciCallback callbacks);
+ void unregisterVendorExtensionCallback(in INfcVendorNciCallback callbacks);
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/animation/ActivityLaunchAnimatorKosmos.kt b/nfc/java/android/nfc/INfcVendorNciCallback.aidl
similarity index 66%
copy from packages/SystemUI/tests/utils/src/com/android/systemui/animation/ActivityLaunchAnimatorKosmos.kt
copy to nfc/java/android/nfc/INfcVendorNciCallback.aidl
index 128f58b..821dc6f 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/animation/ActivityLaunchAnimatorKosmos.kt
+++ b/nfc/java/android/nfc/INfcVendorNciCallback.aidl
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2023 The Android Open Source Project
+ * Copyright (C) 2024 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.
@@ -13,9 +13,12 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+package android.nfc;
-package com.android.systemui.animation
-
-import com.android.systemui.kosmos.Kosmos
-
-val Kosmos.activityLaunchAnimator by Kosmos.Fixture { ActivityLaunchAnimator() }
+/**
+ * @hide
+ */
+oneway interface INfcVendorNciCallback {
+ void onVendorResponseReceived(int gid, int oid, in byte[] payload);
+ void onVendorNotificationReceived(int gid, int oid, in byte[] payload);
+}
diff --git a/nfc/java/android/nfc/NfcAdapter.java b/nfc/java/android/nfc/NfcAdapter.java
index 4d56c11..252f46f 100644
--- a/nfc/java/android/nfc/NfcAdapter.java
+++ b/nfc/java/android/nfc/NfcAdapter.java
@@ -19,6 +19,7 @@
import android.annotation.CallbackExecutor;
import android.annotation.FlaggedApi;
import android.annotation.IntDef;
+import android.annotation.IntRange;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.RequiresPermission;
@@ -77,6 +78,7 @@
private final NfcControllerAlwaysOnListener mControllerAlwaysOnListener;
private final NfcWlcStateListener mNfcWlcStateListener;
+ private final NfcVendorNciCallbackListener mNfcVendorNciCallbackListener;
/**
* Intent to start an activity when a tag with NDEF payload is discovered.
@@ -416,18 +418,18 @@
/**
* Flags for use with {@link #setDiscoveryTechnology(Activity, int, int)}.
* <p>
- * Setting this flag makes listening to use current flags.
+ * Setting this flag makes listening to keep the current technology configuration.
*/
@FlaggedApi(Flags.FLAG_ENABLE_NFC_SET_DISCOVERY_TECH)
- public static final int FLAG_LISTEN_KEEP = -1;
+ public static final int FLAG_LISTEN_KEEP = 0x80000000;
/**
* Flags for use with {@link #setDiscoveryTechnology(Activity, int, int)}.
* <p>
- * Setting this flag makes polling to use current flags.
+ * Setting this flag makes polling to keep the current technology configuration.
*/
@FlaggedApi(Flags.FLAG_ENABLE_NFC_SET_DISCOVERY_TECH)
- public static final int FLAG_READER_KEEP = -1;
+ public static final int FLAG_READER_KEEP = 0x80000000;
/** @hide */
public static final int FLAG_USE_ALL_TECH = 0xff;
@@ -866,6 +868,7 @@
mLock = new Object();
mControllerAlwaysOnListener = new NfcControllerAlwaysOnListener(getService());
mNfcWlcStateListener = new NfcWlcStateListener(getService());
+ mNfcVendorNciCallbackListener = new NfcVendorNciCallbackListener(getService());
}
/**
@@ -1204,18 +1207,16 @@
}
}
- /**
- * Disables observe mode to allow the transaction to proceed. See
- * {@link #isObserveModeSupported()} for a description of observe mode and
- * use {@link #disallowTransaction()} to enable observe mode and block
- * transactions again.
- *
- * @return boolean indicating success or failure.
- */
+ /**
+ * Returns whether Observe Mode is currently enabled or not.
+ *
+ * @return true if observe mode is enabled, false otherwise.
+ */
+
@FlaggedApi(Flags.FLAG_NFC_OBSERVE_MODE)
- public boolean allowTransaction() {
+ public boolean isObserveModeEnabled() {
try {
- return sService.setObserveMode(false);
+ return sService.isObserveModeEnabled();
} catch (RemoteException e) {
attemptDeadServiceRecovery(e);
return false;
@@ -1223,18 +1224,20 @@
}
/**
- * Signals that the transaction has completed and observe mode may be
- * reenabled. See {@link #isObserveModeSupported()} for a description of
- * observe mode and use {@link #allowTransaction()} to disable observe
- * mode and allow transactions to proceed.
- *
- * @return boolean indicating success or failure.
- */
+ * Controls whether the NFC adapter will allow transactions to proceed or be in observe mode
+ * and simply observe and notify the APDU service of polling loop frames. See
+ * {@link #isObserveModeSupported()} for a description of observe mode.
+ *
+ * @param allowed true disables observe mode to allow the transaction to proceed while false
+ * enables observe mode and does not allow transactions to proceed.
+ *
+ * @return boolean indicating success or failure.
+ */
@FlaggedApi(Flags.FLAG_NFC_OBSERVE_MODE)
- public boolean disallowTransaction() {
+ public boolean setTransactionAllowed(boolean allowed) {
try {
- return sService.setObserveMode(true);
+ return sService.setObserveMode(!allowed);
} catch (RemoteException e) {
attemptDeadServiceRecovery(e);
return false;
@@ -1801,6 +1804,8 @@
*
* Use {@link #FLAG_READER_KEEP} to keep current polling technology.
* Use {@link #FLAG_LISTEN_KEEP} to keep current listenig technology.
+ * (if the _KEEP flag is specified the other technology flags shouldn't be set
+ * and are quietly ignored otherwise).
* Use {@link #FLAG_READER_DISABLE} to disable polling.
* Use {@link #FLAG_LISTEN_DISABLE} to disable listening.
* Also refer to {@link #resetDiscoveryTechnology(Activity)} to restore these changes.
@@ -1832,6 +1837,15 @@
@FlaggedApi(Flags.FLAG_ENABLE_NFC_SET_DISCOVERY_TECH)
public void setDiscoveryTechnology(@NonNull Activity activity,
@PollTechnology int pollTechnology, @ListenTechnology int listenTechnology) {
+
+ // A special treatment of the _KEEP flags
+ if ((listenTechnology & FLAG_LISTEN_KEEP) != 0) {
+ listenTechnology = -1;
+ }
+ if ((pollTechnology & FLAG_READER_KEEP) != 0) {
+ pollTechnology = -1;
+ }
+
if (listenTechnology == FLAG_LISTEN_DISABLE) {
synchronized (sLock) {
if (!sHasNfcFeature) {
@@ -1858,10 +1872,10 @@
}
/**
- * Restore the poll/listen technologies of NFC controller,
+ * Restore the poll/listen technologies of NFC controller to its default state,
* which were changed by {@link #setDiscoveryTechnology(Activity , int , int)}
*
- * @param activity The Activity that requests to changed technologies.
+ * @param activity The Activity that requested to change technologies.
*/
@FlaggedApi(Flags.FLAG_ENABLE_NFC_SET_DISCOVERY_TECH)
@@ -2967,4 +2981,163 @@
return null;
}
}
+
+ /**
+ * Vendor NCI command success.
+ * @hide
+ */
+ @SystemApi
+ @FlaggedApi(Flags.FLAG_NFC_VENDOR_CMD)
+ public static final int SEND_VENDOR_NCI_STATUS_SUCCESS = 0;
+ /**
+ * Vendor NCI command rejected.
+ * @hide
+ */
+ @SystemApi
+ @FlaggedApi(Flags.FLAG_NFC_VENDOR_CMD)
+ public static final int SEND_VENDOR_NCI_STATUS_REJECTED = 1;
+ /**
+ * Vendor NCI command corrupted.
+ * @hide
+ */
+ @SystemApi
+ @FlaggedApi(Flags.FLAG_NFC_VENDOR_CMD)
+ public static final int SEND_VENDOR_NCI_STATUS_MESSAGE_CORRUPTED = 2;
+ /**
+ * Vendor NCI command failed with unknown reason.
+ * @hide
+ */
+ @SystemApi
+ @FlaggedApi(Flags.FLAG_NFC_VENDOR_CMD)
+ public static final int SEND_VENDOR_NCI_STATUS_FAILED = 3;
+
+ /**
+ * @hide
+ */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(value = {
+ SEND_VENDOR_NCI_STATUS_SUCCESS,
+ SEND_VENDOR_NCI_STATUS_REJECTED,
+ SEND_VENDOR_NCI_STATUS_MESSAGE_CORRUPTED,
+ SEND_VENDOR_NCI_STATUS_FAILED,
+ })
+ @interface SendVendorNciStatus {}
+
+ /**
+ * Message Type for NCI Command.
+ * @hide
+ */
+ @SystemApi
+ @FlaggedApi(Flags.FLAG_NFC_VENDOR_CMD)
+ public static final int MESSAGE_TYPE_COMMAND = 1;
+
+ /**
+ * @hide
+ */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(value = {
+ MESSAGE_TYPE_COMMAND,
+ })
+ @interface MessageType {}
+
+ /**
+ * Send Vendor specific Nci Messages with custom message type.
+ *
+ * The format of the NCI messages are defined in the NCI specification. The platform is
+ * responsible for fragmenting the payload if necessary.
+ *
+ * Note that mt (message type) is added at the beginning of method parameters as it is more
+ * distinctive than other parameters and was requested from vendor.
+ *
+ * @param mt message Type of the command
+ * @param gid group ID of the command. This needs to be one of the vendor reserved GIDs from
+ * the NCI specification
+ * @param oid opcode ID of the command. This is left to the OEM / vendor to decide
+ * @param payload containing vendor Nci message payload
+ * @return message send status
+ * @hide
+ */
+ @SystemApi
+ @FlaggedApi(Flags.FLAG_NFC_VENDOR_CMD)
+ @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS)
+ public @SendVendorNciStatus int sendVendorNciMessage(@MessageType int mt,
+ @IntRange(from = 0, to = 15) int gid, @IntRange(from = 0) int oid,
+ @NonNull byte[] payload) {
+ Objects.requireNonNull(payload, "Payload must not be null");
+ try {
+ return sService.sendVendorNciMessage(mt, gid, oid, payload);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Register an {@link NfcVendorNciCallback} to listen for Nfc vendor responses and notifications
+ * <p>The provided callback will be invoked by the given {@link Executor}.
+ *
+ * <p>When first registering a callback, the callbacks's
+ * {@link NfcVendorNciCallback#onVendorNciCallBack(byte[])} is immediately invoked to
+ * notify the vendor notification.
+ *
+ * @param executor an {@link Executor} to execute given callback
+ * @param callback user implementation of the {@link NfcVendorNciCallback}
+ * @hide
+ */
+ @SystemApi
+ @FlaggedApi(Flags.FLAG_NFC_VENDOR_CMD)
+ @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS)
+ public void registerNfcVendorNciCallback(@NonNull @CallbackExecutor Executor executor,
+ @NonNull NfcVendorNciCallback callback) {
+ mNfcVendorNciCallbackListener.register(executor, callback);
+ }
+
+ /**
+ * Unregister the specified {@link NfcVendorNciCallback}
+ *
+ * <p>The same {@link NfcVendorNciCallback} object used when calling
+ * {@link #registerNfcVendorNciCallback(Executor, NfcVendorNciCallback)} must be used.
+ *
+ * <p>Callbacks are automatically unregistered when application process goes away
+ *
+ * @param callback user implementation of the {@link NfcVendorNciCallback}
+ * @hide
+ */
+ @SystemApi
+ @FlaggedApi(Flags.FLAG_NFC_VENDOR_CMD)
+ @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS)
+ public void unregisterNfcVendorNciCallback(@NonNull NfcVendorNciCallback callback) {
+ mNfcVendorNciCallbackListener.unregister(callback);
+ }
+
+ /**
+ * Interface for receiving vendor NCI responses and notifications.
+ * @hide
+ */
+ @SystemApi
+ @FlaggedApi(Flags.FLAG_NFC_VENDOR_CMD)
+ public interface NfcVendorNciCallback {
+ /**
+ * Invoked when a vendor specific NCI response is received.
+ *
+ * @param gid group ID of the command. This needs to be one of the vendor reserved GIDs from
+ * the NCI specification.
+ * @param oid opcode ID of the command. This is left to the OEM / vendor to decide.
+ * @param payload containing vendor Nci message payload.
+ */
+ @FlaggedApi(Flags.FLAG_NFC_VENDOR_CMD)
+ void onVendorNciResponse(
+ @IntRange(from = 0, to = 15) int gid, int oid, @NonNull byte[] payload);
+
+ /**
+ * Invoked when a vendor specific NCI notification is received.
+ *
+ * @param gid group ID of the command. This needs to be one of the vendor reserved GIDs from
+ * the NCI specification.
+ * @param oid opcode ID of the command. This is left to the OEM / vendor to decide.
+ * @param payload containing vendor Nci message payload.
+ */
+ @FlaggedApi(Flags.FLAG_NFC_VENDOR_CMD)
+ void onVendorNciNotification(
+ @IntRange(from = 9, to = 15) int gid, int oid, @NonNull byte[] payload);
+ }
}
diff --git a/nfc/java/android/nfc/NfcVendorNciCallbackListener.java b/nfc/java/android/nfc/NfcVendorNciCallbackListener.java
new file mode 100644
index 0000000..742d75f
--- /dev/null
+++ b/nfc/java/android/nfc/NfcVendorNciCallbackListener.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright (C) 2024 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.nfc;
+
+import android.annotation.NonNull;
+import android.nfc.NfcAdapter.NfcVendorNciCallback;
+import android.os.Binder;
+import android.os.RemoteException;
+import android.util.Log;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.Executor;
+
+/**
+ * @hide
+ */
+public final class NfcVendorNciCallbackListener extends INfcVendorNciCallback.Stub {
+ private static final String TAG = "Nfc.NfcVendorNciCallbacks";
+ private final INfcAdapter mAdapter;
+ private boolean mIsRegistered = false;
+ private final Map<NfcVendorNciCallback, Executor> mCallbackMap = new HashMap<>();
+
+ public NfcVendorNciCallbackListener(@NonNull INfcAdapter adapter) {
+ mAdapter = adapter;
+ }
+
+ public void register(@NonNull Executor executor, @NonNull NfcVendorNciCallback callback) {
+ synchronized (this) {
+ if (mCallbackMap.containsKey(callback)) {
+ return;
+ }
+ mCallbackMap.put(callback, executor);
+ if (!mIsRegistered) {
+ try {
+ mAdapter.registerVendorExtensionCallback(this);
+ mIsRegistered = true;
+ } catch (RemoteException e) {
+ Log.w(TAG, "Failed to register adapter state callback");
+ mCallbackMap.remove(callback);
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ }
+ }
+
+ public void unregister(@NonNull NfcVendorNciCallback callback) {
+ synchronized (this) {
+ if (!mCallbackMap.containsKey(callback) || !mIsRegistered) {
+ return;
+ }
+ if (mCallbackMap.size() == 1) {
+ try {
+ mAdapter.unregisterVendorExtensionCallback(this);
+ mIsRegistered = false;
+ mCallbackMap.remove(callback);
+ } catch (RemoteException e) {
+ Log.w(TAG, "Failed to unregister AdapterStateCallback with service");
+ throw e.rethrowFromSystemServer();
+ }
+ } else {
+ mCallbackMap.remove(callback);
+ }
+ }
+ }
+
+ @Override
+ public void onVendorResponseReceived(int gid, int oid, @NonNull byte[] payload)
+ throws RemoteException {
+ synchronized (this) {
+ final long identity = Binder.clearCallingIdentity();
+ try {
+ for (NfcVendorNciCallback callback : mCallbackMap.keySet()) {
+ Executor executor = mCallbackMap.get(callback);
+ executor.execute(() -> callback.onVendorNciResponse(gid, oid, payload));
+ }
+ } catch (RuntimeException ex) {
+ throw ex;
+ } finally {
+ Binder.restoreCallingIdentity(identity);
+ }
+ }
+ }
+
+ @Override
+ public void onVendorNotificationReceived(int gid, int oid, @NonNull byte[] payload)
+ throws RemoteException {
+ synchronized (this) {
+ final long identity = Binder.clearCallingIdentity();
+ try {
+ for (NfcVendorNciCallback callback : mCallbackMap.keySet()) {
+ Executor executor = mCallbackMap.get(callback);
+ executor.execute(() -> callback.onVendorNciNotification(gid, oid, payload));
+ }
+ } catch (RuntimeException ex) {
+ throw ex;
+ } finally {
+ Binder.restoreCallingIdentity(identity);
+ }
+ }
+ }
+}
diff --git a/nfc/java/android/nfc/cardemulation/ApduServiceInfo.java b/nfc/java/android/nfc/cardemulation/ApduServiceInfo.java
index 426c5aa..3254a39 100644
--- a/nfc/java/android/nfc/cardemulation/ApduServiceInfo.java
+++ b/nfc/java/android/nfc/cardemulation/ApduServiceInfo.java
@@ -105,6 +105,8 @@
private final ArrayList<String> mPollingLoopFilters;
+ private final Map<String, Boolean> mAutoTransact;
+
/**
* Whether this service should only be started when the device is unlocked.
*/
@@ -173,6 +175,7 @@
this.mStaticAidGroups = new HashMap<String, AidGroup>();
this.mDynamicAidGroups = new HashMap<String, AidGroup>();
this.mPollingLoopFilters = new ArrayList<String>();
+ this.mAutoTransact = new HashMap<String, Boolean>();
this.mOffHostName = offHost;
this.mStaticOffHostName = staticOffHost;
this.mOnHost = onHost;
@@ -287,6 +290,7 @@
mStaticAidGroups = new HashMap<String, AidGroup>();
mDynamicAidGroups = new HashMap<String, AidGroup>();
mPollingLoopFilters = new ArrayList<String>();
+ mAutoTransact = new HashMap<String, Boolean>();
mOnHost = onHost;
final int depth = parser.getDepth();
@@ -377,6 +381,10 @@
a.getString(com.android.internal.R.styleable.PollingLoopFilter_name)
.toUpperCase(Locale.ROOT);
mPollingLoopFilters.add(plf);
+ boolean autoTransact = a.getBoolean(
+ com.android.internal.R.styleable.PollingLoopFilter_autoTransact,
+ false);
+ mAutoTransact.put(plf, autoTransact);
a.recycle();
}
}
@@ -444,6 +452,17 @@
}
/**
+ * Returns whether this service would like to automatically transact for a given plf.
+ *
+ * @param plf the polling loop filter to query.
+ * @return {@code true} indicating to auto transact, {@code false} indicating to not.
+ */
+ @FlaggedApi(Flags.FLAG_NFC_READ_POLLING_LOOP)
+ public boolean getShouldAutoTransact(@NonNull String plf) {
+ return mAutoTransact.getOrDefault(plf.toUpperCase(Locale.ROOT), false);
+ }
+
+ /**
* Returns a consolidated list of AIDs with prefixes from the AID groups
* registered by this service. Note that if a service has both
* a static (manifest-based) AID group for a category and a dynamic
@@ -630,6 +649,21 @@
}
/**
+ * Add a Polling Loop Filter. Custom NFC polling frames that match this filter will cause the
+ * device to exit observe mode, just as if
+ * {@link android.nfc.NfcAdapter#setTransactionAllowed(boolean)} had been called with true,
+ * allowing transactions to proceed. The matching frame will also be delivered to
+ * {@link HostApduService#processPollingFrames(List)}.
+ *
+ * @param pollingLoopFilter this polling loop filter to add.
+ */
+ @FlaggedApi(Flags.FLAG_NFC_READ_POLLING_LOOP)
+ public void addPollingLoopFilterToAutoTransact(@NonNull String pollingLoopFilter) {
+ mPollingLoopFilters.add(pollingLoopFilter.toUpperCase(Locale.ROOT));
+ mAutoTransact.put(pollingLoopFilter.toUpperCase(Locale.ROOT), true);
+ }
+
+ /**
* Remove a Polling Loop Filter. Custom NFC polling frames that match this filter will no
* longer be delivered to {@link HostApduService#processPollingFrames(List)}.
* @param pollingLoopFilter this polling loop filter to add.
diff --git a/nfc/java/android/nfc/cardemulation/HostApduService.java b/nfc/java/android/nfc/cardemulation/HostApduService.java
index 7cd2533..89b0322 100644
--- a/nfc/java/android/nfc/cardemulation/HostApduService.java
+++ b/nfc/java/android/nfc/cardemulation/HostApduService.java
@@ -244,11 +244,11 @@
public static final String KEY_DATA = "data";
/**
- * POLLING_LOOP_TYPE_KEY is the Bundle key for the type of
+ * KEY_POLLING_LOOP_TYPE is the Bundle key for the type of
* polling loop frame in the Bundle passed to {@link #processPollingFrames(List)}
*/
@FlaggedApi(android.nfc.Flags.FLAG_NFC_READ_POLLING_LOOP)
- public static final String POLLING_LOOP_TYPE_KEY = "android.nfc.cardemulation.TYPE";
+ public static final String KEY_POLLING_LOOP_TYPE = "android.nfc.cardemulation.TYPE";
/**
* POLLING_LOOP_TYPE_A is the value associated with the key
@@ -299,33 +299,33 @@
public static final char POLLING_LOOP_TYPE_UNKNOWN = 'U';
/**
- * POLLING_LOOP_DATA is the Bundle key for the raw data of captured from
+ * KEY_POLLING_LOOP_DATA is the Bundle key for the raw data of captured from
* the polling loop frame in the Bundle passed to {@link #processPollingFrames(List)}
* when the frame type isn't recognized.
*/
@FlaggedApi(android.nfc.Flags.FLAG_NFC_READ_POLLING_LOOP)
- public static final String POLLING_LOOP_DATA_KEY = "android.nfc.cardemulation.DATA";
+ public static final String KEY_POLLING_LOOP_DATA = "android.nfc.cardemulation.DATA";
/**
- * POLLING_LOOP_GAIN_KEY is the Bundle key for the field strength of
+ * KEY_POLLING_LOOP_GAIN is the Bundle key for the field strength of
* the polling loop frame in the Bundle passed to {@link #processPollingFrames(List)}
* when the frame type isn't recognized.
*/
@FlaggedApi(android.nfc.Flags.FLAG_NFC_READ_POLLING_LOOP)
- public static final String POLLING_LOOP_GAIN_KEY = "android.nfc.cardemulation.GAIN";
+ public static final String KEY_POLLING_LOOP_GAIN = "android.nfc.cardemulation.GAIN";
/**
- * POLLING_LOOP_TIMESTAMP_KEY is the Bundle key for the timestamp of
+ * KEY_POLLING_LOOP_TIMESTAMP is the Bundle key for the timestamp of
* the polling loop frame in the Bundle passed to {@link #processPollingFrames(List)}
* when the frame type isn't recognized.
*/
@FlaggedApi(android.nfc.Flags.FLAG_NFC_READ_POLLING_LOOP)
- public static final String POLLING_LOOP_TIMESTAMP_KEY = "android.nfc.cardemulation.TIMESTAMP";
+ public static final String KEY_POLLING_LOOP_TIMESTAMP = "android.nfc.cardemulation.TIMESTAMP";
/**
* @hide
*/
- public static final String POLLING_LOOP_FRAMES_BUNDLE_KEY =
+ public static final String KEY_POLLING_LOOP_FRAMES_BUNDLE =
"android.nfc.cardemulation.POLLING_FRAMES";
/**
@@ -405,7 +405,7 @@
break;
case MSG_POLLING_LOOP:
ArrayList<Bundle> frames =
- msg.getData().getParcelableArrayList(POLLING_LOOP_FRAMES_BUNDLE_KEY,
+ msg.getData().getParcelableArrayList(KEY_POLLING_LOOP_FRAMES_BUNDLE,
Bundle.class);
processPollingFrames(frames);
break;
diff --git a/nfc/java/android/nfc/flags.aconfig b/nfc/java/android/nfc/flags.aconfig
index 01a4570..0a2619c 100644
--- a/nfc/java/android/nfc/flags.aconfig
+++ b/nfc/java/android/nfc/flags.aconfig
@@ -69,3 +69,10 @@
description: "Flag for NFC set discovery tech API"
bug: "300351519"
}
+
+flag {
+ name: "nfc_vendor_cmd"
+ namespace: "nfc"
+ description: "Enable NFC vendor command support"
+ bug: "289879306"
+}
diff --git a/opengl/java/android/opengl/EGLExt.java b/opengl/java/android/opengl/EGLExt.java
index 1570e0e..31104a0 100644
--- a/opengl/java/android/opengl/EGLExt.java
+++ b/opengl/java/android/opengl/EGLExt.java
@@ -46,14 +46,6 @@
_nativeClassInit();
}
- // C function EGLBoolean eglPresentationTimeANDROID ( EGLDisplay dpy, EGLSurface sur, EGLnsecsANDROID time )
-
- public static native boolean eglPresentationTimeANDROID(
- EGLDisplay dpy,
- EGLSurface sur,
- long time
- );
-
/**
* Retrieves the SyncFence for an EGLSync created with EGL_SYNC_NATIVE_FENCE_ANDROID
*
@@ -83,4 +75,13 @@
}
private static native int eglDupNativeFenceFDANDROIDImpl(EGLDisplay display, EGLSync sync);
+
+ // C function EGLBoolean eglPresentationTimeANDROID ( EGLDisplay dpy, EGLSurface sur, EGLnsecsANDROID time )
+
+ public static native boolean eglPresentationTimeANDROID(
+ EGLDisplay dpy,
+ EGLSurface sur,
+ long time
+ );
+
}
diff --git a/packages/CompanionDeviceManager/Android.bp b/packages/CompanionDeviceManager/Android.bp
index f6458c2..ce32ec4 100644
--- a/packages/CompanionDeviceManager/Android.bp
+++ b/packages/CompanionDeviceManager/Android.bp
@@ -46,4 +46,6 @@
],
platform_apis: true,
+
+ generate_product_characteristics_rro: true,
}
diff --git a/packages/CrashRecovery/aconfig/flags.aconfig b/packages/CrashRecovery/aconfig/flags.aconfig
index 5636266..572a669 100644
--- a/packages/CrashRecovery/aconfig/flags.aconfig
+++ b/packages/CrashRecovery/aconfig/flags.aconfig
@@ -6,4 +6,11 @@
description: "Feature flag for recoverability detection"
bug: "310236690"
is_fixed_read_only: true
-}
\ No newline at end of file
+}
+
+flag {
+ name: "enable_crashrecovery"
+ namespace: "crashrecovery"
+ description: "Enables various dependencies of crashrecovery module"
+ bug: "289203818"
+}
diff --git a/packages/CrashRecovery/framework/Android.bp b/packages/CrashRecovery/framework/Android.bp
index b2af315..c0d93531 100644
--- a/packages/CrashRecovery/framework/Android.bp
+++ b/packages/CrashRecovery/framework/Android.bp
@@ -1,9 +1,55 @@
-filegroup {
+soong_config_module_type {
+ name: "platform_filegroup",
+ module_type: "filegroup",
+ config_namespace: "ANDROID",
+ bool_variables: [
+ "move_crashrecovery_files",
+ ],
+ properties: [
+ "srcs",
+ ],
+}
+
+platform_filegroup {
name: "framework-crashrecovery-sources",
srcs: [
"java/**/*.java",
"java/**/*.aidl",
],
+ soong_config_variables: {
+ // if the flag is enabled, then files would be moved to module
+ move_crashrecovery_files: {
+ srcs: [],
+ },
+ },
path: "java",
visibility: ["//frameworks/base:__subpackages__"],
}
+
+soong_config_module_type {
+ name: "module_filegroup",
+ module_type: "filegroup",
+ config_namespace: "ANDROID",
+ bool_variables: [
+ "move_crashrecovery_files",
+ ],
+ properties: [
+ "srcs",
+ ],
+}
+
+module_filegroup {
+ name: "framework-crashrecovery-module-sources",
+ srcs: [],
+ soong_config_variables: {
+ // if the flag is enabled, then files would be moved to module
+ move_crashrecovery_files: {
+ srcs: [
+ "java/**/*.java",
+ "java/**/*.aidl",
+ ],
+ },
+ },
+ path: "java",
+ visibility: ["//packages/modules/CrashRecovery/framework"],
+}
diff --git a/packages/CrashRecovery/services/Android.bp b/packages/CrashRecovery/services/Android.bp
index 27ddff9..ab10b5a 100644
--- a/packages/CrashRecovery/services/Android.bp
+++ b/packages/CrashRecovery/services/Android.bp
@@ -1,9 +1,63 @@
-filegroup {
+soong_config_module_type {
+ name: "platform_filegroup",
+ module_type: "filegroup",
+ config_namespace: "ANDROID",
+ bool_variables: [
+ "move_crashrecovery_files",
+ ],
+ properties: [
+ "srcs",
+ ],
+}
+
+platform_filegroup {
name: "services-crashrecovery-sources",
srcs: [
"java/**/*.java",
"java/**/*.aidl",
+ ":statslog-crashrecovery-java-gen",
],
- path: "java",
+ soong_config_variables: {
+ // if the flag is enabled, then files would be moved to module
+ move_crashrecovery_files: {
+ srcs: [],
+ },
+ },
visibility: ["//frameworks/base:__subpackages__"],
}
+
+soong_config_module_type {
+ name: "module_filegroup",
+ module_type: "filegroup",
+ config_namespace: "ANDROID",
+ bool_variables: [
+ "move_crashrecovery_files",
+ ],
+ properties: [
+ "srcs",
+ ],
+}
+
+module_filegroup {
+ name: "services-crashrecovery-module-sources",
+ srcs: [],
+ soong_config_variables: {
+ // if the flag is enabled, then files would be moved to module
+ move_crashrecovery_files: {
+ srcs: [
+ "java/**/*.java",
+ "java/**/*.aidl",
+ ":statslog-crashrecovery-java-gen",
+ ],
+ },
+ },
+ visibility: ["//packages/modules/CrashRecovery/service"],
+}
+
+genrule {
+ name: "statslog-crashrecovery-java-gen",
+ tools: ["stats-log-api-gen"],
+ cmd: "$(location stats-log-api-gen) --java $(out) --module crashrecovery " +
+ "--javaPackage com.android.server.crashrecovery.proto --javaClass CrashRecoveryStatsLog --worksource",
+ out: ["com/android/server/crashrecovery/proto/CrashRecoveryStatsLog.java"],
+}
diff --git a/packages/CrashRecovery/services/java/com/android/server/RescueParty.java b/packages/CrashRecovery/services/java/com/android/server/RescueParty.java
index dd54334..5d03ef5 100644
--- a/packages/CrashRecovery/services/java/com/android/server/RescueParty.java
+++ b/packages/CrashRecovery/services/java/com/android/server/RescueParty.java
@@ -46,11 +46,11 @@
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.ArrayUtils;
-import com.android.internal.util.FrameworkStatsLog;
import com.android.server.PackageWatchdog.FailureReasons;
import com.android.server.PackageWatchdog.PackageHealthObserver;
import com.android.server.PackageWatchdog.PackageHealthObserverImpact;
import com.android.server.am.SettingsToPropertiesMapper;
+import com.android.server.crashrecovery.proto.CrashRecoveryStatsLog;
import java.io.File;
import java.util.ArrayList;
@@ -390,7 +390,8 @@
return;
}
- FrameworkStatsLog.write(FrameworkStatsLog.RESCUE_PARTY_RESET_REPORTED, level);
+ CrashRecoveryStatsLog.write(CrashRecoveryStatsLog.RESCUE_PARTY_RESET_REPORTED,
+ level, levelToString(level));
// Try our best to reset all settings possible, and once finished
// rethrow any exception that we encountered
Exception res = null;
diff --git a/packages/CrashRecovery/services/java/com/android/server/rollback/RollbackPackageHealthObserver.java b/packages/CrashRecovery/services/java/com/android/server/rollback/RollbackPackageHealthObserver.java
index 2007079..50322f0 100644
--- a/packages/CrashRecovery/services/java/com/android/server/rollback/RollbackPackageHealthObserver.java
+++ b/packages/CrashRecovery/services/java/com/android/server/rollback/RollbackPackageHealthObserver.java
@@ -39,13 +39,13 @@
import android.util.SparseArray;
import com.android.internal.annotations.VisibleForTesting;
-import com.android.internal.util.FrameworkStatsLog;
import com.android.internal.util.Preconditions;
import com.android.server.PackageWatchdog;
import com.android.server.PackageWatchdog.FailureReasons;
import com.android.server.PackageWatchdog.PackageHealthObserver;
import com.android.server.PackageWatchdog.PackageHealthObserverImpact;
import com.android.server.SystemConfig;
+import com.android.server.crashrecovery.proto.CrashRecoveryStatsLog;
import com.android.server.pm.ApexManager;
import java.io.BufferedReader;
@@ -418,7 +418,7 @@
final VersionedPackage logPackage = logPackageTemp;
WatchdogRollbackLogger.logEvent(logPackage,
- FrameworkStatsLog.WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_TYPE__ROLLBACK_INITIATE,
+ CrashRecoveryStatsLog.WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_TYPE__ROLLBACK_INITIATE,
reasonToLog, failedPackageToLog);
Consumer<Intent> onResult = result -> {
@@ -430,19 +430,19 @@
int rollbackId = rollback.getRollbackId();
saveStagedRollbackId(rollbackId, logPackage);
WatchdogRollbackLogger.logEvent(logPackage,
- FrameworkStatsLog
+ CrashRecoveryStatsLog
.WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_TYPE__ROLLBACK_BOOT_TRIGGERED,
reasonToLog, failedPackageToLog);
} else {
WatchdogRollbackLogger.logEvent(logPackage,
- FrameworkStatsLog
+ CrashRecoveryStatsLog
.WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_TYPE__ROLLBACK_SUCCESS,
reasonToLog, failedPackageToLog);
}
} else {
WatchdogRollbackLogger.logEvent(logPackage,
- FrameworkStatsLog
+ CrashRecoveryStatsLog
.WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_TYPE__ROLLBACK_FAILURE,
reasonToLog, failedPackageToLog);
}
diff --git a/packages/CrashRecovery/services/java/com/android/server/rollback/WatchdogRollbackLogger.java b/packages/CrashRecovery/services/java/com/android/server/rollback/WatchdogRollbackLogger.java
index f9ef994..898c543 100644
--- a/packages/CrashRecovery/services/java/com/android/server/rollback/WatchdogRollbackLogger.java
+++ b/packages/CrashRecovery/services/java/com/android/server/rollback/WatchdogRollbackLogger.java
@@ -16,16 +16,16 @@
package com.android.server.rollback;
-import static com.android.internal.util.FrameworkStatsLog.WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_REASON__REASON_APP_CRASH;
-import static com.android.internal.util.FrameworkStatsLog.WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_REASON__REASON_APP_NOT_RESPONDING;
-import static com.android.internal.util.FrameworkStatsLog.WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_REASON__REASON_EXPLICIT_HEALTH_CHECK;
-import static com.android.internal.util.FrameworkStatsLog.WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_REASON__REASON_NATIVE_CRASH;
-import static com.android.internal.util.FrameworkStatsLog.WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_REASON__REASON_NATIVE_CRASH_DURING_BOOT;
-import static com.android.internal.util.FrameworkStatsLog.WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_REASON__REASON_UNKNOWN;
-import static com.android.internal.util.FrameworkStatsLog.WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_TYPE__ROLLBACK_BOOT_TRIGGERED;
-import static com.android.internal.util.FrameworkStatsLog.WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_TYPE__ROLLBACK_FAILURE;
-import static com.android.internal.util.FrameworkStatsLog.WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_TYPE__ROLLBACK_INITIATE;
-import static com.android.internal.util.FrameworkStatsLog.WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_TYPE__ROLLBACK_SUCCESS;
+import static com.android.server.crashrecovery.proto.CrashRecoveryStatsLog.WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_REASON__REASON_APP_CRASH;
+import static com.android.server.crashrecovery.proto.CrashRecoveryStatsLog.WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_REASON__REASON_APP_NOT_RESPONDING;
+import static com.android.server.crashrecovery.proto.CrashRecoveryStatsLog.WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_REASON__REASON_EXPLICIT_HEALTH_CHECK;
+import static com.android.server.crashrecovery.proto.CrashRecoveryStatsLog.WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_REASON__REASON_NATIVE_CRASH;
+import static com.android.server.crashrecovery.proto.CrashRecoveryStatsLog.WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_REASON__REASON_NATIVE_CRASH_DURING_BOOT;
+import static com.android.server.crashrecovery.proto.CrashRecoveryStatsLog.WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_REASON__REASON_UNKNOWN;
+import static com.android.server.crashrecovery.proto.CrashRecoveryStatsLog.WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_TYPE__ROLLBACK_BOOT_TRIGGERED;
+import static com.android.server.crashrecovery.proto.CrashRecoveryStatsLog.WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_TYPE__ROLLBACK_FAILURE;
+import static com.android.server.crashrecovery.proto.CrashRecoveryStatsLog.WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_TYPE__ROLLBACK_INITIATE;
+import static com.android.server.crashrecovery.proto.CrashRecoveryStatsLog.WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_TYPE__ROLLBACK_SUCCESS;
import android.annotation.NonNull;
import android.annotation.Nullable;
@@ -42,8 +42,8 @@
import android.util.Slog;
import com.android.internal.annotations.VisibleForTesting;
-import com.android.internal.util.FrameworkStatsLog;
import com.android.server.PackageWatchdog;
+import com.android.server.crashrecovery.proto.CrashRecoveryStatsLog;
import java.util.List;
import java.util.Set;
@@ -197,8 +197,8 @@
+ " rollbackReason: " + rollbackReasonToString(rollbackReason)
+ " failedPackageName: " + failingPackageName);
if (logPackage != null) {
- FrameworkStatsLog.write(
- FrameworkStatsLog.WATCHDOG_ROLLBACK_OCCURRED,
+ CrashRecoveryStatsLog.write(
+ CrashRecoveryStatsLog.WATCHDOG_ROLLBACK_OCCURRED,
type,
logPackage.getPackageName(),
logPackage.getVersionCode(),
@@ -208,8 +208,8 @@
} else {
// In the case that the log package is null, still log an empty string as an
// indication that retrieving the logging parent failed.
- FrameworkStatsLog.write(
- FrameworkStatsLog.WATCHDOG_ROLLBACK_OCCURRED,
+ CrashRecoveryStatsLog.write(
+ CrashRecoveryStatsLog.WATCHDOG_ROLLBACK_OCCURRED,
type,
"",
0,
diff --git a/packages/CredentialManager/res/layout/credman_dropdown_bottom_sheet.xml b/packages/CredentialManager/res/layout/credman_dropdown_bottom_sheet.xml
index fdda9ea..910ff96 100644
--- a/packages/CredentialManager/res/layout/credman_dropdown_bottom_sheet.xml
+++ b/packages/CredentialManager/res/layout/credman_dropdown_bottom_sheet.xml
@@ -17,7 +17,7 @@
android:id="@android:id/content"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:minWidth="@dimen/dropdown_touch_target_min_width"
+ android:minHeight="@dimen/dropdown_touch_target_min_height"
android:orientation="horizontal"
android:layout_marginEnd="@dimen/dropdown_layout_horizontal_margin"
android:elevation="3dp">
diff --git a/packages/CredentialManager/res/layout/credman_dropdown_presentation_layout.xml b/packages/CredentialManager/res/layout/credman_dropdown_presentation_layout.xml
index c7c2fda..4bf0e99 100644
--- a/packages/CredentialManager/res/layout/credman_dropdown_presentation_layout.xml
+++ b/packages/CredentialManager/res/layout/credman_dropdown_presentation_layout.xml
@@ -17,7 +17,7 @@
android:id="@android:id/content"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:minWidth="@dimen/dropdown_touch_target_min_width"
+ android:minHeight="@dimen/dropdown_touch_target_min_height"
android:layout_marginEnd="@dimen/dropdown_layout_horizontal_margin"
android:elevation="3dp">
diff --git a/packages/CredentialManager/res/values/dimens.xml b/packages/CredentialManager/res/values/dimens.xml
index 53852cb..b47a4dc 100644
--- a/packages/CredentialManager/res/values/dimens.xml
+++ b/packages/CredentialManager/res/values/dimens.xml
@@ -26,6 +26,6 @@
<dimen name="autofill_dropdown_textview_min_width">112dp</dimen>
<dimen name="autofill_dropdown_textview_max_width">230dp</dimen>
<dimen name="dropdown_layout_horizontal_margin">24dp</dimen>
- <integer name="autofill_max_visible_datasets">3</integer>
- <dimen name="dropdown_touch_target_min_width">48dp</dimen>
+ <integer name="autofill_max_visible_datasets">5</integer>
+ <dimen name="dropdown_touch_target_min_height">48dp</dimen>
</resources>
\ No newline at end of file
diff --git a/packages/CredentialManager/shared/src/com/android/credentialmanager/IntentParser.kt b/packages/CredentialManager/shared/src/com/android/credentialmanager/IntentParser.kt
index 0fa248d..e7d1072 100644
--- a/packages/CredentialManager/shared/src/com/android/credentialmanager/IntentParser.kt
+++ b/packages/CredentialManager/shared/src/com/android/credentialmanager/IntentParser.kt
@@ -36,11 +36,11 @@
fun Intent.parseCancelUiRequest(packageManager: PackageManager): Request? =
this.cancelUiRequest?.let { cancelUiRequest ->
- val showCancel = cancelUiRequest.shouldShowCancellationUi().apply {
+ val showCancel = cancelUiRequest.shouldShowCancellationExplanation().apply {
Log.d(TAG, "Received UI cancel request, shouldShowCancellationUi: $this")
}
if (showCancel) {
- val appLabel = packageManager.appLabel(cancelUiRequest.appPackageName)
+ val appLabel = packageManager.appLabel(cancelUiRequest.packageName)
if (appLabel == null) {
Log.d(TAG, "Received UI cancel request with an invalid package name.")
null
diff --git a/packages/CredentialManager/shared/src/com/android/credentialmanager/ktx/IntentKtx.kt b/packages/CredentialManager/shared/src/com/android/credentialmanager/ktx/IntentKtx.kt
index 4155b03..9242141 100644
--- a/packages/CredentialManager/shared/src/com/android/credentialmanager/ktx/IntentKtx.kt
+++ b/packages/CredentialManager/shared/src/com/android/credentialmanager/ktx/IntentKtx.kt
@@ -17,7 +17,7 @@
package com.android.credentialmanager.ktx
import android.content.Intent
-import android.credentials.selection.CancelUiRequest
+import android.credentials.selection.CancelSelectionRequest
import android.credentials.selection.Constants
import android.credentials.selection.CreateCredentialProviderData
import android.credentials.selection.GetCredentialProviderData
@@ -25,10 +25,10 @@
import android.credentials.selection.RequestInfo
import android.os.ResultReceiver
-val Intent.cancelUiRequest: CancelUiRequest?
+val Intent.cancelUiRequest: CancelSelectionRequest?
get() = this.extras?.getParcelable(
- CancelUiRequest.EXTRA_CANCEL_UI_REQUEST,
- CancelUiRequest::class.java
+ CancelSelectionRequest.EXTRA_CANCEL_UI_REQUEST,
+ CancelSelectionRequest::class.java
)
val Intent.requestInfo: RequestInfo?
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt b/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt
index 6cafcf7..3097387 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt
@@ -18,7 +18,7 @@
import android.content.Context
import android.content.Intent
-import android.credentials.selection.CancelUiRequest
+import android.credentials.selection.CancelSelectionRequest
import android.credentials.selection.Constants
import android.credentials.selection.CreateCredentialProviderData
import android.credentials.selection.GetCredentialProviderData
@@ -32,6 +32,7 @@
import android.os.Bundle
import android.os.ResultReceiver
import android.util.Log
+import android.view.autofill.AutofillManager
import com.android.credentialmanager.createflow.DisabledProviderInfo
import com.android.credentialmanager.createflow.EnabledProviderInfo
import com.android.credentialmanager.createflow.RequestDisplayInfo
@@ -57,6 +58,7 @@
private val providerEnabledList: List<ProviderData>
private val providerDisabledList: List<DisabledProviderData>?
val resultReceiver: ResultReceiver?
+ val finalResponseReceiver: ResultReceiver?
var initialUiState: UiState
@@ -80,9 +82,10 @@
CreateCredentialProviderData::class.java
) ?: emptyList()
RequestInfo.TYPE_GET ->
- intent.extras?.getParcelableArrayList(
- ProviderData.EXTRA_ENABLED_PROVIDER_DATA_LIST,
- GetCredentialProviderData::class.java
+ getEnabledProviderDataList(
+ intent
+ ) ?: getEnabledProviderDataListFromAuthExtras(
+ intent
) ?: emptyList()
else -> {
Log.d(
@@ -103,14 +106,20 @@
ResultReceiver::class.java
)
+ finalResponseReceiver = intent.getParcelableExtra(
+ Constants.EXTRA_FINAL_RESPONSE_RECEIVER,
+ ResultReceiver::class.java
+ )
+
isReqForAllOptions = intent.getBooleanExtra(
Constants.EXTRA_REQ_FOR_ALL_OPTIONS,
/*defaultValue=*/ false
- )
+ ) || (requestInfo?.isShowAllOptionsRequested ?: false) // TODO(b/323552850) - Remove
+ // usage on Constants.EXTRA_REQ_FOR_ALL_OPTIONS once it is deprecated.
val cancellationRequest = getCancelUiRequest(intent)
val cancelUiRequestState = cancellationRequest?.let {
- CancelUiRequestState(getAppLabel(context.getPackageManager(), it.appPackageName))
+ CancelUiRequestState(getAppLabel(context.getPackageManager(), it.packageName))
}
initialUiState = when (requestInfo?.type) {
@@ -197,7 +206,7 @@
}
fun onCancel(cancelCode: Int) {
- sendCancellationCode(cancelCode, requestInfo?.token, resultReceiver)
+ sendCancellationCode(cancelCode, requestInfo?.token, resultReceiver, finalResponseReceiver)
}
fun onOptionSelected(
@@ -216,6 +225,10 @@
)
val resultDataBundle = Bundle()
UserSelectionDialogResult.addToBundle(userSelectionDialogResult, resultDataBundle)
+
+ resultDataBundle.putParcelable(Constants.EXTRA_FINAL_RESPONSE_RECEIVER,
+ finalResponseReceiver)
+
resultReceiver?.send(
BaseDialogResult.RESULT_CODE_DIALOG_COMPLETE_WITH_SELECTION,
resultDataBundle
@@ -238,6 +251,24 @@
)
}
+ private fun getEnabledProviderDataList(intent: Intent): List<GetCredentialProviderData>? {
+ return intent.extras?.getParcelableArrayList(
+ ProviderData.EXTRA_ENABLED_PROVIDER_DATA_LIST,
+ GetCredentialProviderData::class.java
+ )
+ }
+
+ private fun getEnabledProviderDataListFromAuthExtras(
+ intent: Intent
+ ): List<GetCredentialProviderData>? {
+ return intent.getBundleExtra(
+ AutofillManager.EXTRA_AUTH_STATE
+ ) ?.getParcelableArrayList(
+ ProviderData.EXTRA_ENABLED_PROVIDER_DATA_LIST,
+ GetCredentialProviderData::class.java
+ )
+ }
+
// IMPORTANT: new invocation should be mindful that this method can throw.
private fun getCreateProviderEnableListInitialUiState(): List<EnabledProviderInfo> {
return CreateFlowUtils.toEnabledProviderList(
@@ -265,20 +296,24 @@
fun sendCancellationCode(
cancelCode: Int,
requestToken: IBinder?,
- resultReceiver: ResultReceiver?
+ resultReceiver: ResultReceiver?,
+ finalResponseReceiver: ResultReceiver?
) {
if (requestToken != null && resultReceiver != null) {
val resultData = Bundle()
+ resultData.putParcelable(Constants.EXTRA_FINAL_RESPONSE_RECEIVER,
+ finalResponseReceiver)
+
BaseDialogResult.addToBundle(BaseDialogResult(requestToken), resultData)
resultReceiver.send(cancelCode, resultData)
}
}
/** Return the cancellation request if present. */
- fun getCancelUiRequest(intent: Intent): CancelUiRequest? {
+ fun getCancelUiRequest(intent: Intent): CancelSelectionRequest? {
return intent.extras?.getParcelable(
- CancelUiRequest.EXTRA_CANCEL_UI_REQUEST,
- CancelUiRequest::class.java
+ CancelSelectionRequest.EXTRA_CANCEL_UI_REQUEST,
+ CancelSelectionRequest::class.java
)
}
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorActivity.kt b/packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorActivity.kt
index fa975aa..4771237 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorActivity.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorActivity.kt
@@ -131,11 +131,11 @@
// Cancellation was for a different request, don't cancel the current UI.
return Triple(true, false, null)
}
- val shouldShowCancellationUi = cancelUiRequest.shouldShowCancellationUi()
+ val shouldShowCancellationUi = cancelUiRequest.shouldShowCancellationExplanation()
Log.d(
Constants.LOG_TAG, "Received UI cancellation intent. Should show cancellation" +
" ui = $shouldShowCancellationUi")
- val appDisplayName = getAppLabel(packageManager, cancelUiRequest.appPackageName)
+ val appDisplayName = getAppLabel(packageManager, cancelUiRequest.packageName)
if (!shouldShowCancellationUi) {
this.finish()
}
@@ -216,13 +216,18 @@
android.credentials.selection.Constants.EXTRA_RESULT_RECEIVER,
ResultReceiver::class.java
)
+ val finalResponseResultReceiver = intent.getParcelableExtra(
+ android.credentials.selection.Constants.EXTRA_FINAL_RESPONSE_RECEIVER,
+ ResultReceiver::class.java
+ )
+
val requestInfo = intent.extras?.getParcelable(
RequestInfo.EXTRA_REQUEST_INFO,
RequestInfo::class.java
)
CredentialManagerRepo.sendCancellationCode(
BaseDialogResult.RESULT_CODE_DATA_PARSING_FAILURE,
- requestInfo?.token, resultReceiver
+ requestInfo?.token, resultReceiver, finalResponseResultReceiver
)
this.finish()
}
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorViewModel.kt b/packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorViewModel.kt
index 6c5a984..f4da1e6 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorViewModel.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorViewModel.kt
@@ -72,7 +72,7 @@
init {
uiMetrics.logNormal(LifecycleEvent.CREDMAN_ACTIVITY_INIT,
- credManRepo.requestInfo?.appPackageName)
+ credManRepo.requestInfo?.packageName)
}
/**************************************************************************/
@@ -107,7 +107,7 @@
if (this.credManRepo.requestInfo?.token != credManRepo.requestInfo?.token) {
this.uiMetrics.resetInstanceId()
this.uiMetrics.logNormal(LifecycleEvent.CREDMAN_ACTIVITY_NEW_REQUEST,
- credManRepo.requestInfo?.appPackageName)
+ credManRepo.requestInfo?.packageName)
}
}
@@ -189,7 +189,7 @@
private fun onInternalError() {
Log.w(Constants.LOG_TAG, "UI closed due to illegal internal state")
this.uiMetrics.logNormal(LifecycleEvent.CREDMAN_ACTIVITY_INTERNAL_ERROR,
- credManRepo.requestInfo?.appPackageName)
+ credManRepo.requestInfo?.packageName)
credManRepo.onParsingFailureCancel()
uiState = uiState.copy(dialogState = DialogState.COMPLETE)
}
@@ -399,6 +399,6 @@
@Composable
fun logUiEvent(uiEventEnum: UiEventEnum) {
- this.uiMetrics.log(uiEventEnum, credManRepo.requestInfo?.appPackageName)
+ this.uiMetrics.log(uiEventEnum, credManRepo.requestInfo?.packageName)
}
}
\ No newline at end of file
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt b/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt
index 64595e2..997c45e 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt
@@ -195,7 +195,7 @@
}
return com.android.credentialmanager.getflow.RequestDisplayInfo(
appName = originName?.ifEmpty { null }
- ?: getAppLabel(context.packageManager, requestInfo.appPackageName)
+ ?: getAppLabel(context.packageManager, requestInfo.packageName)
?: return null,
preferImmediatelyAvailableCredentials = preferImmediatelyAvailableCredentials,
preferIdentityDocUi = getCredentialRequest.data.getBoolean(
@@ -269,7 +269,7 @@
return null
}
val appLabel = originName?.ifEmpty { null }
- ?: getAppLabel(context.packageManager, requestInfo.appPackageName)
+ ?: getAppLabel(context.packageManager, requestInfo.packageName)
?: return null
val createCredentialRequest = requestInfo.createCredentialRequest ?: return null
val createCredentialRequestJetpack = CreateCredentialRequest.createFrom(
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/autofill/CredentialAutofillService.kt b/packages/CredentialManager/src/com/android/credentialmanager/autofill/CredentialAutofillService.kt
index 1f1d236..8fde5d7 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/autofill/CredentialAutofillService.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/autofill/CredentialAutofillService.kt
@@ -16,22 +16,23 @@
package com.android.credentialmanager.autofill
-import android.R
+import android.app.PendingIntent
import android.app.assist.AssistStructure
import android.content.Context
-import android.app.PendingIntent
-import android.credentials.GetCredentialResponse
+import android.content.Intent
+import android.credentials.CredentialManager
import android.credentials.GetCredentialRequest
import android.credentials.GetCandidateCredentialsResponse
import android.credentials.GetCandidateCredentialsException
import android.credentials.CredentialOption
+import android.credentials.selection.Entry
import android.credentials.selection.GetCredentialProviderData
+import android.credentials.selection.ProviderData
import android.graphics.drawable.Icon
import android.os.Bundle
import android.os.CancellationSignal
import android.os.OutcomeReceiver
import android.provider.Settings
-import android.credentials.Credential
import android.service.autofill.AutofillService
import android.service.autofill.Dataset
import android.service.autofill.Field
@@ -44,12 +45,10 @@
import android.service.autofill.SaveRequest
import android.service.credentials.CredentialProviderService
import android.util.Log
-import android.view.autofill.AutofillValue
-import android.view.autofill.IAutoFillManagerClient
import android.view.autofill.AutofillId
-import android.widget.inline.InlinePresentationSpec
-import android.credentials.CredentialManager
+import android.view.autofill.IAutoFillManagerClient
import android.widget.RemoteViews
+import android.widget.inline.InlinePresentationSpec
import androidx.autofill.inline.v1.InlineSuggestionUi
import androidx.credentials.provider.CustomCredentialEntry
import androidx.credentials.provider.PasswordCredentialEntry
@@ -61,10 +60,9 @@
import com.android.credentialmanager.ktx.credentialEntry
import com.android.credentialmanager.model.CredentialType
import com.android.credentialmanager.model.get.CredentialEntryInfo
-import com.android.credentialmanager.model.get.ProviderInfo
+import java.util.concurrent.Executors
import org.json.JSONException
import org.json.JSONObject
-import java.util.concurrent.Executors
class CredentialAutofillService : AutofillService() {
@@ -116,8 +114,10 @@
return
}
+ val responseClientState = Bundle()
+ responseClientState.putBoolean(WEBVIEW_REQUESTED_CREDENTIAL_KEY, false)
val getCredRequest: GetCredentialRequest? = getCredManRequest(structure, sessionId,
- requestId)
+ requestId, responseClientState)
if (getCredRequest == null) {
Log.i(TAG, "No credential manager request found")
callback.onFailure("No credential manager request found")
@@ -129,32 +129,9 @@
val outcome = object : OutcomeReceiver<GetCandidateCredentialsResponse,
GetCandidateCredentialsException> {
override fun onResult(result: GetCandidateCredentialsResponse) {
- Log.i(TAG, "getCandidateCredentials onResponse")
-
- if (result.getCredentialResponse != null) {
- val autofillId: AutofillId? = result.getCredentialResponse
- .credential.data.getParcelable(
- CredentialProviderService.EXTRA_AUTOFILL_ID,
- AutofillId::class.java)
- Log.i(TAG, "getCandidateCredentials final response, autofillId: " +
- autofillId)
-
- if (autofillId != null) {
- autofillCallback.autofill(
- sessionId,
- mutableListOf(autofillId),
- mutableListOf(
- AutofillValue.forText(
- convertResponseToJson(result.getCredentialResponse)
- )
- ),
- false)
- }
- return
- }
-
+ Log.i(TAG, "getCandidateCredentials onResult")
val fillResponse = convertToFillResponse(result, request,
- this@CredentialAutofillService)
+ responseClientState)
if (fillResponse != null) {
callback.onSuccess(fillResponse)
} else {
@@ -179,59 +156,8 @@
)
}
- // TODO(b/318118018): Use from Jetpack
- private fun convertResponseToJson(response: GetCredentialResponse): String? {
- try {
- val jsonObject = JSONObject()
- jsonObject.put("type", "get")
- val jsonCred = JSONObject()
- jsonCred.put("type", response.credential.type)
- jsonCred.put("data", credentialToJSON(
- response.credential))
- jsonObject.put("credential", jsonCred)
- return jsonObject.toString()
- } catch (e: JSONException) {
- Log.i(
- TAG, "Exception while constructing response JSON: " +
- e.message
- )
- }
- return null
- }
-
- // TODO(b/318118018): Replace with calls to Jetpack
- private fun credentialToJSON(credential: Credential): JSONObject? {
- Log.i(TAG, "credentialToJSON")
- try {
- if (credential.type == "android.credentials.TYPE_PASSWORD_CREDENTIAL") {
- Log.i(TAG, "toJSON PasswordCredential")
-
- val json = JSONObject()
- val id = credential.data.getString("androidx.credentials.BUNDLE_KEY_ID")
- val pass = credential.data.getString("androidx.credentials.BUNDLE_KEY_PASSWORD")
- json.put("androidx.credentials.BUNDLE_KEY_ID", id)
- json.put("androidx.credentials.BUNDLE_KEY_PASSWORD", pass)
- return json
- } else if (credential.type == "androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL") {
- Log.i(TAG, "toJSON PublicKeyCredential")
-
- val json = JSONObject()
- val responseJson = credential
- .data
- .getString("androidx.credentials.BUNDLE_KEY_AUTHENTICATION_RESPONSE_JSON")
- json.put("androidx.credentials.BUNDLE_KEY_AUTHENTICATION_RESPONSE_JSON",
- responseJson)
- return json
- }
- } catch (e: JSONException) {
- Log.i(TAG, "issue while converting credential response to JSON")
- }
- Log.i(TAG, "Unsupported credential type")
- return null
- }
-
private fun getEntryToIconMap(
- candidateProviderDataList: MutableList<GetCredentialProviderData>
+ candidateProviderDataList: List<GetCredentialProviderData>
): Map<String, Icon> {
val entryIconMap: MutableMap<String, Icon> = mutableMapOf()
candidateProviderDataList.forEach { provider ->
@@ -262,20 +188,18 @@
private fun convertToFillResponse(
getCredResponse: GetCandidateCredentialsResponse,
filLRequest: FillRequest,
- context: Context
+ responseClientState: Bundle
): FillResponse? {
- val providerList = GetFlowUtils.toProviderList(
- getCredResponse.candidateProviderDataList,
- context)
- if (providerList.isEmpty()) {
+ val candidateProviders = getCredResponse.candidateProviderDataList
+ if (candidateProviders.isEmpty()) {
return null
}
- val entryIconMap: Map<String, Icon> =
- getEntryToIconMap(getCredResponse.candidateProviderDataList)
- val autofillIdToProvidersMap: Map<AutofillId, List<ProviderInfo>> =
- mapAutofillIdToProviders(providerList)
+ val entryIconMap: Map<String, Icon> = getEntryToIconMap(candidateProviders)
+ val autofillIdToProvidersMap: Map<AutofillId, ArrayList<GetCredentialProviderData>> =
+ mapAutofillIdToProviders(candidateProviders)
val fillResponseBuilder = FillResponse.Builder()
+ fillResponseBuilder.setFlags(FillResponse.FLAG_CREDENTIAL_MANAGER_RESPONSE)
var validFillResponse = false
autofillIdToProvidersMap.forEach { (autofillId, providers) ->
validFillResponse = processProvidersForAutofillId(
@@ -286,17 +210,21 @@
if (!validFillResponse) {
return null
}
+ fillResponseBuilder.setClientState(responseClientState)
return fillResponseBuilder.build()
}
private fun processProvidersForAutofillId(
filLRequest: FillRequest,
autofillId: AutofillId,
- providerList: List<ProviderInfo>,
+ providerDataList: ArrayList<GetCredentialProviderData>,
entryIconMap: Map<String, Icon>,
fillResponseBuilder: FillResponse.Builder,
bottomSheetPendingIntent: PendingIntent?
): Boolean {
+ val providerList = GetFlowUtils.toProviderList(
+ providerDataList,
+ this@CredentialAutofillService)
if (providerList.isEmpty()) {
return false
}
@@ -315,7 +243,7 @@
maxInlineItemCount = maxInlineItemCount.coerceAtMost(inlineMaxSuggestedCount)
val lastDropdownDatasetIndex = Settings.Global.getInt(this.contentResolver,
Settings.Global.AUTOFILL_MAX_VISIBLE_DATASETS,
- (maxDropdownDisplayLimit - 1).coerceAtMost(totalEntryCount - 1))
+ (maxDropdownDisplayLimit - 1)).coerceAtMost(totalEntryCount - 1)
var i = 0
var datasetAdded = false
@@ -340,7 +268,7 @@
return@usernameLoop
}
if (i >= maxInlineItemCount && i >= lastDropdownDatasetIndex) {
- return@usernameLoop;
+ return@usernameLoop
}
val icon: Icon = if (primaryEntry.icon == null) {
// The empty entry icon has non-null icon reference but null drawable reference.
@@ -384,7 +312,7 @@
presentationBuilder.build())
.build())
.setAuthentication(pendingIntent.intentSender)
- .setAuthenticationExtras(fillInIntent.extras)
+ .setCredentialFillInIntent(fillInIntent)
.build())
datasetAdded = true
i++
@@ -398,19 +326,20 @@
inlinePresentationSpecsCount)
if (datasetAdded && bottomSheetPendingIntent != null && pinnedSpec != null) {
addPinnedInlineSuggestion(bottomSheetPendingIntent, pinnedSpec, autofillId,
- fillResponseBuilder)
+ fillResponseBuilder, providerDataList)
}
return datasetAdded
}
- private fun createInlinePresentation(primaryEntry: CredentialEntryInfo,
- pendingIntent: PendingIntent,
- icon: Icon,
- spec: InlinePresentationSpec,
- duplicateDisplayNameForPasskeys: MutableMap<String, Boolean>):
- InlinePresentation {
- val displayName: String = if (primaryEntry.credentialType == CredentialType.PASSKEY
- && primaryEntry.displayName != null) {
+ private fun createInlinePresentation(
+ primaryEntry: CredentialEntryInfo,
+ pendingIntent: PendingIntent,
+ icon: Icon,
+ spec: InlinePresentationSpec,
+ duplicateDisplayNameForPasskeys: MutableMap<String, Boolean>
+ ): InlinePresentation {
+ val displayName: String = if (primaryEntry.credentialType == CredentialType.PASSKEY &&
+ primaryEntry.displayName != null) {
primaryEntry.displayName!!
} else {
primaryEntry.userName
@@ -430,9 +359,11 @@
private fun addDropdownMoreOptionsPresentation(
bottomSheetPendingIntent: PendingIntent,
autofillId: AutofillId,
- fillResponseBuilder: FillResponse.Builder) {
+ fillResponseBuilder: FillResponse.Builder
+ ) {
val presentationBuilder = Presentations.Builder()
- .setMenuPresentation(RemoteViewsFactory.createMoreSignInOptionsPresentation(this))
+ .setMenuPresentation(
+ RemoteViewsFactory.createMoreSignInOptionsPresentation(this))
fillResponseBuilder.addDataset(
Dataset.Builder()
@@ -460,7 +391,8 @@
bottomSheetPendingIntent: PendingIntent,
spec: InlinePresentationSpec,
autofillId: AutofillId,
- fillResponseBuilder: FillResponse.Builder
+ fillResponseBuilder: FillResponse.Builder,
+ providerDataList: ArrayList<GetCredentialProviderData>
) {
val dataSetBuilder = Dataset.Builder()
val sliceBuilder = InlineSuggestionUi
@@ -471,6 +403,9 @@
.setInlinePresentation(InlinePresentation(
sliceBuilder.build().slice, spec, /* pinned= */ true))
+ val extrasIntent = Intent()
+ extrasIntent.putExtra(ProviderData.EXTRA_ENABLED_PROVIDER_DATA_LIST, providerDataList)
+
fillResponseBuilder.addDataset(
dataSetBuilder
.setField(
@@ -479,6 +414,7 @@
presentationBuilder.build())
.build())
.setAuthentication(bottomSheetPendingIntent.intentSender)
+ .setCredentialFillInIntent(extrasIntent)
.build()
)
}
@@ -514,16 +450,16 @@
* }
*/
private fun mapAutofillIdToProviders(
- providerList: List<ProviderInfo>
- ): Map<AutofillId, List<ProviderInfo>> {
- val autofillIdToProviders: MutableMap<AutofillId, MutableList<ProviderInfo>> =
- mutableMapOf()
+ providerList: List<GetCredentialProviderData>
+ ): Map<AutofillId, ArrayList<GetCredentialProviderData>> {
+ val autofillIdToProviders: MutableMap<AutofillId, ArrayList<GetCredentialProviderData>> =
+ mutableMapOf()
providerList.forEach { provider ->
val autofillIdToCredentialEntries:
- MutableMap<AutofillId, MutableList<CredentialEntryInfo>> =
- mapAutofillIdToCredentialEntries(provider.credentialEntryList)
+ MutableMap<AutofillId, ArrayList<Entry>> =
+ mapAutofillIdToCredentialEntries(provider.credentialEntries)
autofillIdToCredentialEntries.forEach { (autofillId, entries) ->
- autofillIdToProviders.getOrPut(autofillId) { mutableListOf() }
+ autofillIdToProviders.getOrPut(autofillId) { ArrayList() }
.add(copyProviderInfo(provider, entries))
}
}
@@ -531,13 +467,13 @@
}
private fun mapAutofillIdToCredentialEntries(
- credentialEntryList: List<CredentialEntryInfo>
- ): MutableMap<AutofillId, MutableList<CredentialEntryInfo>> {
+ credentialEntryList: List<Entry>
+ ): MutableMap<AutofillId, ArrayList<Entry>> {
val autofillIdToCredentialEntries:
- MutableMap<AutofillId, MutableList<CredentialEntryInfo>> = mutableMapOf()
+ MutableMap<AutofillId, ArrayList<Entry>> = mutableMapOf()
credentialEntryList.forEach entryLoop@{ credentialEntry ->
val autofillId: AutofillId? = credentialEntry
- .fillInIntent
+ .frameworkExtrasIntent
?.getParcelableExtra(
CredentialProviderService.EXTRA_AUTOFILL_ID,
AutofillId::class.java)
@@ -546,24 +482,22 @@
" Integration might be disabled.")
return@entryLoop
}
- autofillIdToCredentialEntries.getOrPut(autofillId) { mutableListOf() }
+ autofillIdToCredentialEntries.getOrPut(autofillId) { ArrayList() }
.add(credentialEntry)
}
return autofillIdToCredentialEntries
}
private fun copyProviderInfo(
- providerInfo: ProviderInfo,
- credentialList: List<CredentialEntryInfo>
- ): ProviderInfo {
- return ProviderInfo(
- providerInfo.id,
- providerInfo.icon,
- providerInfo.displayName,
- credentialList,
- providerInfo.authenticationEntryList,
- providerInfo.remoteEntry,
- providerInfo.actionEntryList
+ providerInfo: GetCredentialProviderData,
+ credentialList: List<Entry>
+ ): GetCredentialProviderData {
+ return GetCredentialProviderData(
+ providerInfo.providerFlattenedComponentName,
+ credentialList,
+ providerInfo.actionChips,
+ providerInfo.authenticationEntries,
+ providerInfo.remoteEntry
)
}
@@ -574,10 +508,11 @@
private fun getCredManRequest(
structure: AssistStructure,
sessionId: Int,
- requestId: Int
+ requestId: Int,
+ responseClientState: Bundle
): GetCredentialRequest? {
val credentialOptions: MutableList<CredentialOption> = mutableListOf()
- traverseStructure(structure, credentialOptions)
+ traverseStructure(structure, credentialOptions, responseClientState)
if (credentialOptions.isNotEmpty()) {
val dataBundle = Bundle()
@@ -592,7 +527,8 @@
private fun traverseStructure(
structure: AssistStructure,
- cmRequests: MutableList<CredentialOption>
+ cmRequests: MutableList<CredentialOption>,
+ responseClientState: Bundle
) {
val windowNodes: List<AssistStructure.WindowNode> =
structure.run {
@@ -600,16 +536,17 @@
}
windowNodes.forEach { windowNode: AssistStructure.WindowNode ->
- traverseNode(windowNode.rootViewNode, cmRequests)
+ traverseNode(windowNode.rootViewNode, cmRequests, responseClientState)
}
}
private fun traverseNode(
viewNode: AssistStructure.ViewNode,
- cmRequests: MutableList<CredentialOption>
+ cmRequests: MutableList<CredentialOption>,
+ responseClientState: Bundle
) {
viewNode.autofillId?.let {
- val options = getCredentialOptionsFromViewNode(viewNode, it)
+ val options = getCredentialOptionsFromViewNode(viewNode, it, responseClientState)
cmRequests.addAll(options)
}
@@ -619,20 +556,23 @@
}
children.forEach { childNode: AssistStructure.ViewNode ->
- traverseNode(childNode, cmRequests)
+ traverseNode(childNode, cmRequests, responseClientState)
}
}
private fun getCredentialOptionsFromViewNode(
viewNode: AssistStructure.ViewNode,
- autofillId: AutofillId
+ autofillId: AutofillId,
+ responseClientState: Bundle
): List<CredentialOption> {
- // TODO(b/293945193) Replace with isCredential check from viewNode
val credentialHints: MutableList<String> = mutableListOf()
if (viewNode.autofillHints != null) {
for (hint in viewNode.autofillHints!!) {
if (hint.startsWith(CRED_HINT_PREFIX)) {
credentialHints.add(hint.substringAfter(CRED_HINT_PREFIX))
+ if (viewNode.webDomain != null) {
+ responseClientState.putBoolean(WEBVIEW_REQUESTED_CREDENTIAL_KEY, true)
+ }
}
}
}
diff --git a/packages/CredentialManager/wear/res/values/strings.xml b/packages/CredentialManager/wear/res/values/strings.xml
index 109644f..be7e448 100644
--- a/packages/CredentialManager/wear/res/values/strings.xml
+++ b/packages/CredentialManager/wear/res/values/strings.xml
@@ -21,11 +21,16 @@
<!-- Title of a screen prompting if the user would like to use their saved passkey.
[CHAR LIMIT=80] -->
<string name="use_passkey_title">Use passkey?</string>
- <!-- Title of a screen prompting if the user would like to use their saved password.
+ <!-- Title of a screen prompting if the user would like to use their saved passkey.
+[CHAR LIMIT=80] -->
+ <string name="use_sign_in_with_provider_title">Use your sign in for %1$s</string>
+ <!-- Title of a screen prompting if the user would like to sign in with provider
[CHAR LIMIT=80] -->
<string name="use_password_title">Use password?</string>
- <!-- Content description for the cancel button of a screen. [CHAR LIMIT=NONE] -->
- <string name="dialog_cancel_button_cd">Cancel</string>
- <!-- Content description for the OK button of a screen. [CHAR LIMIT=NONE] -->
- <string name="dialog_ok_button_cd">OK</string>
+ <!-- Content description for the dismiss button of a screen. [CHAR LIMIT=NONE] -->
+ <string name="dialog_dismiss_button">Dismiss</string>
+ <!-- Content description for the continue button of a screen. [CHAR LIMIT=NONE] -->
+ <string name="dialog_continue_button">Continue</string>
+ <!-- Content description for the sign in options button of a screen. [CHAR LIMIT=NONE] -->
+ <string name="dialog_sign_in_options_button">Sign-in Options</string>
</resources>
\ No newline at end of file
diff --git a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/WearApp.kt b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/WearApp.kt
index 790f5b1..f8e22ee 100644
--- a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/WearApp.kt
+++ b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/WearApp.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2023 The Android Open Source Project
+ * Copyright (C) 2024 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.
@@ -58,9 +58,9 @@
scrollable(Screen.SinglePasswordScreen.route) {
SinglePasswordScreen(
- state = viewModel.uiState.value as SingleEntry,
+ credentialSelectorUiState = viewModel.uiState.value as SingleEntry,
+ screenIcon = null,
columnState = it.columnState,
- onCloseApp = onCloseApp,
)
}
}
diff --git a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/components/AccountRow.kt b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/components/AccountRow.kt
index c20ee0c..b2812d3 100644
--- a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/components/AccountRow.kt
+++ b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/components/AccountRow.kt
@@ -30,26 +30,28 @@
@Composable
fun AccountRow(
- name: String,
- email: String,
+ primaryText: String,
+ secondaryText: String? = null,
modifier: Modifier = Modifier,
) {
Column(modifier = modifier, horizontalAlignment = Alignment.CenterHorizontally) {
Text(
- text = name,
+ text = primaryText,
color = Color(0xFFE6FF7B),
overflow = TextOverflow.Ellipsis,
maxLines = 1,
style = MaterialTheme.typography.title2
)
- Text(
- text = email,
- modifier = Modifier.padding(top = 7.dp),
- color = Color(0xFFCAC5BC),
- overflow = TextOverflow.Ellipsis,
- maxLines = 2,
- style = MaterialTheme.typography.body1,
- )
+ if (secondaryText != null) {
+ Text(
+ text = secondaryText,
+ modifier = Modifier.padding(top = 7.dp),
+ color = Color(0xFFCAC5BC),
+ overflow = TextOverflow.Ellipsis,
+ maxLines = 2,
+ style = MaterialTheme.typography.body1,
+ )
+ }
}
}
@@ -57,7 +59,7 @@
@Composable
fun AccountRowPreview() {
AccountRow(
- name = "Elisa Beckett",
- email = "beckett_bakery@gmail.com",
+ primaryText = "Elisa Beckett",
+ secondaryText = "beckett_bakery@gmail.com",
)
}
diff --git a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/components/CredentialsScreenChip.kt b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/components/CredentialsScreenChip.kt
new file mode 100644
index 0000000..5590219
--- /dev/null
+++ b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/components/CredentialsScreenChip.kt
@@ -0,0 +1,153 @@
+/*
+ * Copyright (C) 2023 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.credentialmanager.ui.components
+
+import android.graphics.drawable.Drawable
+import androidx.compose.foundation.layout.BoxScope
+import androidx.compose.foundation.layout.RowScope
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clipToBounds
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.wear.compose.material.Chip
+import androidx.wear.compose.material.ChipColors
+import androidx.wear.compose.material.ChipDefaults
+import androidx.wear.compose.material.Text
+import com.android.credentialmanager.R
+import com.android.credentialmanager.ui.components.CredentialsScreenChip.TOPPADDING
+
+@Composable
+fun CredentialsScreenChip(
+ label: String,
+ onClick: () -> Unit,
+ secondaryLabel: String? = null,
+ icon: Drawable? = null,
+ modifier: Modifier = Modifier,
+ colors: ChipColors = ChipDefaults.secondaryChipColors(),
+) {
+ val labelParam: (@Composable RowScope.() -> Unit) =
+ {
+ Text(
+ text = label,
+ modifier = Modifier.fillMaxWidth(),
+ textAlign = TextAlign.Center,
+ overflow = TextOverflow.Ellipsis,
+ maxLines = if (secondaryLabel != null) 1 else 2,
+ )
+ }
+
+ val secondaryLabelParam: (@Composable RowScope.() -> Unit)? =
+ secondaryLabel?.let {
+ {
+ Text(
+ text = secondaryLabel,
+ overflow = TextOverflow.Ellipsis,
+ maxLines = 1,
+ )
+ }
+ }
+
+ val iconParam: (@Composable BoxScope.() -> Unit)? =
+ icon?.let {
+ {
+ ChipDefaults.IconSize
+ }
+ }
+
+ Chip(
+ label = labelParam,
+ onClick = onClick,
+ modifier = modifier.fillMaxWidth(),
+ secondaryLabel = secondaryLabelParam,
+ icon = iconParam,
+ colors = colors,
+ enabled = true,
+ )
+}
+
+@Preview
+@Composable
+fun CredentialsScreenChipPreview() {
+ CredentialsScreenChip(
+ label = "Elisa Beckett",
+ onClick = { },
+ secondaryLabel = "beckett_bakery@gmail.com",
+ icon = null,
+ modifier = Modifier
+ .clipToBounds()
+ .padding(top = 2.dp)
+ )
+}
+
+@Composable
+fun SignInOptionsChip(onClick: () -> Unit) {
+ CredentialsScreenChip(
+ label = stringResource(R.string.dialog_sign_in_options_button),
+ onClick = onClick,
+ modifier = Modifier
+ .padding(top = TOPPADDING)
+ )
+}
+
+@Preview
+@Composable
+fun SignInOptionsChipPreview() {
+ SignInOptionsChip({})
+}
+
+@Composable
+fun ContinueChip(onClick: () -> Unit) {
+ CredentialsScreenChip(
+ label = stringResource(R.string.dialog_continue_button),
+ onClick = onClick,
+ modifier = Modifier
+ .padding(top = TOPPADDING),
+ colors = ChipDefaults.primaryChipColors(),
+ )
+}
+
+@Preview
+@Composable
+fun ContinueChipPreview() {
+ ContinueChip({})
+}
+
+@Composable
+fun DismissChip(onClick: () -> Unit) {
+ CredentialsScreenChip(
+ label = stringResource(R.string.dialog_dismiss_button),
+ onClick = onClick,
+ modifier = Modifier
+ .padding(top = TOPPADDING),
+ )
+}
+
+@Preview
+@Composable
+fun DismissChipPreview() {
+ DismissChip({})
+}
+
+private object CredentialsScreenChip {
+ val TOPPADDING = 8.dp
+}
+
diff --git a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/components/DialogButtonsRow.kt b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/components/DialogButtonsRow.kt
deleted file mode 100644
index 5cb3c15..0000000
--- a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/components/DialogButtonsRow.kt
+++ /dev/null
@@ -1,71 +0,0 @@
-/*
- * Copyright 2023 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
- *
- * https://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.credentialmanager.ui.components
-
-import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.padding
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.filled.Check
-import androidx.compose.material.icons.filled.Close
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.vector.ImageVector
-import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.unit.dp
-import androidx.wear.compose.material.ButtonDefaults
-import com.google.android.horologist.compose.material.Button
-import com.google.android.horologist.compose.tools.WearPreview
-import com.android.credentialmanager.R
-import com.google.android.horologist.annotations.ExperimentalHorologistApi
-
-@OptIn(ExperimentalHorologistApi::class)
-@Composable
-fun DialogButtonsRow(
- onCancelClick: () -> Unit,
- onOKClick: () -> Unit,
- modifier: Modifier = Modifier,
- cancelButtonIcon: ImageVector = Icons.Default.Close,
- okButtonIcon: ImageVector = Icons.Default.Check,
- cancelButtonContentDescription: String = stringResource(R.string.dialog_cancel_button_cd),
- okButtonContentDescription: String = stringResource(R.string.dialog_ok_button_cd),
-) {
- Row(
- modifier = modifier,
- horizontalArrangement = Arrangement.Center,
- ) {
- Button(
- imageVector = cancelButtonIcon,
- contentDescription = cancelButtonContentDescription,
- onClick = onCancelClick,
- colors = ButtonDefaults.secondaryButtonColors(),
- )
- Button(
- imageVector = okButtonIcon,
- contentDescription = okButtonContentDescription,
- onClick = onOKClick,
- modifier = Modifier.padding(start = 20.dp)
- )
- }
-}
-
-@WearPreview
-@Composable
-fun DialogButtonsRowPreview() {
- DialogButtonsRow(onCancelClick = {}, onOKClick = {})
-}
-
diff --git a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/components/SignInHeader.kt b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/components/SignInHeader.kt
index 956c56b..1ddf4af 100644
--- a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/components/SignInHeader.kt
+++ b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/components/SignInHeader.kt
@@ -16,45 +16,24 @@
package com.android.credentialmanager.ui.components
-import androidx.annotation.DrawableRes
+import android.graphics.drawable.Drawable
import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
-import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.unit.dp
-import androidx.wear.compose.material.MaterialTheme
+import androidx.core.graphics.drawable.toBitmap
import androidx.wear.compose.material.Text
-import com.android.credentialmanager.R
-import com.google.android.horologist.annotations.ExperimentalHorologistApi
-import com.google.android.horologist.compose.material.Icon
-import com.google.android.horologist.compose.material.util.DECORATIVE_ELEMENT_CONTENT_DESCRIPTION
-import com.google.android.horologist.compose.tools.WearPreview
-
-@OptIn(ExperimentalHorologistApi::class)
-@Composable
-fun SignInHeader(
- @DrawableRes icon: Int,
- title: String,
- modifier: Modifier = Modifier,
-) {
- SignInHeader(
- iconContent = {
- Icon(
- id = icon,
- contentDescription = DECORATIVE_ELEMENT_CONTENT_DESCRIPTION
- )
- },
- title = title,
- modifier = modifier,
- )
-}
+import androidx.compose.material3.Icon
+import androidx.wear.compose.material.MaterialTheme as WearMaterialTheme
+import androidx.compose.ui.text.style.TextAlign
@Composable
fun SignInHeader(
- iconContent: @Composable ColumnScope.() -> Unit,
+ icon: Drawable?,
title: String,
modifier: Modifier = Modifier,
) {
@@ -62,22 +41,22 @@
modifier = modifier,
horizontalAlignment = Alignment.CenterHorizontally
) {
- iconContent()
+ if (icon != null) {
+ Icon(
+ bitmap = icon.toBitmap().asImageBitmap(),
+ modifier = Modifier.size(32.dp),
+ // Decorative purpose only.
+ contentDescription = null
+ )
+ }
+
Text(
text = title,
+ textAlign = TextAlign.Center,
modifier = Modifier
.padding(top = 6.dp)
.padding(horizontal = 10.dp),
- style = MaterialTheme.typography.title3
+ style = WearMaterialTheme.typography.title3
)
}
}
-
-@WearPreview
-@Composable
-fun SignInHeaderPreview() {
- SignInHeader(
- icon = R.drawable.passkey_icon,
- title = stringResource(R.string.use_passkey_title)
- )
-}
diff --git a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/model/PasskeyUiModel.kt b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/model/PasskeyUiModel.kt
deleted file mode 100644
index a368de2..0000000
--- a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/model/PasskeyUiModel.kt
+++ /dev/null
@@ -1,22 +0,0 @@
-/*
- * Copyright (C) 2023 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.0N
- *
- * 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.credentialmanager.ui.model
-
-data class PasskeyUiModel(
- val name: String,
- val email: String,
-)
diff --git a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/model/PasswordUiModel.kt b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/model/PasswordUiModel.kt
deleted file mode 100644
index 52bbfaa..0000000
--- a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/model/PasswordUiModel.kt
+++ /dev/null
@@ -1,19 +0,0 @@
-/*
- * Copyright (C) 2023 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.0N
- *
- * 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.credentialmanager.ui.model
-
-data class PasswordUiModel(val email: String)
diff --git a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/single/SingleAccountScreen.kt b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/single/SingleAccountScreen.kt
index 8532783..e94dd68 100644
--- a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/single/SingleAccountScreen.kt
+++ b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/single/SingleAccountScreen.kt
@@ -19,21 +19,12 @@
package com.android.credentialmanager.ui.screens.single
import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
-import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.unit.dp
import androidx.wear.compose.foundation.lazy.ScalingLazyListScope
-import com.android.credentialmanager.R
-import com.android.credentialmanager.ui.components.AccountRow
-import com.android.credentialmanager.ui.components.DialogButtonsRow
-import com.android.credentialmanager.ui.components.SignInHeader
import com.google.android.horologist.annotations.ExperimentalHorologistApi
import com.google.android.horologist.compose.layout.ScalingLazyColumn
import com.google.android.horologist.compose.layout.ScalingLazyColumnState
-import com.google.android.horologist.compose.layout.belowTimeTextPreview
-import com.google.android.horologist.compose.tools.WearPreview
@Composable
fun SingleAccountScreen(
@@ -51,33 +42,4 @@
item { accountContent() }
content()
}
-}
-
-@WearPreview
-@Composable
-fun SingleAccountScreenPreview() {
- SingleAccountScreen(
- headerContent = {
- SignInHeader(
- icon = R.drawable.passkey_icon,
- title = stringResource(R.string.use_passkey_title),
- )
- },
- accountContent = {
- AccountRow(
- name = "Elisa Beckett",
- email = "beckett_bakery@gmail.com",
- modifier = Modifier.padding(top = 10.dp)
- )
- },
- columnState = belowTimeTextPreview(),
- ) {
- item {
- DialogButtonsRow(
- onCancelClick = {},
- onOKClick = {},
- modifier = Modifier.padding(top = 10.dp)
- )
- }
- }
-}
+}
\ No newline at end of file
diff --git a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/single/UiState.kt b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/single/UiState.kt
new file mode 100644
index 0000000..42a88e2
--- /dev/null
+++ b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/single/UiState.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2024 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.credentialmanager.ui.screens.single
+
+import androidx.activity.result.IntentSenderRequest
+
+sealed class UiState {
+ data object CredentialScreen : UiState()
+
+ data class CredentialSelected(
+ val intentSenderRequest: IntentSenderRequest?
+ ) : UiState()
+
+ data object Cancel : UiState()
+}
\ No newline at end of file
diff --git a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/single/passkey/SinglePasskeyScreen.kt b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/single/passkey/SinglePasskeyScreen.kt
index c9b0230..92d8a39 100644
--- a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/single/passkey/SinglePasskeyScreen.kt
+++ b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/single/passkey/SinglePasskeyScreen.kt
@@ -1,5 +1,5 @@
/*
- * Copyright 2023 The Android Open Source Project
+ * Copyright 2024 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.
@@ -18,66 +18,119 @@
package com.android.credentialmanager.ui.screens.single.passkey
+import android.graphics.drawable.Drawable
+import androidx.compose.foundation.layout.Column
+import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.SideEffect
+import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
+import androidx.hilt.navigation.compose.hiltViewModel
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import androidx.navigation.NavHostController
+import androidx.navigation.compose.rememberNavController
+import com.android.credentialmanager.CredentialSelectorUiState
+import com.android.credentialmanager.model.get.CredentialEntryInfo
import com.android.credentialmanager.R
+import com.android.credentialmanager.activity.StartBalIntentSenderForResultContract
import com.android.credentialmanager.ui.components.AccountRow
-import com.android.credentialmanager.ui.components.DialogButtonsRow
+import com.android.credentialmanager.ui.components.ContinueChip
+import com.android.credentialmanager.ui.components.DismissChip
import com.android.credentialmanager.ui.components.SignInHeader
+import com.android.credentialmanager.ui.components.SignInOptionsChip
import com.android.credentialmanager.ui.screens.single.SingleAccountScreen
+import com.android.credentialmanager.ui.screens.single.UiState
import com.google.android.horologist.annotations.ExperimentalHorologistApi
import com.google.android.horologist.compose.layout.ScalingLazyColumnState
-import com.google.android.horologist.compose.layout.belowTimeTextPreview
-import com.google.android.horologist.compose.tools.WearPreview
+@OptIn(ExperimentalHorologistApi::class)
@Composable
fun SinglePasskeyScreen(
- name: String,
- email: String,
- onCancelClick: () -> Unit,
- onOKClick: () -> Unit,
+ credentialSelectorUiState: CredentialSelectorUiState.Get.SingleEntry,
+ screenIcon: Drawable?,
columnState: ScalingLazyColumnState,
modifier: Modifier = Modifier,
+ viewModel: SinglePasskeyScreenViewModel = hiltViewModel(),
+ navController: NavHostController = rememberNavController(),
+) {
+ viewModel.initialize(credentialSelectorUiState.entry)
+
+ val uiState by viewModel.uiState.collectAsStateWithLifecycle()
+
+ when (val state = uiState) {
+ UiState.CredentialScreen -> {
+ SinglePasskeyScreen(
+ credentialSelectorUiState.entry,
+ screenIcon,
+ columnState,
+ modifier,
+ viewModel
+ )
+ }
+
+ is UiState.CredentialSelected -> {
+ val launcher = rememberLauncherForActivityResult(
+ StartBalIntentSenderForResultContract()
+ ) {
+ viewModel.onPasskeyInfoRetrieved(it.resultCode, null)
+ }
+
+ SideEffect {
+ state.intentSenderRequest?.let {
+ launcher.launch(it)
+ }
+ }
+ }
+
+ UiState.Cancel -> {
+ // TODO(b/322797032) add valid navigation path here for going back
+ navController.popBackStack()
+ }
+ }
+}
+
+@OptIn(ExperimentalHorologistApi::class)
+@Composable
+fun SinglePasskeyScreen(
+ entry: CredentialEntryInfo,
+ screenIcon: Drawable?,
+ columnState: ScalingLazyColumnState,
+ modifier: Modifier = Modifier,
+ viewModel: SinglePasskeyScreenViewModel,
) {
SingleAccountScreen(
headerContent = {
SignInHeader(
- icon = R.drawable.passkey_icon,
+ icon = screenIcon,
title = stringResource(R.string.use_passkey_title),
)
},
accountContent = {
- AccountRow(
- name = name,
- email = email,
- modifier = Modifier.padding(top = 10.dp),
- )
+ if (entry.displayName != null) {
+ AccountRow(
+ primaryText = checkNotNull(entry.displayName),
+ secondaryText = entry.userName,
+ modifier = Modifier.padding(top = 10.dp),
+ )
+ } else {
+ AccountRow(
+ primaryText = entry.userName,
+ modifier = Modifier.padding(top = 10.dp),
+ )
+ }
},
columnState = columnState,
modifier = modifier.padding(horizontal = 10.dp)
) {
item {
- DialogButtonsRow(
- onCancelClick = onCancelClick,
- onOKClick = onOKClick,
- modifier = Modifier.padding(top = 10.dp)
- )
+ Column {
+ ContinueChip(viewModel::onContinueClick)
+ SignInOptionsChip(viewModel::onSignInOptionsClick)
+ DismissChip(viewModel::onDismissClick)
+ }
}
}
}
-
-@WearPreview
-@Composable
-fun SinglePasskeyScreenPreview() {
- SinglePasskeyScreen(
- name = "Elisa Beckett",
- email = "beckett_bakery@gmail.com",
- onCancelClick = {},
- onOKClick = {},
- columnState = belowTimeTextPreview(),
- )
-}
-
diff --git a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/single/passkey/SinglePasskeyScreenViewModel.kt b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/single/passkey/SinglePasskeyScreenViewModel.kt
new file mode 100644
index 0000000..35c39f6
--- /dev/null
+++ b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/single/passkey/SinglePasskeyScreenViewModel.kt
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2024 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.credentialmanager.ui.screens.single.passkey
+
+import android.content.Intent
+import android.credentials.selection.UserSelectionDialogResult
+import android.credentials.selection.ProviderPendingIntentResponse
+import androidx.annotation.MainThread
+import androidx.lifecycle.ViewModel
+import com.android.credentialmanager.ktx.getIntentSenderRequest
+import com.android.credentialmanager.model.Request
+import com.android.credentialmanager.client.CredentialManagerClient
+import com.android.credentialmanager.model.get.CredentialEntryInfo
+import dagger.hilt.android.lifecycle.HiltViewModel
+import com.android.credentialmanager.ui.screens.single.UiState
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import javax.inject.Inject
+
+@HiltViewModel
+class SinglePasskeyScreenViewModel @Inject constructor(
+ private val credentialManagerClient: CredentialManagerClient,
+) : ViewModel() {
+
+ private val _uiState =
+ MutableStateFlow<UiState>(UiState.CredentialScreen)
+ val uiState: StateFlow<UiState> = _uiState
+
+ private lateinit var requestGet: Request.Get
+ private lateinit var entryInfo: CredentialEntryInfo
+
+
+ @MainThread
+ fun initialize(entry: CredentialEntryInfo) {
+ this.entryInfo = entry
+ }
+
+ fun onDismissClick() {
+ _uiState.value = UiState.Cancel
+ }
+
+ fun onContinueClick() {
+ _uiState.value = UiState.CredentialSelected(
+ intentSenderRequest = entryInfo.getIntentSenderRequest()
+ )
+ }
+
+ fun onSignInOptionsClick() {
+ }
+
+ fun onPasskeyInfoRetrieved(
+ resultCode: Int? = null,
+ resultData: Intent? = null,
+ ) {
+ val userSelectionDialogResult = UserSelectionDialogResult(
+ requestGet.token,
+ entryInfo.providerId,
+ entryInfo.entryKey,
+ entryInfo.entrySubkey,
+ if (resultCode != null) ProviderPendingIntentResponse(resultCode, resultData) else null
+ )
+ credentialManagerClient.sendResult(userSelectionDialogResult)
+ }
+}
+
diff --git a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/single/password/SinglePasswordScreen.kt b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/single/password/SinglePasswordScreen.kt
index 9f971ae..a8be944 100644
--- a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/single/password/SinglePasswordScreen.kt
+++ b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/single/password/SinglePasswordScreen.kt
@@ -18,8 +18,9 @@
package com.android.credentialmanager.ui.screens.single.password
-import android.util.Log
+import android.graphics.drawable.Drawable
import androidx.activity.compose.rememberLauncherForActivityResult
+import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.runtime.SideEffect
@@ -29,52 +30,52 @@
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
-import com.android.credentialmanager.CredentialSelectorUiState.Get.SingleEntry
+import androidx.navigation.NavHostController
+import androidx.navigation.compose.rememberNavController
+import com.android.credentialmanager.CredentialSelectorUiState
import com.android.credentialmanager.R
-import com.android.credentialmanager.TAG
import com.android.credentialmanager.activity.StartBalIntentSenderForResultContract
-import com.android.credentialmanager.ui.components.DialogButtonsRow
import com.android.credentialmanager.ui.components.PasswordRow
+import com.android.credentialmanager.ui.components.ContinueChip
+import com.android.credentialmanager.ui.components.DismissChip
import com.android.credentialmanager.ui.components.SignInHeader
-import com.android.credentialmanager.ui.model.PasswordUiModel
+import com.android.credentialmanager.ui.components.SignInOptionsChip
import com.android.credentialmanager.ui.screens.single.SingleAccountScreen
+import com.android.credentialmanager.model.get.CredentialEntryInfo
+import com.android.credentialmanager.ui.screens.single.UiState
import com.google.android.horologist.annotations.ExperimentalHorologistApi
import com.google.android.horologist.compose.layout.ScalingLazyColumnState
-import com.google.android.horologist.compose.layout.belowTimeTextPreview
-import com.google.android.horologist.compose.tools.WearPreview
+@OptIn(ExperimentalHorologistApi::class)
@Composable
fun SinglePasswordScreen(
- state: SingleEntry,
+ credentialSelectorUiState: CredentialSelectorUiState.Get.SingleEntry,
+ screenIcon: Drawable?,
columnState: ScalingLazyColumnState,
- onCloseApp: () -> Unit,
modifier: Modifier = Modifier,
viewModel: SinglePasswordScreenViewModel = hiltViewModel(),
+ navController: NavHostController = rememberNavController(),
) {
- viewModel.initialize(state.entry)
+ viewModel.initialize(credentialSelectorUiState.entry)
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
when (val state = uiState) {
- SinglePasswordScreenUiState.Idle -> {
- // TODO: b/301206470 implement latency version of the screen
- }
-
- is SinglePasswordScreenUiState.Loaded -> {
+ UiState.CredentialScreen -> {
SinglePasswordScreen(
- passwordUiModel = state.passwordUiModel,
- onCancelClick = viewModel::onCancelClick,
- onOKClick = viewModel::onOKClick,
- columnState = columnState,
- modifier = modifier
+ credentialSelectorUiState.entry,
+ screenIcon,
+ columnState,
+ modifier,
+ viewModel
)
}
- is SinglePasswordScreenUiState.PasswordSelected -> {
+ is UiState.CredentialSelected -> {
val launcher = rememberLauncherForActivityResult(
StartBalIntentSenderForResultContract()
) {
- viewModel.onPasswordInfoRetrieved(it.resultCode, it.data)
+ viewModel.onPasswordInfoRetrieved(it.resultCode, null)
}
SideEffect {
@@ -84,39 +85,32 @@
}
}
- SinglePasswordScreenUiState.Cancel -> {
- // TODO: b/301206470 implement navigation for when user taps cancel
- }
-
- SinglePasswordScreenUiState.Error -> {
- // TODO: b/301206470 implement navigation for when there is an error to load screen
- }
-
- SinglePasswordScreenUiState.Completed -> {
- Log.d(TAG, "Received signal to finish the activity.")
- onCloseApp()
+ UiState.Cancel -> {
+ // TODO(b/322797032) add valid navigation path here for going back
+ navController.popBackStack()
}
}
}
+@OptIn(ExperimentalHorologistApi::class)
@Composable
-fun SinglePasswordScreen(
- passwordUiModel: PasswordUiModel,
- onCancelClick: () -> Unit,
- onOKClick: () -> Unit,
+private fun SinglePasswordScreen(
+ entry: CredentialEntryInfo,
+ screenIcon: Drawable?,
columnState: ScalingLazyColumnState,
modifier: Modifier = Modifier,
+ viewModel: SinglePasswordScreenViewModel,
) {
SingleAccountScreen(
headerContent = {
SignInHeader(
- icon = R.drawable.passkey_icon,
+ icon = screenIcon,
title = stringResource(R.string.use_password_title),
)
},
accountContent = {
PasswordRow(
- email = passwordUiModel.email,
+ email = entry.userName,
modifier = Modifier.padding(top = 10.dp),
)
},
@@ -124,23 +118,13 @@
modifier = modifier.padding(horizontal = 10.dp)
) {
item {
- DialogButtonsRow(
- onCancelClick = onCancelClick,
- onOKClick = onOKClick,
- modifier = Modifier.padding(top = 10.dp)
- )
+ Column {
+ ContinueChip(viewModel::onContinueClick)
+ SignInOptionsChip(viewModel::onSignInOptionsClick)
+ DismissChip(viewModel::onDismissClick)
+ }
}
}
}
-@WearPreview
-@Composable
-fun SinglePasswordScreenPreview() {
- SinglePasswordScreen(
- passwordUiModel = PasswordUiModel(email = "beckett_bakery@gmail.com"),
- onCancelClick = {},
- onOKClick = {},
- columnState = belowTimeTextPreview(),
- )
-}
diff --git a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/single/password/SinglePasswordScreenViewModel.kt b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/single/password/SinglePasswordScreenViewModel.kt
index c9c66b4..3f841b8 100644
--- a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/single/password/SinglePasswordScreenViewModel.kt
+++ b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/single/password/SinglePasswordScreenViewModel.kt
@@ -17,16 +17,15 @@
package com.android.credentialmanager.ui.screens.single.password
import android.content.Intent
-import android.credentials.selection.ProviderPendingIntentResponse
import android.credentials.selection.UserSelectionDialogResult
-import androidx.activity.result.IntentSenderRequest
+import android.credentials.selection.ProviderPendingIntentResponse
import androidx.annotation.MainThread
import androidx.lifecycle.ViewModel
import com.android.credentialmanager.ktx.getIntentSenderRequest
import com.android.credentialmanager.model.Request
import com.android.credentialmanager.client.CredentialManagerClient
import com.android.credentialmanager.model.get.CredentialEntryInfo
-import com.android.credentialmanager.ui.model.PasswordUiModel
+import com.android.credentialmanager.ui.screens.single.UiState
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
@@ -37,36 +36,31 @@
private val credentialManagerClient: CredentialManagerClient,
) : ViewModel() {
- private var initializeCalled = false
-
private lateinit var requestGet: Request.Get
private lateinit var entryInfo: CredentialEntryInfo
private val _uiState =
- MutableStateFlow<SinglePasswordScreenUiState>(SinglePasswordScreenUiState.Idle)
- val uiState: StateFlow<SinglePasswordScreenUiState> = _uiState
+ MutableStateFlow<UiState>(UiState.CredentialScreen)
+ val uiState: StateFlow<UiState> = _uiState
@MainThread
fun initialize(entryInfo: CredentialEntryInfo) {
- if (initializeCalled) return
- initializeCalled = true
- _uiState.value = SinglePasswordScreenUiState.Loaded(
- PasswordUiModel(
- email = entryInfo.userName,
- )
- )
+ this.entryInfo = entryInfo
}
- fun onCancelClick() {
- _uiState.value = SinglePasswordScreenUiState.Cancel
+ fun onDismissClick() {
+ _uiState.value = UiState.Cancel
}
- fun onOKClick() {
- _uiState.value = SinglePasswordScreenUiState.PasswordSelected(
+ fun onContinueClick() {
+ _uiState.value = UiState.CredentialSelected(
intentSenderRequest = entryInfo.getIntentSenderRequest()
)
}
+ fun onSignInOptionsClick() {
+ }
+
fun onPasswordInfoRetrieved(
resultCode: Int? = null,
resultData: Intent? = null,
@@ -79,18 +73,5 @@
if (resultCode != null) ProviderPendingIntentResponse(resultCode, resultData) else null
)
credentialManagerClient.sendResult(userSelectionDialogResult)
- _uiState.value = SinglePasswordScreenUiState.Completed
}
}
-
-sealed class SinglePasswordScreenUiState {
- data object Idle : SinglePasswordScreenUiState()
- data class Loaded(val passwordUiModel: PasswordUiModel) : SinglePasswordScreenUiState()
- data class PasswordSelected(
- val intentSenderRequest: IntentSenderRequest?
- ) : SinglePasswordScreenUiState()
-
- data object Cancel : SinglePasswordScreenUiState()
- data object Error : SinglePasswordScreenUiState()
- data object Completed : SinglePasswordScreenUiState()
-}
diff --git a/packages/PackageInstaller/AndroidManifest.xml b/packages/PackageInstaller/AndroidManifest.xml
index 09e0d61..bf69d3b 100644
--- a/packages/PackageInstaller/AndroidManifest.xml
+++ b/packages/PackageInstaller/AndroidManifest.xml
@@ -45,11 +45,9 @@
<activity android:name=".v2.ui.InstallLaunch"
android:configChanges="orientation|keyboardHidden|screenSize"
- android:theme="@style/Theme.AlertDialogActivity"
android:exported="false"/>
<activity android:name=".InstallStart"
- android:theme="@style/Theme.AlertDialogActivity"
android:exported="true"
android:excludeFromRecents="true">
<intent-filter android:priority="1">
@@ -79,14 +77,12 @@
android:exported="false" />
<activity android:name=".DeleteStagedFileOnResult"
- android:theme="@style/Theme.AlertDialogActivity.NoActionBar"
android:exported="false" />
<activity android:name=".PackageInstallerActivity"
android:exported="false" />
<activity android:name=".InstallInstalling"
- android:theme="@style/Theme.AlertDialogActivity.NoAnimation"
android:exported="false" />
<receiver android:name=".common.InstallEventReceiver"
@@ -98,16 +94,13 @@
</receiver>
<activity android:name=".InstallSuccess"
- android:theme="@style/Theme.AlertDialogActivity.NoAnimation"
android:exported="false" />
<activity android:name=".InstallFailed"
- android:theme="@style/Theme.AlertDialogActivity.NoAnimation"
android:exported="false" />
<activity android:name=".UninstallerActivity"
android:configChanges="orientation|keyboardHidden|screenSize"
- android:theme="@style/Theme.AlertDialogActivity.NoActionBar"
android:excludeFromRecents="true"
android:noHistory="true"
android:exported="true">
@@ -121,7 +114,6 @@
<activity android:name=".v2.ui.UninstallLaunch"
android:configChanges="orientation|keyboardHidden|screenSize"
- android:theme="@style/Theme.AlertDialogActivity.NoActionBar"
android:excludeFromRecents="true"
android:noHistory="true"
android:exported="false">
@@ -144,7 +136,6 @@
</receiver>
<activity android:name=".UninstallUninstalling"
- android:theme="@style/Theme.AlertDialogActivity.NoActionBar"
android:excludeFromRecents="true"
android:exported="false" />
@@ -171,7 +162,6 @@
<activity android:name=".UnarchiveActivity"
android:configChanges="orientation|keyboardHidden|screenSize"
- android:theme="@style/Theme.AlertDialogActivity.NoActionBar"
android:excludeFromRecents="true"
android:noHistory="true"
android:exported="true">
@@ -183,7 +173,6 @@
<activity android:name=".UnarchiveErrorActivity"
android:configChanges="orientation|keyboardHidden|screenSize"
- android:theme="@style/Theme.AlertDialogActivity.NoActionBar"
android:excludeFromRecents="true"
android:noHistory="true"
android:exported="true">
diff --git a/packages/PackageInstaller/res/values/strings.xml b/packages/PackageInstaller/res/values/strings.xml
index f425f52..3242935 100644
--- a/packages/PackageInstaller/res/values/strings.xml
+++ b/packages/PackageInstaller/res/values/strings.xml
@@ -288,7 +288,7 @@
cannot happen immediately because the device is offline (has no internet connection.
[CHAR LIMIT=none] -->
<string name="unarchive_error_offline_body">
- This app will automatically restore when you\'re connected to the internet
+ To restore this app, check your internet connection and try again
</string>
<!-- Dialog title shown when the user is trying to restore an app but a generic error happened.
diff --git a/packages/PackageInstaller/res/values/themes.xml b/packages/PackageInstaller/res/values/themes.xml
index 9a06229..f5af510 100644
--- a/packages/PackageInstaller/res/values/themes.xml
+++ b/packages/PackageInstaller/res/values/themes.xml
@@ -17,19 +17,12 @@
<resources>
- <style name="Theme.AlertDialogActivity.NoAnimation"
- parent="@style/Theme.AlertDialogActivity.NoActionBar">
- <item name="android:windowAnimationStyle">@null</item>
- </style>
-
<style name="Theme.AlertDialogActivity"
parent="@android:style/Theme.DeviceDefault.Light.Dialog.Alert">
<item name="alertDialogStyle">@style/AlertDialog</item>
- </style>
-
- <style name="Theme.AlertDialogActivity.NoActionBar">
<item name="android:windowActionBar">false</item>
<item name="android:windowNoTitle">true</item>
+ <item name="android:windowAnimationStyle">@null</item>
</style>
</resources>
diff --git a/packages/SettingsLib/OWNERS b/packages/SettingsLib/OWNERS
index b2b7b61..5f5f1d5 100644
--- a/packages/SettingsLib/OWNERS
+++ b/packages/SettingsLib/OWNERS
@@ -6,7 +6,6 @@
edgarwang@google.com
evanlaird@google.com
juliacr@google.com
-yantingyang@google.com
ykhung@google.com
# Exempt resource files (because they are in a flat directory and too hard to manage via OWNERS)
diff --git a/packages/SettingsLib/SettingsTheme/res/values-v31/styles.xml b/packages/SettingsLib/SettingsTheme/res/values-v31/styles.xml
index f44b161..aed985e 100644
--- a/packages/SettingsLib/SettingsTheme/res/values-v31/styles.xml
+++ b/packages/SettingsLib/SettingsTheme/res/values-v31/styles.xml
@@ -14,10 +14,10 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
-<resources>
+<resources xmlns:androidprv="http://schemas.android.com/apk/prv/res/android">
<style name="TextAppearance.PreferenceTitle.SettingsLib"
parent="@android:style/TextAppearance.Material.Subhead">
- <item name="android:textColor">@color/settingslib_text_color_primary</item>
+ <item name="android:textColor">?androidprv:attr/materialColorOnSurface</item>
<item name="android:fontFamily">@string/settingslib_config_headlineFontFamily</item>
<item name="android:textSize">20sp</item>
</style>
diff --git a/packages/SettingsLib/Spa/build.gradle.kts b/packages/SettingsLib/Spa/build.gradle.kts
index 6f9556f..ec519ca 100644
--- a/packages/SettingsLib/Spa/build.gradle.kts
+++ b/packages/SettingsLib/Spa/build.gradle.kts
@@ -29,7 +29,7 @@
allprojects {
extra["androidTop"] = androidTop
- extra["jetpackComposeVersion"] = "1.6.0-rc01"
+ extra["jetpackComposeVersion"] = "1.7.0-alpha01"
}
subprojects {
diff --git a/packages/SettingsLib/Spa/gradle/libs.versions.toml b/packages/SettingsLib/Spa/gradle/libs.versions.toml
index 1f78a9c..f6fbc02 100644
--- a/packages/SettingsLib/Spa/gradle/libs.versions.toml
+++ b/packages/SettingsLib/Spa/gradle/libs.versions.toml
@@ -15,11 +15,11 @@
#
[versions]
-agp = "8.2.1"
-compose-compiler = "1.5.1"
+agp = "8.2.2"
+compose-compiler = "1.5.8"
dexmaker-mockito = "2.28.3"
jvm = "17"
-kotlin = "1.9.0"
+kotlin = "1.9.22"
truth = "1.1.5"
[libraries]
diff --git a/packages/SettingsLib/Spa/spa/build.gradle.kts b/packages/SettingsLib/Spa/spa/build.gradle.kts
index 618dc37..08a8797 100644
--- a/packages/SettingsLib/Spa/spa/build.gradle.kts
+++ b/packages/SettingsLib/Spa/spa/build.gradle.kts
@@ -57,13 +57,13 @@
api("androidx.slice:slice-builders:1.1.0-alpha02")
api("androidx.slice:slice-core:1.1.0-alpha02")
api("androidx.slice:slice-view:1.1.0-alpha02")
- api("androidx.compose.material3:material3:1.2.0-beta02")
+ api("androidx.compose.material3:material3:1.2.0-rc01")
api("androidx.compose.material:material-icons-extended:$jetpackComposeVersion")
api("androidx.compose.runtime:runtime-livedata:$jetpackComposeVersion")
api("androidx.compose.ui:ui-tooling-preview:$jetpackComposeVersion")
api("androidx.lifecycle:lifecycle-livedata-ktx")
api("androidx.lifecycle:lifecycle-runtime-compose")
- api("androidx.navigation:navigation-compose:2.7.6")
+ api("androidx.navigation:navigation-compose:2.8.0-alpha01")
api("com.github.PhilJay:MPAndroidChart:v3.1.0-alpha")
api("com.google.android.material:material:1.7.0-alpha03")
debugApi("androidx.compose.ui:ui-tooling:$jetpackComposeVersion")
diff --git a/packages/SettingsLib/Spa/spa/res/values/themes.xml b/packages/SettingsLib/Spa/spa/res/values/themes.xml
index 4b5a9bc..b55dd1b 100644
--- a/packages/SettingsLib/Spa/spa/res/values/themes.xml
+++ b/packages/SettingsLib/Spa/spa/res/values/themes.xml
@@ -24,4 +24,8 @@
</style>
<style name="Theme.SpaLib.Dialog" parent="Theme.Material3.DayNight.Dialog"/>
+ <style name="Theme.SpaLib.BottomSheetDialog" parent="Theme.SpaLib">
+ <item name="android:windowBackground">@android:color/transparent</item>
+ <item name="android:windowIsTranslucent">true</item>
+ </style>
</resources>
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/lifecycle/FlowExt.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/lifecycle/FlowExt.kt
new file mode 100644
index 0000000..1d3eb51
--- /dev/null
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/lifecycle/FlowExt.kt
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2024 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.lifecycle
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import kotlinx.coroutines.flow.Flow
+
+@Composable
+fun <T> Flow<T>.collectAsCallbackWithLifecycle(): () -> T? {
+ val value by collectAsStateWithLifecycle(initialValue = null)
+ return { value }
+}
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/editor/SettingsExposedDropdownMenuCheckBox.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/editor/SettingsExposedDropdownMenuCheckBox.kt
index b34c310..e704505 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/editor/SettingsExposedDropdownMenuCheckBox.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/editor/SettingsExposedDropdownMenuCheckBox.kt
@@ -18,7 +18,6 @@
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
@@ -95,56 +94,60 @@
if (options.isNotEmpty()) {
ExposedDropdownMenu(
expanded = expanded,
- modifier = Modifier
- .fillMaxWidth()
- .width(with(LocalDensity.current) { dropDownWidth.toDp() }),
+ modifier = Modifier.width(with(LocalDensity.current) { dropDownWidth.toDp() }),
onDismissRequest = { expanded = false },
) {
options.forEachIndexed { index, option ->
- TextButton(
- modifier = Modifier
- .fillMaxHeight()
- .fillMaxWidth(),
- onClick = {
- if (selectedOptionsState.contains(index)) {
- if (index == allIndex)
- selectedOptionsState.clear()
- else {
- selectedOptionsState.remove(
- index
- )
- if (selectedOptionsState.contains(allIndex))
- selectedOptionsState.remove(
- allIndex
- )
- }
- } else {
- selectedOptionsState.add(
- index
- )
- }
- onSelectedOptionStateChange()
- }) {
- Row(
- modifier = Modifier
- .fillMaxHeight()
- .fillMaxWidth(),
- horizontalArrangement = Arrangement.Start,
- verticalAlignment = Alignment.CenterVertically
- ) {
- Checkbox(
- checked = selectedOptionsState.contains(index),
- onCheckedChange = null,
- )
- Text(text = option)
- }
- }
+ CheckboxItem(
+ selectedOptionsState,
+ index,
+ allIndex,
+ onSelectedOptionStateChange,
+ option,
+ )
}
}
}
}
}
+@Composable
+private fun CheckboxItem(
+ selectedOptionsState: SnapshotStateList<Int>,
+ index: Int,
+ allIndex: Int,
+ onSelectedOptionStateChange: () -> Unit,
+ option: String
+) {
+ TextButton(
+ modifier = Modifier.fillMaxWidth(),
+ onClick = {
+ if (selectedOptionsState.contains(index)) {
+ if (index == allIndex) {
+ selectedOptionsState.clear()
+ } else {
+ selectedOptionsState.remove(index)
+ selectedOptionsState.remove(allIndex)
+ }
+ } else {
+ selectedOptionsState.add(index)
+ }
+ onSelectedOptionStateChange()
+ }) {
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.spacedBy(SettingsDimension.itemPaddingAround),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Checkbox(
+ checked = selectedOptionsState.contains(index),
+ onCheckedChange = null,
+ )
+ Text(text = option)
+ }
+ }
+}
+
@Preview
@Composable
private fun ActionButtonsPreview() {
@@ -158,4 +161,4 @@
enabled = true,
onSelectedOptionStateChange = {})
}
-}
\ No newline at end of file
+}
diff --git a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/lifecycle/FlowExtTest.kt b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/lifecycle/FlowExtTest.kt
new file mode 100644
index 0000000..de915ef
--- /dev/null
+++ b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/lifecycle/FlowExtTest.kt
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2024 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.lifecycle
+
+import androidx.compose.material3.Text
+import androidx.compose.ui.test.hasText
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.settingslib.spa.testutils.waitUntilExists
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.flowOf
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class FlowExtTest {
+ @get:Rule
+ val composeTestRule = createComposeRule()
+
+ @Test
+ fun collectAsCallbackWithLifecycle() {
+ val flow = flowOf(TEXT)
+
+ composeTestRule.setContent {
+ val callback = flow.collectAsCallbackWithLifecycle()
+ Text(callback() ?: "")
+ }
+
+ composeTestRule.waitUntilExists(hasText(TEXT))
+ }
+
+ @Test
+ fun collectAsCallbackWithLifecycle_changed() {
+ val flow = MutableStateFlow(TEXT)
+
+ composeTestRule.setContent {
+ val callback = flow.collectAsCallbackWithLifecycle()
+ Text(callback() ?: "")
+ }
+ flow.value = NEW_TEXT
+
+ composeTestRule.waitUntilExists(hasText(NEW_TEXT))
+ }
+
+ private companion object {
+ const val TEXT = "Text"
+ const val NEW_TEXT = "New Text"
+ }
+}
diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppListRepository.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppListRepository.kt
index 0a98791..988afd7 100644
--- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppListRepository.kt
+++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppListRepository.kt
@@ -17,10 +17,11 @@
package com.android.settingslib.spaprivileged.model.app
import android.content.Context
-import android.content.pm.FeatureFlags
-import android.content.pm.FeatureFlagsImpl
import android.content.Intent
import android.content.pm.ApplicationInfo
+import android.content.pm.FeatureFlags
+import android.content.pm.FeatureFlagsImpl
+import android.content.pm.Flags
import android.content.pm.PackageManager
import android.content.pm.PackageManager.ApplicationInfoFlags
import android.content.pm.ResolveInfo
@@ -85,13 +86,7 @@
loadInstantApps: Boolean,
matchAnyUserForAdmin: Boolean,
): List<ApplicationInfo> = coroutineScope {
- val hiddenSystemModulesDeferred = async {
- packageManager.getInstalledModules(0)
- .filter { it.isHidden }
- .map { it.packageName }
- .filterNotNull()
- .toSet()
- }
+ val hiddenSystemModulesDeferred = async { packageManager.getHiddenSystemModules() }
val hideWhenDisabledPackagesDeferred = async {
context.resources.getStringArray(R.array.config_hideWhenDisabled_packageNames)
}
@@ -205,6 +200,15 @@
private fun isSystemApp(app: ApplicationInfo, homeOrLauncherPackages: Set<String>): Boolean =
app.isSystemApp && !app.isUpdatedSystemApp && app.packageName !in homeOrLauncherPackages
+ private fun PackageManager.getHiddenSystemModules(): Set<String> {
+ val moduleInfos = getInstalledModules(0).filter { it.isHidden }
+ val hiddenApps = moduleInfos.mapNotNull { it.packageName }.toMutableSet()
+ if (Flags.provideInfoOfApkInApex()) {
+ hiddenApps += moduleInfos.flatMap { it.apkInApexPackageNames }
+ }
+ return hiddenApps
+ }
+
companion object {
private fun ApplicationInfo.isInAppList(
showInstantApps: Boolean,
diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppOpsController.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppOpsController.kt
index 9f33fcb..6e9bde4 100644
--- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppOpsController.kt
+++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppOpsController.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2022 The Android Open Source Project
+ * Copyright (C) 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -16,7 +16,7 @@
package com.android.settingslib.spaprivileged.model.app
-import android.app.AppOpsManager;
+import android.app.AppOpsManager
import android.app.AppOpsManager.MODE_ALLOWED
import android.app.AppOpsManager.MODE_ERRORED
import android.app.AppOpsManager.Mode
@@ -24,14 +24,13 @@
import android.content.pm.ApplicationInfo
import android.content.pm.PackageManager
import android.os.UserHandle
-import androidx.lifecycle.LiveData
-import androidx.lifecycle.MutableLiveData
-import androidx.lifecycle.map
import com.android.settingslib.spaprivileged.framework.common.appOpsManager
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.map
interface IAppOpsController {
- val mode: LiveData<Int>
- val isAllowed: LiveData<Boolean>
+ val mode: Flow<Int>
+ val isAllowed: Flow<Boolean>
get() = mode.map { it == MODE_ALLOWED }
fun setAllowed(allowed: Boolean)
@@ -48,9 +47,7 @@
) : IAppOpsController {
private val appOpsManager = context.appOpsManager
private val packageManager = context.packageManager
-
- override val mode: LiveData<Int>
- get() = _mode
+ override val mode = appOpsManager.opModeFlow(op, app)
override fun setAllowed(allowed: Boolean) {
val mode = if (allowed) MODE_ALLOWED else modeForNotAllowed
@@ -68,15 +65,7 @@
PackageManager.FLAG_PERMISSION_USER_SET,
UserHandle.getUserHandleForUid(app.uid))
}
- _mode.postValue(mode)
}
- @Mode override fun getMode(): Int = appOpsManager.checkOpNoThrow(op, app.uid, app.packageName)
-
- private val _mode =
- object : MutableLiveData<Int>() {
- override fun onActive() {
- postValue(getMode())
- }
- }
+ @Mode override fun getMode(): Int = appOpsManager.getOpMode(op, app)
}
diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppOpsRepository.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppOpsRepository.kt
new file mode 100644
index 0000000..0b5604e
--- /dev/null
+++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppOpsRepository.kt
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.spaprivileged.model.app
+
+import android.app.AppOpsManager
+import android.content.pm.ApplicationInfo
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.callbackFlow
+import kotlinx.coroutines.flow.conflate
+import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.flow.map
+
+fun AppOpsManager.getOpMode(op: Int, app: ApplicationInfo) =
+ checkOpNoThrow(op, app.uid, app.packageName)
+
+fun AppOpsManager.opModeFlow(op: Int, app: ApplicationInfo) =
+ opChangedFlow(op, app).map { getOpMode(op, app) }.flowOn(Dispatchers.Default)
+
+private fun AppOpsManager.opChangedFlow(op: Int, app: ApplicationInfo) = callbackFlow {
+ val listener = object : AppOpsManager.OnOpChangedListener {
+ override fun onOpChanged(op: String, packageName: String) {}
+
+ override fun onOpChanged(op: String, packageName: String, userId: Int) {
+ if (userId == app.userId) trySend(Unit)
+ }
+ }
+ startWatchingMode(op, app.packageName, listener)
+ trySend(Unit)
+
+ awaitClose { stopWatchingMode(listener) }
+}.conflate().flowOn(Dispatchers.Default)
diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppOpPermissionAppList.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppOpPermissionAppList.kt
index 25c3bc5..5db5eae 100644
--- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppOpPermissionAppList.kt
+++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppOpPermissionAppList.kt
@@ -20,7 +20,7 @@
import android.content.Context
import android.content.pm.ApplicationInfo
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.livedata.observeAsState
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.android.settingslib.spa.framework.util.asyncMapItem
import com.android.settingslib.spa.framework.util.filterItem
import com.android.settingslib.spaprivileged.model.app.AppOpsController
@@ -166,7 +166,7 @@
return { true }
}
- val mode = appOpsController.mode.observeAsState()
+ val mode = appOpsController.mode.collectAsStateWithLifecycle(initialValue = null)
return {
when (mode.value) {
null -> null
diff --git a/packages/SettingsLib/SpaPrivileged/tests/Android.bp b/packages/SettingsLib/SpaPrivileged/tests/Android.bp
index a28ebc6..458fcc9 100644
--- a/packages/SettingsLib/SpaPrivileged/tests/Android.bp
+++ b/packages/SettingsLib/SpaPrivileged/tests/Android.bp
@@ -35,5 +35,6 @@
"androidx.test.ext.junit",
"androidx.test.runner",
"mockito-target-minus-junit4",
+ "flag-junit",
],
}
diff --git a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/AppListRepositoryTest.kt b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/AppListRepositoryTest.kt
index f292231..efd53a4 100644
--- a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/AppListRepositoryTest.kt
+++ b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/AppListRepositoryTest.kt
@@ -21,6 +21,7 @@
import android.content.pm.ApplicationInfo
import android.content.pm.FakeFeatureFlagsImpl
import android.content.pm.Flags
+import android.content.pm.ModuleInfo
import android.content.pm.PackageManager
import android.content.pm.PackageManager.ApplicationInfoFlags
import android.content.pm.PackageManager.ResolveInfoFlags
@@ -28,6 +29,7 @@
import android.content.pm.UserInfo
import android.content.res.Resources
import android.os.UserManager
+import android.platform.test.flag.junit.SetFlagsRule
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.android.internal.R
@@ -35,6 +37,7 @@
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.runTest
+import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.kotlin.any
@@ -50,6 +53,9 @@
@RunWith(AndroidJUnit4::class)
class AppListRepositoryTest {
+ @get:Rule
+ val mSetFlagsRule = SetFlagsRule()
+
private val resources = mock<Resources> {
on { getStringArray(R.array.config_hideWhenDisabled_packageNames) } doReturn emptyArray()
}
@@ -273,6 +279,38 @@
}
@Test
+ fun loadApps_hasApkInApexInfo_shouldNotIncludeAllHiddenApps() = runTest {
+ mSetFlagsRule.enableFlags(Flags.FLAG_PROVIDE_INFO_OF_APK_IN_APEX)
+ packageManager.stub {
+ on { getInstalledModules(any()) } doReturn listOf(HIDDEN_MODULE)
+ }
+ mockInstalledApplications(
+ listOf(NORMAL_APP, HIDDEN_APEX_APP, HIDDEN_MODULE_APP),
+ ADMIN_USER_ID
+ )
+
+ val appList = repository.loadApps(userId = ADMIN_USER_ID)
+
+ assertThat(appList).containsExactly(NORMAL_APP)
+ }
+
+ @Test
+ fun loadApps_noApkInApexInfo_shouldNotIncludeHiddenSystemModule() = runTest {
+ mSetFlagsRule.disableFlags(Flags.FLAG_PROVIDE_INFO_OF_APK_IN_APEX)
+ packageManager.stub {
+ on { getInstalledModules(any()) } doReturn listOf(HIDDEN_MODULE)
+ }
+ mockInstalledApplications(
+ listOf(NORMAL_APP, HIDDEN_APEX_APP, HIDDEN_MODULE_APP),
+ ADMIN_USER_ID
+ )
+
+ val appList = repository.loadApps(userId = ADMIN_USER_ID)
+
+ assertThat(appList).containsExactly(NORMAL_APP, HIDDEN_APEX_APP)
+ }
+
+ @Test
fun showSystemPredicate_showSystem() = runTest {
val app = SYSTEM_APP
@@ -402,6 +440,20 @@
isArchived = true
}
+ val HIDDEN_APEX_APP = ApplicationInfo().apply {
+ packageName = "hidden.apex.package"
+ }
+
+ val HIDDEN_MODULE_APP = ApplicationInfo().apply {
+ packageName = "hidden.module.package"
+ }
+
+ val HIDDEN_MODULE = ModuleInfo().apply {
+ packageName = "hidden.module.package"
+ apkInApexPackageNames = listOf("hidden.apex.package")
+ isHidden = true
+ }
+
fun resolveInfoOf(packageName: String) = ResolveInfo().apply {
activityInfo = ActivityInfo().apply {
this.packageName = packageName
diff --git a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/AppOpsRepositoryTest.kt b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/AppOpsRepositoryTest.kt
new file mode 100644
index 0000000..97c7441
--- /dev/null
+++ b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/AppOpsRepositoryTest.kt
@@ -0,0 +1,101 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.spaprivileged.model.app
+
+import android.app.AppOpsManager
+import android.content.Context
+import android.content.pm.ApplicationInfo
+import android.os.UserHandle
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.settingslib.spa.testutils.toListWithTimeout
+import com.android.settingslib.spaprivileged.framework.common.appOpsManager
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.async
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.runBlocking
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.any
+import org.mockito.kotlin.doAnswer
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.eq
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.spy
+import org.mockito.kotlin.stub
+
+@RunWith(AndroidJUnit4::class)
+class AppOpsRepositoryTest {
+
+ private var listener: AppOpsManager.OnOpChangedListener? = null
+
+ private val mockAppOpsManager = mock<AppOpsManager> {
+ on {
+ checkOpNoThrow(AppOpsManager.OP_MANAGE_MEDIA, UID, PACKAGE_NAME)
+ } doReturn AppOpsManager.MODE_ERRORED
+
+ on {
+ startWatchingMode(eq(AppOpsManager.OP_MANAGE_MEDIA), eq(PACKAGE_NAME), any())
+ } doAnswer { listener = it.arguments[2] as AppOpsManager.OnOpChangedListener }
+ }
+
+ private val context: Context = spy(ApplicationProvider.getApplicationContext()) {
+ on { appOpsManager } doReturn mockAppOpsManager
+ }
+
+ @Test
+ fun getOpMode() {
+ val mode = context.appOpsManager.getOpMode(AppOpsManager.OP_MANAGE_MEDIA, APP)
+
+ assertThat(mode).isEqualTo(AppOpsManager.MODE_ERRORED)
+ }
+
+ @Test
+ fun opModeFlow() = runBlocking {
+ val flow = context.appOpsManager.opModeFlow(AppOpsManager.OP_MANAGE_MEDIA, APP)
+
+ val mode = flow.first()
+
+ assertThat(mode).isEqualTo(AppOpsManager.MODE_ERRORED)
+ }
+
+ @Test
+ fun opModeFlow_changed() = runBlocking {
+ val listDeferred = async {
+ context.appOpsManager.opModeFlow(AppOpsManager.OP_MANAGE_MEDIA, APP).toListWithTimeout()
+ }
+ delay(100)
+
+ mockAppOpsManager.stub {
+ on { checkOpNoThrow(AppOpsManager.OP_MANAGE_MEDIA, UID, PACKAGE_NAME) } doReturn
+ AppOpsManager.MODE_IGNORED
+ }
+ listener?.onOpChanged("", "", UserHandle.getUserId(UID))
+
+ assertThat(listDeferred.await()).contains(AppOpsManager.MODE_IGNORED)
+ }
+
+ private companion object {
+ const val UID = 110000
+ const val PACKAGE_NAME = "package.name"
+ val APP = ApplicationInfo().apply {
+ packageName = PACKAGE_NAME
+ uid = UID
+ }
+ }
+}
diff --git a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/app/AppOpPermissionAppListTest.kt b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/app/AppOpPermissionAppListTest.kt
index d158a24..bb25cf3 100644
--- a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/app/AppOpPermissionAppListTest.kt
+++ b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/app/AppOpPermissionAppListTest.kt
@@ -21,7 +21,6 @@
import android.content.pm.ApplicationInfo
import android.content.pm.PackageManager
import androidx.compose.ui.test.junit4.createComposeRule
-import androidx.lifecycle.MutableLiveData
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.android.settingslib.spa.testutils.firstWithTimeoutOrNull
@@ -330,7 +329,7 @@
private class FakeAppOpsController(private val fakeMode: Int) : IAppOpsController {
var setAllowedCalledWith: Boolean? = null
- override val mode = MutableLiveData(fakeMode)
+ override val mode = flowOf(fakeMode)
override fun setAllowed(allowed: Boolean) {
setAllowedCalledWith = allowed
diff --git a/packages/SettingsLib/aconfig/settingslib.aconfig b/packages/SettingsLib/aconfig/settingslib.aconfig
index fd2f9bd..bab6781 100644
--- a/packages/SettingsLib/aconfig/settingslib.aconfig
+++ b/packages/SettingsLib/aconfig/settingslib.aconfig
@@ -7,3 +7,13 @@
bug: "314812750"
}
+flag {
+ name: "enable_cached_bluetooth_device_dedup"
+ namespace: "bluetooth"
+ description: "Enable dedup in CachedBluetoothDevice"
+ bug: "319197962"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
+
diff --git a/packages/SettingsLib/res/layout/edit_user_info_dialog_content.xml b/packages/SettingsLib/res/layout/edit_user_info_dialog_content.xml
index 2ded3c6..89d6ac3 100644
--- a/packages/SettingsLib/res/layout/edit_user_info_dialog_content.xml
+++ b/packages/SettingsLib/res/layout/edit_user_info_dialog_content.xml
@@ -38,6 +38,17 @@
android:src="@drawable/add_a_photo_circled"
android:layout_gravity="bottom|right"/>
</FrameLayout>
+ <TextView
+ android:id="@+id/edit_user_info_message"
+ android:gravity="center"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="6dp"
+ android:layout_marginEnd="6dp"
+ android:layout_marginTop="24dp"
+ android:textAppearance="@style/android:TextAppearance.Material.Body1"
+ android:text="@string/edit_user_info_message"
+ />
<EditText
android:id="@+id/user_name"
@@ -46,6 +57,8 @@
android:layout_gravity="center"
android:minWidth="200dp"
android:layout_marginStart="6dp"
+ android:layout_marginEnd="6dp"
+ android:layout_marginTop="24dp"
android:minHeight="@dimen/min_tap_target_size"
android:ellipsize="end"
android:singleLine="true"
diff --git a/packages/SettingsLib/res/values/strings.xml b/packages/SettingsLib/res/values/strings.xml
index 2e64212..1092a16 100644
--- a/packages/SettingsLib/res/values/strings.xml
+++ b/packages/SettingsLib/res/values/strings.xml
@@ -1484,6 +1484,9 @@
<!-- Title for the preference to enter the nickname of the user to display in the user switcher [CHAR LIMIT=25]-->
<string name="user_nickname">Nickname</string>
+ <!-- Confirmation message on dialog for editing user name and profile picture. Inform user on who will be able to see the changes [CHAR LIMIT=NONE]-->
+ <string name="edit_user_info_message">Your name and picture will be visible to anyone that uses this device.</string>
+
<!-- Label for adding a new user in the user switcher [CHAR LIMIT=35] -->
<string name="user_add_user">Add user</string>
<!-- Label for adding a new guest in the user switcher [CHAR LIMIT=35] -->
diff --git a/packages/SettingsLib/src/com/android/settingslib/Utils.java b/packages/SettingsLib/src/com/android/settingslib/Utils.java
index fb14a17..58e0a89 100644
--- a/packages/SettingsLib/src/com/android/settingslib/Utils.java
+++ b/packages/SettingsLib/src/com/android/settingslib/Utils.java
@@ -41,6 +41,7 @@
import android.os.UserManager;
import android.print.PrintManager;
import android.provider.Settings;
+import android.provider.Settings.Secure;
import android.telephony.AccessNetworkConstants;
import android.telephony.NetworkRegistrationInfo;
import android.telephony.ServiceState;
@@ -66,18 +67,29 @@
import com.android.settingslib.utils.BuildCompatUtils;
import java.text.NumberFormat;
+import java.time.Duration;
import java.util.List;
public class Utils {
private static final String TAG = "Utils";
- @VisibleForTesting
- static final String STORAGE_MANAGER_ENABLED_PROPERTY =
- "ro.storage_manager.enabled";
-
public static final String INCOMPATIBLE_CHARGER_WARNING_DISABLED =
"incompatible_charger_warning_disabled";
+ public static final String WIRELESS_CHARGING_NOTIFICATION_TIMESTAMP =
+ "wireless_charging_notification_timestamp";
+
+ @VisibleForTesting
+ static final String STORAGE_MANAGER_ENABLED_PROPERTY = "ro.storage_manager.enabled";
+
+ @VisibleForTesting static final long WIRELESS_CHARGING_DEFAULT_TIMESTAMP = -1L;
+
+ @VisibleForTesting
+ static final long WIRELESS_CHARGING_NOTIFICATION_THRESHOLD_MILLIS =
+ Duration.ofDays(30).toMillis();
+
+ @VisibleForTesting
+ static final String WIRELESS_CHARGING_WARNING_ENABLED = "wireless_charging_warning_enabled";
private static Signature[] sSystemSignature;
private static String sPermissionControllerPackageName;
@@ -101,19 +113,19 @@
R.drawable.ic_show_x_wifi_signal_4
};
- public static void updateLocationEnabled(Context context, boolean enabled, int userId,
- int source) {
+ /** Update the location enable state. */
+ public static void updateLocationEnabled(
+ @NonNull Context context, boolean enabled, int userId, int source) {
Settings.Secure.putIntForUser(
- context.getContentResolver(), Settings.Secure.LOCATION_CHANGER, source,
- userId);
+ context.getContentResolver(), Settings.Secure.LOCATION_CHANGER, source, userId);
LocationManager locationManager = context.getSystemService(LocationManager.class);
locationManager.setLocationEnabledForUser(enabled, UserHandle.of(userId));
}
/**
- * Return string resource that best describes combination of tethering
- * options available on this device.
+ * Return string resource that best describes combination of tethering options available on this
+ * device.
*/
public static int getTetheringLabel(TetheringManager tm) {
String[] usbRegexs = tm.getTetherableUsbRegexs();
@@ -141,14 +153,12 @@
}
}
- /**
- * Returns a label for the user, in the form of "User: user name" or "Work profile".
- */
+ /** Returns a label for the user, in the form of "User: user name" or "Work profile". */
public static String getUserLabel(Context context, UserInfo info) {
String name = info != null ? info.name : null;
if (info.isManagedProfile()) {
// We use predefined values for managed profiles
- return BuildCompatUtils.isAtLeastT()
+ return BuildCompatUtils.isAtLeastT()
? getUpdatableManagedUserTitle(context)
: context.getString(R.string.managed_user_title);
} else if (info.isGuest()) {
@@ -164,14 +174,14 @@
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
private static String getUpdatableManagedUserTitle(Context context) {
- return context.getSystemService(DevicePolicyManager.class).getResources().getString(
- WORK_PROFILE_USER_LABEL,
- () -> context.getString(R.string.managed_user_title));
+ return context.getSystemService(DevicePolicyManager.class)
+ .getResources()
+ .getString(
+ WORK_PROFILE_USER_LABEL,
+ () -> context.getString(R.string.managed_user_title));
}
- /**
- * Returns a circular icon for a user.
- */
+ /** Returns a circular icon for a user. */
public static Drawable getUserIcon(Context context, UserManager um, UserInfo user) {
final int iconSize = UserIconDrawable.getDefaultSize(context);
if (user.isManagedProfile()) {
@@ -185,12 +195,14 @@
return new UserIconDrawable(iconSize).setIcon(icon).bake();
}
}
- return new UserIconDrawable(iconSize).setIconDrawable(
- UserIcons.getDefaultUserIcon(context.getResources(), user.id, /* light= */ false))
+ return new UserIconDrawable(iconSize)
+ .setIconDrawable(
+ UserIcons.getDefaultUserIcon(
+ context.getResources(), user.id, /* light= */ false))
.bake();
}
- /** Formats a double from 0.0..100.0 with an option to round **/
+ /** Formats a double from 0.0..100.0 with an option to round */
public static String formatPercentage(double percentage, boolean round) {
final int localPercentage = round ? Math.round((float) percentage) : (int) percentage;
return formatPercentage(localPercentage);
@@ -222,23 +234,27 @@
*
* @param context the context
* @param batteryChangedIntent battery broadcast intent received from {@link
- * Intent.ACTION_BATTERY_CHANGED}.
+ * Intent.ACTION_BATTERY_CHANGED}.
* @param compactStatus to present compact battery charging string if {@code true}
* @return battery status string
*/
- public static String getBatteryStatus(Context context, Intent batteryChangedIntent,
- boolean compactStatus) {
- final int status = batteryChangedIntent.getIntExtra(BatteryManager.EXTRA_STATUS,
- BatteryManager.BATTERY_STATUS_UNKNOWN);
+ @NonNull
+ public static String getBatteryStatus(
+ @NonNull Context context, @NonNull Intent batteryChangedIntent, boolean compactStatus) {
+ final int status =
+ batteryChangedIntent.getIntExtra(
+ BatteryManager.EXTRA_STATUS, BatteryManager.BATTERY_STATUS_UNKNOWN);
final Resources res = context.getResources();
String statusString = res.getString(R.string.battery_info_status_unknown);
final BatteryStatus batteryStatus = new BatteryStatus(batteryChangedIntent);
if (batteryStatus.isCharged()) {
- statusString = res.getString(compactStatus
- ? R.string.battery_info_status_full_charged
- : R.string.battery_info_status_full);
+ statusString =
+ res.getString(
+ compactStatus
+ ? R.string.battery_info_status_full_charged
+ : R.string.battery_info_status_full);
} else {
if (status == BatteryManager.BATTERY_STATUS_CHARGING) {
if (compactStatus) {
@@ -246,12 +262,12 @@
} else if (batteryStatus.isPluggedInWired()) {
switch (batteryStatus.getChargingSpeed(context)) {
case BatteryStatus.CHARGING_FAST:
- statusString = res.getString(
- R.string.battery_info_status_charging_fast);
+ statusString =
+ res.getString(R.string.battery_info_status_charging_fast);
break;
case BatteryStatus.CHARGING_SLOWLY:
- statusString = res.getString(
- R.string.battery_info_status_charging_slow);
+ statusString =
+ res.getString(R.string.battery_info_status_charging_slow);
break;
default:
statusString = res.getString(R.string.battery_info_status_charging);
@@ -311,7 +327,7 @@
@ColorInt
public static int applyAlphaAttr(Context context, int attr, int inputColor) {
- TypedArray ta = context.obtainStyledAttributes(new int[]{attr});
+ TypedArray ta = context.obtainStyledAttributes(new int[] {attr});
float alpha = ta.getFloat(0, 0);
ta.recycle();
return applyAlpha(alpha, inputColor);
@@ -320,7 +336,10 @@
@ColorInt
public static int applyAlpha(float alpha, int inputColor) {
alpha *= Color.alpha(inputColor);
- return Color.argb((int) (alpha), Color.red(inputColor), Color.green(inputColor),
+ return Color.argb(
+ (int) (alpha),
+ Color.red(inputColor),
+ Color.green(inputColor),
Color.blue(inputColor));
}
@@ -329,19 +348,17 @@
return getColorAttrDefaultColor(context, attr, 0);
}
- /**
- * Get color styled attribute {@code attr}, default to {@code defValue} if not found.
- */
+ /** Get color styled attribute {@code attr}, default to {@code defValue} if not found. */
@ColorInt
public static int getColorAttrDefaultColor(Context context, int attr, @ColorInt int defValue) {
- TypedArray ta = context.obtainStyledAttributes(new int[]{attr});
+ TypedArray ta = context.obtainStyledAttributes(new int[] {attr});
@ColorInt int colorAccent = ta.getColor(0, defValue);
ta.recycle();
return colorAccent;
}
public static ColorStateList getColorAttr(Context context, int attr) {
- TypedArray ta = context.obtainStyledAttributes(new int[]{attr});
+ TypedArray ta = context.obtainStyledAttributes(new int[] {attr});
ColorStateList stateList = null;
try {
stateList = ta.getColorStateList(0);
@@ -356,35 +373,38 @@
}
public static int getThemeAttr(Context context, int attr, int defaultValue) {
- TypedArray ta = context.obtainStyledAttributes(new int[]{attr});
+ TypedArray ta = context.obtainStyledAttributes(new int[] {attr});
int theme = ta.getResourceId(0, defaultValue);
ta.recycle();
return theme;
}
public static Drawable getDrawable(Context context, int attr) {
- TypedArray ta = context.obtainStyledAttributes(new int[]{attr});
+ TypedArray ta = context.obtainStyledAttributes(new int[] {attr});
Drawable drawable = ta.getDrawable(0);
ta.recycle();
return drawable;
}
/**
- * Create a color matrix suitable for a ColorMatrixColorFilter that modifies only the color but
- * preserves the alpha for a given drawable
- * @param color
- * @return a color matrix that uses the source alpha and given color
- */
+ * Create a color matrix suitable for a ColorMatrixColorFilter that modifies only the color but
+ * preserves the alpha for a given drawable
+ *
+ * @return a color matrix that uses the source alpha and given color
+ */
public static ColorMatrix getAlphaInvariantColorMatrixForColor(@ColorInt int color) {
int r = Color.red(color);
int g = Color.green(color);
int b = Color.blue(color);
- ColorMatrix cm = new ColorMatrix(new float[] {
- 0, 0, 0, 0, r,
- 0, 0, 0, 0, g,
- 0, 0, 0, 0, b,
- 0, 0, 0, 1, 0 });
+ ColorMatrix cm =
+ new ColorMatrix(
+ new float[] {
+ 0, 0, 0, 0, r,
+ 0, 0, 0, 0, g,
+ 0, 0, 0, 0, b,
+ 0, 0, 0, 1, 0
+ });
return cm;
}
@@ -393,7 +413,7 @@
* Create a ColorMatrixColorFilter to tint a drawable but retain its alpha characteristics
*
* @return a ColorMatrixColorFilter which changes the color of the output but is invariant on
- * the source alpha
+ * the source alpha
*/
public static ColorFilter getAlphaInvariantColorFilterForColor(@ColorInt int color) {
return new ColorMatrixColorFilter(getAlphaInvariantColorMatrixForColor(color));
@@ -402,16 +422,17 @@
/**
* Determine whether a package is a "system package", in which case certain things (like
* disabling notifications or disabling the package altogether) should be disallowed.
- * <p>
- * Note: This function is just for UI treatment, and should not be used for security purposes.
*
- * @deprecated Use {@link ApplicationInfo#isSignedWithPlatformKey()} and
- * {@link #isEssentialPackage} instead.
+ * <p>Note: This function is just for UI treatment, and should not be used for security
+ * purposes.
+ *
+ * @deprecated Use {@link ApplicationInfo#isSignedWithPlatformKey()} and {@link
+ * #isEssentialPackage} instead.
*/
@Deprecated
public static boolean isSystemPackage(Resources resources, PackageManager pm, PackageInfo pkg) {
if (sSystemSignature == null) {
- sSystemSignature = new Signature[]{getSystemSignature(pm)};
+ sSystemSignature = new Signature[] {getSystemSignature(pm)};
}
return (sSystemSignature[0] != null && sSystemSignature[0].equals(getFirstSignature(pkg)))
|| isEssentialPackage(resources, pm, pkg.packageName);
@@ -435,8 +456,8 @@
/**
* Determine whether a package is a "essential package".
- * <p>
- * In which case certain things (like disabling the package) should be disallowed.
+ *
+ * <p>In which case certain things (like disabling the package) should be disallowed.
*/
public static boolean isEssentialPackage(
Resources resources, PackageManager pm, String packageName) {
@@ -462,14 +483,12 @@
* returns {@code false}.
*/
public static boolean isDeviceProvisioningPackage(Resources resources, String packageName) {
- String deviceProvisioningPackage = resources.getString(
- com.android.internal.R.string.config_deviceProvisioningPackage);
+ String deviceProvisioningPackage =
+ resources.getString(com.android.internal.R.string.config_deviceProvisioningPackage);
return deviceProvisioningPackage != null && deviceProvisioningPackage.equals(packageName);
}
- /**
- * Fetch the package name of the default WebView provider.
- */
+ /** Fetch the package name of the default WebView provider. */
@Nullable
private static String getDefaultWebViewPackageName() {
if (sDefaultWebViewPackageName != null) {
@@ -503,8 +522,8 @@
/**
* Returns the Wifi icon resource for a given RSSI level.
*
- * @param showX True if a connected Wi-Fi network has the problem which should show Pie+x
- * signal icon to users.
+ * @param showX True if a connected Wi-Fi network has the problem which should show Pie+x signal
+ * icon to users.
* @param level The number of bars to show (0-4)
* @throws IllegalArgumentException if an invalid RSSI level is given.
*/
@@ -520,10 +539,7 @@
try {
defaultDays =
resources.getInteger(
- com.android
- .internal
- .R
- .integer
+ com.android.internal.R.integer
.config_storageManagerDaystoRetainDefault);
} catch (Resources.NotFoundException e) {
// We are likely in a test environment.
@@ -535,7 +551,7 @@
return !context.getSystemService(TelephonyManager.class).isDataCapable();
}
- /** Returns if the automatic storage management feature is turned on or not. **/
+ /** Returns if the automatic storage management feature is turned on or not. */
public static boolean isStorageManagerEnabled(Context context) {
boolean isDefaultOn;
try {
@@ -543,15 +559,14 @@
} catch (Resources.NotFoundException e) {
isDefaultOn = false;
}
- return Settings.Secure.getInt(context.getContentResolver(),
- Settings.Secure.AUTOMATIC_STORAGE_MANAGER_ENABLED,
- isDefaultOn ? 1 : 0)
+ return Settings.Secure.getInt(
+ context.getContentResolver(),
+ Settings.Secure.AUTOMATIC_STORAGE_MANAGER_ENABLED,
+ isDefaultOn ? 1 : 0)
!= 0;
}
- /**
- * get that {@link AudioManager#getMode()} is in ringing/call/communication(VoIP) status.
- */
+ /** get that {@link AudioManager#getMode()} is in ringing/call/communication(VoIP) status. */
public static boolean isAudioModeOngoingCall(Context context) {
final AudioManager audioManager = context.getSystemService(AudioManager.class);
final int audioMode = audioManager.getMode();
@@ -561,8 +576,8 @@
}
/**
- * Return the service state is in-service or not.
- * To make behavior consistent with SystemUI and Settings/AboutPhone/SIM status UI
+ * Return the service state is in-service or not. To make behavior consistent with SystemUI and
+ * Settings/AboutPhone/SIM status UI
*
* @param serviceState Service state. {@link ServiceState}
*/
@@ -581,13 +596,12 @@
}
/**
- * Return the combined service state.
- * To make behavior consistent with SystemUI and Settings/AboutPhone/SIM status UI.
+ * Return the combined service state. To make behavior consistent with SystemUI and
+ * Settings/AboutPhone/SIM status UI.
*
- * This method returns a single service state int if either the voice reg state is
- * {@link ServiceState#STATE_IN_SERVICE} or if data network is registered via a
- * WWAN transport type. We consider the combined service state of an IWLAN network
- * to be OOS.
+ * <p>This method returns a single service state int if either the voice reg state is {@link
+ * ServiceState#STATE_IN_SERVICE} or if data network is registered via a WWAN transport type. We
+ * consider the combined service state of an IWLAN network to be OOS.
*
* @param serviceState Service state. {@link ServiceState}
*/
@@ -618,9 +632,10 @@
// on either a WLAN or WWAN network. Since we want to exclude the WLAN network, we can
// query the WWAN network directly and check for its registration state
private static boolean isDataRegInWwanAndInService(ServiceState serviceState) {
- final NetworkRegistrationInfo networkRegWwan = serviceState.getNetworkRegistrationInfo(
- NetworkRegistrationInfo.DOMAIN_PS,
- AccessNetworkConstants.TRANSPORT_TYPE_WWAN);
+ final NetworkRegistrationInfo networkRegWwan =
+ serviceState.getNetworkRegistrationInfo(
+ NetworkRegistrationInfo.DOMAIN_PS,
+ AccessNetworkConstants.TRANSPORT_TYPE_WWAN);
if (networkRegWwan == null) {
return false;
@@ -633,8 +648,8 @@
public static Drawable getBadgedIcon(Context context, Drawable icon, UserHandle user) {
int userType = UserIconInfo.TYPE_MAIN;
try {
- UserInfo ui = context.getSystemService(UserManager.class).getUserInfo(
- user.getIdentifier());
+ UserInfo ui =
+ context.getSystemService(UserManager.class).getUserInfo(user.getIdentifier());
if (ui != null) {
if (ui.isCloneProfile()) {
userType = UserIconInfo.TYPE_CLONED;
@@ -650,15 +665,16 @@
try (IconFactory iconFactory = IconFactory.obtain(context)) {
return iconFactory
.createBadgedIconBitmap(
- icon,
- new IconOptions().setUser(new UserIconInfo(user, userType)))
+ icon, new IconOptions().setUser(new UserIconInfo(user, userType)))
.newIcon(context);
}
}
/** Get the {@link Drawable} that represents the app icon */
public static Drawable getBadgedIcon(Context context, ApplicationInfo appInfo) {
- return getBadgedIcon(context, appInfo.loadUnbadgedIcon(context.getPackageManager()),
+ return getBadgedIcon(
+ context,
+ appInfo.loadUnbadgedIcon(context.getPackageManager()),
UserHandle.getUserHandleForUid(appInfo.uid));
}
@@ -669,10 +685,11 @@
* @param source bitmap to apply round corner.
* @param cornerRadius corner radius value.
*/
- public static Bitmap convertCornerRadiusBitmap(@NonNull Context context,
- @NonNull Bitmap source, @NonNull float cornerRadius) {
- final Bitmap roundedBitmap = Bitmap.createBitmap(source.getWidth(), source.getHeight(),
- Bitmap.Config.ARGB_8888);
+ @NonNull
+ public static Bitmap convertCornerRadiusBitmap(
+ @NonNull Context context, @NonNull Bitmap source, @NonNull float cornerRadius) {
+ final Bitmap roundedBitmap =
+ Bitmap.createBitmap(source.getWidth(), source.getHeight(), Bitmap.Config.ARGB_8888);
final RoundedBitmapDrawable drawable =
RoundedBitmapDrawableFactory.create(context.getResources(), source);
drawable.setAntiAlias(true);
@@ -687,9 +704,6 @@
* Returns the WifiInfo for the underlying WiFi network of the VCN network, returns null if the
* input NetworkCapabilities is not for a VCN network with underlying WiFi network.
*
- * TODO(b/238425913): Move this method to be inside systemui not settingslib once we've migrated
- * off of {@link WifiStatusTracker} and {@link NetworkControllerImpl}.
- *
* @param networkCapabilities NetworkCapabilities of the network.
*/
@Nullable
@@ -708,8 +722,9 @@
// Avoid the caller doesn't have permission to read the "Settings.Secure" data.
try {
// Whether the incompatible charger warning is disabled or not
- if (Settings.Secure.getInt(context.getContentResolver(),
- INCOMPATIBLE_CHARGER_WARNING_DISABLED, 0) == 1) {
+ if (Settings.Secure.getInt(
+ context.getContentResolver(), INCOMPATIBLE_CHARGER_WARNING_DISABLED, 0)
+ == 1) {
Log.d(tag, "containsIncompatibleChargers: disabled");
return false;
}
@@ -718,8 +733,7 @@
return false;
}
- final List<UsbPort> usbPortList =
- context.getSystemService(UsbManager.class).getPorts();
+ final List<UsbPort> usbPortList = context.getSystemService(UsbManager.class).getPorts();
if (usbPortList == null || usbPortList.isEmpty()) {
return false;
}
@@ -760,4 +774,85 @@
return false;
}
+ /** Whether to show the wireless charging notification. */
+ public static boolean shouldShowWirelessChargingNotification(
+ @NonNull Context context, @NonNull String tag) {
+ try {
+ return shouldShowWirelessChargingNotificationInternal(context, tag);
+ } catch (Exception e) {
+ Log.e(tag, "shouldShowWirelessChargingNotification()", e);
+ return false;
+ }
+ }
+
+ /** Stores the timestamp of the wireless charging notification. */
+ public static void updateWirelessChargingNotificationTimestamp(
+ @NonNull Context context, long timestamp, @NonNull String tag) {
+ try {
+ Secure.putLong(
+ context.getContentResolver(),
+ WIRELESS_CHARGING_NOTIFICATION_TIMESTAMP,
+ timestamp);
+ } catch (Exception e) {
+ Log.e(tag, "setWirelessChargingNotificationTimestamp()", e);
+ }
+ }
+
+ /** Whether to show the wireless charging warning in Settings. */
+ public static boolean shouldShowWirelessChargingWarningTip(
+ @NonNull Context context, @NonNull String tag) {
+ try {
+ return Secure.getInt(context.getContentResolver(), WIRELESS_CHARGING_WARNING_ENABLED, 0)
+ == 1;
+ } catch (Exception e) {
+ Log.e(tag, "shouldShowWirelessChargingWarningTip()", e);
+ }
+ return false;
+ }
+
+ /** Stores the state of whether the wireless charging warning in Settings is enabled. */
+ public static void updateWirelessChargingWarningEnabled(
+ @NonNull Context context, boolean enabled, @NonNull String tag) {
+ try {
+ Secure.putInt(
+ context.getContentResolver(),
+ WIRELESS_CHARGING_WARNING_ENABLED,
+ enabled ? 1 : 0);
+ } catch (Exception e) {
+ Log.e(tag, "setWirelessChargingWarningEnabled()", e);
+ }
+ }
+
+ private static boolean shouldShowWirelessChargingNotificationInternal(
+ @NonNull Context context, @NonNull String tag) {
+ final long lastNotificationTimeMillis =
+ Secure.getLong(
+ context.getContentResolver(),
+ WIRELESS_CHARGING_NOTIFICATION_TIMESTAMP,
+ WIRELESS_CHARGING_DEFAULT_TIMESTAMP);
+ if (isWirelessChargingNotificationDisabled(lastNotificationTimeMillis)) {
+ return false;
+ }
+ if (isInitialWirelessChargingNotification(lastNotificationTimeMillis)) {
+ updateWirelessChargingNotificationTimestamp(context, System.currentTimeMillis(), tag);
+ updateWirelessChargingWarningEnabled(context, /* enabled= */ true, tag);
+ return true;
+ }
+ final long durationMillis = System.currentTimeMillis() - lastNotificationTimeMillis;
+ final boolean show = durationMillis > WIRELESS_CHARGING_NOTIFICATION_THRESHOLD_MILLIS;
+ Log.d(tag, "shouldShowWirelessChargingNotification = " + show);
+ if (show) {
+ updateWirelessChargingNotificationTimestamp(context, System.currentTimeMillis(), tag);
+ updateWirelessChargingWarningEnabled(context, /* enabled= */ true, tag);
+ }
+ return show;
+ }
+
+ private static boolean isWirelessChargingNotificationDisabled(long lastNotificationTimeMillis) {
+ return lastNotificationTimeMillis == Long.MIN_VALUE;
+ }
+
+ private static boolean isInitialWirelessChargingNotification(long lastNotificationTimeMillis) {
+ return lastNotificationTimeMillis == WIRELESS_CHARGING_DEFAULT_TIMESTAMP;
+ }
}
diff --git a/packages/SettingsLib/src/com/android/settingslib/applications/AppUtils.java b/packages/SettingsLib/src/com/android/settingslib/applications/AppUtils.java
index 07de7fd..c482995 100644
--- a/packages/SettingsLib/src/com/android/settingslib/applications/AppUtils.java
+++ b/packages/SettingsLib/src/com/android/settingslib/applications/AppUtils.java
@@ -22,6 +22,7 @@
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.ApplicationInfo;
+import android.content.pm.Flags;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
@@ -164,12 +165,16 @@
try {
final PackageInfo pkg = pm.getPackageInfo(packageName, 0 /* flags */);
- // Check if the package is contained in an APEX. There is no public API to properly
- // check whether a given APK package comes from an APEX registered as module.
- // Therefore we conservatively assume that any package scanned from an /apex path is
- // a system package.
- return pkg.applicationInfo.sourceDir.startsWith(
- Environment.getApexDirectory().getAbsolutePath());
+ if (Flags.provideInfoOfApkInApex()) {
+ return pkg.getApexPackageName() != null;
+ } else {
+ // Check if the package is contained in an APEX. There is no public API to properly
+ // check whether a given APK package comes from an APEX registered as module.
+ // Therefore we conservatively assume that any package scanned from an /apex path is
+ // a system package.
+ return pkg.applicationInfo.sourceDir.startsWith(
+ Environment.getApexDirectory().getAbsolutePath());
+ }
} catch (PackageManager.NameNotFoundException e) {
return false;
}
diff --git a/packages/SettingsLib/src/com/android/settingslib/applications/ApplicationsState.java b/packages/SettingsLib/src/com/android/settingslib/applications/ApplicationsState.java
index 8e1067f..e3012cd 100644
--- a/packages/SettingsLib/src/com/android/settingslib/applications/ApplicationsState.java
+++ b/packages/SettingsLib/src/com/android/settingslib/applications/ApplicationsState.java
@@ -27,6 +27,7 @@
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.ApplicationInfo;
+import android.content.pm.Flags;
import android.content.pm.IPackageManager;
import android.content.pm.IPackageStatsObserver;
import android.content.pm.ModuleInfo;
@@ -226,6 +227,11 @@
final List<ModuleInfo> moduleInfos = mPm.getInstalledModules(0 /* flags */);
for (ModuleInfo info : moduleInfos) {
mSystemModules.put(info.getPackageName(), info.isHidden());
+ if (Flags.provideInfoOfApkInApex()) {
+ for (String apkInApexPackageName : info.getApkInApexPackageNames()) {
+ mSystemModules.put(apkInApexPackageName, info.isHidden());
+ }
+ }
}
/**
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothEventManager.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothEventManager.java
index c97445f..647fcb9 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothEventManager.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothEventManager.java
@@ -16,6 +16,8 @@
package com.android.settingslib.bluetooth;
+import static com.android.settingslib.flags.Flags.enableCachedBluetoothDeviceDedup;
+
import android.bluetooth.BluetoothA2dp;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothCsipSetCoordinator;
@@ -377,6 +379,10 @@
cachedDevice = mDeviceManager.addDevice(device);
}
+ if (enableCachedBluetoothDeviceDedup() && bondState == BluetoothDevice.BOND_BONDED) {
+ mDeviceManager.removeDuplicateInstanceForIdentityAddress(device);
+ }
+
for (BluetoothCallback callback : mCallbacks) {
callback.onDeviceBondStateChanged(cachedDevice, bondState);
}
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothUtils.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothUtils.java
index f7f0673..09b1eaf 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothUtils.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothUtils.java
@@ -9,6 +9,7 @@
import android.bluetooth.BluetoothProfile;
import android.content.Context;
import android.content.Intent;
+import android.content.pm.PackageManager;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.Canvas;
@@ -24,6 +25,7 @@
import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
import androidx.annotation.WorkerThread;
import androidx.core.graphics.drawable.IconCompat;
@@ -31,9 +33,12 @@
import com.android.settingslib.widget.AdaptiveIcon;
import com.android.settingslib.widget.AdaptiveOutlineDrawable;
+import com.google.common.collect.ImmutableSet;
+
import java.io.IOException;
import java.util.List;
import java.util.Locale;
+import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -47,6 +52,8 @@
public static final String BT_ADVANCED_HEADER_ENABLED = "bt_advanced_header_enabled";
private static final int METADATA_FAST_PAIR_CUSTOMIZED_FIELDS = 25;
private static final String KEY_HEARABLE_CONTROL_SLICE = "HEARABLE_CONTROL_SLICE_WITH_WIDTH";
+ private static final Set<String> EXCLUSIVE_MANAGERS = ImmutableSet.of(
+ "com.google.android.gms.dck");
private static ErrorListener sErrorListener;
@@ -647,4 +654,66 @@
private static String generateExpressionWithTag(String tag, String value) {
return getTagStart(tag) + value + getTagEnd(tag);
}
+
+ /**
+ * Returns the BluetoothDevice's exclusive manager
+ * ({@link BluetoothDevice.METADATA_EXCLUSIVE_MANAGER} in metadata) if it exists and is in the
+ * given set, otherwise null.
+ */
+ @Nullable
+ private static String getAllowedExclusiveManager(BluetoothDevice bluetoothDevice) {
+ byte[] exclusiveManagerNameBytes = bluetoothDevice.getMetadata(
+ BluetoothDevice.METADATA_EXCLUSIVE_MANAGER);
+ if (exclusiveManagerNameBytes == null) {
+ Log.d(TAG, "Bluetooth device " + bluetoothDevice.getName()
+ + " doesn't have exclusive manager");
+ return null;
+ }
+ String exclusiveManagerName = new String(exclusiveManagerNameBytes);
+ return getExclusiveManagers().contains(exclusiveManagerName) ? exclusiveManagerName
+ : null;
+ }
+
+ /**
+ * Checks if given package is installed
+ */
+ private static boolean isPackageInstalled(Context context,
+ String packageName) {
+ PackageManager packageManager = context.getPackageManager();
+ try {
+ packageManager.getPackageInfo(packageName, 0);
+ return true;
+ } catch (PackageManager.NameNotFoundException e) {
+ Log.d(TAG, "Package " + packageName + " is not installed");
+ }
+ return false;
+ }
+
+ /**
+ * A BluetoothDevice is exclusively managed if
+ * 1) it has field {@link BluetoothDevice.METADATA_EXCLUSIVE_MANAGER} in metadata.
+ * 2) the exclusive manager app name is in the allowlist.
+ * 3) the exclusive manager app is installed.
+ */
+ public static boolean isExclusivelyManagedBluetoothDevice(@NonNull Context context,
+ @NonNull BluetoothDevice bluetoothDevice) {
+ String exclusiveManagerName = getAllowedExclusiveManager(bluetoothDevice);
+ if (exclusiveManagerName == null) {
+ return false;
+ }
+ if (!isPackageInstalled(context, exclusiveManagerName)) {
+ return false;
+ } else {
+ Log.d(TAG, "Found exclusively managed app " + exclusiveManagerName);
+ return true;
+ }
+ }
+
+ /**
+ * Return the allowlist for exclusive manager names.
+ */
+ @NonNull
+ public static Set<String> getExclusiveManagers() {
+ return EXCLUSIVE_MANAGERS;
+ }
}
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java
index 560bc46..bcdb64d 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java
@@ -1745,12 +1745,14 @@
final BluetoothDevice tmpDevice = mDevice;
final short tmpRssi = mRssi;
final boolean tmpJustDiscovered = mJustDiscovered;
+ final HearingAidInfo tmpHearingAidInfo = mHearingAidInfo;
// Set main device from sub device
release();
mDevice = newMainDevice.mDevice;
mRssi = newMainDevice.mRssi;
mJustDiscovered = newMainDevice.mJustDiscovered;
+ mHearingAidInfo = newMainDevice.mHearingAidInfo;
fillData();
// Set sub device from backup
@@ -1758,6 +1760,7 @@
newMainDevice.mDevice = tmpDevice;
newMainDevice.mRssi = tmpRssi;
newMainDevice.mJustDiscovered = tmpJustDiscovered;
+ newMainDevice.mHearingAidInfo = tmpHearingAidInfo;
newMainDevice.fillData();
// Add the sub device back into mMemberDevices with correct hash
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceManager.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceManager.java
index 89fe268..32eec7e 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceManager.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceManager.java
@@ -340,6 +340,20 @@
}
}
+ synchronized void removeDuplicateInstanceForIdentityAddress(BluetoothDevice device) {
+ String identityAddress = device.getIdentityAddress();
+ if (identityAddress == null || identityAddress.equals(device.getAddress())) {
+ return;
+ }
+ mCachedDevices.removeIf(d -> {
+ boolean shouldRemove = d.getDevice().getAddress().equals(identityAddress);
+ if (shouldRemove) {
+ Log.d(TAG, "Remove instance for identity address " + d);
+ }
+ return shouldRemove;
+ });
+ }
+
public synchronized boolean onProfileConnectionStateChangedIfProcessed(CachedBluetoothDevice
cachedDevice, int state, int profileId) {
if (profileId == BluetoothProfile.HEARING_AID) {
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcast.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcast.java
index 1d2f790..6ee403d 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcast.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcast.java
@@ -21,6 +21,7 @@
import android.annotation.CallbackExecutor;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothClass;
+import android.bluetooth.BluetoothCsipSetCoordinator;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothLeAudioContentMetadata;
import android.bluetooth.BluetoothLeBroadcast;
@@ -62,6 +63,7 @@
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadLocalRandom;
+import java.util.stream.Collectors;
/**
* LocalBluetoothLeBroadcast provides an interface between the Settings app and the functionality of
@@ -88,6 +90,8 @@
Settings.Secure.BLUETOOTH_LE_BROADCAST_IMPROVE_COMPATIBILITY),
};
+ private final Context mContext;
+ private final CachedBluetoothDeviceManager mDeviceManager;
private BluetoothLeBroadcast mServiceBroadcast;
private BluetoothLeBroadcastAssistant mServiceBroadcastAssistant;
private BluetoothLeAudioContentMetadata mBluetoothLeAudioContentMetadata;
@@ -256,8 +260,19 @@
private final BluetoothLeBroadcastAssistant.Callback mBroadcastAssistantCallback =
new BluetoothLeBroadcastAssistant.Callback() {
@Override
- public void onSourceAdded(
- @NonNull BluetoothDevice sink, int sourceId, int reason) {}
+ public void onSourceAdded(@NonNull BluetoothDevice sink, int sourceId, int reason) {
+ if (DEBUG) {
+ Log.d(
+ TAG,
+ "onSourceAdded(), sink = "
+ + sink
+ + ", reason = "
+ + reason
+ + ", sourceId = "
+ + sourceId);
+ }
+ updateFallbackActiveDeviceIfNeeded();
+ }
@Override
public void onSearchStarted(int reason) {}
@@ -301,6 +316,7 @@
+ ", sourceId = "
+ sourceId);
}
+ updateFallbackActiveDeviceIfNeeded();
}
@Override
@@ -348,7 +364,9 @@
}
}
- LocalBluetoothLeBroadcast(Context context) {
+ LocalBluetoothLeBroadcast(Context context, CachedBluetoothDeviceManager deviceManager) {
+ mContext = context;
+ mDeviceManager = deviceManager;
mExecutor = Executors.newSingleThreadExecutor();
mBuilder = new BluetoothLeAudioContentMetadata.Builder();
mContentResolver = context.getContentResolver();
@@ -430,49 +448,6 @@
mServiceBroadcast.startBroadcast(settings);
}
- /**
- * Start the private Broadcast for personal audio sharing or qr code sharing.
- *
- * <p>The broadcast will use random string for both broadcast name and subgroup program info;
- * The broadcast will use random string for broadcast code; The broadcast will only have one
- * subgroup due to system limitation; The subgroup language will be null.
- *
- * <p>If the system started the LE Broadcast, then the system calls the corresponding callback
- * {@link BluetoothLeBroadcast.Callback}.
- */
- public void startPrivateBroadcast(int quality) {
- mNewAppSourceName = "Sharing audio";
- if (mServiceBroadcast == null) {
- Log.d(TAG, "The BluetoothLeBroadcast is null when starting the private broadcast.");
- return;
- }
- if (mServiceBroadcast.getAllBroadcastMetadata().size()
- >= mServiceBroadcast.getMaximumNumberOfBroadcasts()) {
- Log.d(TAG, "Skip starting the broadcast due to number limit.");
- return;
- }
- String programInfo = getProgramInfo();
- if (DEBUG) {
- Log.d(TAG, "startBroadcast: language = null ,programInfo = " + programInfo);
- }
- // Current broadcast framework only support one subgroup
- BluetoothLeBroadcastSubgroupSettings subgroupSettings =
- buildBroadcastSubgroupSettings(
- /* language= */ null,
- programInfo,
- /* improveCompatibility= */
- BluetoothLeBroadcastSubgroupSettings.QUALITY_STANDARD == quality);
- BluetoothLeBroadcastSettings settings =
- buildBroadcastSettings(
- true, // TODO: set to false after framework fix
- TextUtils.isEmpty(programInfo) ? null : programInfo,
- (mBroadcastCode != null && mBroadcastCode.length > 0)
- ? mBroadcastCode
- : null,
- ImmutableList.of(subgroupSettings));
- mServiceBroadcast.startBroadcast(settings);
- }
-
private BluetoothLeBroadcastSettings buildBroadcastSettings(
boolean isPublic,
@Nullable String broadcastName,
@@ -1027,4 +1002,80 @@
}
}
}
+
+ /** Update fallback active device if needed. */
+ public void updateFallbackActiveDeviceIfNeeded() {
+ if (!isEnabled(null)) {
+ Log.d(TAG, "Skip updateFallbackActiveDeviceIfNeeded due to no ongoing broadcast");
+ return;
+ }
+ if (mServiceBroadcastAssistant == null) {
+ Log.d(TAG, "Skip updateFallbackActiveDeviceIfNeeded due to assistant profile is null");
+ return;
+ }
+ List<BluetoothDevice> connectedDevices = mServiceBroadcastAssistant.getConnectedDevices();
+ List<BluetoothDevice> devicesInSharing =
+ connectedDevices.stream()
+ .filter(
+ bluetoothDevice -> {
+ List<BluetoothLeBroadcastReceiveState> sourceList =
+ mServiceBroadcastAssistant.getAllSources(
+ bluetoothDevice);
+ return !sourceList.isEmpty();
+ })
+ .collect(Collectors.toList());
+ if (devicesInSharing.isEmpty()) {
+ Log.d(TAG, "Skip updateFallbackActiveDeviceIfNeeded due to no sinks in broadcast");
+ return;
+ }
+ List<BluetoothDevice> devices =
+ BluetoothAdapter.getDefaultAdapter().getMostRecentlyConnectedDevices();
+ BluetoothDevice targetDevice = null;
+ // Find the earliest connected device in sharing session.
+ int targetDeviceIdx = -1;
+ for (BluetoothDevice device : devicesInSharing) {
+ if (devices.contains(device)) {
+ int idx = devices.indexOf(device);
+ if (idx > targetDeviceIdx) {
+ targetDeviceIdx = idx;
+ targetDevice = device;
+ }
+ }
+ }
+ if (targetDevice == null) {
+ Log.d(TAG, "Skip updateFallbackActiveDeviceIfNeeded, target is null");
+ return;
+ }
+ Log.d(
+ TAG,
+ "updateFallbackActiveDeviceIfNeeded, set active device: "
+ + targetDevice.getAnonymizedAddress());
+ CachedBluetoothDevice targetCachedDevice = mDeviceManager.findDevice(targetDevice);
+ if (targetCachedDevice == null) {
+ Log.d(TAG, "Skip updateFallbackActiveDeviceIfNeeded, fail to find cached bt device");
+ return;
+ }
+ int fallbackActiveGroupId = getFallbackActiveGroupId();
+ if (targetCachedDevice.getGroupId() == fallbackActiveGroupId) {
+ Log.d(
+ TAG,
+ "Skip updateFallbackActiveDeviceIfNeeded, already is fallback: "
+ + fallbackActiveGroupId);
+ return;
+ }
+ targetCachedDevice.setActive();
+ }
+
+ private boolean isDecryptedSource(BluetoothLeBroadcastReceiveState state) {
+ return state.getPaSyncState() == BluetoothLeBroadcastReceiveState.PA_SYNC_STATE_SYNCHRONIZED
+ && state.getBigEncryptionState()
+ == BluetoothLeBroadcastReceiveState.BIG_ENCRYPTION_STATE_DECRYPTING;
+ }
+
+ private int getFallbackActiveGroupId() {
+ return Settings.Secure.getInt(
+ mContext.getContentResolver(),
+ "bluetooth_le_broadcast_fallback_active_group_id",
+ BluetoothCsipSetCoordinator.GROUP_ID_INVALID);
+ }
}
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothManagerExt.kt b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothManagerExt.kt
new file mode 100644
index 0000000..5dc0237
--- /dev/null
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothManagerExt.kt
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.settingslib.bluetooth
+
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.callbackFlow
+import kotlinx.coroutines.launch
+
+/** Returns a [Flow] that emits a [Unit] whenever the headset audio mode changes. */
+val LocalBluetoothManager.headsetAudioModeChanges: Flow<Unit>
+ get() {
+ return callbackFlow {
+ val callback =
+ object : BluetoothCallback {
+ override fun onAudioModeChanged() {
+ launch { send(Unit) }
+ }
+ }
+
+ eventManager.registerCallback(callback)
+ awaitClose { eventManager.unregisterCallback(callback) }
+ }
+ }
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothProfileManager.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothProfileManager.java
index 119aef6..79e4c37 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothProfileManager.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothProfileManager.java
@@ -257,7 +257,7 @@
if (DEBUG) {
Log.d(TAG, "Adding local LE_AUDIO_BROADCAST profile");
}
- mLeAudioBroadcast = new LocalBluetoothLeBroadcast(mContext);
+ mLeAudioBroadcast = new LocalBluetoothLeBroadcast(mContext, mDeviceManager);
// no event handler for the LE boradcast.
mProfileNameMap.put(LocalBluetoothLeBroadcast.NAME, mLeAudioBroadcast);
}
diff --git a/packages/SettingsLib/src/com/android/settingslib/media/data/repository/SpatializerRepository.kt b/packages/SettingsLib/src/com/android/settingslib/media/data/repository/SpatializerRepository.kt
new file mode 100644
index 0000000..2a4658b
--- /dev/null
+++ b/packages/SettingsLib/src/com/android/settingslib/media/data/repository/SpatializerRepository.kt
@@ -0,0 +1,69 @@
+/*
+ * Copyright (C) 2024 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.media.data.repository
+
+import android.media.AudioDeviceAttributes
+import android.media.Spatializer
+import kotlin.coroutines.CoroutineContext
+import kotlinx.coroutines.withContext
+
+interface SpatializerRepository {
+
+ /**
+ * Returns true when Spatial audio feature is supported for the [audioDeviceAttributes] and
+ * false the otherwise.
+ */
+ suspend fun isAvailableForDevice(audioDeviceAttributes: AudioDeviceAttributes): Boolean
+
+ /** Returns a list [AudioDeviceAttributes] that are compatible with spatial audio. */
+ suspend fun getCompatibleDevices(): Collection<AudioDeviceAttributes>
+
+ /** Adds a [audioDeviceAttributes] to [getCompatibleDevices] list. */
+ suspend fun addCompatibleDevice(audioDeviceAttributes: AudioDeviceAttributes)
+
+ /** Removes a [audioDeviceAttributes] to [getCompatibleDevices] list. */
+ suspend fun removeCompatibleDevice(audioDeviceAttributes: AudioDeviceAttributes)
+}
+
+class SpatializerRepositoryImpl(
+ private val spatializer: Spatializer,
+ private val backgroundContext: CoroutineContext,
+) : SpatializerRepository {
+
+ override suspend fun isAvailableForDevice(
+ audioDeviceAttributes: AudioDeviceAttributes
+ ): Boolean {
+ return withContext(backgroundContext) {
+ spatializer.isAvailableForDevice(audioDeviceAttributes)
+ }
+ }
+
+ override suspend fun getCompatibleDevices(): Collection<AudioDeviceAttributes> =
+ withContext(backgroundContext) { spatializer.compatibleAudioDevices }
+
+ override suspend fun addCompatibleDevice(audioDeviceAttributes: AudioDeviceAttributes) {
+ withContext(backgroundContext) {
+ spatializer.addCompatibleAudioDevice(audioDeviceAttributes)
+ }
+ }
+
+ override suspend fun removeCompatibleDevice(audioDeviceAttributes: AudioDeviceAttributes) {
+ withContext(backgroundContext) {
+ spatializer.removeCompatibleAudioDevice(audioDeviceAttributes)
+ }
+ }
+}
diff --git a/packages/SettingsLib/src/com/android/settingslib/media/domain/interactor/SpatializerInteractor.kt b/packages/SettingsLib/src/com/android/settingslib/media/domain/interactor/SpatializerInteractor.kt
new file mode 100644
index 0000000..c3cc340
--- /dev/null
+++ b/packages/SettingsLib/src/com/android/settingslib/media/domain/interactor/SpatializerInteractor.kt
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2024 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.media.domain.interactor
+
+import android.media.AudioDeviceAttributes
+import com.android.settingslib.media.data.repository.SpatializerRepository
+
+class SpatializerInteractor(private val repository: SpatializerRepository) {
+
+ suspend fun isAvailable(audioDeviceAttributes: AudioDeviceAttributes): Boolean =
+ repository.isAvailableForDevice(audioDeviceAttributes)
+
+ /** Checks if spatial audio is enabled for the [audioDeviceAttributes]. */
+ suspend fun isEnabled(audioDeviceAttributes: AudioDeviceAttributes): Boolean =
+ repository.getCompatibleDevices().contains(audioDeviceAttributes)
+
+ /** Enblaes or disables spatial audio for [audioDeviceAttributes]. */
+ suspend fun setEnabled(audioDeviceAttributes: AudioDeviceAttributes, isEnabled: Boolean) {
+ if (isEnabled) {
+ repository.addCompatibleDevice(audioDeviceAttributes)
+ } else {
+ repository.removeCompatibleDevice(audioDeviceAttributes)
+ }
+ }
+}
diff --git a/packages/SettingsLib/src/com/android/settingslib/view/accessibility/data/repository/CaptioningRepository.kt b/packages/SettingsLib/src/com/android/settingslib/view/accessibility/data/repository/CaptioningRepository.kt
new file mode 100644
index 0000000..5bcb82d
--- /dev/null
+++ b/packages/SettingsLib/src/com/android/settingslib/view/accessibility/data/repository/CaptioningRepository.kt
@@ -0,0 +1,110 @@
+/*
+ * Copyright (C) 2024 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.view.accessibility.data.repository
+
+import android.view.accessibility.CaptioningManager
+import kotlin.coroutines.CoroutineContext
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.channels.ProducerScope
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.SharedFlow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.callbackFlow
+import kotlinx.coroutines.flow.filterIsInstance
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.shareIn
+import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+
+interface CaptioningRepository {
+
+ /** The system audio caption enabled state. */
+ val isSystemAudioCaptioningEnabled: StateFlow<Boolean>
+
+ /** The system audio caption UI enabled state. */
+ val isSystemAudioCaptioningUiEnabled: StateFlow<Boolean>
+
+ /** Sets [isSystemAudioCaptioningEnabled]. */
+ suspend fun setIsSystemAudioCaptioningEnabled(isEnabled: Boolean)
+}
+
+class CaptioningRepositoryImpl(
+ private val captioningManager: CaptioningManager,
+ private val backgroundCoroutineContext: CoroutineContext,
+ coroutineScope: CoroutineScope,
+) : CaptioningRepository {
+
+ private val captioningChanges: SharedFlow<CaptioningChange> =
+ callbackFlow {
+ val listener = CaptioningChangeProducingListener(this)
+ captioningManager.addCaptioningChangeListener(listener)
+ awaitClose { captioningManager.removeCaptioningChangeListener(listener) }
+ }
+ .shareIn(coroutineScope, SharingStarted.WhileSubscribed(), replay = 0)
+
+ override val isSystemAudioCaptioningEnabled: StateFlow<Boolean> =
+ captioningChanges
+ .filterIsInstance(CaptioningChange.IsSystemAudioCaptioningEnabled::class)
+ .map { it.isEnabled }
+ .stateIn(
+ coroutineScope,
+ SharingStarted.WhileSubscribed(),
+ captioningManager.isSystemAudioCaptioningEnabled
+ )
+
+ override val isSystemAudioCaptioningUiEnabled: StateFlow<Boolean> =
+ captioningChanges
+ .filterIsInstance(CaptioningChange.IsSystemUICaptioningEnabled::class)
+ .map { it.isEnabled }
+ .stateIn(
+ coroutineScope,
+ SharingStarted.WhileSubscribed(),
+ captioningManager.isSystemAudioCaptioningUiEnabled,
+ )
+
+ override suspend fun setIsSystemAudioCaptioningEnabled(isEnabled: Boolean) {
+ withContext(backgroundCoroutineContext) {
+ captioningManager.isSystemAudioCaptioningEnabled = isEnabled
+ }
+ }
+
+ private sealed interface CaptioningChange {
+
+ data class IsSystemAudioCaptioningEnabled(val isEnabled: Boolean) : CaptioningChange
+
+ data class IsSystemUICaptioningEnabled(val isEnabled: Boolean) : CaptioningChange
+ }
+
+ private class CaptioningChangeProducingListener(
+ private val scope: ProducerScope<CaptioningChange>
+ ) : CaptioningManager.CaptioningChangeListener() {
+
+ override fun onSystemAudioCaptioningChanged(enabled: Boolean) {
+ emitChange(CaptioningChange.IsSystemAudioCaptioningEnabled(enabled))
+ }
+
+ override fun onSystemAudioCaptioningUiChanged(enabled: Boolean) {
+ emitChange(CaptioningChange.IsSystemUICaptioningEnabled(enabled))
+ }
+
+ private fun emitChange(change: CaptioningChange) {
+ scope.launch { scope.send(change) }
+ }
+ }
+}
diff --git a/packages/SettingsLib/src/com/android/settingslib/view/accessibility/domain/interactor/CaptioningInteractor.kt b/packages/SettingsLib/src/com/android/settingslib/view/accessibility/domain/interactor/CaptioningInteractor.kt
new file mode 100644
index 0000000..858c8b3
--- /dev/null
+++ b/packages/SettingsLib/src/com/android/settingslib/view/accessibility/domain/interactor/CaptioningInteractor.kt
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2024 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.view.accessibility.domain.interactor
+
+import com.android.settingslib.view.accessibility.data.repository.CaptioningRepository
+import kotlinx.coroutines.flow.StateFlow
+
+class CaptioningInteractor(private val repository: CaptioningRepository) {
+
+ val isSystemAudioCaptioningEnabled: StateFlow<Boolean>
+ get() = repository.isSystemAudioCaptioningEnabled
+
+ val isSystemAudioCaptioningUiEnabled: StateFlow<Boolean>
+ get() = repository.isSystemAudioCaptioningUiEnabled
+
+ suspend fun setIsSystemAudioCaptioningEnabled(enabled: Boolean) =
+ repository.setIsSystemAudioCaptioningEnabled(enabled)
+}
diff --git a/packages/SettingsLib/src/com/android/settingslib/volume/data/model/RoutingSession.kt b/packages/SettingsLib/src/com/android/settingslib/volume/data/model/RoutingSession.kt
new file mode 100644
index 0000000..a98f3e2
--- /dev/null
+++ b/packages/SettingsLib/src/com/android/settingslib/volume/data/model/RoutingSession.kt
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2024 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.volume.data.model
+
+import android.media.RoutingSessionInfo
+
+/** Models a routing session which is created when a media route is selected. */
+data class RoutingSession(
+ val routingSessionInfo: RoutingSessionInfo,
+ val isVolumeSeekBarEnabled: Boolean,
+ val isMediaOutputDisabled: Boolean,
+)
diff --git a/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/AudioRepository.kt b/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/AudioRepository.kt
index 6761aa7..f729c04 100644
--- a/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/AudioRepository.kt
+++ b/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/AudioRepository.kt
@@ -16,15 +16,12 @@
package com.android.settingslib.volume.data.repository
-import android.content.BroadcastReceiver
-import android.content.Context
-import android.content.Intent
-import android.content.IntentFilter
import android.media.AudioDeviceInfo
import android.media.AudioManager
import android.media.AudioManager.OnCommunicationDeviceChangedListener
import androidx.concurrent.futures.DirectExecutor
import com.android.internal.util.ConcurrentUtils
+import com.android.settingslib.volume.shared.AudioManagerIntentsReceiver
import com.android.settingslib.volume.shared.model.AudioStream
import com.android.settingslib.volume.shared.model.AudioStreamModel
import com.android.settingslib.volume.shared.model.RingerMode
@@ -32,7 +29,6 @@
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.callbackFlow
@@ -40,7 +36,6 @@
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.flow.shareIn
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@@ -77,7 +72,7 @@
}
class AudioRepositoryImpl(
- private val context: Context,
+ private val audioManagerIntentsReceiver: AudioManagerIntentsReceiver,
private val audioManager: AudioManager,
private val backgroundCoroutineContext: CoroutineContext,
private val coroutineScope: CoroutineScope,
@@ -93,30 +88,9 @@
.flowOn(backgroundCoroutineContext)
.stateIn(coroutineScope, SharingStarted.WhileSubscribed(), audioManager.mode)
- private val audioManagerIntents: SharedFlow<String> =
- callbackFlow {
- val receiver =
- object : BroadcastReceiver() {
- override fun onReceive(context: Context?, intent: Intent) {
- intent.action?.let { action -> launch { send(action) } }
- }
- }
- context.registerReceiver(
- receiver,
- IntentFilter().apply {
- for (action in allActions) {
- addAction(action)
- }
- }
- )
-
- awaitClose { context.unregisterReceiver(receiver) }
- }
- .shareIn(coroutineScope, SharingStarted.WhileSubscribed())
-
override val ringerMode: StateFlow<RingerMode> =
- audioManagerIntents
- .filter { ringerActions.contains(it) }
+ audioManagerIntentsReceiver.intents
+ .filter { AudioManager.INTERNAL_RINGER_MODE_CHANGED_ACTION == it.action }
.map { RingerMode(audioManager.ringerModeInternal) }
.flowOn(backgroundCoroutineContext)
.stateIn(
@@ -146,8 +120,7 @@
)
override suspend fun getAudioStream(audioStream: AudioStream): Flow<AudioStreamModel> {
- return audioManagerIntents
- .filter { modelActions.contains(it) }
+ return audioManagerIntentsReceiver.intents
.map { getCurrentAudioStream(audioStream) }
.flowOn(backgroundCoroutineContext)
}
@@ -189,20 +162,4 @@
// return STREAM_VOICE_CALL in getAudioStream
audioManager.getStreamMinVolume(AudioManager.STREAM_VOICE_CALL)
}
-
- private companion object {
- val modelActions =
- setOf(
- AudioManager.STREAM_MUTE_CHANGED_ACTION,
- AudioManager.MASTER_MUTE_CHANGED_ACTION,
- AudioManager.VOLUME_CHANGED_ACTION,
- AudioManager.INTERNAL_RINGER_MODE_CHANGED_ACTION,
- AudioManager.STREAM_DEVICES_CHANGED_ACTION,
- )
- val ringerActions =
- setOf(
- AudioManager.INTERNAL_RINGER_MODE_CHANGED_ACTION,
- )
- val allActions = ringerActions + modelActions
- }
}
diff --git a/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/LocalMediaRepository.kt b/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/LocalMediaRepository.kt
new file mode 100644
index 0000000..aa9ae76
--- /dev/null
+++ b/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/LocalMediaRepository.kt
@@ -0,0 +1,150 @@
+/*
+ * Copyright (C) 2024 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.volume.data.repository
+
+import android.media.AudioManager
+import android.media.MediaRouter2Manager
+import android.media.RoutingSessionInfo
+import com.android.settingslib.media.LocalMediaManager
+import com.android.settingslib.media.MediaDevice
+import com.android.settingslib.volume.data.model.RoutingSession
+import com.android.settingslib.volume.shared.AudioManagerIntentsReceiver
+import kotlin.coroutines.CoroutineContext
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.callbackFlow
+import kotlinx.coroutines.flow.filter
+import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.mapNotNull
+import kotlinx.coroutines.flow.merge
+import kotlinx.coroutines.flow.onStart
+import kotlinx.coroutines.flow.shareIn
+import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.withContext
+
+/** Repository providing data about connected media devices. */
+interface LocalMediaRepository {
+
+ /** Available devices list */
+ val mediaDevices: StateFlow<Collection<MediaDevice>>
+
+ /** Currently connected media device */
+ val currentConnectedDevice: StateFlow<MediaDevice?>
+
+ val remoteRoutingSessions: StateFlow<Collection<RoutingSession>>
+
+ suspend fun adjustSessionVolume(sessionId: String?, volume: Int)
+}
+
+class LocalMediaRepositoryImpl(
+ audioManagerIntentsReceiver: AudioManagerIntentsReceiver,
+ private val localMediaManager: LocalMediaManager,
+ private val mediaRouter2Manager: MediaRouter2Manager,
+ coroutineScope: CoroutineScope,
+ private val backgroundContext: CoroutineContext,
+) : LocalMediaRepository {
+
+ private val devicesChanges =
+ audioManagerIntentsReceiver.intents.filter {
+ AudioManager.STREAM_DEVICES_CHANGED_ACTION == it.action
+ }
+ private val mediaDevicesUpdates: Flow<DevicesUpdate> =
+ callbackFlow {
+ val callback =
+ object : LocalMediaManager.DeviceCallback {
+ override fun onDeviceListUpdate(newDevices: List<MediaDevice>?) {
+ trySend(DevicesUpdate.DeviceListUpdate(newDevices ?: emptyList()))
+ }
+
+ override fun onSelectedDeviceStateChanged(
+ device: MediaDevice?,
+ state: Int,
+ ) {
+ trySend(DevicesUpdate.SelectedDeviceStateChanged)
+ }
+
+ override fun onDeviceAttributesChanged() {
+ trySend(DevicesUpdate.DeviceAttributesChanged)
+ }
+ }
+ localMediaManager.registerCallback(callback)
+ localMediaManager.startScan()
+
+ awaitClose {
+ localMediaManager.stopScan()
+ localMediaManager.unregisterCallback(callback)
+ }
+ }
+ .shareIn(coroutineScope, SharingStarted.WhileSubscribed(), replay = 0)
+
+ override val mediaDevices: StateFlow<Collection<MediaDevice>> =
+ mediaDevicesUpdates
+ .mapNotNull {
+ if (it is DevicesUpdate.DeviceListUpdate) {
+ it.newDevices ?: emptyList()
+ } else {
+ null
+ }
+ }
+ .flowOn(backgroundContext)
+ .stateIn(coroutineScope, SharingStarted.WhileSubscribed(), emptyList())
+
+ override val currentConnectedDevice: StateFlow<MediaDevice?> =
+ merge(devicesChanges, mediaDevicesUpdates)
+ .map { localMediaManager.currentConnectedDevice }
+ .stateIn(
+ coroutineScope,
+ SharingStarted.WhileSubscribed(),
+ localMediaManager.currentConnectedDevice
+ )
+
+ override val remoteRoutingSessions: StateFlow<Collection<RoutingSession>> =
+ merge(devicesChanges, mediaDevicesUpdates)
+ .onStart { emit(Unit) }
+ .map { localMediaManager.remoteRoutingSessions.map(::toRoutingSession) }
+ .flowOn(backgroundContext)
+ .stateIn(coroutineScope, SharingStarted.WhileSubscribed(), emptyList())
+
+ override suspend fun adjustSessionVolume(sessionId: String?, volume: Int) {
+ withContext(backgroundContext) {
+ if (sessionId == null) {
+ localMediaManager.adjustSessionVolume(volume)
+ } else {
+ localMediaManager.adjustSessionVolume(sessionId, volume)
+ }
+ }
+ }
+
+ private fun toRoutingSession(info: RoutingSessionInfo): RoutingSession =
+ RoutingSession(
+ info,
+ isMediaOutputDisabled = mediaRouter2Manager.getTransferableRoutes(info).isEmpty(),
+ isVolumeSeekBarEnabled = localMediaManager.shouldEnableVolumeSeekBar(info)
+ )
+
+ private sealed interface DevicesUpdate {
+
+ data class DeviceListUpdate(val newDevices: List<MediaDevice>?) : DevicesUpdate
+
+ data object SelectedDeviceStateChanged : DevicesUpdate
+
+ data object DeviceAttributesChanged : DevicesUpdate
+ }
+}
diff --git a/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/MediaControllerRepository.kt b/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/MediaControllerRepository.kt
new file mode 100644
index 0000000..ab8c6b8
--- /dev/null
+++ b/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/MediaControllerRepository.kt
@@ -0,0 +1,102 @@
+/*
+ * Copyright (C) 2024 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.volume.data.repository
+
+import android.media.AudioManager
+import android.media.session.MediaController
+import android.media.session.MediaSessionManager
+import android.media.session.PlaybackState
+import com.android.settingslib.bluetooth.LocalBluetoothManager
+import com.android.settingslib.bluetooth.headsetAudioModeChanges
+import com.android.settingslib.volume.shared.AudioManagerIntentsReceiver
+import kotlin.coroutines.CoroutineContext
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.filter
+import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.merge
+import kotlinx.coroutines.flow.onStart
+import kotlinx.coroutines.flow.stateIn
+
+/** Provides controllers for currently active device media sessions. */
+interface MediaControllerRepository {
+
+ /** Current [MediaController]. Null is emitted when there is no active [MediaController]. */
+ val activeMediaController: StateFlow<MediaController?>
+}
+
+class MediaControllerRepositoryImpl(
+ audioManagerIntentsReceiver: AudioManagerIntentsReceiver,
+ private val mediaSessionManager: MediaSessionManager,
+ localBluetoothManager: LocalBluetoothManager?,
+ coroutineScope: CoroutineScope,
+ backgroundContext: CoroutineContext,
+) : MediaControllerRepository {
+
+ private val devicesChanges =
+ audioManagerIntentsReceiver.intents.filter {
+ AudioManager.STREAM_DEVICES_CHANGED_ACTION == it.action
+ }
+ override val activeMediaController: StateFlow<MediaController?> =
+ buildList {
+ localBluetoothManager?.headsetAudioModeChanges?.let { add(it) }
+ add(devicesChanges)
+ }
+ .merge()
+ .onStart { emit(Unit) }
+ .map { getActiveLocalMediaController() }
+ .flowOn(backgroundContext)
+ .stateIn(coroutineScope, SharingStarted.WhileSubscribed(), null)
+
+ private fun getActiveLocalMediaController(): MediaController? {
+ var localController: MediaController? = null
+ val remoteMediaSessionLists: MutableList<String> = ArrayList()
+ for (controller in mediaSessionManager.getActiveSessions(null)) {
+ val playbackInfo: MediaController.PlaybackInfo = controller.playbackInfo ?: continue
+ val playbackState = controller.playbackState ?: continue
+ if (inactivePlaybackStates.contains(playbackState.state)) {
+ continue
+ }
+ when (playbackInfo.playbackType) {
+ MediaController.PlaybackInfo.PLAYBACK_TYPE_REMOTE -> {
+ if (localController?.packageName.equals(controller.packageName)) {
+ localController = null
+ }
+ if (!remoteMediaSessionLists.contains(controller.packageName)) {
+ remoteMediaSessionLists.add(controller.packageName)
+ }
+ }
+ MediaController.PlaybackInfo.PLAYBACK_TYPE_LOCAL -> {
+ if (
+ localController == null &&
+ !remoteMediaSessionLists.contains(controller.packageName)
+ ) {
+ localController = controller
+ }
+ }
+ }
+ }
+ return localController
+ }
+
+ private companion object {
+ val inactivePlaybackStates =
+ setOf(PlaybackState.STATE_STOPPED, PlaybackState.STATE_NONE, PlaybackState.STATE_ERROR)
+ }
+}
diff --git a/packages/SettingsLib/src/com/android/settingslib/volume/domain/interactor/LocalMediaInteractor.kt b/packages/SettingsLib/src/com/android/settingslib/volume/domain/interactor/LocalMediaInteractor.kt
new file mode 100644
index 0000000..f621335
--- /dev/null
+++ b/packages/SettingsLib/src/com/android/settingslib/volume/domain/interactor/LocalMediaInteractor.kt
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2024 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.volume.domain.interactor
+
+import com.android.settingslib.media.MediaDevice
+import com.android.settingslib.volume.data.repository.LocalMediaRepository
+import com.android.settingslib.volume.domain.model.RoutingSession
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.stateIn
+
+class LocalMediaInteractor(
+ private val repository: LocalMediaRepository,
+ coroutineScope: CoroutineScope,
+) {
+
+ /** Available devices list */
+ val mediaDevices: StateFlow<Collection<MediaDevice>>
+ get() = repository.mediaDevices
+
+ /** Currently connected media device */
+ val currentConnectedDevice: StateFlow<MediaDevice?>
+ get() = repository.currentConnectedDevice
+
+ val remoteRoutingSessions: StateFlow<List<RoutingSession>> =
+ repository.remoteRoutingSessions
+ .map { sessions ->
+ sessions.map {
+ RoutingSession(
+ routingSessionInfo = it.routingSessionInfo,
+ isMediaOutputDisabled = it.isMediaOutputDisabled,
+ isVolumeSeekBarEnabled =
+ it.isVolumeSeekBarEnabled && it.routingSessionInfo.volumeMax > 0
+ )
+ }
+ }
+ .stateIn(coroutineScope, SharingStarted.WhileSubscribed(), emptyList())
+
+ suspend fun adjustSessionVolume(sessionId: String?, volume: Int) =
+ repository.adjustSessionVolume(sessionId, volume)
+}
diff --git a/packages/SettingsLib/src/com/android/settingslib/volume/domain/model/RoutingSession.kt b/packages/SettingsLib/src/com/android/settingslib/volume/domain/model/RoutingSession.kt
new file mode 100644
index 0000000..dfc4703
--- /dev/null
+++ b/packages/SettingsLib/src/com/android/settingslib/volume/domain/model/RoutingSession.kt
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2024 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.volume.domain.model
+
+import android.media.RoutingSessionInfo
+
+/** Models a routing session which is created when a media route is selected. */
+data class RoutingSession(
+ val routingSessionInfo: RoutingSessionInfo,
+ val isMediaOutputDisabled: Boolean,
+ val isVolumeSeekBarEnabled: Boolean,
+)
diff --git a/packages/SettingsLib/src/com/android/settingslib/volume/shared/AudioManagerIntentsReceiver.kt b/packages/SettingsLib/src/com/android/settingslib/volume/shared/AudioManagerIntentsReceiver.kt
new file mode 100644
index 0000000..9fa4c86
--- /dev/null
+++ b/packages/SettingsLib/src/com/android/settingslib/volume/shared/AudioManagerIntentsReceiver.kt
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2024 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.volume.shared
+
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import android.media.AudioManager
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.SharedFlow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.callbackFlow
+import kotlinx.coroutines.flow.filter
+import kotlinx.coroutines.flow.filterNotNull
+import kotlinx.coroutines.flow.shareIn
+import kotlinx.coroutines.launch
+
+/** Exposes [AudioManager] intents as a observable shared flow. */
+interface AudioManagerIntentsReceiver {
+
+ val intents: SharedFlow<Intent>
+}
+
+class AudioManagerIntentsReceiverImpl(
+ private val context: Context,
+ coroutineScope: CoroutineScope,
+) : AudioManagerIntentsReceiver {
+
+ private val allActions: Collection<String>
+ get() =
+ setOf(
+ AudioManager.STREAM_MUTE_CHANGED_ACTION,
+ AudioManager.MASTER_MUTE_CHANGED_ACTION,
+ AudioManager.VOLUME_CHANGED_ACTION,
+ AudioManager.INTERNAL_RINGER_MODE_CHANGED_ACTION,
+ AudioManager.STREAM_DEVICES_CHANGED_ACTION,
+ )
+
+ override val intents: SharedFlow<Intent> =
+ callbackFlow {
+ val receiver =
+ object : BroadcastReceiver() {
+ override fun onReceive(context: Context?, intent: Intent?) {
+ launch { send(intent) }
+ }
+ }
+ context.registerReceiver(
+ receiver,
+ IntentFilter().apply {
+ for (action in allActions) {
+ addAction(action)
+ }
+ }
+ )
+
+ awaitClose { context.unregisterReceiver(receiver) }
+ }
+ .filterNotNull()
+ .filter { intent -> allActions.contains(intent.action) }
+ .shareIn(coroutineScope, SharingStarted.WhileSubscribed())
+}
diff --git a/packages/SettingsLib/tests/integ/src/com/android/settingslib/media/domain/interactor/FakeSpatializerRepository.kt b/packages/SettingsLib/tests/integ/src/com/android/settingslib/media/domain/interactor/FakeSpatializerRepository.kt
new file mode 100644
index 0000000..3f52f24
--- /dev/null
+++ b/packages/SettingsLib/tests/integ/src/com/android/settingslib/media/domain/interactor/FakeSpatializerRepository.kt
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2024 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.media.domain.interactor
+
+import android.media.AudioDeviceAttributes
+import com.android.settingslib.media.data.repository.SpatializerRepository
+
+class FakeSpatializerRepository : SpatializerRepository {
+
+ private val availabilityByDevice: MutableMap<AudioDeviceAttributes, Boolean> = mutableMapOf()
+ private val compatibleDevices: MutableList<AudioDeviceAttributes> = mutableListOf()
+
+ override suspend fun isAvailableForDevice(
+ audioDeviceAttributes: AudioDeviceAttributes
+ ): Boolean = availabilityByDevice.getOrDefault(audioDeviceAttributes, false)
+
+ override suspend fun getCompatibleDevices(): Collection<AudioDeviceAttributes> =
+ compatibleDevices
+
+ override suspend fun addCompatibleDevice(audioDeviceAttributes: AudioDeviceAttributes) {
+ compatibleDevices.add(audioDeviceAttributes)
+ }
+
+ override suspend fun removeCompatibleDevice(audioDeviceAttributes: AudioDeviceAttributes) {
+ compatibleDevices.remove(audioDeviceAttributes)
+ }
+
+ fun setIsAvailable(audioDeviceAttributes: AudioDeviceAttributes, isAvailable: Boolean) {
+ availabilityByDevice[audioDeviceAttributes] = isAvailable
+ }
+}
diff --git a/packages/SettingsLib/tests/integ/src/com/android/settingslib/media/domain/interactor/SpatializerInteractorTest.kt b/packages/SettingsLib/tests/integ/src/com/android/settingslib/media/domain/interactor/SpatializerInteractorTest.kt
new file mode 100644
index 0000000..a44baeb
--- /dev/null
+++ b/packages/SettingsLib/tests/integ/src/com/android/settingslib/media/domain/interactor/SpatializerInteractorTest.kt
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2024 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.media.domain.interactor
+
+import android.media.AudioDeviceAttributes
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class SpatializerInteractorTest {
+
+ private val testScope = TestScope()
+ private val underTest = SpatializerInteractor(FakeSpatializerRepository())
+
+ @Test
+ fun setEnabledFalse_isEnabled_false() {
+ testScope.runTest {
+ underTest.setEnabled(deviceAttributes, false)
+
+ assertThat(underTest.isEnabled(deviceAttributes)).isFalse()
+ }
+ }
+
+ @Test
+ fun setEnabledTrue_isEnabled_true() {
+ testScope.runTest {
+ underTest.setEnabled(deviceAttributes, true)
+
+ assertThat(underTest.isEnabled(deviceAttributes)).isTrue()
+ }
+ }
+
+ private companion object {
+ val deviceAttributes = AudioDeviceAttributes(0, 0, "test_device")
+ }
+}
diff --git a/packages/SettingsLib/tests/integ/src/com/android/settingslib/view/accessibility/data/repository/CaptioningRepositoryTest.kt b/packages/SettingsLib/tests/integ/src/com/android/settingslib/view/accessibility/data/repository/CaptioningRepositoryTest.kt
new file mode 100644
index 0000000..a5233e7
--- /dev/null
+++ b/packages/SettingsLib/tests/integ/src/com/android/settingslib/view/accessibility/data/repository/CaptioningRepositoryTest.kt
@@ -0,0 +1,113 @@
+/*
+ * Copyright (C) 2024 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.view.accessibility.data.repository
+
+import android.view.accessibility.CaptioningManager
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.Captor
+import org.mockito.Mock
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when`
+import org.mockito.MockitoAnnotations
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@Suppress("UnspecifiedRegisterReceiverFlag")
+@RunWith(AndroidJUnit4::class)
+class CaptioningRepositoryTest {
+
+ @Captor
+ private lateinit var listenerCaptor: ArgumentCaptor<CaptioningManager.CaptioningChangeListener>
+
+ @Mock private lateinit var captioningManager: CaptioningManager
+
+ private lateinit var underTest: CaptioningRepository
+
+ private val testScope = TestScope()
+
+ @Before
+ fun setup() {
+ MockitoAnnotations.initMocks(this)
+
+ underTest =
+ CaptioningRepositoryImpl(
+ captioningManager,
+ testScope.testScheduler,
+ testScope.backgroundScope
+ )
+ }
+
+ @Test
+ fun isSystemAudioCaptioningEnabled_change_repositoryEmits() {
+ testScope.runTest {
+ `when`(captioningManager.isEnabled).thenReturn(false)
+ val isSystemAudioCaptioningEnabled = mutableListOf<Boolean>()
+ underTest.isSystemAudioCaptioningEnabled
+ .onEach { isSystemAudioCaptioningEnabled.add(it) }
+ .launchIn(backgroundScope)
+ runCurrent()
+
+ triggerOnSystemAudioCaptioningChange()
+ runCurrent()
+
+ assertThat(isSystemAudioCaptioningEnabled)
+ .containsExactlyElementsIn(listOf(false, true))
+ .inOrder()
+ }
+ }
+
+ @Test
+ fun isSystemAudioCaptioningUiEnabled_change_repositoryEmits() {
+ testScope.runTest {
+ `when`(captioningManager.isSystemAudioCaptioningUiEnabled).thenReturn(false)
+ val isSystemAudioCaptioningUiEnabled = mutableListOf<Boolean>()
+ underTest.isSystemAudioCaptioningUiEnabled
+ .onEach { isSystemAudioCaptioningUiEnabled.add(it) }
+ .launchIn(backgroundScope)
+ runCurrent()
+
+ triggerSystemAudioCaptioningUiChange()
+ runCurrent()
+
+ assertThat(isSystemAudioCaptioningUiEnabled)
+ .containsExactlyElementsIn(listOf(false, true))
+ .inOrder()
+ }
+ }
+
+ private fun triggerSystemAudioCaptioningUiChange(enabled: Boolean = true) {
+ verify(captioningManager).addCaptioningChangeListener(listenerCaptor.capture())
+ listenerCaptor.value.onSystemAudioCaptioningUiChanged(enabled)
+ }
+
+ private fun triggerOnSystemAudioCaptioningChange(enabled: Boolean = true) {
+ verify(captioningManager).addCaptioningChangeListener(listenerCaptor.capture())
+ listenerCaptor.value.onSystemAudioCaptioningChanged(enabled)
+ }
+}
diff --git a/packages/SettingsLib/tests/integ/src/com/android/settingslib/view/accessibility/data/repository/FakeCaptioningRepository.kt b/packages/SettingsLib/tests/integ/src/com/android/settingslib/view/accessibility/data/repository/FakeCaptioningRepository.kt
new file mode 100644
index 0000000..fd253c6
--- /dev/null
+++ b/packages/SettingsLib/tests/integ/src/com/android/settingslib/view/accessibility/data/repository/FakeCaptioningRepository.kt
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2024 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.view.accessibility.data.repository
+
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
+
+class FakeCaptioningRepository : CaptioningRepository {
+
+ private val mutableIsSystemAudioCaptioningEnabled = MutableStateFlow(false)
+ override val isSystemAudioCaptioningEnabled: StateFlow<Boolean>
+ get() = mutableIsSystemAudioCaptioningEnabled.asStateFlow()
+
+ private val mutableIsSystemAudioCaptioningUiEnabled = MutableStateFlow(false)
+ override val isSystemAudioCaptioningUiEnabled: StateFlow<Boolean>
+ get() = mutableIsSystemAudioCaptioningUiEnabled.asStateFlow()
+
+ override suspend fun setIsSystemAudioCaptioningEnabled(isEnabled: Boolean) {
+ mutableIsSystemAudioCaptioningEnabled.value = isEnabled
+ }
+
+ fun setIsSystemAudioCaptioningUiEnabled(isSystemAudioCaptioningUiEnabled: Boolean) {
+ mutableIsSystemAudioCaptioningUiEnabled.value = isSystemAudioCaptioningUiEnabled
+ }
+}
diff --git a/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/AudioRepositoryTest.kt b/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/AudioRepositoryTest.kt
index 7b70c64..48b04db 100644
--- a/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/AudioRepositoryTest.kt
+++ b/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/AudioRepositoryTest.kt
@@ -16,13 +16,11 @@
package com.android.settingslib.volume.data.repository
-import android.content.BroadcastReceiver
-import android.content.Context
-import android.content.Intent
import android.media.AudioDeviceInfo
import android.media.AudioManager
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
+import com.android.settingslib.volume.shared.FakeAudioManagerIntentsReceiver
import com.android.settingslib.volume.shared.model.AudioStream
import com.android.settingslib.volume.shared.model.AudioStreamModel
import com.android.settingslib.volume.shared.model.RingerMode
@@ -30,6 +28,7 @@
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.launch
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
@@ -51,17 +50,16 @@
@RunWith(AndroidJUnit4::class)
class AudioRepositoryTest {
- @Captor private lateinit var receiverCaptor: ArgumentCaptor<BroadcastReceiver>
@Captor
private lateinit var modeListenerCaptor: ArgumentCaptor<AudioManager.OnModeChangedListener>
@Captor
private lateinit var communicationDeviceListenerCaptor:
ArgumentCaptor<AudioManager.OnCommunicationDeviceChangedListener>
- @Mock private lateinit var context: Context
@Mock private lateinit var audioManager: AudioManager
@Mock private lateinit var communicationDevice: AudioDeviceInfo
+ private val intentsReceiver = FakeAudioManagerIntentsReceiver()
private val volumeByStream: MutableMap<Int, Int> = mutableMapOf()
private val isAffectedByRingerModeByStream: MutableMap<Int, Boolean> = mutableMapOf()
private val isMuteByStream: MutableMap<Int, Boolean> = mutableMapOf()
@@ -98,7 +96,7 @@
underTest =
AudioRepositoryImpl(
- context,
+ intentsReceiver,
audioManager,
testScope.testScheduler,
testScope.backgroundScope,
@@ -270,8 +268,7 @@
}
private fun triggerIntent(action: String) {
- verify(context).registerReceiver(receiverCaptor.capture(), any())
- receiverCaptor.value.onReceive(context, Intent(action))
+ testScope.launch { intentsReceiver.triggerIntent(action) }
}
private companion object {
diff --git a/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/FakeLocalMediaRepository.kt b/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/FakeLocalMediaRepository.kt
new file mode 100644
index 0000000..642b72c
--- /dev/null
+++ b/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/FakeLocalMediaRepository.kt
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2024 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.volume.data.repository
+
+import com.android.settingslib.media.MediaDevice
+import com.android.settingslib.volume.data.model.RoutingSession
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
+
+class FakeLocalMediaRepository : LocalMediaRepository {
+
+ private val volumeBySession: MutableMap<String?, Int> = mutableMapOf()
+
+ private val mutableMediaDevices = MutableStateFlow<Collection<MediaDevice>>(emptyList())
+ override val mediaDevices: StateFlow<Collection<MediaDevice>>
+ get() = mutableMediaDevices.asStateFlow()
+
+ private val mutableCurrentConnectedDevice = MutableStateFlow<MediaDevice?>(null)
+ override val currentConnectedDevice: StateFlow<MediaDevice?>
+ get() = mutableCurrentConnectedDevice.asStateFlow()
+
+ private val mutableRemoteRoutingSessions =
+ MutableStateFlow<Collection<RoutingSession>>(emptyList())
+ override val remoteRoutingSessions: StateFlow<Collection<RoutingSession>>
+ get() = mutableRemoteRoutingSessions.asStateFlow()
+
+ fun updateMediaDevices(devices: Collection<MediaDevice>) {
+ mutableMediaDevices.value = devices
+ }
+
+ fun updateCurrentConnectedDevice(device: MediaDevice?) {
+ mutableCurrentConnectedDevice.value = device
+ }
+
+ fun updateRemoteRoutingSessions(sessions: List<RoutingSession>) {
+ mutableRemoteRoutingSessions.value = sessions
+ }
+
+ fun getSessionVolume(sessionId: String?): Int = volumeBySession.getOrDefault(sessionId, 0)
+
+ override suspend fun adjustSessionVolume(sessionId: String?, volume: Int) {
+ volumeBySession[sessionId] = volume
+ }
+}
diff --git a/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/LocalMediaRepositoryImplTest.kt b/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/LocalMediaRepositoryImplTest.kt
new file mode 100644
index 0000000..dc9ea10
--- /dev/null
+++ b/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/LocalMediaRepositoryImplTest.kt
@@ -0,0 +1,187 @@
+/*
+ * Copyright (C) 2024 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.volume.data.repository
+
+import android.media.MediaRoute2Info
+import android.media.MediaRouter2Manager
+import android.media.RoutingSessionInfo
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.settingslib.media.LocalMediaManager
+import com.android.settingslib.media.MediaDevice
+import com.android.settingslib.volume.data.model.RoutingSession
+import com.android.settingslib.volume.shared.FakeAudioManagerIntentsReceiver
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.Captor
+import org.mockito.Mock
+import org.mockito.Mockito.any
+import org.mockito.Mockito.anyInt
+import org.mockito.Mockito.anyString
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when`
+import org.mockito.MockitoAnnotations
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+class LocalMediaRepositoryImplTest {
+
+ @Mock private lateinit var localMediaManager: LocalMediaManager
+ @Mock private lateinit var mediaDevice1: MediaDevice
+ @Mock private lateinit var mediaDevice2: MediaDevice
+ @Mock private lateinit var mediaRouter2Manager: MediaRouter2Manager
+
+ @Captor
+ private lateinit var deviceCallbackCaptor: ArgumentCaptor<LocalMediaManager.DeviceCallback>
+
+ private val intentsReceiver = FakeAudioManagerIntentsReceiver()
+ private val testScope = TestScope()
+
+ private lateinit var underTest: LocalMediaRepository
+
+ @Before
+ fun setup() {
+ MockitoAnnotations.initMocks(this)
+
+ underTest =
+ LocalMediaRepositoryImpl(
+ intentsReceiver,
+ localMediaManager,
+ mediaRouter2Manager,
+ testScope.backgroundScope,
+ testScope.testScheduler,
+ )
+ }
+
+ @Test
+ fun mediaDevices_areUpdated() {
+ testScope.runTest {
+ var mediaDevices: Collection<MediaDevice>? = null
+ underTest.mediaDevices.onEach { mediaDevices = it }.launchIn(backgroundScope)
+ runCurrent()
+ verify(localMediaManager).registerCallback(deviceCallbackCaptor.capture())
+ deviceCallbackCaptor.value.onDeviceListUpdate(listOf(mediaDevice1, mediaDevice2))
+ runCurrent()
+
+ assertThat(mediaDevices).hasSize(2)
+ assertThat(mediaDevices).contains(mediaDevice1)
+ assertThat(mediaDevices).contains(mediaDevice2)
+ }
+ }
+
+ @Test
+ fun deviceListUpdated_currentConnectedDeviceUpdated() {
+ testScope.runTest {
+ var currentConnectedDevice: MediaDevice? = null
+ underTest.currentConnectedDevice
+ .onEach { currentConnectedDevice = it }
+ .launchIn(backgroundScope)
+ runCurrent()
+
+ `when`(localMediaManager.currentConnectedDevice).thenReturn(mediaDevice1)
+ verify(localMediaManager).registerCallback(deviceCallbackCaptor.capture())
+ deviceCallbackCaptor.value.onDeviceListUpdate(listOf(mediaDevice1, mediaDevice2))
+ runCurrent()
+
+ assertThat(currentConnectedDevice).isEqualTo(mediaDevice1)
+ }
+ }
+
+ @Test
+ fun kek() {
+ testScope.runTest {
+ `when`(localMediaManager.remoteRoutingSessions)
+ .thenReturn(
+ listOf(
+ testRoutingSessionInfo1,
+ testRoutingSessionInfo2,
+ testRoutingSessionInfo3,
+ )
+ )
+ `when`(localMediaManager.shouldEnableVolumeSeekBar(any())).then {
+ (it.arguments[0] as RoutingSessionInfo) == testRoutingSessionInfo1
+ }
+ `when`(mediaRouter2Manager.getTransferableRoutes(any<RoutingSessionInfo>())).then {
+ if ((it.arguments[0] as RoutingSessionInfo) == testRoutingSessionInfo2) {
+ return@then listOf(mock(MediaRoute2Info::class.java))
+ }
+ emptyList<MediaRoute2Info>()
+ }
+ var remoteRoutingSessions: Collection<RoutingSession>? = null
+ underTest.remoteRoutingSessions
+ .onEach { remoteRoutingSessions = it }
+ .launchIn(backgroundScope)
+
+ runCurrent()
+
+ assertThat(remoteRoutingSessions)
+ .containsExactlyElementsIn(
+ listOf(
+ RoutingSession(
+ routingSessionInfo = testRoutingSessionInfo1,
+ isVolumeSeekBarEnabled = true,
+ isMediaOutputDisabled = true,
+ ),
+ RoutingSession(
+ routingSessionInfo = testRoutingSessionInfo2,
+ isVolumeSeekBarEnabled = false,
+ isMediaOutputDisabled = false,
+ ),
+ RoutingSession(
+ routingSessionInfo = testRoutingSessionInfo3,
+ isVolumeSeekBarEnabled = false,
+ isMediaOutputDisabled = true,
+ )
+ )
+ )
+ }
+ }
+
+ @Test
+ fun adjustSessionVolume_adjusts() {
+ testScope.runTest {
+ var volume = 0
+ `when`(localMediaManager.adjustSessionVolume(anyString(), anyInt())).then {
+ volume = it.arguments[1] as Int
+ Unit
+ }
+
+ underTest.adjustSessionVolume("test_session", 10)
+
+ assertThat(volume).isEqualTo(10)
+ }
+ }
+
+ private companion object {
+ val testRoutingSessionInfo1 =
+ RoutingSessionInfo.Builder("id_1", "test.pkg.1").addSelectedRoute("route_1").build()
+ val testRoutingSessionInfo2 =
+ RoutingSessionInfo.Builder("id_2", "test.pkg.2").addSelectedRoute("route_2").build()
+ val testRoutingSessionInfo3 =
+ RoutingSessionInfo.Builder("id_3", "test.pkg.3").addSelectedRoute("route_3").build()
+ }
+}
diff --git a/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/MediaControllerRepositoryImplTest.kt b/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/MediaControllerRepositoryImplTest.kt
new file mode 100644
index 0000000..430d733
--- /dev/null
+++ b/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/MediaControllerRepositoryImplTest.kt
@@ -0,0 +1,172 @@
+/*
+ * Copyright (C) 2024 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.volume.data.repository
+
+import android.media.AudioManager
+import android.media.session.MediaController
+import android.media.session.MediaController.PlaybackInfo
+import android.media.session.MediaSessionManager
+import android.media.session.PlaybackState
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.settingslib.bluetooth.BluetoothCallback
+import com.android.settingslib.bluetooth.BluetoothEventManager
+import com.android.settingslib.bluetooth.LocalBluetoothManager
+import com.android.settingslib.volume.shared.FakeAudioManagerIntentsReceiver
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.Captor
+import org.mockito.Mock
+import org.mockito.Mockito.any
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when`
+import org.mockito.MockitoAnnotations
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+class MediaControllerRepositoryImplTest {
+
+ @Captor private lateinit var callbackCaptor: ArgumentCaptor<BluetoothCallback>
+
+ @Mock private lateinit var mediaSessionManager: MediaSessionManager
+ @Mock private lateinit var localBluetoothManager: LocalBluetoothManager
+ @Mock private lateinit var eventManager: BluetoothEventManager
+
+ @Mock private lateinit var stoppedMediaController: MediaController
+ @Mock private lateinit var statelessMediaController: MediaController
+ @Mock private lateinit var errorMediaController: MediaController
+ @Mock private lateinit var remoteMediaController: MediaController
+ @Mock private lateinit var localMediaController: MediaController
+
+ @Mock private lateinit var remotePlaybackInfo: PlaybackInfo
+ @Mock private lateinit var localPlaybackInfo: PlaybackInfo
+
+ private val testScope = TestScope()
+ private val intentsReceiver = FakeAudioManagerIntentsReceiver()
+
+ private lateinit var underTest: MediaControllerRepository
+
+ @Before
+ fun setup() {
+ MockitoAnnotations.initMocks(this)
+
+ `when`(localBluetoothManager.eventManager).thenReturn(eventManager)
+
+ `when`(stoppedMediaController.playbackState).thenReturn(stateStopped)
+ `when`(stoppedMediaController.packageName).thenReturn("test.pkg.stopped")
+ `when`(statelessMediaController.playbackState).thenReturn(stateNone)
+ `when`(statelessMediaController.packageName).thenReturn("test.pkg.stateless")
+ `when`(errorMediaController.playbackState).thenReturn(stateError)
+ `when`(errorMediaController.packageName).thenReturn("test.pkg.error")
+ `when`(remoteMediaController.playbackState).thenReturn(statePlaying)
+ `when`(remoteMediaController.playbackInfo).thenReturn(remotePlaybackInfo)
+ `when`(remoteMediaController.packageName).thenReturn("test.pkg.remote")
+ `when`(localMediaController.playbackState).thenReturn(statePlaying)
+ `when`(localMediaController.playbackInfo).thenReturn(localPlaybackInfo)
+ `when`(localMediaController.packageName).thenReturn("test.pkg.local")
+
+ `when`(remotePlaybackInfo.playbackType).thenReturn(PlaybackInfo.PLAYBACK_TYPE_REMOTE)
+ `when`(localPlaybackInfo.playbackType).thenReturn(PlaybackInfo.PLAYBACK_TYPE_LOCAL)
+
+ underTest =
+ MediaControllerRepositoryImpl(
+ intentsReceiver,
+ mediaSessionManager,
+ localBluetoothManager,
+ testScope.backgroundScope,
+ testScope.testScheduler,
+ )
+ }
+
+ @Test
+ fun playingMediaDevicesAvailable_sessionIsActive() {
+ testScope.runTest {
+ `when`(mediaSessionManager.getActiveSessions(any()))
+ .thenReturn(
+ listOf(
+ stoppedMediaController,
+ statelessMediaController,
+ errorMediaController,
+ remoteMediaController,
+ localMediaController
+ )
+ )
+ var mediaController: MediaController? = null
+ underTest.activeMediaController
+ .onEach { mediaController = it }
+ .launchIn(backgroundScope)
+ runCurrent()
+
+ intentsReceiver.triggerIntent(AudioManager.STREAM_DEVICES_CHANGED_ACTION)
+ triggerOnAudioModeChanged()
+ runCurrent()
+
+ assertThat(mediaController).isSameInstanceAs(localMediaController)
+ }
+ }
+
+ @Test
+ fun noPlayingMediaDevicesAvailable_sessionIsInactive() {
+ testScope.runTest {
+ `when`(mediaSessionManager.getActiveSessions(any()))
+ .thenReturn(
+ listOf(
+ stoppedMediaController,
+ statelessMediaController,
+ errorMediaController,
+ )
+ )
+ var mediaController: MediaController? = null
+ underTest.activeMediaController
+ .onEach { mediaController = it }
+ .launchIn(backgroundScope)
+ runCurrent()
+
+ intentsReceiver.triggerIntent(AudioManager.STREAM_DEVICES_CHANGED_ACTION)
+ triggerOnAudioModeChanged()
+ runCurrent()
+
+ assertThat(mediaController).isNull()
+ }
+ }
+
+ private fun triggerOnAudioModeChanged() {
+ verify(eventManager).registerCallback(callbackCaptor.capture())
+ callbackCaptor.value.onAudioModeChanged()
+ }
+
+ private companion object {
+ val statePlaying: PlaybackState =
+ PlaybackState.Builder().setState(PlaybackState.STATE_PLAYING, 0, 0f).build()
+ val stateError: PlaybackState =
+ PlaybackState.Builder().setState(PlaybackState.STATE_ERROR, 0, 0f).build()
+ val stateStopped: PlaybackState =
+ PlaybackState.Builder().setState(PlaybackState.STATE_STOPPED, 0, 0f).build()
+ val stateNone: PlaybackState =
+ PlaybackState.Builder().setState(PlaybackState.STATE_NONE, 0, 0f).build()
+ }
+}
diff --git a/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/shared/FakeAudioManagerIntentsReceiver.kt b/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/shared/FakeAudioManagerIntentsReceiver.kt
new file mode 100644
index 0000000..530690a
--- /dev/null
+++ b/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/shared/FakeAudioManagerIntentsReceiver.kt
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2024 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.volume.shared
+
+import android.content.Intent
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.SharedFlow
+import kotlinx.coroutines.flow.asSharedFlow
+
+class FakeAudioManagerIntentsReceiver : AudioManagerIntentsReceiver {
+
+ private val mutableIntents = MutableSharedFlow<Intent>()
+ override val intents: SharedFlow<Intent> = mutableIntents.asSharedFlow()
+
+ suspend fun triggerIntent(intent: Intent) {
+ mutableIntents.emit(intent)
+ }
+
+ suspend fun triggerIntent(action: String) {
+ triggerIntent(Intent(action))
+ }
+}
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/UtilsTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/UtilsTest.java
index a88a9c7..2d07e5d 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/UtilsTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/UtilsTest.java
@@ -16,6 +16,9 @@
package com.android.settingslib;
import static com.android.settingslib.Utils.STORAGE_MANAGER_ENABLED_PROPERTY;
+import static com.android.settingslib.Utils.WIRELESS_CHARGING_DEFAULT_TIMESTAMP;
+import static com.android.settingslib.Utils.shouldShowWirelessChargingWarningTip;
+import static com.android.settingslib.Utils.updateWirelessChargingNotificationTimestamp;
import static com.google.common.truth.Truth.assertThat;
@@ -28,10 +31,10 @@
import android.content.Context;
import android.content.Intent;
import android.content.res.Resources;
-import android.hardware.usb.flags.Flags;
import android.hardware.usb.UsbManager;
import android.hardware.usb.UsbPort;
import android.hardware.usb.UsbPortStatus;
+import android.hardware.usb.flags.Flags;
import android.location.LocationManager;
import android.media.AudioManager;
import android.os.BatteryManager;
@@ -59,6 +62,7 @@
import org.robolectric.annotation.Implements;
import org.robolectric.shadows.ShadowSettings;
+import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
@@ -72,21 +76,16 @@
private static final String PERCENTAGE_49 = "49%";
private static final String PERCENTAGE_50 = "50%";
private static final String PERCENTAGE_100 = "100%";
+ private static final long CURRENT_TIMESTAMP = System.currentTimeMillis();
private AudioManager mAudioManager;
private Context mContext;
- @Mock
- private LocationManager mLocationManager;
- @Mock
- private ServiceState mServiceState;
- @Mock
- private NetworkRegistrationInfo mNetworkRegistrationInfo;
- @Mock
- private UsbPort mUsbPort;
- @Mock
- private UsbManager mUsbManager;
- @Mock
- private UsbPortStatus mUsbPortStatus;
+ @Mock private LocationManager mLocationManager;
+ @Mock private ServiceState mServiceState;
+ @Mock private NetworkRegistrationInfo mNetworkRegistrationInfo;
+ @Mock private UsbPort mUsbPort;
+ @Mock private UsbManager mUsbManager;
+ @Mock private UsbPortStatus mUsbPortStatus;
@Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
@@ -102,27 +101,37 @@
@After
public void reset() {
- Settings.Secure.putInt(mContext.getContentResolver(),
- Utils.INCOMPATIBLE_CHARGER_WARNING_DISABLED, 0);
+ Settings.Secure.putInt(
+ mContext.getContentResolver(), Utils.INCOMPATIBLE_CHARGER_WARNING_DISABLED, 0);
}
@Test
public void testUpdateLocationEnabled() {
int currentUserId = ActivityManager.getCurrentUser();
- Utils.updateLocationEnabled(mContext, true, currentUserId,
- Settings.Secure.LOCATION_CHANGER_QUICK_SETTINGS);
+ Utils.updateLocationEnabled(
+ mContext, true, currentUserId, Settings.Secure.LOCATION_CHANGER_QUICK_SETTINGS);
- assertThat(Settings.Secure.getInt(mContext.getContentResolver(),
- Settings.Secure.LOCATION_CHANGER,
- Settings.Secure.LOCATION_CHANGER_UNKNOWN)).isEqualTo(
- Settings.Secure.LOCATION_CHANGER_QUICK_SETTINGS);
+ assertThat(
+ Settings.Secure.getInt(
+ mContext.getContentResolver(),
+ Settings.Secure.LOCATION_CHANGER,
+ Settings.Secure.LOCATION_CHANGER_UNKNOWN))
+ .isEqualTo(Settings.Secure.LOCATION_CHANGER_QUICK_SETTINGS);
}
@Test
public void testFormatPercentage_RoundTrue_RoundUpIfPossible() {
- final String[] expectedPercentages =
- {PERCENTAGE_0, PERCENTAGE_0, PERCENTAGE_1, PERCENTAGE_1, PERCENTAGE_49,
- PERCENTAGE_49, PERCENTAGE_50, PERCENTAGE_50, PERCENTAGE_100};
+ final String[] expectedPercentages = {
+ PERCENTAGE_0,
+ PERCENTAGE_0,
+ PERCENTAGE_1,
+ PERCENTAGE_1,
+ PERCENTAGE_49,
+ PERCENTAGE_49,
+ PERCENTAGE_50,
+ PERCENTAGE_50,
+ PERCENTAGE_100
+ };
for (int i = 0, size = TEST_PERCENTAGES.length; i < size; i++) {
final String percentage = Utils.formatPercentage(TEST_PERCENTAGES[i], true);
@@ -132,9 +141,17 @@
@Test
public void testFormatPercentage_RoundFalse_NoRound() {
- final String[] expectedPercentages =
- {PERCENTAGE_0, PERCENTAGE_0, PERCENTAGE_0, PERCENTAGE_0, PERCENTAGE_49,
- PERCENTAGE_49, PERCENTAGE_49, PERCENTAGE_50, PERCENTAGE_100};
+ final String[] expectedPercentages = {
+ PERCENTAGE_0,
+ PERCENTAGE_0,
+ PERCENTAGE_0,
+ PERCENTAGE_0,
+ PERCENTAGE_49,
+ PERCENTAGE_49,
+ PERCENTAGE_49,
+ PERCENTAGE_50,
+ PERCENTAGE_100
+ };
for (int i = 0, size = TEST_PERCENTAGES.length; i < size; i++) {
final String percentage = Utils.formatPercentage(TEST_PERCENTAGES[i], false);
@@ -146,7 +163,9 @@
public void testGetDefaultStorageManagerDaysToRetain_storageManagerDaysToRetainUsesResources() {
Resources resources = mock(Resources.class);
when(resources.getInteger(
- eq(com.android.internal.R.integer.config_storageManagerDaystoRetainDefault)))
+ eq(
+ com.android.internal.R.integer
+ .config_storageManagerDaystoRetainDefault)))
.thenReturn(60);
assertThat(Utils.getDefaultStorageManagerDaysToRetain(resources)).isEqualTo(60);
}
@@ -214,8 +233,10 @@
public void isInService_voiceOutOfServiceDataInService_returnTrue() {
when(mServiceState.getVoiceRegState()).thenReturn(ServiceState.STATE_OUT_OF_SERVICE);
when(mServiceState.getDataRegistrationState()).thenReturn(ServiceState.STATE_IN_SERVICE);
- when(mServiceState.getNetworkRegistrationInfo(NetworkRegistrationInfo.DOMAIN_PS,
- AccessNetworkConstants.TRANSPORT_TYPE_WWAN)).thenReturn(mNetworkRegistrationInfo);
+ when(mServiceState.getNetworkRegistrationInfo(
+ NetworkRegistrationInfo.DOMAIN_PS,
+ AccessNetworkConstants.TRANSPORT_TYPE_WWAN))
+ .thenReturn(mNetworkRegistrationInfo);
when(mNetworkRegistrationInfo.isInService()).thenReturn(true);
assertThat(Utils.isInService(mServiceState)).isTrue();
@@ -224,8 +245,10 @@
@Test
public void isInService_voiceOutOfServiceDataInServiceOnIwLan_returnFalse() {
when(mServiceState.getVoiceRegState()).thenReturn(ServiceState.STATE_OUT_OF_SERVICE);
- when(mServiceState.getNetworkRegistrationInfo(NetworkRegistrationInfo.DOMAIN_PS,
- AccessNetworkConstants.TRANSPORT_TYPE_WLAN)).thenReturn(mNetworkRegistrationInfo);
+ when(mServiceState.getNetworkRegistrationInfo(
+ NetworkRegistrationInfo.DOMAIN_PS,
+ AccessNetworkConstants.TRANSPORT_TYPE_WLAN))
+ .thenReturn(mNetworkRegistrationInfo);
when(mServiceState.getDataRegistrationState()).thenReturn(ServiceState.STATE_IN_SERVICE);
when(mNetworkRegistrationInfo.isInService()).thenReturn(true);
@@ -235,8 +258,10 @@
@Test
public void isInService_voiceOutOfServiceDataNull_returnFalse() {
when(mServiceState.getVoiceRegState()).thenReturn(ServiceState.STATE_OUT_OF_SERVICE);
- when(mServiceState.getNetworkRegistrationInfo(NetworkRegistrationInfo.DOMAIN_PS,
- AccessNetworkConstants.TRANSPORT_TYPE_WWAN)).thenReturn(null);
+ when(mServiceState.getNetworkRegistrationInfo(
+ NetworkRegistrationInfo.DOMAIN_PS,
+ AccessNetworkConstants.TRANSPORT_TYPE_WWAN))
+ .thenReturn(null);
assertThat(Utils.isInService(mServiceState)).isFalse();
}
@@ -244,8 +269,10 @@
@Test
public void isInService_voiceOutOfServiceDataOutOfService_returnFalse() {
when(mServiceState.getVoiceRegState()).thenReturn(ServiceState.STATE_OUT_OF_SERVICE);
- when(mServiceState.getNetworkRegistrationInfo(NetworkRegistrationInfo.DOMAIN_PS,
- AccessNetworkConstants.TRANSPORT_TYPE_WWAN)).thenReturn(mNetworkRegistrationInfo);
+ when(mServiceState.getNetworkRegistrationInfo(
+ NetworkRegistrationInfo.DOMAIN_PS,
+ AccessNetworkConstants.TRANSPORT_TYPE_WWAN))
+ .thenReturn(mNetworkRegistrationInfo);
when(mNetworkRegistrationInfo.isInService()).thenReturn(false);
assertThat(Utils.isInService(mServiceState)).isFalse();
@@ -260,96 +287,106 @@
@Test
public void getCombinedServiceState_servicestateNull_returnOutOfService() {
- assertThat(Utils.getCombinedServiceState(null)).isEqualTo(
- ServiceState.STATE_OUT_OF_SERVICE);
+ assertThat(Utils.getCombinedServiceState(null))
+ .isEqualTo(ServiceState.STATE_OUT_OF_SERVICE);
}
@Test
public void getCombinedServiceState_ServiceStatePowerOff_returnPowerOff() {
when(mServiceState.getVoiceRegState()).thenReturn(ServiceState.STATE_POWER_OFF);
- assertThat(Utils.getCombinedServiceState(mServiceState)).isEqualTo(
- ServiceState.STATE_POWER_OFF);
+ assertThat(Utils.getCombinedServiceState(mServiceState))
+ .isEqualTo(ServiceState.STATE_POWER_OFF);
}
@Test
public void getCombinedServiceState_voiceInService_returnInService() {
when(mServiceState.getVoiceRegState()).thenReturn(ServiceState.STATE_IN_SERVICE);
- assertThat(Utils.getCombinedServiceState(mServiceState)).isEqualTo(
- ServiceState.STATE_IN_SERVICE);
+ assertThat(Utils.getCombinedServiceState(mServiceState))
+ .isEqualTo(ServiceState.STATE_IN_SERVICE);
}
@Test
public void getCombinedServiceState_voiceOutOfServiceDataInService_returnInService() {
when(mServiceState.getVoiceRegState()).thenReturn(ServiceState.STATE_OUT_OF_SERVICE);
- when(mServiceState.getNetworkRegistrationInfo(NetworkRegistrationInfo.DOMAIN_PS,
- AccessNetworkConstants.TRANSPORT_TYPE_WWAN)).thenReturn(mNetworkRegistrationInfo);
+ when(mServiceState.getNetworkRegistrationInfo(
+ NetworkRegistrationInfo.DOMAIN_PS,
+ AccessNetworkConstants.TRANSPORT_TYPE_WWAN))
+ .thenReturn(mNetworkRegistrationInfo);
when(mNetworkRegistrationInfo.isInService()).thenReturn(true);
- assertThat(Utils.getCombinedServiceState(mServiceState)).isEqualTo(
- ServiceState.STATE_IN_SERVICE);
+ assertThat(Utils.getCombinedServiceState(mServiceState))
+ .isEqualTo(ServiceState.STATE_IN_SERVICE);
}
@Test
public void getCombinedServiceState_voiceOutOfServiceDataInServiceOnIwLan_returnOutOfService() {
when(mServiceState.getVoiceRegState()).thenReturn(ServiceState.STATE_OUT_OF_SERVICE);
- when(mServiceState.getNetworkRegistrationInfo(NetworkRegistrationInfo.DOMAIN_PS,
- AccessNetworkConstants.TRANSPORT_TYPE_WLAN)).thenReturn(mNetworkRegistrationInfo);
+ when(mServiceState.getNetworkRegistrationInfo(
+ NetworkRegistrationInfo.DOMAIN_PS,
+ AccessNetworkConstants.TRANSPORT_TYPE_WLAN))
+ .thenReturn(mNetworkRegistrationInfo);
when(mNetworkRegistrationInfo.isInService()).thenReturn(true);
- assertThat(Utils.getCombinedServiceState(mServiceState)).isEqualTo(
- ServiceState.STATE_OUT_OF_SERVICE);
+ assertThat(Utils.getCombinedServiceState(mServiceState))
+ .isEqualTo(ServiceState.STATE_OUT_OF_SERVICE);
}
@Test
public void getCombinedServiceState_voiceOutOfServiceDataOutOfService_returnOutOfService() {
when(mServiceState.getVoiceRegState()).thenReturn(ServiceState.STATE_OUT_OF_SERVICE);
- when(mServiceState.getDataRegistrationState()).thenReturn(
- ServiceState.STATE_OUT_OF_SERVICE);
+ when(mServiceState.getDataRegistrationState())
+ .thenReturn(ServiceState.STATE_OUT_OF_SERVICE);
- assertThat(Utils.getCombinedServiceState(mServiceState)).isEqualTo(
- ServiceState.STATE_OUT_OF_SERVICE);
+ assertThat(Utils.getCombinedServiceState(mServiceState))
+ .isEqualTo(ServiceState.STATE_OUT_OF_SERVICE);
}
@Test
public void getBatteryStatus_statusIsFull_returnFullString() {
- final Intent intent = new Intent().putExtra(BatteryManager.EXTRA_LEVEL, 100).putExtra(
- BatteryManager.EXTRA_SCALE, 100);
+ final Intent intent =
+ new Intent()
+ .putExtra(BatteryManager.EXTRA_LEVEL, 100)
+ .putExtra(BatteryManager.EXTRA_SCALE, 100);
final Resources resources = mContext.getResources();
- assertThat(Utils.getBatteryStatus(mContext, intent, /* compactStatus= */ false)).isEqualTo(
- resources.getString(R.string.battery_info_status_full));
+ assertThat(Utils.getBatteryStatus(mContext, intent, /* compactStatus= */ false))
+ .isEqualTo(resources.getString(R.string.battery_info_status_full));
}
@Test
public void getBatteryStatus_statusIsFullAndUseCompactStatus_returnFullyChargedString() {
- final Intent intent = new Intent().putExtra(BatteryManager.EXTRA_LEVEL, 100).putExtra(
- BatteryManager.EXTRA_SCALE, 100);
+ final Intent intent =
+ new Intent()
+ .putExtra(BatteryManager.EXTRA_LEVEL, 100)
+ .putExtra(BatteryManager.EXTRA_SCALE, 100);
final Resources resources = mContext.getResources();
- assertThat(Utils.getBatteryStatus(mContext, intent, /* compactStatus= */ true)).isEqualTo(
- resources.getString(R.string.battery_info_status_full_charged));
+ assertThat(Utils.getBatteryStatus(mContext, intent, /* compactStatus= */ true))
+ .isEqualTo(resources.getString(R.string.battery_info_status_full_charged));
}
@Test
public void getBatteryStatus_batteryLevelIs100_returnFullString() {
- final Intent intent = new Intent().putExtra(BatteryManager.EXTRA_STATUS,
- BatteryManager.BATTERY_STATUS_FULL);
+ final Intent intent =
+ new Intent()
+ .putExtra(BatteryManager.EXTRA_STATUS, BatteryManager.BATTERY_STATUS_FULL);
final Resources resources = mContext.getResources();
- assertThat(Utils.getBatteryStatus(mContext, intent, /* compactStatus= */ false)).isEqualTo(
- resources.getString(R.string.battery_info_status_full));
+ assertThat(Utils.getBatteryStatus(mContext, intent, /* compactStatus= */ false))
+ .isEqualTo(resources.getString(R.string.battery_info_status_full));
}
@Test
public void getBatteryStatus_batteryLevelIs100AndUseCompactStatus_returnFullyString() {
- final Intent intent = new Intent().putExtra(BatteryManager.EXTRA_STATUS,
- BatteryManager.BATTERY_STATUS_FULL);
+ final Intent intent =
+ new Intent()
+ .putExtra(BatteryManager.EXTRA_STATUS, BatteryManager.BATTERY_STATUS_FULL);
final Resources resources = mContext.getResources();
- assertThat(Utils.getBatteryStatus(mContext, intent, /* compactStatus= */ true)).isEqualTo(
- resources.getString(R.string.battery_info_status_full_charged));
+ assertThat(Utils.getBatteryStatus(mContext, intent, /* compactStatus= */ true))
+ .isEqualTo(resources.getString(R.string.battery_info_status_full_charged));
}
@Test
@@ -359,8 +396,8 @@
intent.putExtra(BatteryManager.EXTRA_PLUGGED, BatteryManager.BATTERY_PLUGGED_USB);
final Resources resources = mContext.getResources();
- assertThat(Utils.getBatteryStatus(mContext, intent, /* compactStatus= */ false)).isEqualTo(
- resources.getString(R.string.battery_info_status_charging));
+ assertThat(Utils.getBatteryStatus(mContext, intent, /* compactStatus= */ false))
+ .isEqualTo(resources.getString(R.string.battery_info_status_charging));
}
@Test
@@ -370,8 +407,8 @@
intent.putExtra(BatteryManager.EXTRA_PLUGGED, BatteryManager.BATTERY_PLUGGED_DOCK);
final Resources resources = mContext.getResources();
- assertThat(Utils.getBatteryStatus(mContext, intent, /* compactStatus= */ false)).isEqualTo(
- resources.getString(R.string.battery_info_status_charging_dock));
+ assertThat(Utils.getBatteryStatus(mContext, intent, /* compactStatus= */ false))
+ .isEqualTo(resources.getString(R.string.battery_info_status_charging_dock));
}
@Test
@@ -381,8 +418,8 @@
intent.putExtra(BatteryManager.EXTRA_PLUGGED, BatteryManager.BATTERY_PLUGGED_WIRELESS);
final Resources resources = mContext.getResources();
- assertThat(Utils.getBatteryStatus(mContext, intent, /* compactStatus= */ false)).isEqualTo(
- resources.getString(R.string.battery_info_status_charging_wireless));
+ assertThat(Utils.getBatteryStatus(mContext, intent, /* compactStatus= */ false))
+ .isEqualTo(resources.getString(R.string.battery_info_status_charging_wireless));
}
@Test
@@ -392,8 +429,8 @@
intent.putExtra(BatteryManager.EXTRA_PLUGGED, BatteryManager.BATTERY_PLUGGED_USB);
final Resources resources = mContext.getResources();
- assertThat(Utils.getBatteryStatus(mContext, intent, /* compactStatus= */ true)).isEqualTo(
- resources.getString(R.string.battery_info_status_charging));
+ assertThat(Utils.getBatteryStatus(mContext, intent, /* compactStatus= */ true))
+ .isEqualTo(resources.getString(R.string.battery_info_status_charging));
}
@Test
@@ -403,8 +440,8 @@
intent.putExtra(BatteryManager.EXTRA_PLUGGED, BatteryManager.BATTERY_PLUGGED_WIRELESS);
final Resources resources = mContext.getResources();
- assertThat(Utils.getBatteryStatus(mContext, intent, /* compactStatus= */ true)).isEqualTo(
- resources.getString(R.string.battery_info_status_charging));
+ assertThat(Utils.getBatteryStatus(mContext, intent, /* compactStatus= */ true))
+ .isEqualTo(resources.getString(R.string.battery_info_status_charging));
}
@Test
@@ -503,12 +540,97 @@
@Test
public void containsIncompatibleChargers_disableWarning_returnFalse() {
setupIncompatibleCharging();
- Settings.Secure.putInt(mContext.getContentResolver(),
- Utils.INCOMPATIBLE_CHARGER_WARNING_DISABLED, 1);
+ Settings.Secure.putInt(
+ mContext.getContentResolver(), Utils.INCOMPATIBLE_CHARGER_WARNING_DISABLED, 1);
assertThat(Utils.containsIncompatibleChargers(mContext, TAG)).isFalse();
}
+ @Test
+ public void shouldShowWirelessChargingNotification_neverSendNotification_returnTrue() {
+ updateWirelessChargingNotificationTimestamp(
+ mContext, WIRELESS_CHARGING_DEFAULT_TIMESTAMP, TAG);
+
+ assertThat(Utils.shouldShowWirelessChargingNotification(mContext, TAG)).isTrue();
+ }
+
+ @Test
+ public void shouldShowNotification_neverSendNotification_updateTimestampAndEnabledState() {
+ updateWirelessChargingNotificationTimestamp(
+ mContext, WIRELESS_CHARGING_DEFAULT_TIMESTAMP, TAG);
+
+ Utils.shouldShowWirelessChargingNotification(mContext, TAG);
+
+ assertThat(getWirelessChargingNotificationTimestamp())
+ .isNotEqualTo(WIRELESS_CHARGING_DEFAULT_TIMESTAMP);
+ assertThat(shouldShowWirelessChargingWarningTip(mContext, TAG)).isTrue();
+ }
+
+ @Test
+ public void shouldShowWirelessChargingNotification_notificationDisabled_returnFalse() {
+ updateWirelessChargingNotificationTimestamp(mContext, CURRENT_TIMESTAMP, TAG);
+
+ assertThat(Utils.shouldShowWirelessChargingNotification(mContext, TAG)).isFalse();
+ }
+
+ @Test
+ public void shouldShowWirelessChargingNotification_withinTimeThreshold_returnFalse() {
+ updateWirelessChargingNotificationTimestamp(mContext, CURRENT_TIMESTAMP, TAG);
+
+ assertThat(Utils.shouldShowWirelessChargingNotification(mContext, TAG)).isFalse();
+ }
+
+ @Test
+ public void shouldShowWirelessChargingNotification_exceedTimeThreshold_returnTrue() {
+ final long monthAgo = Duration.ofDays(31).toMillis();
+ final long timestamp = CURRENT_TIMESTAMP - monthAgo;
+ updateWirelessChargingNotificationTimestamp(mContext, timestamp, TAG);
+
+ assertThat(Utils.shouldShowWirelessChargingNotification(mContext, TAG)).isTrue();
+ }
+
+ @Test
+ public void shouldShowNotification_exceedTimeThreshold_updateTimestampAndEnabledState() {
+ final long monthAgo = Duration.ofDays(31).toMillis();
+ final long timestamp = CURRENT_TIMESTAMP - monthAgo;
+ updateWirelessChargingNotificationTimestamp(mContext, timestamp, TAG);
+
+ Utils.shouldShowWirelessChargingNotification(mContext, TAG);
+
+ assertThat(getWirelessChargingNotificationTimestamp()).isNotEqualTo(timestamp);
+ assertThat(shouldShowWirelessChargingWarningTip(mContext, TAG)).isTrue();
+ }
+
+ @Test
+ public void updateWirelessChargingNotificationTimestamp_dismissForever_setMinValue() {
+ updateWirelessChargingNotificationTimestamp(mContext, Long.MIN_VALUE, TAG);
+
+ assertThat(getWirelessChargingNotificationTimestamp()).isEqualTo(Long.MIN_VALUE);
+ }
+
+ @Test
+ public void updateWirelessChargingNotificationTimestamp_notDismissForever_setTimestamp() {
+ updateWirelessChargingNotificationTimestamp(mContext, CURRENT_TIMESTAMP, TAG);
+
+ assertThat(getWirelessChargingNotificationTimestamp())
+ .isNotEqualTo(WIRELESS_CHARGING_DEFAULT_TIMESTAMP);
+ assertThat(getWirelessChargingNotificationTimestamp()).isNotEqualTo(Long.MIN_VALUE);
+ }
+
+ @Test
+ public void shouldShowWirelessChargingWarningTip_enabled_returnTrue() {
+ Utils.updateWirelessChargingWarningEnabled(mContext, true, TAG);
+
+ assertThat(shouldShowWirelessChargingWarningTip(mContext, TAG)).isTrue();
+ }
+
+ @Test
+ public void shouldShowWirelessChargingWarningTip_disabled_returnFalse() {
+ Utils.updateWirelessChargingWarningEnabled(mContext, false, TAG);
+
+ assertThat(shouldShowWirelessChargingWarningTip(mContext, TAG)).isFalse();
+ }
+
private void setupIncompatibleCharging() {
setupIncompatibleCharging(UsbPortStatus.COMPLIANCE_WARNING_DEBUG_ACCESSORY);
}
@@ -520,6 +642,13 @@
when(mUsbPort.getStatus()).thenReturn(mUsbPortStatus);
when(mUsbPort.supportsComplianceWarnings()).thenReturn(true);
when(mUsbPortStatus.isConnected()).thenReturn(true);
- when(mUsbPortStatus.getComplianceWarnings()).thenReturn(new int[]{complianceWarningType});
+ when(mUsbPortStatus.getComplianceWarnings()).thenReturn(new int[] {complianceWarningType});
+ }
+
+ private long getWirelessChargingNotificationTimestamp() {
+ return Settings.Secure.getLong(
+ mContext.getContentResolver(),
+ Utils.WIRELESS_CHARGING_NOTIFICATION_TIMESTAMP,
+ WIRELESS_CHARGING_DEFAULT_TIMESTAMP);
}
}
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/applications/AppUtilsTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/applications/AppUtilsTest.java
index 994c1ee..e32d880 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/applications/AppUtilsTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/applications/AppUtilsTest.java
@@ -16,6 +16,8 @@
package com.android.settingslib.applications;
+import static android.content.pm.Flags.FLAG_PROVIDE_INFO_OF_APK_IN_APEX;
+
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.fail;
@@ -24,12 +26,16 @@
import android.content.Context;
import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageInfo;
import android.graphics.drawable.Drawable;
+import android.os.Environment;
+import android.platform.test.flag.junit.SetFlagsRule;
import com.android.settingslib.Utils;
import org.junit.After;
import org.junit.Before;
+import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
@@ -39,6 +45,8 @@
import org.robolectric.annotation.Config;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
+import org.robolectric.shadow.api.Shadow;
+import org.robolectric.shadows.ShadowPackageManager;
import java.io.File;
import java.lang.reflect.Field;
@@ -60,6 +68,10 @@
private ApplicationInfo mAppInfo;
private ApplicationsState.AppEntry mAppEntry;
private ArrayList<ApplicationsState.AppEntry> mAppEntries;
+ private ShadowPackageManager mShadowPackageManager;
+
+ @Rule
+ public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
@Before
public void setUp() {
@@ -70,6 +82,7 @@
mAppEntry = createAppEntry(mAppInfo, /* id= */ 1);
mAppEntries = new ArrayList<>(Arrays.asList(mAppEntry));
doReturn(mIcon).when(mIcon).mutate();
+ mShadowPackageManager = Shadow.extract(mContext.getPackageManager());
}
@After
@@ -151,6 +164,32 @@
assertThat(AppUtils.isAppInstalled(appEntry)).isFalse();
}
+ @Test
+ public void isMainlineModule_hasApexPackageName_shouldCheckByPackageInfo() {
+ mSetFlagsRule.enableFlags(FLAG_PROVIDE_INFO_OF_APK_IN_APEX);
+ PackageInfo packageInfo = new PackageInfo();
+ packageInfo.packageName = APP_PACKAGE_NAME;
+ packageInfo.setApexPackageName("com.test.apex.package");
+ mShadowPackageManager.installPackage(packageInfo);
+
+ assertThat(
+ AppUtils.isMainlineModule(mContext.getPackageManager(), APP_PACKAGE_NAME)).isTrue();
+ }
+
+ @Test
+ public void isMainlineModule_noApexPackageName_shouldCheckBySourceDirPath() {
+ mSetFlagsRule.disableFlags(FLAG_PROVIDE_INFO_OF_APK_IN_APEX);
+ ApplicationInfo applicationInfo = new ApplicationInfo();
+ applicationInfo.sourceDir = Environment.getApexDirectory().getAbsolutePath();
+ PackageInfo packageInfo = new PackageInfo();
+ packageInfo.packageName = APP_PACKAGE_NAME;
+ packageInfo.applicationInfo = applicationInfo;
+ mShadowPackageManager.installPackage(packageInfo);
+
+ assertThat(
+ AppUtils.isMainlineModule(mContext.getPackageManager(), APP_PACKAGE_NAME)).isTrue();
+ }
+
private ApplicationsState.AppEntry createAppEntry(ApplicationInfo appInfo, int id) {
ApplicationsState.AppEntry appEntry = new ApplicationsState.AppEntry(mContext, appInfo, id);
appEntry.label = "label";
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/applications/ApplicationsStateRoboTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/applications/ApplicationsStateRoboTest.java
index 34d8148..827d8fa 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/applications/ApplicationsStateRoboTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/applications/ApplicationsStateRoboTest.java
@@ -16,6 +16,7 @@
package com.android.settingslib.applications;
+import static android.content.pm.Flags.FLAG_PROVIDE_INFO_OF_APK_IN_APEX;
import static android.os.UserHandle.MU_ENABLED;
import static android.os.UserHandle.USER_SYSTEM;
@@ -58,6 +59,7 @@
import android.os.RemoteException;
import android.os.UserHandle;
import android.os.UserManager;
+import android.platform.test.flag.junit.SetFlagsRule;
import android.text.TextUtils;
import android.util.IconDrawableFactory;
@@ -70,6 +72,7 @@
import org.junit.After;
import org.junit.Before;
+import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
@@ -89,6 +92,7 @@
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.UUID;
@@ -137,6 +141,9 @@
@Mock
private IPackageManager mPackageManagerService;
+ @Rule
+ public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+
@Implements(value = IconDrawableFactory.class)
public static class ShadowIconDrawableFactory {
@@ -169,7 +176,9 @@
public List<ModuleInfo> getInstalledModules(int flags) {
if (mInstalledModules.isEmpty()) {
for (String moduleName : mModuleNames) {
- mInstalledModules.add(createModuleInfo(moduleName));
+ mInstalledModules.add(
+ createModuleInfo(moduleName,
+ TextUtils.concat(moduleName, ".apex").toString()));
}
}
return mInstalledModules;
@@ -188,10 +197,11 @@
return resolveInfos;
}
- private ModuleInfo createModuleInfo(String packageName) {
+ private ModuleInfo createModuleInfo(String packageName, String apexPackageName) {
final ModuleInfo info = new ModuleInfo();
info.setName(packageName);
info.setPackageName(packageName);
+ info.setApkInApexPackageNames(Collections.singletonList(apexPackageName));
// will treat any app with package name that contains "hidden" as hidden module
info.setHidden(!TextUtils.isEmpty(packageName) && packageName.contains("hidden"));
return info;
@@ -822,4 +832,32 @@
assertThat(mApplicationsState.getEntry(PKG_1, /* userId= */ 0).info.packageName)
.isEqualTo(PKG_1);
}
+
+ @Test
+ public void isHiddenModule_hasApkInApexInfo_shouldSupportHiddenApexPackage() {
+ mSetFlagsRule.enableFlags(FLAG_PROVIDE_INFO_OF_APK_IN_APEX);
+ ApplicationsState.sInstance = null;
+ mApplicationsState = ApplicationsState.getInstance(mApplication, mPackageManagerService);
+ String normalModulePackage = "test.module.1";
+ String hiddenModulePackage = "test.hidden.module.2";
+ String hiddenApexPackage = "test.hidden.module.2.apex";
+
+ assertThat(mApplicationsState.isHiddenModule(normalModulePackage)).isFalse();
+ assertThat(mApplicationsState.isHiddenModule(hiddenModulePackage)).isTrue();
+ assertThat(mApplicationsState.isHiddenModule(hiddenApexPackage)).isTrue();
+ }
+
+ @Test
+ public void isHiddenModule_noApkInApexInfo_onlySupportHiddenModule() {
+ mSetFlagsRule.disableFlags(FLAG_PROVIDE_INFO_OF_APK_IN_APEX);
+ ApplicationsState.sInstance = null;
+ mApplicationsState = ApplicationsState.getInstance(mApplication, mPackageManagerService);
+ String normalModulePackage = "test.module.1";
+ String hiddenModulePackage = "test.hidden.module.2";
+ String hiddenApexPackage = "test.hidden.module.2.apex";
+
+ assertThat(mApplicationsState.isHiddenModule(normalModulePackage)).isFalse();
+ assertThat(mApplicationsState.isHiddenModule(hiddenModulePackage)).isTrue();
+ assertThat(mApplicationsState.isHiddenModule(hiddenApexPackage)).isFalse();
+ }
}
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothUtilsTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothUtilsTest.java
index f7ec80b..475a6d6 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothUtilsTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothUtilsTest.java
@@ -17,6 +17,8 @@
import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -24,6 +26,8 @@
import android.bluetooth.BluetoothClass;
import android.bluetooth.BluetoothDevice;
import android.content.Context;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
import android.graphics.drawable.Drawable;
import android.media.AudioManager;
import android.net.Uri;
@@ -49,6 +53,8 @@
private BluetoothDevice mBluetoothDevice;
@Mock
private AudioManager mAudioManager;
+ @Mock
+ private PackageManager mPackageManager;
private Context mContext;
private static final String STRING_METADATA = "string_metadata";
@@ -59,6 +65,7 @@
private static final String CONTROL_METADATA =
"<HEARABLE_CONTROL_SLICE_WITH_WIDTH>" + STRING_METADATA
+ "</HEARABLE_CONTROL_SLICE_WITH_WIDTH>";
+ private static final String FAKE_EXCLUSIVE_MANAGER_NAME = "com.fake.name";
@Before
public void setUp() {
@@ -372,4 +379,55 @@
assertThat(BluetoothUtils.isConnectedBluetoothDevice(mCachedBluetoothDevice,
mAudioManager)).isEqualTo(false);
}
+
+ @Test
+ public void isExclusivelyManagedBluetoothDevice_isNotExclusivelyManaged_returnFalse() {
+ when(mBluetoothDevice.getMetadata(BluetoothDevice.METADATA_EXCLUSIVE_MANAGER)).thenReturn(
+ null);
+
+ assertThat(BluetoothUtils.isExclusivelyManagedBluetoothDevice(mContext,
+ mBluetoothDevice)).isEqualTo(false);
+ }
+
+ @Test
+ public void isExclusivelyManagedBluetoothDevice_isNotInAllowList_returnFalse() {
+ when(mBluetoothDevice.getMetadata(BluetoothDevice.METADATA_EXCLUSIVE_MANAGER)).thenReturn(
+ FAKE_EXCLUSIVE_MANAGER_NAME.getBytes());
+
+ assertThat(BluetoothUtils.isExclusivelyManagedBluetoothDevice(mContext,
+ mBluetoothDevice)).isEqualTo(false);
+ }
+
+ @Test
+ public void isExclusivelyManagedBluetoothDevice_packageNotInstalled_returnFalse()
+ throws Exception {
+ final String exclusiveManagerName =
+ BluetoothUtils.getExclusiveManagers().stream().findAny().orElse(
+ FAKE_EXCLUSIVE_MANAGER_NAME);
+
+ when(mBluetoothDevice.getMetadata(BluetoothDevice.METADATA_EXCLUSIVE_MANAGER)).thenReturn(
+ exclusiveManagerName.getBytes());
+ when(mContext.getPackageManager()).thenReturn(mPackageManager);
+ doThrow(new PackageManager.NameNotFoundException()).when(mPackageManager).getPackageInfo(
+ exclusiveManagerName, 0);
+
+ assertThat(BluetoothUtils.isExclusivelyManagedBluetoothDevice(mContext,
+ mBluetoothDevice)).isEqualTo(false);
+ }
+
+ @Test
+ public void isExclusivelyManagedBluetoothDevice_isExclusivelyManaged_returnTrue()
+ throws Exception {
+ final String exclusiveManagerName =
+ BluetoothUtils.getExclusiveManagers().stream().findAny().orElse(
+ FAKE_EXCLUSIVE_MANAGER_NAME);
+
+ when(mBluetoothDevice.getMetadata(BluetoothDevice.METADATA_EXCLUSIVE_MANAGER)).thenReturn(
+ exclusiveManagerName.getBytes());
+ when(mContext.getPackageManager()).thenReturn(mPackageManager);
+ doReturn(new PackageInfo()).when(mPackageManager).getPackageInfo(exclusiveManagerName, 0);
+
+ assertThat(BluetoothUtils.isExclusivelyManagedBluetoothDevice(mContext,
+ mBluetoothDevice)).isEqualTo(true);
+ }
}
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 9db8b47..461ecf5d 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
@@ -1769,8 +1769,10 @@
public void switchMemberDeviceContent_switchMainDevice_switchesSuccessful() {
mCachedDevice.mRssi = RSSI_1;
mCachedDevice.mJustDiscovered = JUSTDISCOVERED_1;
+ mCachedDevice.setHearingAidInfo(getLeftAshaHearingAidInfo());
mSubCachedDevice.mRssi = RSSI_2;
mSubCachedDevice.mJustDiscovered = JUSTDISCOVERED_2;
+ mSubCachedDevice.setHearingAidInfo(getRightAshaHearingAidInfo());
mCachedDevice.addMemberDevice(mSubCachedDevice);
mCachedDevice.switchMemberDeviceContent(mSubCachedDevice);
@@ -1778,10 +1780,12 @@
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(mCachedDevice).fillData();
assertThat(mSubCachedDevice.mRssi).isEqualTo(RSSI_1);
assertThat(mSubCachedDevice.mJustDiscovered).isEqualTo(JUSTDISCOVERED_1);
assertThat(mSubCachedDevice.mDevice).isEqualTo(mDevice);
+ assertThat(mSubCachedDevice.getDeviceSide()).isEqualTo(HearingAidInfo.DeviceSide.SIDE_LEFT);
verify(mSubCachedDevice).fillData();
assertThat(mCachedDevice.getMemberDevice().contains(mSubCachedDevice)).isTrue();
}
diff --git a/packages/SettingsLib/tests/unit/Android.bp b/packages/SettingsLib/tests/unit/Android.bp
index e2eda4f..6d6e2ff 100644
--- a/packages/SettingsLib/tests/unit/Android.bp
+++ b/packages/SettingsLib/tests/unit/Android.bp
@@ -32,7 +32,5 @@
"androidx.test.ext.junit",
"androidx.test.runner",
"truth",
- "kotlinx_coroutines_test",
- "mockito-target-minus-junit4",
],
}
diff --git a/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java b/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java
index 8ad5f24..dc8116d 100644
--- a/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java
+++ b/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java
@@ -261,6 +261,7 @@
Settings.Secure.CREDENTIAL_SERVICE,
Settings.Secure.CREDENTIAL_SERVICE_PRIMARY,
Settings.Secure.EVEN_DIMMER_ACTIVATED,
- Settings.Secure.EVEN_DIMMER_MIN_NITS
+ Settings.Secure.EVEN_DIMMER_MIN_NITS,
+ Settings.Secure.STYLUS_POINTER_ICON_ENABLED,
};
}
diff --git a/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java b/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java
index d854df38..fabdafc 100644
--- a/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java
+++ b/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java
@@ -416,5 +416,6 @@
VALIDATORS.put(Secure.CREDENTIAL_SERVICE, CREDENTIAL_SERVICE_VALIDATOR);
VALIDATORS.put(Secure.CREDENTIAL_SERVICE_PRIMARY, NULLABLE_COMPONENT_NAME_VALIDATOR);
VALIDATORS.put(Secure.AUTOFILL_SERVICE, AUTOFILL_SERVICE_VALIDATOR);
+ VALIDATORS.put(Secure.STYLUS_POINTER_ICON_ENABLED, BOOLEAN_VALIDATOR);
}
}
diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java
index 1e146a5..bc07836 100644
--- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java
+++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java
@@ -1113,6 +1113,10 @@
dumpSetting(s, p,
Settings.Global.SMART_SUGGESTIONS_IN_NOTIFICATIONS_FLAGS,
GlobalSettingsProto.Notification.SMART_SUGGESTIONS_IN_NOTIFICATIONS_FLAGS);
+ dumpSetting(s, p,
+ Settings.Global.DISABLE_SCREEN_SHARE_PROTECTIONS_FOR_APPS_AND_NOTIFICATIONS,
+ GlobalSettingsProto.Notification
+ .DISABLE_SCREEN_SHARE_PROTECTIONS_FOR_APPS_AND_NOTIFICATIONS);
p.end(notificationToken);
dumpSetting(s, p,
@@ -2597,6 +2601,9 @@
p.end(soundsToken);
dumpSetting(s, p,
+ Settings.Secure.STYLUS_POINTER_ICON_ENABLED,
+ SecureSettingsProto.STYLUS_POINTER_ICON_ENABLED);
+ dumpSetting(s, p,
Settings.Secure.SWIPE_BOTTOM_TO_NOTIFICATION_ENABLED,
SecureSettingsProto.SWIPE_BOTTOM_TO_NOTIFICATION_ENABLED);
dumpSetting(s, p,
diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsState.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsState.java
index 6f3c88f..1c9e748 100644
--- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsState.java
+++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsState.java
@@ -612,12 +612,19 @@
String packageName) {
List<String> changedKeys = new ArrayList<>();
final Iterator<Map.Entry<String, Setting>> iterator = mSettings.entrySet().iterator();
+ int index = prefix.lastIndexOf('/');
+ String namespace = index < 0 ? "" : prefix.substring(0, index);
+ Map<String, String> trunkFlagMap = (mNamespaceDefaults == null)
+ ? null : mNamespaceDefaults.get(namespace);
// Delete old keys with the prefix that are not part of the new set.
+ // trunk flags will not be configured with restricted propagation
+ // trunk flags will be explicitly set, so not removing them here
while (iterator.hasNext()) {
Map.Entry<String, Setting> entry = iterator.next();
final String key = entry.getKey();
final Setting oldState = entry.getValue();
- if (key != null && key.startsWith(prefix) && !keyValues.containsKey(key)) {
+ if (key != null && (trunkFlagMap == null || !trunkFlagMap.containsKey(key))
+ && key.startsWith(prefix) && !keyValues.containsKey(key)) {
iterator.remove();
FrameworkStatsLog.write(FrameworkStatsLog.SETTING_CHANGED, key,
diff --git a/packages/SettingsProvider/src/com/android/providers/settings/device_config_service.aconfig b/packages/SettingsProvider/src/com/android/providers/settings/device_config_service.aconfig
index edbc0b3..ecac5ee 100644
--- a/packages/SettingsProvider/src/com/android/providers/settings/device_config_service.aconfig
+++ b/packages/SettingsProvider/src/com/android/providers/settings/device_config_service.aconfig
@@ -14,11 +14,3 @@
bug: "311155098"
is_fixed_read_only: true
}
-
-flag {
- name: "configurable_font_scale_default"
- namespace: "large_screen_experiences_app_compat"
- description: "Whether the font_scale is read from a device dependent configuration file"
- bug: "319808237"
- is_fixed_read_only: true
-}
\ No newline at end of file
diff --git a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java
index 16de478..b58187d 100644
--- a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java
+++ b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java
@@ -185,6 +185,7 @@
Settings.Global.DEVELOPMENT_WM_DISPLAY_SETTINGS_PATH,
Settings.Global.DEVICE_DEMO_MODE,
Settings.Global.DEVICE_IDLE_CONSTANTS,
+ Settings.Global.DISABLE_SCREEN_SHARE_PROTECTIONS_FOR_APPS_AND_NOTIFICATIONS,
Settings.Global.DISABLE_WINDOW_BLURS,
Settings.Global.BATTERY_SAVER_CONSTANTS,
Settings.Global.BATTERY_TIP_CONSTANTS,
diff --git a/packages/Shell/AndroidManifest.xml b/packages/Shell/AndroidManifest.xml
index 32c7433..84ef6e5 100644
--- a/packages/Shell/AndroidManifest.xml
+++ b/packages/Shell/AndroidManifest.xml
@@ -911,6 +911,12 @@
<!-- Permissions required for CTS test - CtsContactKeysProviderPrivilegedApp -->
<uses-permission android:name="android.permission.WRITE_VERIFICATION_STATE_E2EE_CONTACT_KEYS"/>
+ <!-- Permission required for Cts test ScreenRecordingCallbackTests -->
+ <uses-permission android:name="android.permission.DETECT_SCREEN_RECORDING" />
+
+ <!-- Permissions required for CTS test - GrammaticalInflectionManagerTest -->
+ <uses-permission android:name="android.permission.READ_SYSTEM_GRAMMATICAL_GENDER" />
+
<application
android:label="@string/app_label"
android:theme="@android:style/Theme.DeviceDefault.DayNight"
diff --git a/packages/SoundPicker/OWNERS b/packages/SoundPicker/OWNERS
deleted file mode 100644
index 5bf46e0..0000000
--- a/packages/SoundPicker/OWNERS
+++ /dev/null
@@ -1,2 +0,0 @@
-# Haptics team works on the SoundPicker
-include platform/frameworks/base:/services/core/java/com/android/server/vibrator/OWNERS
diff --git a/packages/SoundPicker2/Android.bp b/packages/SoundPicker2/Android.bp
deleted file mode 100644
index f4d8bf2..0000000
--- a/packages/SoundPicker2/Android.bp
+++ /dev/null
@@ -1,46 +0,0 @@
-package {
- // See: http://go/android-license-faq
- // A large-scale-change added 'default_applicable_licenses' to import
- // all of the 'license_kinds' from "frameworks_base_license"
- // to get the below license kinds:
- // SPDX-license-identifier-Apache-2.0
- default_applicable_licenses: ["frameworks_base_license"],
-}
-
-android_library {
- name: "SoundPicker2Lib",
- srcs: [
- "src/**/*.java",
- ],
- resource_dirs: [
- "res",
- ],
- static_libs: [
- "androidx.appcompat_appcompat",
- "hilt_android",
- "guava",
- "androidx.recyclerview_recyclerview",
- "androidx-constraintlayout_constraintlayout",
- "androidx.viewpager2_viewpager2",
- "com.google.android.material_material",
- ],
-}
-
-android_app {
- name: "SoundPicker2",
- defaults: ["platform_app_defaults"],
- manifest: "AndroidManifest.xml",
- static_libs: ["SoundPicker2Lib"],
- platform_apis: true,
- certificate: "media",
- privileged: true,
-
- optimize: {
- enabled: true,
- optimize: true,
- shrink: true,
- shrink_resources: true,
- obfuscate: false,
- proguard_compatibility: false,
- },
-}
diff --git a/packages/SoundPicker2/AndroidManifest.xml b/packages/SoundPicker2/AndroidManifest.xml
deleted file mode 100644
index 934b003..0000000
--- a/packages/SoundPicker2/AndroidManifest.xml
+++ /dev/null
@@ -1,43 +0,0 @@
-<manifest xmlns:android="http://schemas.android.com/apk/res/android"
- package="com.android.soundpicker"
- android:sharedUserId="android.media">
-
- <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
- <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
-
- <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
- <uses-permission android:name="android.permission.RECEIVE_DEVICE_CUSTOMIZATION_READY" />
- <uses-permission android:name="android.permission.WRITE_SETTINGS" />
-
- <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS" />
-
- <application
- android:name=".RingtonePickerApplication"
- android:allowBackup="false"
- android:label="@string/app_label"
- android:theme="@style/Theme.AppCompat"
- android:supportsRtl="true">
- <receiver android:name="RingtoneReceiver"
- android:exported="true">
- <intent-filter>
- <action android:name="android.intent.action.DEVICE_CUSTOMIZATION_READY"/>
- </intent-filter>
- </receiver>
-
- <service android:name="RingtoneOverlayService" />
-
- <activity android:name="RingtonePickerActivity"
- android:theme="@style/Theme.AppCompat.Dialog"
- android:enabled="@*android:bool/config_defaultRingtonePickerEnabled"
- android:excludeFromRecents="true"
- android:exported="true">
- <intent-filter>
- <action android:name="android.intent.action.RINGTONE_PICKER" />
- <category android:name="android.intent.category.DEFAULT" />
- <category android:name="android.intent.category.RINGTONE_PICKER_SOUND" />
- <category android:name="android.intent.category.RINGTONE_PICKER_VIBRATION" />
- <category android:name="android.intent.category.RINGTONE_PICKER_RINGTONE" />
- </intent-filter>
- </activity>
- </application>
-</manifest>
diff --git a/packages/SoundPicker2/OWNERS b/packages/SoundPicker2/OWNERS
deleted file mode 100644
index 5bf46e0..0000000
--- a/packages/SoundPicker2/OWNERS
+++ /dev/null
@@ -1,2 +0,0 @@
-# Haptics team works on the SoundPicker
-include platform/frameworks/base:/services/core/java/com/android/server/vibrator/OWNERS
diff --git a/packages/SoundPicker2/res/drawable/ic_add.xml b/packages/SoundPicker2/res/drawable/ic_add.xml
deleted file mode 100644
index 22b3fe9..0000000
--- a/packages/SoundPicker2/res/drawable/ic_add.xml
+++ /dev/null
@@ -1,24 +0,0 @@
-<!--
- Copyright (C) 2016 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="24.0dp"
- android:height="24.0dp"
- android:viewportWidth="48.0"
- android:viewportHeight="48.0">
- <path
- android:fillColor="?android:attr/colorAccent"
- android:pathData="M38.0,26.0L26.0,26.0l0.0,12.0l-4.0,0.0L22.0,26.0L10.0,26.0l0.0,-4.0l12.0,0.0L22.0,10.0l4.0,0.0l0.0,12.0l12.0,0.0l0.0,4.0z"/>
-</vector>
\ No newline at end of file
diff --git a/packages/SoundPicker2/res/drawable/ic_add_padded.xml b/packages/SoundPicker2/res/drawable/ic_add_padded.xml
deleted file mode 100644
index c376867..0000000
--- a/packages/SoundPicker2/res/drawable/ic_add_padded.xml
+++ /dev/null
@@ -1,22 +0,0 @@
-<!--
- Copyright (C) 2017 The Android Open Source Project
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
--->
-
-<inset xmlns:android="http://schemas.android.com/apk/res/android"
- android:drawable="@drawable/ic_add"
- android:insetTop="4dp"
- android:insetRight="4dp"
- android:insetBottom="4dp"
- android:insetLeft="4dp"/>
diff --git a/packages/SoundPicker2/res/layout-watch/add_new_sound_item.xml b/packages/SoundPicker2/res/layout-watch/add_new_sound_item.xml
deleted file mode 100644
index edfc0ab..0000000
--- a/packages/SoundPicker2/res/layout-watch/add_new_sound_item.xml
+++ /dev/null
@@ -1,36 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
- Copyright (C) 2017 The Android Open Source Project
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
--->
-
-<!--
- Currently, no file manager app on watch could handle ACTION_GET_CONTENT intent.
- Make the visibility to "gone" to prevent failures.
- -->
-<TextView xmlns:android="http://schemas.android.com/apk/res/android"
- android:id="@+id/add_new_sound_text"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:minHeight="?android:attr/listPreferredItemHeightSmall"
- android:textAppearance="?android:attr/textAppearanceMedium"
- android:text="@null"
- android:textColor="?android:attr/colorAccent"
- android:gravity="center_vertical"
- android:paddingStart="?android:attr/listPreferredItemPaddingStart"
- android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
- android:drawableStart="@drawable/ic_add_padded"
- android:drawablePadding="8dp"
- android:ellipsize="marquee"
- android:visibility="gone" />
diff --git a/packages/SoundPicker2/res/layout-watch/radio_with_work_badge.xml b/packages/SoundPicker2/res/layout-watch/radio_with_work_badge.xml
deleted file mode 100644
index ee29a37..0000000
--- a/packages/SoundPicker2/res/layout-watch/radio_with_work_badge.xml
+++ /dev/null
@@ -1,47 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2017 The Android Open Source Project
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
--->
-<com.android.soundpicker.CheckedListItem xmlns:android="http://schemas.android.com/apk/res/android"
- android:layout_width="fill_parent"
- android:layout_height="wrap_content"
- android:gravity="center_vertical"
- android:background="?android:attr/selectableItemBackground"
- >
-
- <CheckedTextView
- android:id="@+id/checked_text_view"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:minHeight="?android:attr/listPreferredItemHeightSmall"
- android:textAppearance="?android:attr/textAppearanceMedium"
- android:textColor="?android:attr/textColorAlertDialogListItem"
- android:gravity="center_vertical"
- android:paddingStart="?android:attr/listPreferredItemPaddingStart"
- android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
- android:drawableStart="?android:attr/listChoiceIndicatorSingle"
- android:drawablePadding="8dp"
- android:ellipsize="marquee"
- android:layout_toLeftOf="@+id/work_icon"
- android:maxLines="3" />
-
- <ImageView
- android:id="@id/work_icon"
- android:layout_width="18dp"
- android:layout_height="18dp"
- android:layout_alignParentRight="true"
- android:layout_centerVertical="true"
- android:scaleType="centerCrop"
- android:layout_marginRight="20dp" />
-</com.android.soundpicker.CheckedListItem>
diff --git a/packages/SoundPicker2/res/layout/activity_ringtone_picker.xml b/packages/SoundPicker2/res/layout/activity_ringtone_picker.xml
deleted file mode 100644
index 6fc6080..0000000
--- a/packages/SoundPicker2/res/layout/activity_ringtone_picker.xml
+++ /dev/null
@@ -1,21 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
- Copyright (C) 2023 The Android Open Source Project
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
--->
-
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
- android:orientation="vertical"
- android:layout_width="match_parent"
- android:layout_height="match_parent"/>
\ No newline at end of file
diff --git a/packages/SoundPicker2/res/layout/add_new_sound_item.xml b/packages/SoundPicker2/res/layout/add_new_sound_item.xml
deleted file mode 100644
index 024b97e..0000000
--- a/packages/SoundPicker2/res/layout/add_new_sound_item.xml
+++ /dev/null
@@ -1,49 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
- Copyright (C) 2016 The Android Open Source Project
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
--->
-
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
- android:layout_width="fill_parent"
- android:layout_height="wrap_content"
- android:gravity="center_vertical"
- android:background="?android:attr/selectableItemBackground"
- android:focusable="true"
- android:clickable="true">
-
- <ImageView
- android:layout_width="24dp"
- android:layout_height="24dp"
- android:layout_alignParentRight="true"
- android:layout_centerVertical="true"
- android:scaleType="centerCrop"
- android:layout_marginRight="24dp"
- android:layout_marginLeft="24dp"
- android:src="@drawable/ic_add"/>
-
- <TextView
- android:id="@+id/add_new_sound_text"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:minHeight="?android:attr/listPreferredItemHeightSmall"
- android:text="@null"
- android:textColor="?android:attr/colorAccent"
- android:textAppearance="?android:attr/textAppearanceMedium"
- android:maxLines="3"
- android:gravity="center_vertical"
- android:paddingEnd="?android:attr/dialogPreferredPadding"
- android:drawablePadding="20dp"
- android:ellipsize="marquee"/>
-</LinearLayout>
\ No newline at end of file
diff --git a/packages/SoundPicker2/res/layout/fragment_ringtone_picker.xml b/packages/SoundPicker2/res/layout/fragment_ringtone_picker.xml
deleted file mode 100644
index 787f92e..0000000
--- a/packages/SoundPicker2/res/layout/fragment_ringtone_picker.xml
+++ /dev/null
@@ -1,23 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
- Copyright (C) 2023 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.
--->
-<androidx.recyclerview.widget.RecyclerView
- xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:app="http://schemas.android.com/apk/res-auto"
- android:id="@+id/recycler_view"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
-/>
\ No newline at end of file
diff --git a/packages/SoundPicker2/res/layout/fragment_tabbed_dialog.xml b/packages/SoundPicker2/res/layout/fragment_tabbed_dialog.xml
deleted file mode 100644
index 7efd911..0000000
--- a/packages/SoundPicker2/res/layout/fragment_tabbed_dialog.xml
+++ /dev/null
@@ -1,31 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
- Copyright (C) 2023 The Android Open Source Project
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
--->
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
- android:orientation="vertical"
- android:layout_width="match_parent"
- android:layout_height="match_parent">
- <com.google.android.material.tabs.TabLayout
- android:id="@+id/tabLayout"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"/>
- <androidx.viewpager2.widget.ViewPager2
- android:id="@+id/masterViewPager"
- android:paddingTop="12dp"
- android:paddingBottom="12dp"
- android:layout_width="match_parent"
- android:layout_height="match_parent"/>
-</LinearLayout>
\ No newline at end of file
diff --git a/packages/SoundPicker2/res/layout/radio_with_work_badge.xml b/packages/SoundPicker2/res/layout/radio_with_work_badge.xml
deleted file mode 100644
index 36ac93e..0000000
--- a/packages/SoundPicker2/res/layout/radio_with_work_badge.xml
+++ /dev/null
@@ -1,50 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2016 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.
--->
-
-<com.android.soundpicker.CheckedListItem
- xmlns:android="http://schemas.android.com/apk/res/android"
- android:layout_width="fill_parent"
- android:layout_height="wrap_content"
- android:gravity="center_vertical"
- android:background="?android:attr/selectableItemBackground"
- android:focusable="true"
- android:clickable="true">
-
- <CheckedTextView
- android:id="@+id/checked_text_view"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:minHeight="?android:attr/listPreferredItemHeightSmall"
- android:textAppearance="?android:attr/textAppearanceMedium"
- android:textColor="?android:attr/textColorAlertDialogListItem"
- android:gravity="center_vertical"
- android:paddingStart="20dp"
- android:paddingEnd="?android:attr/dialogPreferredPadding"
- android:drawableStart="?android:attr/listChoiceIndicatorSingle"
- android:drawablePadding="20dp"
- android:ellipsize="marquee"
- android:layout_toLeftOf="@+id/work_icon"
- android:maxLines="3"/>
-
- <ImageView
- android:id="@id/work_icon"
- android:layout_width="18dp"
- android:layout_height="18dp"
- android:layout_alignParentRight="true"
- android:layout_centerVertical="true"
- android:scaleType="centerCrop"
- android:layout_marginRight="20dp"/>
-</com.android.soundpicker.CheckedListItem>
diff --git a/packages/SoundPicker2/res/raw/default_alarm_alert.ogg b/packages/SoundPicker2/res/raw/default_alarm_alert.ogg
deleted file mode 100644
index e69de29..0000000
--- a/packages/SoundPicker2/res/raw/default_alarm_alert.ogg
+++ /dev/null
diff --git a/packages/SoundPicker2/res/raw/default_notification_sound.ogg b/packages/SoundPicker2/res/raw/default_notification_sound.ogg
deleted file mode 100644
index e69de29..0000000
--- a/packages/SoundPicker2/res/raw/default_notification_sound.ogg
+++ /dev/null
diff --git a/packages/SoundPicker2/res/raw/default_ringtone.ogg b/packages/SoundPicker2/res/raw/default_ringtone.ogg
deleted file mode 100644
index e69de29..0000000
--- a/packages/SoundPicker2/res/raw/default_ringtone.ogg
+++ /dev/null
diff --git a/packages/SoundPicker2/res/values/config.xml b/packages/SoundPicker2/res/values/config.xml
deleted file mode 100644
index 4e237a2..0000000
--- a/packages/SoundPicker2/res/values/config.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2016 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.
--->
-
-<!-- These resources are around just to allow their values to be customized
- for different hardware and product builds. Do not translate.
-
- NOTE: The naming convention is "config_camelCaseValue". -->
-<resources xmlns:android="http://schemas.android.com/apk/res/android">
- <!-- True if the ringtone picker should show the ok/cancel buttons. If it is not shown, the
- ringtone will be automatically selected when the picker is closed. -->
- <bool name="config_showOkCancelButtons">true</bool>
-</resources>
diff --git a/packages/SoundPicker2/res/values/strings.xml b/packages/SoundPicker2/res/values/strings.xml
deleted file mode 100644
index ab7b95a..0000000
--- a/packages/SoundPicker2/res/values/strings.xml
+++ /dev/null
@@ -1,47 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2009 The Android Open Source Project
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
--->
-
-<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <!-- Choice in the ringtone picker. If chosen, the default ringtone will be used. -->
- <string name="ringtone_default">Default ringtone</string>
-
- <!-- Choice in the notification sound picker. If chosen, the default notification sound will be
- used. -->
- <string name="notification_sound_default">Default notification sound</string>
-
- <!-- Choice in the alarm sound picker. If chosen, the default alarm sound will be used. -->
- <string name="alarm_sound_default">Default alarm sound</string>
-
- <!-- Text for the RingtonePicker item that allows adding a new ringtone. -->
- <string name="add_ringtone_text">Add ringtone</string>
- <!-- Text for the RingtonePicker item that allows adding a new alarm. -->
- <string name="add_alarm_text">Add alarm</string>
- <!-- Text for the RingtonePicker item that allows adding a new notification. -->
- <string name="add_notification_text">Add notification</string>
- <!-- Text for the RingtonePicker item ContextMenu that allows deleting a custom ringtone. -->
- <string name="delete_ringtone_text">Delete</string>
- <!-- Text for the Toast displayed when adding a custom ringtone fails. -->
- <string name="unable_to_add_ringtone">Unable to add custom ringtone</string>
- <!-- Text for the Toast displayed when deleting a custom ringtone fails. -->
- <string name="unable_to_delete_ringtone">Unable to delete custom ringtone</string>
-
- <!-- Text for the name of the app. [CHAR LIMIT=12] -->
- <string name="app_label">Sounds</string>
-
- <string name="empty_list">The list is empty</string>
- <string name="sound_page_title">Sound</string>
- <string name="vibration_page_title">Vibration</string>
-</resources>
diff --git a/packages/SoundPicker2/res/values/styles.xml b/packages/SoundPicker2/res/values/styles.xml
deleted file mode 100644
index d22d9c4..0000000
--- a/packages/SoundPicker2/res/values/styles.xml
+++ /dev/null
@@ -1,22 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2014 The Android Open Source Project
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
--->
-
-<resources xmlns:android="http://schemas.android.com/apk/res/android">
-
- <style name="PickerDialogTheme" parent="@*android:style/Theme.DeviceDefault.Settings.Dialog">
- </style>
-
-</resources>
diff --git a/packages/SoundPicker2/src/com/android/soundpicker/BasePickerFragment.java b/packages/SoundPicker2/src/com/android/soundpicker/BasePickerFragment.java
deleted file mode 100644
index 4fc2a86..0000000
--- a/packages/SoundPicker2/src/com/android/soundpicker/BasePickerFragment.java
+++ /dev/null
@@ -1,312 +0,0 @@
-/*
- * Copyright (C) 2023 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.soundpicker;
-
-import android.app.Activity;
-import android.content.ContentProvider;
-import android.content.Intent;
-import android.graphics.drawable.Drawable;
-import android.media.RingtoneManager;
-import android.net.Uri;
-import android.os.Bundle;
-import android.os.UserHandle;
-import android.os.UserManager;
-import android.provider.MediaStore;
-import android.util.Log;
-import android.view.View;
-
-import androidx.annotation.NonNull;
-import androidx.fragment.app.Fragment;
-import androidx.lifecycle.ViewModelProvider;
-import androidx.recyclerview.widget.LinearLayoutManager;
-import androidx.recyclerview.widget.RecyclerView;
-
-import dagger.hilt.android.AndroidEntryPoint;
-
-import java.util.Objects;
-
-/**
- * Base class for generic picker fragments.
- *
- * <p>This fragment displays a recycler view that is populated by a {@link RingtoneListViewAdapter}
- * with data provided by a {@link RingtoneListHandler}. Each item can be selected on click,
- * which also triggers a ringtone preview performed by the shared {@link RingtonePickerViewModel}.
- * The ringtone preview uses the selection state of all picker fragments (e.g. sound selected by
- * one fragment and vibration selected by another).
- */
-@AndroidEntryPoint(Fragment.class)
-public abstract class BasePickerFragment extends Hilt_BasePickerFragment implements
- RingtoneListViewAdapter.Callbacks {
-
- private static final String TAG = "BasePickerFragment";
- private static final String COLUMN_LABEL = MediaStore.Audio.Media.TITLE;
- private boolean mIsManagedProfile;
- private Drawable mWorkIconDrawable;
-
- protected RingtoneListViewAdapter mRingtoneListViewAdapter;
- protected RecyclerView mRecyclerView;
- protected RingtonePickerViewModel.Config mPickerConfig;
- protected RingtonePickerViewModel mRingtonePickerViewModel;
- protected RingtoneListHandler.Config mRingtoneListConfig;
- protected RingtoneListHandler mRingtoneListHandler;
-
- public BasePickerFragment() {
- super(R.layout.fragment_ringtone_picker);
- }
-
- @Override
- public void onViewCreated(@NonNull View view, Bundle savedInstanceState) {
- super.onViewCreated(view, savedInstanceState);
- mRingtonePickerViewModel = new ViewModelProvider(requireActivity()).get(
- RingtonePickerViewModel.class);
- mRingtoneListHandler = getRingtoneListHandler();
- mRecyclerView = view.requireViewById(R.id.recycler_view);
-
- mPickerConfig = mRingtonePickerViewModel.getPickerConfig();
- mRingtoneListConfig = mRingtoneListHandler.getRingtoneListConfig();
-
- mIsManagedProfile = UserManager.get(requireActivity()).isManagedProfile(
- mPickerConfig.userId);
-
- mRingtoneListViewAdapter = createRingtoneListViewAdapter();
- mRecyclerView.setHasFixedSize(true);
- mRecyclerView.setAdapter(mRingtoneListViewAdapter);
- mRecyclerView.setLayoutManager(new LinearLayoutManager(requireActivity()));
- setSelectedItem(mRingtoneListHandler.getSelectedItemPosition());
- prepareRecyclerView(mRecyclerView);
- }
-
- @Override
- public boolean isWorkRingtone(int position) {
- if (!mIsManagedProfile) {
- return false;
- }
-
- /*
- * Display the work icon if the ringtone belongs to a work profile. We
- * can tell that a ringtone belongs to a work profile if the picker user
- * is a managed profile, the ringtone Uri is in external storage, and
- * either the uri has no user id or has the id of the picker user
- */
- Uri currentUri = mRingtoneListHandler.getRingtoneUri(position);
- int uriUserId = ContentProvider.getUserIdFromUri(currentUri,
- mPickerConfig.userId);
- Uri uriWithoutUserId = ContentProvider.getUriWithoutUserId(currentUri);
-
- return uriUserId == mPickerConfig.userId
- && uriWithoutUserId.toString().startsWith(
- MediaStore.Audio.Media.EXTERNAL_CONTENT_URI.toString());
- }
-
- @Override
- public Drawable getWorkIconDrawable() {
- if (mWorkIconDrawable == null) {
- mWorkIconDrawable = requireActivity().getPackageManager()
- .getUserBadgeForDensityNoBackground(
- UserHandle.of(mPickerConfig.userId), /* density= */ -1);
- }
-
- return mWorkIconDrawable;
- }
-
- @Override
- public void onRingtoneSelected(int position) {
- setSelectedItem(position);
-
- // In the buttonless (watch-only) version, preemptively set our result since
- // we won't have another chance to do so before the activity closes.
- if (!mPickerConfig.showOkCancelButtons) {
- setSuccessResultWithSelectedRingtone();
- }
-
- // Play clip
- mRingtonePickerViewModel.playRingtone();
- }
-
- @Override
- public void onAddRingtoneSelected() {
- addRingtoneAsync();
- }
-
- /**
- * Sets up the list by adding fixed items to the top and bottom, if required. And sets the
- * selected item in the list.
- * @param recyclerView The recyclerview that contains the list of displayed items.
- */
- protected void prepareRecyclerView(@NonNull RecyclerView recyclerView) {
- // Reset the static item count, as this method can be called multiple times
- mRingtoneListHandler.resetFixedItems();
-
- if (mRingtoneListConfig.hasDefaultItem) {
- int defaultItemPos = addDefaultRingtoneItem();
-
- if (getSelectedItem() < 0
- && RingtoneManager.isDefault(mRingtoneListConfig.initialSelectedUri)) {
- setSelectedItem(defaultItemPos);
- }
- }
-
- if (mRingtoneListConfig.hasSilentItem) {
- int silentItemPos = addSilentItem();
-
- // The 'Silent' item should use a null Uri
- if (getSelectedItem() < 0
- && mRingtoneListConfig.initialSelectedUri == null) {
- setSelectedItem(silentItemPos);
- }
- }
-
- if (getSelectedItem() < 0) {
- setSelectedItem(mRingtoneListHandler.getRingtonePosition(
- mRingtoneListConfig.initialSelectedUri));
- }
-
- // In the buttonless (watch-only) version, preemptively set our result since we won't
- // have another chance to do so before the activity closes.
- if (!mPickerConfig.showOkCancelButtons) {
- setSuccessResultWithSelectedRingtone();
- }
-
- addNewRingtoneItem();
-
- // Enable context menu in ringtone items
- registerForContextMenu(recyclerView);
- }
-
- /**
- * Returns the fragment's sound/vibration list handler.
- * @return The ringtone list handler.
- */
- protected abstract RingtoneListHandler getRingtoneListHandler();
-
- /**
- * Starts the process to add a new ringtone to the list of ringtones asynchronously.
- * Currently, only works for adding sound files.
- */
- protected abstract void addRingtoneAsync();
-
- /**
- * Adds an item to the end of the list that can be used to add new ringtones to the list.
- * Currently, only works for adding sound files.
- */
- protected abstract void addNewRingtoneItem();
-
- protected int getSelectedItem() {
- return mRingtoneListHandler.getSelectedItemPosition();
- }
-
- /**
- * Returns the selected URI to the caller activity.
- */
- protected void setSuccessResultWithSelectedRingtone() {
- requireActivity().setResult(Activity.RESULT_OK,
- new Intent().putExtra(RingtoneManager.EXTRA_RINGTONE_PICKED_URI,
- mRingtonePickerViewModel.getSelectedRingtoneUri()));
- }
-
- /**
- * Creates a ringtone recyclerview adapter using the ringtone manager cursor.
- * @return The created RingtoneListViewAdapter.
- */
- protected RingtoneListViewAdapter createRingtoneListViewAdapter() {
- LocalizedCursor cursor = new LocalizedCursor(
- mRingtoneListHandler.getRingtoneCursor(), getResources(), COLUMN_LABEL);
- return new RingtoneListViewAdapter(cursor, /* RingtoneListViewAdapterCallbacks= */ this);
- }
-
- /**
- * Sets the selected item in the list and scroll to the position in the recyclerview.
- * @param pos the position of the selected item in the list.
- */
- protected void setSelectedItem(int pos) {
- Objects.requireNonNull(mRingtoneListViewAdapter);
- mRingtoneListHandler.setSelectedItemPosition(pos);
- mRingtoneListViewAdapter.setSelectedItem(pos);
- mRingtoneListHandler.setSelectedItemId(mRingtoneListViewAdapter.getItemId(pos));
- mRecyclerView.scrollToPosition(pos);
- }
-
- /**
- * Adds a fixed item to the fixed items list . A fixed item is one that is not from
- * the RingtoneManager.
- *
- * @param textResId The resource ID of the text for the item.
- * @return The index of the inserted fixed item in the adapter.
- */
- protected int addFixedItem(int textResId) {
- return mRingtoneListViewAdapter.addTitleForFixedItem(textResId);
- }
-
- /**
- * Re-query RingtoneManager for the most recent set of installed ringtones. May move the
- * selected item position to match the new position of the chosen ringtone.
- * <p>
- * This should only need to happen after adding or removing a ringtone.
- */
- protected void requeryForAdapter() {
- mRingtonePickerViewModel.reinit();
- // Refresh and set a new cursor, and closing the old one.
- mRingtoneListViewAdapter = createRingtoneListViewAdapter();
- mRecyclerView.setAdapter(mRingtoneListViewAdapter);
- prepareRecyclerView(mRecyclerView);
-
- // Update selected item location.
- for (int i = 0; i < mRingtoneListViewAdapter.getItemCount(); i++) {
- if (mRingtoneListViewAdapter.getItemId(i)
- == mRingtoneListHandler.getSelectedItemId()) {
- setSelectedItem(i);
- return;
- }
- }
-
- // If selected item is still unknown, then set it to the default item, if available.
- // If it's not available, then attempt to set it to the silent item in the list.
- int selectedPosition = mRingtoneListHandler.getDefaultItemPosition();
-
- if (selectedPosition < 0) {
- selectedPosition = mRingtoneListHandler.getSilentItemPosition();
- }
-
- setSelectedItem(selectedPosition);
- }
-
- private int addDefaultRingtoneItem() {
- int defaultItemPosInAdapter = addFixedItem(
- RingtonePickerViewModel.getDefaultRingtoneItemTextByType(
- mPickerConfig.ringtoneType));
- int defaultItemPosInListHandler = mRingtoneListHandler.addDefaultItem();
-
- if (defaultItemPosInAdapter != defaultItemPosInListHandler) {
- Log.wtf(TAG, "Default item position in adapter and list handler must match.");
- return RingtoneListHandler.ITEM_POSITION_UNKNOWN;
- }
-
- return defaultItemPosInListHandler;
- }
-
- private int addSilentItem() {
- int silentItemPosInAdapter = addFixedItem(com.android.internal.R.string.ringtone_silent);
- int silentItemPosInListHandler = mRingtoneListHandler.addSilentItem();
-
- if (silentItemPosInAdapter != silentItemPosInListHandler) {
- Log.wtf(TAG, "Silent item position in adapter and list handler must match.");
- return RingtoneListHandler.ITEM_POSITION_UNKNOWN;
- }
-
- return silentItemPosInListHandler;
- }
-}
diff --git a/packages/SoundPicker2/src/com/android/soundpicker/CheckedListItem.java b/packages/SoundPicker2/src/com/android/soundpicker/CheckedListItem.java
deleted file mode 100644
index 819ae98..0000000
--- a/packages/SoundPicker2/src/com/android/soundpicker/CheckedListItem.java
+++ /dev/null
@@ -1,67 +0,0 @@
-/*
- * Copyright (C) 2016 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.soundpicker;
-
-import android.content.Context;
-import android.util.AttributeSet;
-import android.widget.Checkable;
-import android.widget.CheckedTextView;
-import android.widget.RelativeLayout;
-
-/**
- * The {@link CheckedListItem} is a layout item that represents a ringtone, and is used in
- * {@link RingtonePickerActivity}. It contains the ringtone's name, and a work badge to right of the
- * name if the ringtone belongs to a work profile.
- */
-public class CheckedListItem extends RelativeLayout implements Checkable {
-
- public CheckedListItem(Context context) {
- super(context);
- }
-
- public CheckedListItem(Context context, AttributeSet attrs) {
- super(context, attrs);
- }
-
- public CheckedListItem(Context context, AttributeSet attrs, int defStyleAttr) {
- super(context, attrs, defStyleAttr);
- }
-
- public CheckedListItem(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
- super(context, attrs, defStyleAttr, defStyleRes);
- }
-
- @Override
- public void setChecked(boolean checked) {
- getCheckedTextView().setChecked(checked);
- }
-
- @Override
- public boolean isChecked() {
- return getCheckedTextView().isChecked();
- }
-
- @Override
- public void toggle() {
- getCheckedTextView().toggle();
- }
-
- private CheckedTextView getCheckedTextView() {
- return (CheckedTextView) findViewById(R.id.checked_text_view);
- }
-
-}
diff --git a/packages/SoundPicker2/src/com/android/soundpicker/ListeningExecutorServiceFactory.java b/packages/SoundPicker2/src/com/android/soundpicker/ListeningExecutorServiceFactory.java
deleted file mode 100644
index afdbf05..0000000
--- a/packages/SoundPicker2/src/com/android/soundpicker/ListeningExecutorServiceFactory.java
+++ /dev/null
@@ -1,44 +0,0 @@
-/*
- * Copyright (C) 2023 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.soundpicker;
-
-import com.google.common.util.concurrent.ListeningExecutorService;
-import com.google.common.util.concurrent.MoreExecutors;
-
-import java.util.concurrent.Executors;
-
-import javax.inject.Inject;
-import javax.inject.Singleton;
-
-/**
- * A factory class used to create {@link ListeningExecutorService}.
- */
-@Singleton
-public class ListeningExecutorServiceFactory {
-
- @Inject
- ListeningExecutorServiceFactory() {
- }
-
- /**
- * Returns a single thread {@link ListeningExecutorService}.
- *
- */
- public ListeningExecutorService createSingleThreadExecutor() {
- return MoreExecutors.listeningDecorator(Executors.newSingleThreadExecutor());
- }
-}
diff --git a/packages/SoundPicker2/src/com/android/soundpicker/LocalizedCursor.java b/packages/SoundPicker2/src/com/android/soundpicker/LocalizedCursor.java
deleted file mode 100644
index 83d04a3..0000000
--- a/packages/SoundPicker2/src/com/android/soundpicker/LocalizedCursor.java
+++ /dev/null
@@ -1,117 +0,0 @@
-/*
- * Copyright (C) 2023 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.soundpicker;
-
-import android.content.res.Resources;
-import android.database.Cursor;
-import android.database.CursorWrapper;
-import android.util.Log;
-import android.util.TypedValue;
-
-import androidx.annotation.Nullable;
-
-import java.util.Locale;
-import java.util.regex.Pattern;
-
-/**
- * A cursor wrapper class mainly used to guarantee getting a ringtone title
- */
-final class LocalizedCursor extends CursorWrapper {
-
- private static final String TAG = "LocalizedCursor";
- private static final String SOUND_NAME_RES_PREFIX = "sound_name_";
-
- private final int mTitleIndex;
- private final Resources mResources;
- private final Pattern mSanitizePattern;
- private final String mNamePrefix;
-
- LocalizedCursor(Cursor cursor, Resources resources, String columnLabel) {
- super(cursor);
- mTitleIndex = mCursor.getColumnIndex(columnLabel);
- mResources = resources;
- mSanitizePattern = Pattern.compile("[^a-zA-Z0-9]");
- if (mTitleIndex == -1) {
- Log.e(TAG, "No index for column " + columnLabel);
- mNamePrefix = null;
- } else {
- mNamePrefix = buildNamePrefix(mResources);
- }
- }
-
- /**
- * Builds the prefix for the name of the resource to look up.
- * The format is: "ResourcePackageName::ResourceTypeName/" (the type name is expected to be
- * "string" but let's not hardcode it).
- * Here we use an existing resource "notification_sound_default" which is always expected to be
- * found.
- *
- * @param resources Application's resources
- * @return the built name prefix, or null if failed to build.
- */
- @Nullable
- private static String buildNamePrefix(Resources resources) {
- try {
- return String.format("%s:%s/%s",
- resources.getResourcePackageName(R.string.notification_sound_default),
- resources.getResourceTypeName(R.string.notification_sound_default),
- SOUND_NAME_RES_PREFIX);
- } catch (Resources.NotFoundException e) {
- Log.e(TAG, "Failed to build the prefix for the name of the resource.", e);
- }
-
- return null;
- }
-
- /**
- * Process resource name to generate a valid resource name.
- *
- * @return a non-null String
- */
- private String sanitize(String input) {
- if (input == null) {
- return "";
- }
- return mSanitizePattern.matcher(input).replaceAll("_").toLowerCase(Locale.ROOT);
- }
-
- @Override
- public String getString(int columnIndex) {
- final String defaultName = mCursor.getString(columnIndex);
- if ((columnIndex != mTitleIndex) || (mNamePrefix == null)) {
- return defaultName;
- }
- TypedValue value = new TypedValue();
- try {
- // the name currently in the database is used to derive a name to match
- // against resource names in this package
- mResources.getValue(mNamePrefix + sanitize(defaultName), value,
- /* resolveRefs= */ false);
- } catch (Resources.NotFoundException e) {
- Log.d(TAG, "Failed to get localized string. Using default string instead.", e);
- return defaultName;
- }
- if ((value != null) && (value.type == TypedValue.TYPE_STRING)) {
- Log.d(TAG, String.format("Replacing name %s with %s",
- defaultName, value.string.toString()));
- return value.string.toString();
- } else {
- Log.e(TAG, "Invalid value when looking up localized name, using " + defaultName);
- return defaultName;
- }
- }
-}
diff --git a/packages/SoundPicker2/src/com/android/soundpicker/RingtoneFactory.java b/packages/SoundPicker2/src/com/android/soundpicker/RingtoneFactory.java
deleted file mode 100644
index 6817f53..0000000
--- a/packages/SoundPicker2/src/com/android/soundpicker/RingtoneFactory.java
+++ /dev/null
@@ -1,60 +0,0 @@
-/*
- * Copyright (C) 2023 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.soundpicker;
-
-import android.content.Context;
-import android.media.AudioAttributes;
-import android.media.Ringtone;
-import android.media.RingtoneManager;
-import android.net.Uri;
-
-import dagger.hilt.android.qualifiers.ApplicationContext;
-
-import javax.inject.Inject;
-import javax.inject.Singleton;
-
-/**
- * A factory class used to create {@link Ringtone}.
- */
-@Singleton
-public class RingtoneFactory {
-
- private final Context mApplicationContext;
-
- @Inject
- RingtoneFactory(@ApplicationContext Context applicationContext) {
- mApplicationContext = applicationContext;
- }
-
- /**
- * Returns a {@link Ringtone} built from the provided URI and audio attributes flags.
- *
- * @param uri The URI used to build the {@link Ringtone}.
- * @param audioAttributesFlags A combination of audio attribute flags that affect the volume
- * and settings when playing the ringtone.
- * @return the built {@link Ringtone}.
- */
- public Ringtone create(Uri uri, int audioAttributesFlags) {
- AudioAttributes audioAttributes = new AudioAttributes.Builder()
- .setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE)
- .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
- .setFlags(audioAttributesFlags)
- .build();
- return RingtoneManager.getRingtone(mApplicationContext, uri,
- /* volumeShaperConfig= */ null, audioAttributes);
- }
-}
diff --git a/packages/SoundPicker2/src/com/android/soundpicker/RingtoneListHandler.java b/packages/SoundPicker2/src/com/android/soundpicker/RingtoneListHandler.java
deleted file mode 100644
index bb38e0e..0000000
--- a/packages/SoundPicker2/src/com/android/soundpicker/RingtoneListHandler.java
+++ /dev/null
@@ -1,222 +0,0 @@
-/*
- * Copyright (C) 2023 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.soundpicker;
-
-import static java.util.Objects.requireNonNull;
-
-import android.annotation.Nullable;
-import android.database.Cursor;
-import android.media.RingtoneManager;
-import android.net.Uri;
-import android.util.Log;
-
-import androidx.annotation.NonNull;
-
-import javax.inject.Inject;
-
-/**
- * Handles ringtone list state and actions. This includes keeping track of the selected item,
- * ringtone manager cursor and added items to the list.
- */
-public class RingtoneListHandler {
-
- // TODO: We're using an empty URI instead of null, because null URIs still produce a sound,
- // while empty ones don't (Potentially this might be due to empty URIs being perceived as
- // malformed ones). We will switch to using the official silent URIs (SOUND_OFF, VIBRATION_OFF)
- // once they become available.
- static final Uri SILENT_URI = Uri.EMPTY;
- static final int ITEM_POSITION_UNKNOWN = -1;
-
- private static final String TAG = "RingtoneListHandler";
-
- /** The position in the list of the 'Silent' item. */
- private int mSilentItemPosition = ITEM_POSITION_UNKNOWN;
- /** The position in the list of the 'Default' item. */
- private int mDefaultItemPosition = ITEM_POSITION_UNKNOWN;
- /** The number of fixed items in the list. */
- private int mFixedItemCount;
- /**
- * Stable ID for the ringtone that is currently selected (may be -1 if no ringtone is selected).
- */
- private long mSelectedItemId = -1;
- private int mSelectedItemPosition = ITEM_POSITION_UNKNOWN;
-
- private RingtoneManager mRingtoneManager;
- private Config mRingtoneListConfig;
- private Cursor mRingtoneCursor;
-
- /**
- * Holds immutable info on the ringtone list that is displayed.
- */
- static final class Config {
- /**
- * Whether this list has the 'Default' item.
- */
- public final boolean hasDefaultItem;
- /**
- * The Uri to play when the 'Default' item is clicked.
- */
- public final Uri uriForDefaultItem;
- /**
- * Whether this list has the 'Silent' item.
- */
- public final boolean hasSilentItem;
- /**
- * The initially selected uri in the list.
- */
- public final Uri initialSelectedUri;
-
- Config(boolean hasDefaultItem, Uri uriForDefaultItem, boolean hasSilentItem,
- Uri initialSelectedUri) {
- this.hasDefaultItem = hasDefaultItem;
- this.uriForDefaultItem = uriForDefaultItem;
- this.hasSilentItem = hasSilentItem;
- this.initialSelectedUri = initialSelectedUri;
- }
- }
-
- @Inject
- RingtoneListHandler() {
- }
-
- void init(@NonNull Config ringtoneListConfig,
- @NonNull RingtoneManager ringtoneManager, @NonNull Cursor ringtoneCursor) {
- mRingtoneManager = requireNonNull(ringtoneManager);
- mRingtoneListConfig = requireNonNull(ringtoneListConfig);
- mRingtoneCursor = requireNonNull(ringtoneCursor);
- }
-
- Config getRingtoneListConfig() {
- return mRingtoneListConfig;
- }
-
- Cursor getRingtoneCursor() {
- requireInitCalled();
- return mRingtoneCursor;
- }
-
- Uri getRingtoneUri(int position) {
- if (position < 0) {
- Log.w(TAG, "Selected item position is unknown.");
- // When the selected item is ITEM_POSITION_UNKNOWN, it is not the case we expected.
- // We return SILENT_URI for this case.
- return SILENT_URI;
- } else if (position == mDefaultItemPosition) {
- // Use the default Uri that they originally gave us.
- return mRingtoneListConfig.uriForDefaultItem;
- } else if (position == mSilentItemPosition) {
- // Use SILENT_URI for the 'Silent' item.
- return SILENT_URI;
- } else {
- requireInitCalled();
- return mRingtoneManager.getRingtoneUri(mapListPositionToRingtonePosition(position));
- }
- }
-
- int getRingtonePosition(Uri uri) {
- requireInitCalled();
- return mapRingtonePositionToListPosition(mRingtoneManager.getRingtonePosition(uri));
- }
-
- void resetFixedItems() {
- mFixedItemCount = 0;
- mDefaultItemPosition = ITEM_POSITION_UNKNOWN;
- mSilentItemPosition = ITEM_POSITION_UNKNOWN;
- }
-
- int addDefaultItem() {
- if (mDefaultItemPosition < 0) {
- mDefaultItemPosition = addFixedItem();
- }
- return mDefaultItemPosition;
- }
-
- int getDefaultItemPosition() {
- return mDefaultItemPosition;
- }
-
- int addSilentItem() {
- if (mSilentItemPosition < 0) {
- mSilentItemPosition = addFixedItem();
- }
- return mSilentItemPosition;
- }
-
- public int getSilentItemPosition() {
- return mSilentItemPosition;
- }
-
- int getSelectedItemPosition() {
- return mSelectedItemPosition;
- }
-
- void setSelectedItemPosition(int selectedItemPosition) {
- mSelectedItemPosition = selectedItemPosition;
- }
-
- void setSelectedItemId(long selectedItemId) {
- mSelectedItemId = selectedItemId;
- }
-
- long getSelectedItemId() {
- return mSelectedItemId;
- }
-
- @Nullable
- Uri getSelectedRingtoneUri() {
- return getRingtoneUri(mSelectedItemPosition);
- }
-
- /**
- * Maps the item position in the list, to its equivalent position in the RingtoneManager.
- *
- * @param itemPosition the position of item in the list.
- * @return position of the item in the RingtoneManager.
- */
- private int mapListPositionToRingtonePosition(int itemPosition) {
- // If the manager position is less than add items, then return that.
- if (itemPosition < mFixedItemCount) return itemPosition;
-
- return itemPosition - mFixedItemCount;
- }
-
- /**
- * Maps the item position in the RingtoneManager, to its equivalent position in the list.
- *
- * @param itemPosition the position of the item in the RingtoneManager.
- * @return position of the item in the list.
- */
- private int mapRingtonePositionToListPosition(int itemPosition) {
- // If the manager position is less than add items, then return that.
- if (itemPosition < 0) return itemPosition;
-
- return itemPosition + mFixedItemCount;
- }
-
- /**
- * Increments the number of added fixed items and returns the index of the newest added item.
- * @return index of the newest added fixed item.
- */
- private int addFixedItem() {
- return mFixedItemCount++;
- }
-
- private void requireInitCalled() {
- requireNonNull(mRingtoneManager);
- requireNonNull(mRingtoneCursor);
- }
-}
diff --git a/packages/SoundPicker2/src/com/android/soundpicker/RingtoneListViewAdapter.java b/packages/SoundPicker2/src/com/android/soundpicker/RingtoneListViewAdapter.java
deleted file mode 100644
index 4ca8943..0000000
--- a/packages/SoundPicker2/src/com/android/soundpicker/RingtoneListViewAdapter.java
+++ /dev/null
@@ -1,264 +0,0 @@
-/*
- * Copyright (C) 2023 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.soundpicker;
-
-import static com.android.internal.widget.RecyclerView.NO_ID;
-
-import android.database.Cursor;
-import android.graphics.drawable.Drawable;
-import android.media.RingtoneManager;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.CheckedTextView;
-import android.widget.ImageView;
-import android.widget.TextView;
-
-import androidx.annotation.StringRes;
-import androidx.recyclerview.widget.RecyclerView;
-
-import org.jetbrains.annotations.NotNull;
-
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Objects;
-
-/**
- * The adapter presents a list of ringtones which may include fixed item in the list and an action
- * button at the end.
- *
- * The adapter handles three different types of items:
- * <ul>
- * <li>FIXED: Fixed items are items added to the top of the list. These items can not be modified
- * and their position will never change.
- * <li>DYNAMIC: Dynamic items are items from the ringtone manager. These items can be modified
- * and their position can change.
- * <li>FOOTER: A footer item is an added button to the end of the list. This item can be clicked
- * but not selected and its position will never change.
- * </ul>
- */
-final class RingtoneListViewAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
-
- private static final int VIEW_TYPE_FIXED_ITEM = 0;
- private static final int VIEW_TYPE_DYNAMIC_ITEM = 1;
- private static final int VIEW_TYPE_ADD_RINGTONE_ITEM = 2;
- private final Cursor mCursor;
- private final List<Integer> mFixedItemTitles;
- private final Callbacks mCallbacks;
- private final int mRowIDColumn;
- private int mSelectedItem = -1;
- @StringRes private Integer mAddRingtoneItemTitle;
-
- /** Provides callbacks for the adapter. */
- interface Callbacks {
- void onRingtoneSelected(int position);
- void onAddRingtoneSelected();
- boolean isWorkRingtone(int position);
- Drawable getWorkIconDrawable();
- }
-
- RingtoneListViewAdapter(Cursor cursor,
- Callbacks callbacks) {
- mCursor = cursor;
- mCallbacks = callbacks;
- mFixedItemTitles = new ArrayList<>();
- mRowIDColumn = mCursor != null ? mCursor.getColumnIndex("_id") : -1;
- setHasStableIds(true);
- }
-
- void setSelectedItem(int position) {
- notifyItemChanged(mSelectedItem);
- mSelectedItem = position;
- notifyItemChanged(mSelectedItem);
- }
-
- /**
- * Adds title to the fixed items list and returns the index of the newest added item.
- * @param textResId the title to add to the fixed items list.
- * @return The index of the newest added item in the fixed items list.
- */
- int addTitleForFixedItem(@StringRes int textResId) {
- mFixedItemTitles.add(textResId);
- notifyItemInserted(mFixedItemTitles.size() - 1);
- return mFixedItemTitles.size() - 1;
- }
-
- void addTitleForAddRingtoneItem(@StringRes int textResId) {
- mAddRingtoneItemTitle = textResId;
- notifyItemInserted(getItemCount() - 1);
- }
-
- @NotNull
- @Override
- public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
- LayoutInflater inflater = LayoutInflater.from(parent.getContext());
-
- if (viewType == VIEW_TYPE_FIXED_ITEM) {
- View fixedItemView = inflater.inflate(
- com.android.internal.R.layout.select_dialog_singlechoice_material, parent,
- false);
-
- return new FixedItemViewHolder(fixedItemView, mCallbacks);
- }
-
- if (viewType == VIEW_TYPE_ADD_RINGTONE_ITEM) {
- View addRingtoneItemView = inflater.inflate(R.layout.add_new_sound_item, parent, false);
-
- return new AddRingtoneItemViewHolder(addRingtoneItemView,
- mCallbacks);
- }
-
- View view = inflater.inflate(R.layout.radio_with_work_badge, parent, false);
-
- return new DynamicItemViewHolder(view, mCallbacks);
- }
-
- @Override
- public void onBindViewHolder(@NotNull RecyclerView.ViewHolder holder, int position) {
- if (holder instanceof FixedItemViewHolder) {
- FixedItemViewHolder viewHolder = (FixedItemViewHolder) holder;
-
- viewHolder.onBind(mFixedItemTitles.get(position),
- /* isChecked= */ position == mSelectedItem);
- return;
- }
- if (holder instanceof AddRingtoneItemViewHolder) {
- AddRingtoneItemViewHolder viewHolder = (AddRingtoneItemViewHolder) holder;
-
- viewHolder.onBind(mAddRingtoneItemTitle);
- return;
- }
-
- if (!(holder instanceof DynamicItemViewHolder)) {
- throw new IllegalArgumentException("holder type is not supported");
- }
-
- DynamicItemViewHolder viewHolder = (DynamicItemViewHolder) holder;
- int pos = position - mFixedItemTitles.size();
- if (!mCursor.moveToPosition(pos)) {
- throw new IllegalStateException("Could not move cursor to position: " + pos);
- }
-
- Drawable workIcon = (mCallbacks != null)
- && mCallbacks.isWorkRingtone(position)
- ? mCallbacks.getWorkIconDrawable() : null;
-
- viewHolder.onBind(mCursor.getString(RingtoneManager.TITLE_COLUMN_INDEX),
- /* isChecked= */ position == mSelectedItem, workIcon);
- }
-
- @Override
- public int getItemViewType(int position) {
- if (!mFixedItemTitles.isEmpty() && position < mFixedItemTitles.size()) {
- return VIEW_TYPE_FIXED_ITEM;
- }
- if (mAddRingtoneItemTitle != null && position == getItemCount() - 1) {
- return VIEW_TYPE_ADD_RINGTONE_ITEM;
- }
-
- return VIEW_TYPE_DYNAMIC_ITEM;
- }
-
- @Override
- public int getItemCount() {
- int itemCount = mFixedItemTitles.size() + mCursor.getCount();
-
- if (mAddRingtoneItemTitle != null) {
- itemCount++;
- }
-
- return itemCount;
- }
-
- @Override
- public long getItemId(int position) {
- int itemViewType = getItemViewType(position);
- if (itemViewType == VIEW_TYPE_FIXED_ITEM) {
- // Since the item is a fixed item, then we can use the position as a stable ID
- // since the order of the fixed items should never change.
- return position;
- }
- if (itemViewType == VIEW_TYPE_DYNAMIC_ITEM && mCursor != null
- && mCursor.moveToPosition(position - mFixedItemTitles.size())
- && mRowIDColumn != -1) {
- return mCursor.getLong(mRowIDColumn) + mFixedItemTitles.size();
- }
-
- // The position is either invalid or the item is the add ringtone item view, so no stable
- // ID is returned. Add ringtone item view cannot be selected and only include an action
- // buttons.
- return NO_ID;
- }
-
- private static class DynamicItemViewHolder extends RecyclerView.ViewHolder {
- private final CheckedTextView mTitleTextView;
- private final ImageView mWorkIcon;
-
- DynamicItemViewHolder(View itemView, Callbacks listener) {
- super(itemView);
-
- mTitleTextView = itemView.requireViewById(R.id.checked_text_view);
- mWorkIcon = itemView.requireViewById(R.id.work_icon);
- itemView.setOnClickListener(v -> listener.onRingtoneSelected(this.getLayoutPosition()));
- }
-
- void onBind(String title, boolean isChecked, Drawable workIcon) {
- mTitleTextView.setText(title);
- mTitleTextView.setChecked(isChecked);
-
- if (workIcon == null) {
- mWorkIcon.setVisibility(View.GONE);
- } else {
- mWorkIcon.setImageDrawable(workIcon);
- mWorkIcon.setVisibility(View.VISIBLE);
- }
- }
- }
-
- private static class FixedItemViewHolder extends RecyclerView.ViewHolder {
- private final CheckedTextView mTitleTextView;
-
- FixedItemViewHolder(View itemView, Callbacks listener) {
- super(itemView);
-
- mTitleTextView = (CheckedTextView) itemView;
- itemView.setOnClickListener(v -> listener.onRingtoneSelected(this.getLayoutPosition()));
- }
-
- void onBind(@StringRes int title, boolean isChecked) {
- Objects.requireNonNull(mTitleTextView);
-
- mTitleTextView.setText(title);
- mTitleTextView.setChecked(isChecked);
- }
- }
-
- private static class AddRingtoneItemViewHolder extends RecyclerView.ViewHolder {
- private final TextView mTitleTextView;
-
- AddRingtoneItemViewHolder(View itemView, Callbacks listener) {
- super(itemView);
-
- mTitleTextView = itemView.requireViewById(R.id.add_new_sound_text);
- itemView.setOnClickListener(v -> listener.onAddRingtoneSelected());
- }
-
- void onBind(@StringRes int title) {
- mTitleTextView.setText(title);
- }
- }
-}
diff --git a/packages/SoundPicker2/src/com/android/soundpicker/RingtoneManagerFactory.java b/packages/SoundPicker2/src/com/android/soundpicker/RingtoneManagerFactory.java
deleted file mode 100644
index f08eb24..0000000
--- a/packages/SoundPicker2/src/com/android/soundpicker/RingtoneManagerFactory.java
+++ /dev/null
@@ -1,49 +0,0 @@
-/*
- * Copyright (C) 2023 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.soundpicker;
-
-import android.content.Context;
-import android.media.RingtoneManager;
-
-import dagger.hilt.android.qualifiers.ApplicationContext;
-
-import javax.inject.Inject;
-import javax.inject.Singleton;
-
-/**
- * A factory class used to create {@link RingtoneManager}.
- */
-@Singleton
-public class RingtoneManagerFactory {
-
- private final Context mApplicationContext;
-
- @Inject
- RingtoneManagerFactory(@ApplicationContext Context applicationContext) {
- mApplicationContext = applicationContext;
- }
-
- /**
- * Creates a new {@link RingtoneManager} and returns it.
- *
- * @return a {@link RingtoneManager}
- */
- public RingtoneManager create() {
- return new RingtoneManager(mApplicationContext, /* includeParentRingtones */ true);
- }
-}
-
diff --git a/packages/SoundPicker2/src/com/android/soundpicker/RingtoneOverlayService.java b/packages/SoundPicker2/src/com/android/soundpicker/RingtoneOverlayService.java
deleted file mode 100644
index b94ebeb..0000000
--- a/packages/SoundPicker2/src/com/android/soundpicker/RingtoneOverlayService.java
+++ /dev/null
@@ -1,113 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.soundpicker;
-
-import android.app.Service;
-import android.content.Intent;
-import android.media.RingtoneManager;
-import android.net.Uri;
-import android.os.AsyncTask;
-import android.os.Environment;
-import android.os.FileUtils;
-import android.os.IBinder;
-import android.provider.MediaStore;
-import android.provider.Settings.System;
-import android.util.Log;
-
-import androidx.annotation.IdRes;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
-import java.io.File;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.io.InputStream;
-
-/**
- * Service to copy and set customization of default sounds
- */
-public class RingtoneOverlayService extends Service {
- private static final String TAG = "RingtoneOverlayService";
- private static final boolean DEBUG = false;
-
- @Override
- public int onStartCommand(@Nullable final Intent intent, final int flags, final int startId) {
- AsyncTask.execute(() -> {
- updateRingtones();
- stopSelf();
- });
-
- // Try again later if we are killed before we finish.
- return Service.START_REDELIVER_INTENT;
- }
-
- @Override
- public IBinder onBind(@Nullable final Intent intent) {
- return null;
- }
-
- private void updateRingtones() {
- copyResourceAndSetAsSound(R.raw.default_ringtone,
- System.RINGTONE, Environment.DIRECTORY_RINGTONES);
- copyResourceAndSetAsSound(R.raw.default_notification_sound,
- System.NOTIFICATION_SOUND, Environment.DIRECTORY_NOTIFICATIONS);
- copyResourceAndSetAsSound(R.raw.default_alarm_alert,
- System.ALARM_ALERT, Environment.DIRECTORY_ALARMS);
- }
-
- /* If the resource contains any data, copy a resource to the file system, scan it, and set the
- * file URI as the default for a sound. */
- private void copyResourceAndSetAsSound(@IdRes final int id, @NonNull final String name,
- @NonNull final String subPath) {
- final File destDir = Environment.getExternalStoragePublicDirectory(subPath);
- if (!destDir.exists() && !destDir.mkdirs()) {
- Log.e(TAG, "can't create " + destDir.getAbsolutePath());
- return;
- }
-
- final File dest = new File(destDir, "default_" + name + ".ogg");
- try (
- InputStream is = getResources().openRawResource(id);
- FileOutputStream os = new FileOutputStream(dest);
- ) {
- if (is.available() > 0) {
- FileUtils.copy(is, os);
- final Uri uri = scanFile(dest);
- if (uri != null) {
- set(name, uri);
- }
- } else {
- // TODO Shall we remove any former copied resource in this case and unset
- // the defaults if we use this event a second time to clear the data?
- if (DEBUG) Log.d(TAG, "Resource for " + name + " has no overlay");
- }
- } catch (IOException e) {
- Log.e(TAG, "Unable to open resource for " + name + ": " + e);
- }
- }
-
- private Uri scanFile(@NonNull final File file) {
- return MediaStore.scanFile(getContentResolver(), file);
- }
-
- private void set(@NonNull final String name, @NonNull final Uri uri) {
- final Uri settingUri = System.getUriFor(name);
- RingtoneManager.setActualDefaultRingtoneUri(this,
- RingtoneManager.getDefaultType(settingUri), uri);
- System.putInt(getContentResolver(), name + "_set", 1);
- }
-}
diff --git a/packages/SoundPicker2/src/com/android/soundpicker/RingtonePickerActivity.java b/packages/SoundPicker2/src/com/android/soundpicker/RingtonePickerActivity.java
deleted file mode 100644
index 90a14f9..0000000
--- a/packages/SoundPicker2/src/com/android/soundpicker/RingtonePickerActivity.java
+++ /dev/null
@@ -1,218 +0,0 @@
-/*
- * Copyright (C) 2007 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.soundpicker;
-
-import android.content.Intent;
-import android.media.RingtoneManager;
-import android.net.Uri;
-import android.os.Bundle;
-import android.os.UserHandle;
-import android.util.Log;
-
-import androidx.appcompat.app.AppCompatActivity;
-import androidx.fragment.app.Fragment;
-import androidx.fragment.app.FragmentTransaction;
-import androidx.lifecycle.ViewModelProvider;
-
-import dagger.hilt.android.AndroidEntryPoint;
-
-/**
- * The {@link RingtonePickerActivity} allows the user to choose one from all of the
- * available ringtones. The chosen ringtone's URI will be persisted as a string.
- *
- * @see RingtoneManager#ACTION_RINGTONE_PICKER
- */
-@AndroidEntryPoint(AppCompatActivity.class)
-public final class RingtonePickerActivity extends Hilt_RingtonePickerActivity {
-
- private static final String TAG = "RingtonePickerActivity";
- // TODO: Use the extra keys from RingtoneManager once they're added.
- private static final String EXTRA_RINGTONE_PICKER_CATEGORY = "EXTRA_RINGTONE_PICKER_CATEGORY";
- private static final String EXTRA_VIBRATION_SHOW_DEFAULT = "EXTRA_VIBRATION_SHOW_DEFAULT";
- private static final String EXTRA_VIBRATION_DEFAULT_URI = "EXTRA_VIBRATION_DEFAULT_URI";
- private static final String EXTRA_VIBRATION_SHOW_SILENT = "EXTRA_VIBRATION_SHOW_SILENT";
- private static final String EXTRA_VIBRATION_EXISTING_URI = "EXTRA_VIBRATION_EXISTING_URI";
- private static final boolean RINGTONE_PICKER_CATEGORY_FEATURE_ENABLED = false;
-
- private RingtonePickerViewModel mRingtonePickerViewModel;
- private int mAttributesFlags;
-
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.activity_ringtone_picker);
-
- mRingtonePickerViewModel = new ViewModelProvider(this).get(RingtonePickerViewModel.class);
-
- Intent intent = getIntent();
- /**
- * Id of the user to which the ringtone picker should list the ringtones
- */
- int pickerUserId = UserHandle.myUserId();
-
- // Get the types of ringtones to show
- int ringtoneType = intent.getIntExtra(RingtoneManager.EXTRA_RINGTONE_TYPE,
- RingtonePickerViewModel.RINGTONE_TYPE_UNKNOWN);
-
- // AudioAttributes flags
- mAttributesFlags |= intent.getIntExtra(
- RingtoneManager.EXTRA_RINGTONE_AUDIO_ATTRIBUTES_FLAGS,
- 0 /*defaultValue == no flags*/);
-
- boolean showOkCancelButtons = getResources().getBoolean(R.bool.config_showOkCancelButtons);
-
- String title = intent.getStringExtra(RingtoneManager.EXTRA_RINGTONE_TITLE);
- if (title == null) {
- title = getString(RingtonePickerViewModel.getTitleByType(ringtoneType));
- }
- String ringtonePickerCategory = intent.getStringExtra(EXTRA_RINGTONE_PICKER_CATEGORY);
- RingtonePickerViewModel.PickerType pickerType = mapCategoryToPickerType(
- ringtonePickerCategory);
-
- RingtoneListHandler.Config soundListConfig = getSoundListConfig(pickerType, intent,
- ringtoneType);
- RingtoneListHandler.Config vibrationListConfig = getVibrationListConfig(pickerType, intent);
-
- RingtonePickerViewModel.Config pickerConfig =
- new RingtonePickerViewModel.Config(title, pickerUserId, ringtoneType,
- showOkCancelButtons, mAttributesFlags, pickerType);
-
- mRingtonePickerViewModel.init(pickerConfig, soundListConfig, vibrationListConfig);
-
- if (savedInstanceState == null) {
- TabbedDialogFragment dialogFragment = new TabbedDialogFragment();
-
- FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
- Fragment prev = getSupportFragmentManager().findFragmentByTag(TabbedDialogFragment.TAG);
- if (prev != null) {
- ft.remove(prev);
- }
- ft.addToBackStack(null);
- dialogFragment.show(ft, TabbedDialogFragment.TAG);
- }
-
- // The volume keys will control the stream that we are choosing a ringtone for
- setVolumeControlStream(mRingtonePickerViewModel.getRingtoneStreamType());
- }
-
- private RingtoneListHandler.Config getSoundListConfig(
- RingtonePickerViewModel.PickerType pickerType, Intent intent, int ringtoneType) {
- if (pickerType != RingtonePickerViewModel.PickerType.SOUND_PICKER
- && pickerType != RingtonePickerViewModel.PickerType.RINGTONE_PICKER) {
- // This ringtone picker does not require a sound picker.
- return null;
- }
-
- // Get whether to show the 'Default' sound item, and the URI to play when it's clicked
- boolean hasDefaultSoundItem =
- intent.getBooleanExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_DEFAULT, true);
-
- // The Uri to play when the 'Default' sound item is clicked.
- Uri uriForDefaultSoundItem =
- intent.getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_DEFAULT_URI);
- if (uriForDefaultSoundItem == null) {
- uriForDefaultSoundItem = RingtonePickerViewModel.getDefaultItemUriByType(ringtoneType);
- }
-
- // Get whether this list has the 'Silent' sound item.
- boolean hasSilentSoundItem =
- intent.getBooleanExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_SILENT, true);
-
- // AudioAttributes flags
- mAttributesFlags |= intent.getIntExtra(
- RingtoneManager.EXTRA_RINGTONE_AUDIO_ATTRIBUTES_FLAGS,
- 0 /*defaultValue == no flags*/);
-
- // Get the sound URI whose list item should have a checkmark
- Uri existingSoundUri = intent
- .getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_EXISTING_URI);
-
- return new RingtoneListHandler.Config(hasDefaultSoundItem,
- uriForDefaultSoundItem, hasSilentSoundItem, existingSoundUri);
- }
-
- private RingtoneListHandler.Config getVibrationListConfig(
- RingtonePickerViewModel.PickerType pickerType, Intent intent) {
- if (pickerType != RingtonePickerViewModel.PickerType.VIBRATION_PICKER
- && pickerType != RingtonePickerViewModel.PickerType.RINGTONE_PICKER) {
- // This ringtone picker does not require a vibration picker.
- return null;
- }
-
- // Get whether to show the 'Default' vibration item, and the URI to play when it's clicked
- boolean hasDefaultVibrationItem =
- intent.getBooleanExtra(EXTRA_VIBRATION_SHOW_DEFAULT, false);
-
- // The Uri to play when the 'Default' vibration item is clicked.
- Uri uriForDefaultVibrationItem = intent.getParcelableExtra(EXTRA_VIBRATION_DEFAULT_URI);
-
- // Get whether this list has the 'Silent' vibration item.
- boolean hasSilentVibrationItem =
- intent.getBooleanExtra(EXTRA_VIBRATION_SHOW_SILENT, true);
-
- // Get the vibration URI whose list item should have a checkmark
- Uri existingVibrationUri = intent.getParcelableExtra(EXTRA_VIBRATION_EXISTING_URI);
-
- return new RingtoneListHandler.Config(
- hasDefaultVibrationItem, uriForDefaultVibrationItem, hasSilentVibrationItem,
- existingVibrationUri);
- }
-
- @Override
- public void onDestroy() {
- mRingtonePickerViewModel.cancelPendingAsyncTasks();
- super.onDestroy();
- }
-
- @Override
- protected void onStop() {
- super.onStop();
- mRingtonePickerViewModel.onStop(isChangingConfigurations());
- }
-
- @Override
- protected void onPause() {
- super.onPause();
- mRingtonePickerViewModel.onPause(isChangingConfigurations());
- }
-
- /**
- * Maps the ringtone picker category to the appropriate PickerType.
- * If the category is null or the feature is still not released, then it defaults to sound
- * picker.
- *
- * @param category the ringtone picker category.
- * @return the corresponding picker type.
- */
- private static RingtonePickerViewModel.PickerType mapCategoryToPickerType(String category) {
- if (category == null || !RINGTONE_PICKER_CATEGORY_FEATURE_ENABLED) {
- return RingtonePickerViewModel.PickerType.SOUND_PICKER;
- }
-
- switch (category) {
- case "android.intent.category.RINGTONE_PICKER_RINGTONE":
- return RingtonePickerViewModel.PickerType.RINGTONE_PICKER;
- case "android.intent.category.RINGTONE_PICKER_SOUND":
- return RingtonePickerViewModel.PickerType.SOUND_PICKER;
- case "android.intent.category.RINGTONE_PICKER_VIBRATION":
- return RingtonePickerViewModel.PickerType.VIBRATION_PICKER;
- default:
- Log.w(TAG, "Unrecognized category: " + category + ". Defaulting to sound picker.");
- return RingtonePickerViewModel.PickerType.SOUND_PICKER;
- }
- }
-}
diff --git a/packages/SoundPicker2/src/com/android/soundpicker/RingtonePickerViewModel.java b/packages/SoundPicker2/src/com/android/soundpicker/RingtonePickerViewModel.java
deleted file mode 100644
index 2c09711..0000000
--- a/packages/SoundPicker2/src/com/android/soundpicker/RingtonePickerViewModel.java
+++ /dev/null
@@ -1,340 +0,0 @@
-/*
- * Copyright (C) 2023 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.soundpicker;
-
-import static java.util.Objects.requireNonNull;
-
-import android.annotation.Nullable;
-import android.annotation.StringRes;
-import android.media.Ringtone;
-import android.media.RingtoneManager;
-import android.net.Uri;
-import android.provider.Settings;
-
-import androidx.annotation.NonNull;
-import androidx.lifecycle.ViewModel;
-
-import com.android.internal.annotations.VisibleForTesting;
-
-import com.google.common.util.concurrent.FutureCallback;
-import com.google.common.util.concurrent.Futures;
-import com.google.common.util.concurrent.ListenableFuture;
-import com.google.common.util.concurrent.ListeningExecutorService;
-
-import dagger.hilt.android.lifecycle.HiltViewModel;
-
-import java.io.IOException;
-import java.util.concurrent.Executor;
-
-import javax.inject.Inject;
-
-/**
- * A view model which holds immutable info about the picker state and means to retrieve and play
- * currently selected ringtones.
- */
-@HiltViewModel
-public final class RingtonePickerViewModel extends ViewModel {
-
- static final int RINGTONE_TYPE_UNKNOWN = -1;
-
- /**
- * Keep the currently playing ringtone around when changing orientation, so that it
- * can be stopped later, after the activity is recreated.
- */
- @VisibleForTesting
- static Ringtone sPlayingRingtone;
-
- private static final String TAG = "RingtonePickerViewModel";
-
- private final RingtoneManagerFactory mRingtoneManagerFactory;
- private final RingtoneFactory mRingtoneFactory;
- private final RingtoneListHandler mSoundListHandler;
- private final RingtoneListHandler mVibrationListHandler;
- private final ListeningExecutorService mListeningExecutorService;
-
- private RingtoneManager mRingtoneManager;
-
- /**
- * The ringtone that's currently playing.
- */
- private Ringtone mCurrentRingtone;
-
- private Config mPickerConfig;
-
- private ListenableFuture<Uri> mAddCustomRingtoneFuture;
-
- public enum PickerType {
- RINGTONE_PICKER,
- SOUND_PICKER,
- VIBRATION_PICKER
- }
-
- /**
- * Holds immutable info on the picker that should be displayed.
- */
- static final class Config {
- public final String title;
- /**
- * Id of the user to which the ringtone picker should list the ringtones.
- */
- public final int userId;
- /**
- * Ringtone type.
- */
- public final int ringtoneType;
- /**
- * AudioAttributes flags.
- */
- public final int audioAttributesFlags;
- /**
- * In the buttonless (watch-only) version we don't show the OK/Cancel buttons.
- */
- public final boolean showOkCancelButtons;
-
- public final PickerType mPickerType;
-
- Config(String title, int userId, int ringtoneType, boolean showOkCancelButtons,
- int audioAttributesFlags, PickerType pickerType) {
- this.title = title;
- this.userId = userId;
- this.ringtoneType = ringtoneType;
- this.showOkCancelButtons = showOkCancelButtons;
- this.audioAttributesFlags = audioAttributesFlags;
- this.mPickerType = pickerType;
- }
- }
-
- @Inject
- RingtonePickerViewModel(RingtoneManagerFactory ringtoneManagerFactory,
- RingtoneFactory ringtoneFactory,
- ListeningExecutorServiceFactory listeningExecutorServiceFactory,
- RingtoneListHandler soundListHandler,
- RingtoneListHandler vibrationListHandler) {
- mRingtoneManagerFactory = ringtoneManagerFactory;
- mRingtoneFactory = ringtoneFactory;
- mListeningExecutorService = listeningExecutorServiceFactory.createSingleThreadExecutor();
- mSoundListHandler = soundListHandler;
- mVibrationListHandler = vibrationListHandler;
- }
-
- @StringRes
- static int getTitleByType(int ringtoneType) {
- switch (ringtoneType) {
- case RingtoneManager.TYPE_ALARM:
- return com.android.internal.R.string.ringtone_picker_title_alarm;
- case RingtoneManager.TYPE_NOTIFICATION:
- return com.android.internal.R.string.ringtone_picker_title_notification;
- default:
- return com.android.internal.R.string.ringtone_picker_title;
- }
- }
-
- static Uri getDefaultItemUriByType(int ringtoneType) {
- switch (ringtoneType) {
- case RingtoneManager.TYPE_ALARM:
- return Settings.System.DEFAULT_ALARM_ALERT_URI;
- case RingtoneManager.TYPE_NOTIFICATION:
- return Settings.System.DEFAULT_NOTIFICATION_URI;
- default:
- return Settings.System.DEFAULT_RINGTONE_URI;
- }
- }
-
- @StringRes
- static int getAddNewItemTextByType(int ringtoneType) {
- switch (ringtoneType) {
- case RingtoneManager.TYPE_ALARM:
- return R.string.add_alarm_text;
- case RingtoneManager.TYPE_NOTIFICATION:
- return R.string.add_notification_text;
- default:
- return R.string.add_ringtone_text;
- }
- }
-
- @StringRes
- static int getDefaultRingtoneItemTextByType(int ringtoneType) {
- switch (ringtoneType) {
- case RingtoneManager.TYPE_ALARM:
- return R.string.alarm_sound_default;
- case RingtoneManager.TYPE_NOTIFICATION:
- return R.string.notification_sound_default;
- default:
- return R.string.ringtone_default;
- }
- }
-
- void init(@NonNull Config pickerConfig,
- RingtoneListHandler.Config soundListConfig,
- RingtoneListHandler.Config vibrationListConfig) {
- mRingtoneManager = mRingtoneManagerFactory.create();
- mPickerConfig = pickerConfig;
- if (mPickerConfig.ringtoneType != RINGTONE_TYPE_UNKNOWN) {
- mRingtoneManager.setType(mPickerConfig.ringtoneType);
- }
- if (soundListConfig != null) {
- mSoundListHandler.init(soundListConfig, mRingtoneManager,
- mRingtoneManager.getCursor());
- }
- if (vibrationListConfig != null) {
- // TODO: Switch to the vibration cursor, once the API is made available.
- mVibrationListHandler.init(vibrationListConfig, mRingtoneManager,
- mRingtoneManager.getCursor());
- }
- }
-
- /**
- * Re-initializes the view model which is required after updating any of the picker lists.
- * This could happen when adding a custom ringtone.
- */
- void reinit() {
- init(mPickerConfig, mSoundListHandler.getRingtoneListConfig(),
- mVibrationListHandler.getRingtoneListConfig());
- }
-
- @NonNull
- Config getPickerConfig() {
- requireInitCalled();
- return mPickerConfig;
- }
-
- @NonNull
- RingtoneListHandler getSoundListHandler() {
- return mSoundListHandler;
- }
-
- @NonNull
- RingtoneListHandler getVibrationListHandler() {
- return mVibrationListHandler;
- }
-
- /**
- * Combined the currently selected sound and vibration URIs and returns a unified URI. If the
- * picker does not show either sound or vibration, that portion of the URI will be null.
- *
- * Currently only the sound URI is returned, since we don't have the API to retrieve vibrations
- * yet.
- * @return Combined sound and vibration URI.
- */
- Uri getSelectedRingtoneUri() {
- // TODO: Combine sound and vibration URIs before returning.
- return mSoundListHandler.getSelectedRingtoneUri();
- }
-
- int getRingtoneStreamType() {
- requireInitCalled();
- return mRingtoneManager.inferStreamType();
- }
-
- void onPause(boolean isChangingConfigurations) {
- if (!isChangingConfigurations) {
- stopAnyPlayingRingtone();
- }
- }
-
- void onStop(boolean isChangingConfigurations) {
- if (isChangingConfigurations) {
- saveAnyPlayingRingtone();
- } else {
- stopAnyPlayingRingtone();
- }
- }
-
- /**
- * Plays a ringtone which is created using the currently selected sound and vibration URIs. If
- * this is a sound or vibration only picker, then the other portion of the URI will be empty
- * and should not affect the played ringtone.
- *
- * Currently, we only use the sound URI to create the ringtone, since we still don't have the
- * API to retrieve the available vibrations list.
- */
- void playRingtone() {
- requireInitCalled();
- stopAnyPlayingRingtone();
-
- mCurrentRingtone = mRingtoneFactory.create(getSelectedRingtoneUri(),
- mPickerConfig.audioAttributesFlags);
-
- if (mCurrentRingtone != null) {
- mCurrentRingtone.play();
- }
- }
-
- /**
- * Cancels all pending async tasks.
- */
- void cancelPendingAsyncTasks() {
- if (mAddCustomRingtoneFuture != null && !mAddCustomRingtoneFuture.isDone()) {
- mAddCustomRingtoneFuture.cancel(/* mayInterruptIfRunning= */ true);
- }
- }
-
- /**
- * Adds an audio file to the list of ringtones asynchronously.
- * Any previous async tasks are canceled before start the new one.
- *
- * @param uri Uri of the file to be added as ringtone. Must be a media file.
- * @param type The type of the ringtone to be added.
- * @param callback The callback to invoke when the task is completed.
- * @param executor The executor to run the callback on when the task completes.
- */
- void addSoundRingtoneAsync(Uri uri, int type, FutureCallback<Uri> callback, Executor executor) {
- // Cancel any currently running add ringtone tasks before starting a new one
- cancelPendingAsyncTasks();
- mAddCustomRingtoneFuture = mListeningExecutorService.submit(
- () -> addRingtone(uri, type));
- Futures.addCallback(mAddCustomRingtoneFuture, callback, executor);
- }
-
- /**
- * Adds an audio file to the list of ringtones.
- *
- * @param uri Uri of the file to be added as ringtone. Must be a media file.
- * @param type The type of the ringtone to be added.
- * @return The Uri of the installed ringtone, which may be the {@code uri} if it
- * is already in ringtone storage. Or null if it failed to add the audio file.
- */
- @Nullable
- private Uri addRingtone(Uri uri, int type) throws IOException {
- requireInitCalled();
- return mRingtoneManager.addCustomExternalRingtone(uri, type);
- }
-
- private void saveAnyPlayingRingtone() {
- if (mCurrentRingtone != null && mCurrentRingtone.isPlaying()) {
- sPlayingRingtone = mCurrentRingtone;
- }
- mCurrentRingtone = null;
- }
-
- private void stopAnyPlayingRingtone() {
- if (sPlayingRingtone != null && sPlayingRingtone.isPlaying()) {
- sPlayingRingtone.stop();
- }
- sPlayingRingtone = null;
-
- if (mCurrentRingtone != null && mCurrentRingtone.isPlaying()) {
- mCurrentRingtone.stop();
- }
- mCurrentRingtone = null;
- }
-
- private void requireInitCalled() {
- requireNonNull(mRingtoneManager);
- requireNonNull(mPickerConfig);
- }
-}
diff --git a/packages/SoundPicker2/src/com/android/soundpicker/RingtoneReceiver.java b/packages/SoundPicker2/src/com/android/soundpicker/RingtoneReceiver.java
deleted file mode 100644
index 6a34936..0000000
--- a/packages/SoundPicker2/src/com/android/soundpicker/RingtoneReceiver.java
+++ /dev/null
@@ -1,36 +0,0 @@
-/*
- * Copyright (C) 2007 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.soundpicker;
-
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-
-public class RingtoneReceiver extends BroadcastReceiver {
- @Override
- public void onReceive(Context context, Intent intent) {
- final String action = intent.getAction();
- if (Intent.ACTION_DEVICE_CUSTOMIZATION_READY.equals(action)) {
- initResourceRingtones(context);
- }
- }
-
- private void initResourceRingtones(Context context) {
- context.startService(
- new Intent(context, RingtoneOverlayService.class));
- }
-}
diff --git a/packages/SoundPicker2/src/com/android/soundpicker/SoundPickerFragment.java b/packages/SoundPicker2/src/com/android/soundpicker/SoundPickerFragment.java
deleted file mode 100644
index a37191f..0000000
--- a/packages/SoundPicker2/src/com/android/soundpicker/SoundPickerFragment.java
+++ /dev/null
@@ -1,122 +0,0 @@
-/*
- * Copyright (C) 2023 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.soundpicker;
-
-import android.app.Activity;
-import android.content.Intent;
-import android.net.Uri;
-import android.os.Bundle;
-import android.os.Environment;
-import android.util.Log;
-import android.view.View;
-import android.widget.Toast;
-
-import androidx.activity.result.ActivityResult;
-import androidx.activity.result.ActivityResultCallback;
-import androidx.activity.result.ActivityResultLauncher;
-import androidx.activity.result.contract.ActivityResultContracts;
-import androidx.core.content.ContextCompat;
-import androidx.lifecycle.ViewModelProvider;
-
-import com.google.common.util.concurrent.FutureCallback;
-
-import org.jetbrains.annotations.NotNull;
-
-/**
- * A fragment that displays a picker used to select sound or silent. It also includes the
- * ability to add custom sounds.
- */
-public class SoundPickerFragment extends BasePickerFragment {
-
- private static final String TAG = "SoundPickerFragment";
-
- private final FutureCallback<Uri> mAddCustomRingtoneCallback = new FutureCallback<>() {
- @Override
- public void onSuccess(Uri ringtoneUri) {
- requeryForAdapter();
- }
-
- @Override
- public void onFailure(Throwable throwable) {
- Log.e(TAG, "Failed to add custom ringtone.", throwable);
- // Ringtone was not added, display error Toast
- Toast.makeText(requireActivity().getApplicationContext(),
- R.string.unable_to_add_ringtone, Toast.LENGTH_SHORT).show();
- }
- };
-
- ActivityResultLauncher<Intent> mActivityResultLauncher = registerForActivityResult(
- new ActivityResultContracts.StartActivityForResult(),
- new ActivityResultCallback<ActivityResult>() {
- @Override
- public void onActivityResult(ActivityResult result) {
- if (result.getResultCode() == Activity.RESULT_OK) {
- // There are no request codes
- Intent data = result.getData();
- mRingtonePickerViewModel.addSoundRingtoneAsync(data.getData(),
- mPickerConfig.ringtoneType,
- mAddCustomRingtoneCallback,
- // Causes the callback to be executed on the main thread.
- ContextCompat.getMainExecutor(
- requireActivity().getApplicationContext()));
- }
- }
- });
-
- @Override
- public void onViewCreated(@NotNull View view, Bundle savedInstanceState) {
- mRingtonePickerViewModel = new ViewModelProvider(requireActivity()).get(
- RingtonePickerViewModel.class);
- super.onViewCreated(view, savedInstanceState);
- }
-
- @Override
- protected RingtoneListHandler getRingtoneListHandler() {
- return mRingtonePickerViewModel.getSoundListHandler();
- }
-
- @Override
- protected void addRingtoneAsync() {
- // The "Add new ringtone" item was clicked. Start a file picker intent to
- // select only audio files (MIME type "audio/*")
- final Intent chooseFile = getMediaFilePickerIntent();
- mActivityResultLauncher.launch(chooseFile);
- }
-
- @Override
- protected void addNewRingtoneItem() {
- // If external storage is available, add a button to install sounds from storage.
- if (resolvesMediaFilePicker()
- && Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
- mRingtoneListViewAdapter.addTitleForAddRingtoneItem(
- RingtonePickerViewModel.getAddNewItemTextByType(mPickerConfig.ringtoneType));
- }
- }
-
- private boolean resolvesMediaFilePicker() {
- return getMediaFilePickerIntent().resolveActivity(requireActivity().getPackageManager())
- != null;
- }
-
- private Intent getMediaFilePickerIntent() {
- final Intent chooseFile = new Intent(Intent.ACTION_GET_CONTENT);
- chooseFile.setType("audio/*");
- chooseFile.putExtra(Intent.EXTRA_MIME_TYPES,
- new String[]{"audio/*", "application/ogg"});
- return chooseFile;
- }
-}
diff --git a/packages/SoundPicker2/src/com/android/soundpicker/TabbedDialogFragment.java b/packages/SoundPicker2/src/com/android/soundpicker/TabbedDialogFragment.java
deleted file mode 100644
index 50ea9d7..0000000
--- a/packages/SoundPicker2/src/com/android/soundpicker/TabbedDialogFragment.java
+++ /dev/null
@@ -1,180 +0,0 @@
-/*
- * Copyright (C) 2023 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.soundpicker;
-
-import static android.app.Activity.RESULT_CANCELED;
-
-import android.app.Activity;
-import android.app.Dialog;
-import android.content.DialogInterface;
-import android.content.Intent;
-import android.media.RingtoneManager;
-import android.os.Bundle;
-import android.view.LayoutInflater;
-import android.view.View;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.appcompat.app.AlertDialog;
-import androidx.fragment.app.DialogFragment;
-import androidx.lifecycle.ViewModelProvider;
-import androidx.viewpager2.widget.ViewPager2;
-
-import com.google.android.material.tabs.TabLayout;
-import com.google.android.material.tabs.TabLayoutMediator;
-
-import dagger.hilt.android.AndroidEntryPoint;
-
-import org.jetbrains.annotations.NotNull;
-
-/**
- * A dialog fragment with a sound and/or vibration tab based on the picker type.
- * <ul>
- * <li> Ringtone Pickers will display both sound and vibration tabs.
- * <li> Sound Pickers will only display the sound tab.
- * <li> Vibration Pickers will only display the vibration tab.
- * </ul>
- */
-@AndroidEntryPoint(DialogFragment.class)
-public class TabbedDialogFragment extends Hilt_TabbedDialogFragment {
-
- static final String TAG = "TabbedDialogFragment";
-
- private RingtonePickerViewModel mRingtonePickerViewModel;
-
- private final ViewPager2.OnPageChangeCallback mOnPageChangeCallback =
- new ViewPager2.OnPageChangeCallback() {
- @Override
- public void onPageScrollStateChanged(int state) {
- super.onPageScrollStateChanged(state);
- if (state == ViewPager2.SCROLL_STATE_IDLE) {
- mRingtonePickerViewModel.onStop(/* isChangingConfigurations= */ false);
- }
- }
- };
-
- @Override
- public void onCreate(@Nullable Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- mRingtonePickerViewModel = new ViewModelProvider(requireActivity()).get(
- RingtonePickerViewModel.class);
- }
-
- @NonNull
- @Override
- public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
- AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(requireActivity(),
- android.R.style.ThemeOverlay_Material_Dialog)
- .setTitle(mRingtonePickerViewModel.getPickerConfig().title);
- // Do not show OK/Cancel buttons in the buttonless (watch-only) version.
- if (mRingtonePickerViewModel.getPickerConfig().showOkCancelButtons) {
- dialogBuilder
- .setPositiveButton(getString(com.android.internal.R.string.ok),
- (dialog, whichButton) -> {
- setSuccessResultWithSelectedRingtone();
- requireActivity().finish();
- })
- .setNegativeButton(getString(com.android.internal.R.string.cancel),
- (dialog, whichButton) -> {
- requireActivity().setResult(RESULT_CANCELED);
- requireActivity().finish();
- });
- }
-
- View view = buildTabbedView(requireActivity().getLayoutInflater());
- dialogBuilder.setView(view);
-
- return dialogBuilder.create();
- }
-
- @Override
- public void onCancel(@NonNull @NotNull DialogInterface dialog) {
- super.onCancel(dialog);
- if (!requireActivity().isChangingConfigurations()) {
- requireActivity().finish();
- }
- }
-
- @Override
- public void onDismiss(@NonNull @NotNull DialogInterface dialog) {
- super.onDismiss(dialog);
- if (!requireActivity().isChangingConfigurations()) {
- requireActivity().finish();
- }
- }
-
- private void setSuccessResultWithSelectedRingtone() {
- requireActivity().setResult(Activity.RESULT_OK,
- new Intent().putExtra(RingtoneManager.EXTRA_RINGTONE_PICKED_URI,
- mRingtonePickerViewModel.getSelectedRingtoneUri()));
- }
-
- /**
- * Inflates the tabbed layout view and adds the required fragments. If there's only one
- * fragment to display, then the tab area is hidden.
- * @param inflater The LayoutInflater that is used to inflate the tabbed view.
- * @return The tabbed view.
- */
- private View buildTabbedView(@NonNull LayoutInflater inflater) {
- View view = inflater.inflate(R.layout.fragment_tabbed_dialog, null, false);
- TabLayout tabLayout = view.requireViewById(R.id.tabLayout);
- ViewPager2 viewPager = view.requireViewById(R.id.masterViewPager);
-
- ViewPagerAdapter adapter = new ViewPagerAdapter(requireActivity());
- addFragments(adapter);
-
- if (adapter.getItemCount() == 1) {
- // Hide the tab area since there's only one fragment to display.
- tabLayout.setVisibility(View.GONE);
- }
-
- viewPager.setAdapter(adapter);
- viewPager.registerOnPageChangeCallback(mOnPageChangeCallback);
- new TabLayoutMediator(tabLayout, viewPager,
- (tab, position) -> tab.setText(adapter.getTitle(position))).attach();
-
- return view;
- }
-
- /**
- * Adds the appropriate fragments to the adapter based on the PickerType.
- *
- * @param adapter The adapter to add the fragments to.
- */
- private void addFragments(ViewPagerAdapter adapter) {
- switch (mRingtonePickerViewModel.getPickerConfig().mPickerType) {
- case RINGTONE_PICKER:
- adapter.addFragment(getString(R.string.sound_page_title),
- new SoundPickerFragment());
- adapter.addFragment(getString(R.string.vibration_page_title),
- new VibrationPickerFragment());
- break;
- case SOUND_PICKER:
- adapter.addFragment(getString(R.string.sound_page_title),
- new SoundPickerFragment());
- break;
- case VIBRATION_PICKER:
- adapter.addFragment(getString(R.string.vibration_page_title),
- new VibrationPickerFragment());
- break;
- default:
- adapter.addFragment(getString(R.string.sound_page_title),
- new SoundPickerFragment());
- break;
- }
- }
-}
diff --git a/packages/SoundPicker2/src/com/android/soundpicker/VibrationPickerFragment.java b/packages/SoundPicker2/src/com/android/soundpicker/VibrationPickerFragment.java
deleted file mode 100644
index 7412c19..0000000
--- a/packages/SoundPicker2/src/com/android/soundpicker/VibrationPickerFragment.java
+++ /dev/null
@@ -1,52 +0,0 @@
-/*
- * Copyright (C) 2023 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.soundpicker;
-
-import android.os.Bundle;
-import android.view.View;
-
-import androidx.lifecycle.ViewModelProvider;
-
-import org.jetbrains.annotations.NotNull;
-
-/**
- * A fragment that displays a picker used to select vibration or silent (no vibration).
- */
-public class VibrationPickerFragment extends BasePickerFragment {
-
- @Override
- public void onViewCreated(@NotNull View view, Bundle savedInstanceState) {
- mRingtonePickerViewModel = new ViewModelProvider(requireActivity()).get(
- RingtonePickerViewModel.class);
- super.onViewCreated(view, savedInstanceState);
- }
-
- @Override
- protected RingtoneListHandler getRingtoneListHandler() {
- return mRingtonePickerViewModel.getVibrationListHandler();
- }
-
- @Override
- protected void addRingtoneAsync() {
- // no-op
- }
-
- @Override
- protected void addNewRingtoneItem() {
- // no-op
- }
-}
diff --git a/packages/SoundPicker2/src/com/android/soundpicker/ViewPagerAdapter.java b/packages/SoundPicker2/src/com/android/soundpicker/ViewPagerAdapter.java
deleted file mode 100644
index 179068e..0000000
--- a/packages/SoundPicker2/src/com/android/soundpicker/ViewPagerAdapter.java
+++ /dev/null
@@ -1,70 +0,0 @@
-/*
- * Copyright (C) 2023 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.soundpicker;
-
-import androidx.annotation.NonNull;
-import androidx.fragment.app.Fragment;
-import androidx.fragment.app.FragmentActivity;
-import androidx.viewpager2.adapter.FragmentStateAdapter;
-
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Objects;
-
-/**
- * An adapter used to populate pages inside a ViewPager.
- */
-public class ViewPagerAdapter extends FragmentStateAdapter {
-
- private final List<Fragment> mFragments = new ArrayList<>();
- private final List<String> mTitles = new ArrayList<>();
-
- public ViewPagerAdapter(@NonNull FragmentActivity fragmentActivity) {
- super(fragmentActivity);
- }
-
- /**
- * Adds a fragment and page title to the adapter.
- * @param title the title of the page in the ViewPager.
- * @param fragment the fragment that will be inflated on this page.
- */
- public void addFragment(String title, Fragment fragment) {
- mTitles.add(title);
- mFragments.add(fragment);
- }
-
- /**
- * Returns the title of the requested page.
- * @param position the position of the page in the Viewpager.
- * @return The title of the requested page.
- */
- public String getTitle(int position) {
- return mTitles.get(position);
- }
-
- @NonNull
- @Override
- public Fragment createFragment(int position) {
- return Objects.requireNonNull(mFragments.get(position),
- "Could not find a fragment using position: " + position);
- }
-
- @Override
- public int getItemCount() {
- return mFragments.size();
- }
-}
diff --git a/packages/SoundPicker2/tests/Android.bp b/packages/SoundPicker2/tests/Android.bp
deleted file mode 100644
index d88d442..0000000
--- a/packages/SoundPicker2/tests/Android.bp
+++ /dev/null
@@ -1,38 +0,0 @@
-// Copyright 2023, The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package {
- default_applicable_licenses: ["frameworks_base_license"],
-}
-
-android_test {
- name: "SoundPicker2Tests",
- certificate: "platform",
- libs: [
- "android.test.runner",
- "android.test.base",
- ],
- static_libs: [
- "androidx.test.core",
- "androidx.test.rules",
- "androidx.test.ext.junit",
- "androidx.test.ext.truth",
- "mockito-target-minus-junit4",
- "guava-android-testlib",
- "SoundPicker2Lib",
- ],
- srcs: [
- "src/**/*.java",
- ],
-}
diff --git a/packages/SoundPicker2/tests/AndroidManifest.xml b/packages/SoundPicker2/tests/AndroidManifest.xml
deleted file mode 100644
index 295aeb1..0000000
--- a/packages/SoundPicker2/tests/AndroidManifest.xml
+++ /dev/null
@@ -1,11 +0,0 @@
-<manifest xmlns:android="http://schemas.android.com/apk/res/android"
- package="com.android.soundpicker.tests">
-
- <application android:debuggable="true">
- <uses-library android:name="android.test.runner" />
- </application>
- <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
- android:targetPackage="com.android.soundpicker.tests"
- android:label="Sound picker tests">
- </instrumentation>
-</manifest>
diff --git a/packages/SoundPicker2/tests/src/com/android/soundpicker/RingtoneListHandlerTest.java b/packages/SoundPicker2/tests/src/com/android/soundpicker/RingtoneListHandlerTest.java
deleted file mode 100644
index 80e71e200..0000000
--- a/packages/SoundPicker2/tests/src/com/android/soundpicker/RingtoneListHandlerTest.java
+++ /dev/null
@@ -1,167 +0,0 @@
-/*
- * Copyright (C) 2023 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.soundpicker;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.when;
-
-import android.database.Cursor;
-import android.media.RingtoneManager;
-import android.net.Uri;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-
-public class RingtoneListHandlerTest {
-
- private static final Uri DEFAULT_URI = Uri.parse("media://custom/ringtone/default_uri");
- private static final Uri RINGTONE_URI = Uri.parse("media://custom/ringtone/uri");
- private static final int SILENT_RINGTONE_POSITION = 0;
- private static final int DEFAULT_RINGTONE_POSITION = 1;
- private static final int RINGTONE_POSITION = 2;
-
- @Mock
- private RingtoneManager mMockRingtoneManager;
- @Mock
- private Cursor mMockCursor;
-
- private RingtoneListHandler mRingtoneListHandler;
-
- @Before
- public void setUp() {
- MockitoAnnotations.initMocks(this);
-
- RingtoneListHandler.Config mRingtoneListConfig = createRingtoneListConfig();
-
- mRingtoneListHandler = new RingtoneListHandler();
-
- // Add silent and default options to the list.
- mRingtoneListHandler.addSilentItem();
- mRingtoneListHandler.addDefaultItem();
-
- mRingtoneListHandler.init(mRingtoneListConfig, mMockRingtoneManager, mMockCursor);
- }
-
- @Test
- public void testGetRingtoneCursor_returnsTheCorrectRingtoneCursor() {
- assertThat(mRingtoneListHandler.getRingtoneCursor()).isEqualTo(mMockCursor);
- }
-
- @Test
- public void testGetRingtoneUri_returnsTheCorrectRingtoneUri() {
- Uri expectedUri = RINGTONE_URI;
- when(mMockRingtoneManager.getRingtoneUri(eq(0))).thenReturn(expectedUri);
-
- // Request 3rd item from list.
- Uri actualUri = mRingtoneListHandler.getRingtoneUri(RINGTONE_POSITION);
- assertThat(actualUri).isEqualTo(expectedUri);
- }
-
- @Test
- public void testGetRingtoneUri_withSelectedItemUnknown_returnsTheCorrectRingtoneUri() {
- Uri uri = mRingtoneListHandler.getRingtoneUri(RingtoneListHandler.ITEM_POSITION_UNKNOWN);
- assertThat(uri).isEqualTo(RingtoneListHandler.SILENT_URI);
- }
-
- @Test
- public void testGetRingtoneUri_withSelectedItemDefaultPosition_returnsTheCorrectRingtoneUri() {
- Uri actualUri = mRingtoneListHandler.getRingtoneUri(DEFAULT_RINGTONE_POSITION);
- assertThat(actualUri).isEqualTo(DEFAULT_URI);
- }
-
- @Test
- public void testGetRingtoneUri_withSelectedItemSilentPosition_returnsTheCorrectRingtoneUri() {
- Uri uri = mRingtoneListHandler.getRingtoneUri(SILENT_RINGTONE_POSITION);
- assertThat(uri).isEqualTo(RingtoneListHandler.SILENT_URI);
- }
-
- @Test
- public void testGetCurrentlySelectedRingtoneUri_returnsTheCorrectRingtoneUri() {
- mRingtoneListHandler.setSelectedItemPosition(RingtoneListHandler.ITEM_POSITION_UNKNOWN);
- Uri actualUri = mRingtoneListHandler.getSelectedRingtoneUri();
- assertThat(actualUri).isEqualTo(RingtoneListHandler.SILENT_URI);
-
- mRingtoneListHandler.setSelectedItemPosition(DEFAULT_RINGTONE_POSITION);
- actualUri = mRingtoneListHandler.getSelectedRingtoneUri();
- assertThat(actualUri).isEqualTo(DEFAULT_URI);
-
- mRingtoneListHandler.setSelectedItemPosition(SILENT_RINGTONE_POSITION);
- actualUri = mRingtoneListHandler.getSelectedRingtoneUri();
- assertThat(actualUri).isEqualTo(RingtoneListHandler.SILENT_URI);
-
- when(mMockRingtoneManager.getRingtoneUri(eq(0))).thenReturn(RINGTONE_URI);
- mRingtoneListHandler.setSelectedItemPosition(RINGTONE_POSITION);
- actualUri = mRingtoneListHandler.getSelectedRingtoneUri();
- assertThat(actualUri).isEqualTo(RINGTONE_URI);
- }
-
- @Test
- public void testGetRingtonePosition_returnsTheCorrectRingtonePosition() {
- when(mMockRingtoneManager.getRingtonePosition(RINGTONE_URI)).thenReturn(0);
-
- int actualPosition = mRingtoneListHandler.getRingtonePosition(RINGTONE_URI);
-
- assertThat(actualPosition).isEqualTo(RINGTONE_POSITION);
-
- }
-
- @Test
- public void testFixedItems_onlyAddsItemsOnceAndInOrder() {
- // Clear fixed items before testing the add methods.
- mRingtoneListHandler.resetFixedItems();
-
- assertThat(mRingtoneListHandler.getSilentItemPosition()).isEqualTo(
- RingtoneListHandler.ITEM_POSITION_UNKNOWN);
- assertThat(mRingtoneListHandler.getDefaultItemPosition()).isEqualTo(
- RingtoneListHandler.ITEM_POSITION_UNKNOWN);
-
- mRingtoneListHandler.addSilentItem();
- mRingtoneListHandler.addDefaultItem();
- mRingtoneListHandler.addSilentItem();
- mRingtoneListHandler.addDefaultItem();
-
- assertThat(mRingtoneListHandler.getSilentItemPosition()).isEqualTo(
- SILENT_RINGTONE_POSITION);
- assertThat(mRingtoneListHandler.getDefaultItemPosition()).isEqualTo(
- DEFAULT_RINGTONE_POSITION);
- }
-
- @Test
- public void testResetFixedItems_resetsSilentAndDefaultItemPositions() {
- assertThat(mRingtoneListHandler.getSilentItemPosition()).isEqualTo(
- SILENT_RINGTONE_POSITION);
- assertThat(mRingtoneListHandler.getDefaultItemPosition()).isEqualTo(
- DEFAULT_RINGTONE_POSITION);
-
- mRingtoneListHandler.resetFixedItems();
-
- assertThat(mRingtoneListHandler.getSilentItemPosition()).isEqualTo(
- RingtoneListHandler.ITEM_POSITION_UNKNOWN);
- assertThat(mRingtoneListHandler.getDefaultItemPosition()).isEqualTo(
- RingtoneListHandler.ITEM_POSITION_UNKNOWN);
- }
-
- private RingtoneListHandler.Config createRingtoneListConfig() {
- return new RingtoneListHandler.Config(/* hasDefaultItem= */ true,
- /* uriForDefaultItem= */ DEFAULT_URI, /* hasSilentItem= */ true,
- /* existingUri= */ DEFAULT_URI);
- }
-}
diff --git a/packages/SoundPicker2/tests/src/com/android/soundpicker/RingtonePickerViewModelTest.java b/packages/SoundPicker2/tests/src/com/android/soundpicker/RingtonePickerViewModelTest.java
deleted file mode 100644
index cde6c76..0000000
--- a/packages/SoundPicker2/tests/src/com/android/soundpicker/RingtonePickerViewModelTest.java
+++ /dev/null
@@ -1,534 +0,0 @@
-/*
- * Copyright (C) 2023 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.soundpicker;
-
-import static junit.framework.Assert.assertEquals;
-import static junit.framework.Assert.assertNotNull;
-import static junit.framework.Assert.assertNull;
-
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyInt;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.verifyNoMoreInteractions;
-import static org.mockito.Mockito.when;
-
-import android.database.Cursor;
-import android.media.AudioAttributes;
-import android.media.AudioManager;
-import android.media.Ringtone;
-import android.media.RingtoneManager;
-import android.net.Uri;
-import android.provider.Settings;
-
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-
-import com.google.common.util.concurrent.FutureCallback;
-import com.google.common.util.concurrent.ListeningExecutorService;
-import com.google.common.util.concurrent.testing.TestingExecutors;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-
-import java.io.IOException;
-import java.util.concurrent.ExecutorService;
-
-@RunWith(AndroidJUnit4.class)
-public class RingtonePickerViewModelTest {
-
- private static final Uri DEFAULT_URI = Uri.parse("media://custom/ringtone/default_uri");
- private static final Uri RINGTONE_URI = Uri.parse("media://custom/ringtone/uri");
- private static final int RINGTONE_TYPE_UNKNOWN = -1;
- private static final int DEFAULT_RINGTONE_POSITION = 1;
-
- @Mock
- private RingtoneManagerFactory mMockRingtoneManagerFactory;
- @Mock
- private RingtoneFactory mMockRingtoneFactory;
- @Mock
- private RingtoneManager mMockRingtoneManager;
- @Mock
- private ListeningExecutorServiceFactory mMockListeningExecutorServiceFactory;
- @Mock
- private Cursor mMockCursor;
-
- private RingtoneListHandler mSoundListHandler;
- private RingtoneListHandler mVibrationListHandler;
- private ExecutorService mMainThreadExecutor;
- private ListeningExecutorService mBackgroundThreadExecutor;
- private Ringtone mMockDefaultRingtone;
- private Ringtone mMockRingtone;
- private RingtonePickerViewModel mViewModel;
- private RingtoneListHandler.Config mSoundListConfig;
- private RingtoneListHandler.Config mVibrationListConfig;
-
- @Before
- public void setUp() {
- MockitoAnnotations.initMocks(this);
-
- mSoundListHandler = new RingtoneListHandler();
- mVibrationListHandler = new RingtoneListHandler();
- mSoundListConfig = createRingtoneListConfig();
- mVibrationListConfig = createRingtoneListConfig();
- mMockDefaultRingtone = createMockRingtone();
- mMockRingtone = createMockRingtone();
- when(mMockRingtoneManagerFactory.create()).thenReturn(mMockRingtoneManager);
- when(mMockRingtoneFactory.create(DEFAULT_URI,
- AudioAttributes.FLAG_AUDIBILITY_ENFORCED)).thenReturn(mMockDefaultRingtone);
- when(mMockRingtoneManager.getRingtoneUri(anyInt())).thenReturn(RINGTONE_URI);
- when(mMockRingtoneManager.getCursor()).thenReturn(mMockCursor);
- mMainThreadExecutor = TestingExecutors.sameThreadScheduledExecutor();
- mBackgroundThreadExecutor = TestingExecutors.sameThreadScheduledExecutor();
- when(mMockListeningExecutorServiceFactory.createSingleThreadExecutor()).thenReturn(
- mBackgroundThreadExecutor);
-
- mViewModel = new RingtonePickerViewModel(mMockRingtoneManagerFactory, mMockRingtoneFactory,
- mMockListeningExecutorServiceFactory, mSoundListHandler,
- mVibrationListHandler);
-
- // Add silent and default options to the sound list.
- mSoundListHandler.addSilentItem();
- mSoundListHandler.addDefaultItem();
-
- // Add silent and default options to the vibration list.
- mVibrationListHandler.addSilentItem();
- mVibrationListHandler.addDefaultItem();
-
- mSoundListHandler.setSelectedItemPosition(DEFAULT_RINGTONE_POSITION);
- mVibrationListHandler.setSelectedItemPosition(DEFAULT_RINGTONE_POSITION);
- }
-
- @After
- public void teardown() {
- if (mMainThreadExecutor != null && !mMainThreadExecutor.isShutdown()) {
- mMainThreadExecutor.shutdown();
- }
- if (mBackgroundThreadExecutor != null && !mBackgroundThreadExecutor.isShutdown()) {
- mBackgroundThreadExecutor.shutdown();
- }
- }
-
- @Test
- public void testInitRingtoneManager_whenTypeIsUnknown_createManagerButDoNotSetType() {
- mViewModel.init(createPickerConfig(RINGTONE_TYPE_UNKNOWN), mSoundListConfig,
- mVibrationListConfig);
-
- verify(mMockRingtoneManagerFactory).create();
- verify(mMockRingtoneManager, never()).setType(anyInt());
- assertNotNull(mViewModel.getSoundListHandler().getRingtoneListConfig());
- assertNotNull(mViewModel.getVibrationListHandler().getRingtoneListConfig());
- }
-
- @Test
- public void testInitRingtoneManager_whenTypeIsNotUnknown_createManagerAndSetType() {
- mViewModel.init(createPickerConfig(RingtoneManager.TYPE_NOTIFICATION), mSoundListConfig,
- mVibrationListConfig);
-
- verify(mMockRingtoneManagerFactory).create();
- verify(mMockRingtoneManager).setType(RingtoneManager.TYPE_NOTIFICATION);
- assertNotNull(mViewModel.getSoundListHandler().getRingtoneListConfig());
- assertNotNull(mViewModel.getVibrationListHandler().getRingtoneListConfig());
- }
-
- @Test
- public void testInitRingtoneManager_bothListConfigsAreNull_onlyRecreateRingtoneManager() {
- mViewModel.init(
- createPickerConfig(RingtoneManager.TYPE_NOTIFICATION),
- /* soundListConfig= */ null, /* vibrationListConfig= */ null);
-
- verify(mMockRingtoneManagerFactory).create();
- verify(mMockRingtoneManager).setType(RingtoneManager.TYPE_NOTIFICATION);
- assertNull(mViewModel.getSoundListHandler().getRingtoneListConfig());
- assertNull(mViewModel.getVibrationListHandler().getRingtoneListConfig());
- }
-
- @Test
- public void testReinitialize_bothListConfigsInitialized_recreateManagerAndReinitHandlers() {
- mViewModel.init(createPickerConfig(RingtoneManager.TYPE_NOTIFICATION), mSoundListConfig,
- mVibrationListConfig);
- mViewModel.reinit();
-
- verify(mMockRingtoneManagerFactory, times(2)).create();
- verify(mMockRingtoneManager, times(2)).setType(RingtoneManager.TYPE_NOTIFICATION);
- assertNotNull(mViewModel.getSoundListHandler().getRingtoneListConfig());
- assertNotNull(mViewModel.getVibrationListHandler().getRingtoneListConfig());
- }
-
- @Test
- public void testReinitialize_bothListConfigsAlreadyNull_onlyRecreateRingtoneManager() {
- mViewModel.init(
- createPickerConfig(RingtoneManager.TYPE_NOTIFICATION),
- /* soundListConfig= */ null, /* vibrationListConfig= */ null);
- mViewModel.reinit();
-
- verify(mMockRingtoneManagerFactory, times(2)).create();
- verify(mMockRingtoneManager, times(2)).setType(RingtoneManager.TYPE_NOTIFICATION);
- assertNull(mViewModel.getSoundListHandler().getRingtoneListConfig());
- assertNull(mViewModel.getVibrationListHandler().getRingtoneListConfig());
- }
-
- @Test
- public void testGetStreamType_returnsTheCorrectStreamType() {
- when(mMockRingtoneManager.inferStreamType()).thenReturn(AudioManager.STREAM_ALARM);
- mViewModel.init(createPickerConfig(RingtoneManager.TYPE_RINGTONE), mSoundListConfig,
- mVibrationListConfig);
- assertEquals(mViewModel.getRingtoneStreamType(), AudioManager.STREAM_ALARM);
- }
-
- @Test
- public void testOnPause_withChangingConfigurationTrue_doNotStopPlayingRingtone() {
- mViewModel.init(createPickerConfig(RingtoneManager.TYPE_RINGTONE), mSoundListConfig,
- mVibrationListConfig);
- mViewModel.playRingtone();
- verifyRingtonePlayCalledAndMockPlayingState(mMockDefaultRingtone);
- mViewModel.onPause(/* isChangingConfigurations= */ true);
- verify(mMockDefaultRingtone, never()).stop();
- }
-
- @Test
- public void testOnPause_withChangingConfigurationFalse_stopPlayingRingtone() {
- mViewModel.init(createPickerConfig(RingtoneManager.TYPE_RINGTONE), mSoundListConfig,
- mVibrationListConfig);
- mViewModel.playRingtone();
- verifyRingtonePlayCalledAndMockPlayingState(mMockDefaultRingtone);
- mViewModel.onPause(/* isChangingConfigurations= */ false);
- verify(mMockDefaultRingtone).stop();
- }
-
- @Test
- public void testOnViewModelRecreated_previousRingtoneCanStillBeStopped() {
- mViewModel.init(createPickerConfig(RingtoneManager.TYPE_RINGTONE), mSoundListConfig,
- mVibrationListConfig);
- Ringtone mockRingtone1 = createMockRingtone();
- Ringtone mockRingtone2 = createMockRingtone();
-
- when(mMockRingtoneFactory.create(any(), anyInt())).thenReturn(mockRingtone1, mockRingtone2);
- mViewModel.playRingtone();
- verifyRingtonePlayCalledAndMockPlayingState(mockRingtone1);
- // Fake a scenario where the activity is destroyed and recreated due to a config change.
- // This will result in a new view model getting created.
- mViewModel.onStop(/* isChangingConfigurations= */ true);
- verify(mockRingtone1, never()).stop();
- mViewModel = new RingtonePickerViewModel(mMockRingtoneManagerFactory, mMockRingtoneFactory,
- mMockListeningExecutorServiceFactory, mSoundListHandler,
- mVibrationListHandler);
- mViewModel.init(createPickerConfig(RingtoneManager.TYPE_RINGTONE), mSoundListConfig,
- mVibrationListConfig);
- mViewModel.playRingtone();
- verifyRingtonePlayCalledAndMockPlayingState(mockRingtone2);
- verify(mockRingtone1).stop();
- verify(mockRingtone2, never()).stop();
- }
-
- @Test
- public void testOnStop_withChangingConfigurationTrueAndDefaultRingtonePlaying_saveRingtone() {
- mViewModel.init(createPickerConfig(RingtoneManager.TYPE_RINGTONE), mSoundListConfig,
- mVibrationListConfig);
- mViewModel.playRingtone();
- verifyRingtonePlayCalledAndMockPlayingState(mMockDefaultRingtone);
- mViewModel.onStop(/* isChangingConfigurations= */ true);
- assertEquals(RingtonePickerViewModel.sPlayingRingtone, mMockDefaultRingtone);
- }
-
- @Test
- public void testOnStop_withChangingConfigurationTrueAndCurrentRingtonePlaying_saveRingtone() {
- mViewModel.init(createPickerConfig(RingtoneManager.TYPE_RINGTONE), mSoundListConfig,
- mVibrationListConfig);
- mViewModel.playRingtone();
- verifyRingtonePlayCalledAndMockPlayingState(mMockDefaultRingtone);
- mViewModel.onStop(/* isChangingConfigurations= */ true);
- assertEquals(RingtonePickerViewModel.sPlayingRingtone, mMockDefaultRingtone);
- }
-
- @Test
- public void testOnStop_withChangingConfigurationTrueAndNoPlayingRingtone_saveNothing() {
- mViewModel.onStop(/* isChangingConfigurations= */ true);
- assertNull(RingtonePickerViewModel.sPlayingRingtone);
- }
-
- @Test
- public void testOnStop_withChangingConfigurationFalse_stopPlayingRingtone() {
- mViewModel.init(createPickerConfig(RingtoneManager.TYPE_RINGTONE), mSoundListConfig,
- mVibrationListConfig);
-
- mViewModel.playRingtone();
- verifyRingtonePlayCalledAndMockPlayingState(mMockDefaultRingtone);
- mViewModel.onStop(/* isChangingConfigurations= */ false);
- verify(mMockDefaultRingtone).stop();
- }
-
- @Test
- public void testGetCurrentlySelectedRingtoneUri_returnsTheCorrectRingtoneUri() {
- mViewModel.init(createPickerConfig(RingtoneManager.TYPE_RINGTONE), mSoundListConfig,
- mVibrationListConfig);
-
- assertEquals(DEFAULT_URI, mViewModel.getSelectedRingtoneUri());
- }
-
- @Test
- public void testPlayRingtone_playTheCorrectRingtone() {
- mSoundListHandler.setSelectedItemPosition(DEFAULT_RINGTONE_POSITION);
- mViewModel.init(createPickerConfig(RingtoneManager.TYPE_RINGTONE), mSoundListConfig,
- mVibrationListConfig);
-
- mViewModel.playRingtone();
- verifyRingtonePlayCalledAndMockPlayingState(mMockDefaultRingtone);
- }
-
- @Test
- public void testPlayRingtone_stopsPreviouslyRunningRingtone() {
- // Start playing the first ringtone
- mViewModel.init(createPickerConfig(RingtoneManager.TYPE_RINGTONE), mSoundListConfig,
- mVibrationListConfig);
- mViewModel.playRingtone();
- verifyRingtonePlayCalledAndMockPlayingState(mMockDefaultRingtone);
- // Start playing the second ringtone
- when(mMockRingtoneFactory.create(DEFAULT_URI,
- AudioAttributes.FLAG_AUDIBILITY_ENFORCED)).thenReturn(mMockRingtone);
- mViewModel.playRingtone();
- verifyRingtonePlayCalledAndMockPlayingState(mMockRingtone);
-
- verify(mMockDefaultRingtone).stop();
- }
-
- @Test
- public void testDefaultItemUri_withNotificationIntent_returnDefaultNotificationUri() {
- Uri uri = RingtonePickerViewModel.getDefaultItemUriByType(
- RingtoneManager.TYPE_NOTIFICATION);
- assertEquals(Settings.System.DEFAULT_NOTIFICATION_URI, uri);
- }
-
- @Test
- public void testDefaultItemUri_withAlarmIntent_returnDefaultAlarmUri() {
- Uri uri = RingtonePickerViewModel.getDefaultItemUriByType(RingtoneManager.TYPE_ALARM);
- assertEquals(Settings.System.DEFAULT_ALARM_ALERT_URI, uri);
- }
-
- @Test
- public void testDefaultItemUri_withRingtoneIntent_returnDefaultRingtoneUri() {
- Uri uri = RingtonePickerViewModel.getDefaultItemUriByType(RingtoneManager.TYPE_RINGTONE);
- assertEquals(Settings.System.DEFAULT_RINGTONE_URI, uri);
- }
-
- @Test
- public void testDefaultItemUri_withInvalidRingtoneType_returnDefaultRingtoneUri() {
- Uri uri = RingtonePickerViewModel.getDefaultItemUriByType(-1);
- assertEquals(Settings.System.DEFAULT_RINGTONE_URI, uri);
- }
-
- @Test
- public void testTitle_withNotificationRingtoneType_returnRingtoneNotificationTitle() {
- int title = RingtonePickerViewModel.getTitleByType(RingtoneManager.TYPE_NOTIFICATION);
- assertEquals(com.android.internal.R.string.ringtone_picker_title_notification, title);
- }
-
- @Test
- public void testTitle_withAlarmRingtoneType_returnRingtoneAlarmTitle() {
- int title = RingtonePickerViewModel.getTitleByType(RingtoneManager.TYPE_ALARM);
- assertEquals(com.android.internal.R.string.ringtone_picker_title_alarm, title);
- }
-
- @Test
- public void testTitle_withInvalidRingtoneType_returnDefaultRingtoneTitle() {
- int title = RingtonePickerViewModel.getTitleByType(/*ringtoneType= */ -1);
- assertEquals(com.android.internal.R.string.ringtone_picker_title, title);
- }
-
- @Test
- public void testAddNewItemText_withAlarmType_returnAlarmAddItemText() {
- int addNewItemTextResId = RingtonePickerViewModel.getAddNewItemTextByType(
- RingtoneManager.TYPE_ALARM);
- assertEquals(R.string.add_alarm_text, addNewItemTextResId);
- }
-
- @Test
- public void testAddNewItemText_withNotificationType_returnNotificationAddItemText() {
- int addNewItemTextResId = RingtonePickerViewModel.getAddNewItemTextByType(
- RingtoneManager.TYPE_NOTIFICATION);
- assertEquals(R.string.add_notification_text, addNewItemTextResId);
- }
-
- @Test
- public void testAddNewItemText_withRingtoneType_returnRingtoneAddItemText() {
- int addNewItemTextResId = RingtonePickerViewModel.getAddNewItemTextByType(
- RingtoneManager.TYPE_RINGTONE);
- assertEquals(R.string.add_ringtone_text, addNewItemTextResId);
- }
-
- @Test
- public void testAddNewItemText_withInvalidType_returnRingtoneAddItemText() {
- int addNewItemTextResId = RingtonePickerViewModel.getAddNewItemTextByType(-1);
- assertEquals(R.string.add_ringtone_text, addNewItemTextResId);
- }
-
- @Test
- public void testDefaultItemText_withNotificationType_returnNotificationDefaultItemText() {
- int defaultRingtoneItemText = RingtonePickerViewModel.getDefaultRingtoneItemTextByType(
- RingtoneManager.TYPE_NOTIFICATION);
- assertEquals(R.string.notification_sound_default, defaultRingtoneItemText);
- }
-
- @Test
- public void testDefaultItemText_withAlarmType_returnAlarmDefaultItemText() {
- int defaultRingtoneItemText = RingtonePickerViewModel.getDefaultRingtoneItemTextByType(
- RingtoneManager.TYPE_NOTIFICATION);
- assertEquals(R.string.notification_sound_default, defaultRingtoneItemText);
- }
-
- @Test
- public void testDefaultItemText_withRingtoneType_returnRingtoneDefaultItemText() {
- int defaultRingtoneItemText = RingtonePickerViewModel.getDefaultRingtoneItemTextByType(
- RingtoneManager.TYPE_RINGTONE);
- assertEquals(R.string.ringtone_default, defaultRingtoneItemText);
- }
-
- @Test
- public void testDefaultItemText_withInvalidType_returnRingtoneDefaultItemText() {
- int defaultRingtoneItemText = RingtonePickerViewModel.getDefaultRingtoneItemTextByType(-1);
- assertEquals(R.string.ringtone_default, defaultRingtoneItemText);
- }
-
- @Test
- public void testCancelPendingAsyncTasks_correctlyCancelsPendingTasks()
- throws IOException {
- FutureCallback<Uri> mockCallback = mock(FutureCallback.class);
-
- when(mMockListeningExecutorServiceFactory.createSingleThreadExecutor()).thenReturn(
- TestingExecutors.noOpScheduledExecutor());
-
- mViewModel = new RingtonePickerViewModel(mMockRingtoneManagerFactory, mMockRingtoneFactory,
- mMockListeningExecutorServiceFactory, mSoundListHandler,
- mVibrationListHandler);
- mViewModel.addSoundRingtoneAsync(DEFAULT_URI, RingtoneManager.TYPE_NOTIFICATION,
- mockCallback, mMainThreadExecutor);
- verify(mockCallback, never()).onFailure(any());
- // Calling cancelPendingAsyncTasks should cancel the pending task. Cancelling an async
- // task invokes the onFailure method in the callable.
- mViewModel.cancelPendingAsyncTasks();
- verify(mockCallback).onFailure(any());
- verify(mockCallback, never()).onSuccess(any());
-
- }
-
- @Test
- public void testAddRingtoneAsync_cancelPreviousTaskBeforeStartingNewOne()
- throws IOException {
- FutureCallback<Uri> mockCallback1 = mock(FutureCallback.class);
- FutureCallback<Uri> mockCallback2 = mock(FutureCallback.class);
-
- when(mMockListeningExecutorServiceFactory.createSingleThreadExecutor()).thenReturn(
- TestingExecutors.noOpScheduledExecutor());
-
- mViewModel = new RingtonePickerViewModel(mMockRingtoneManagerFactory, mMockRingtoneFactory,
- mMockListeningExecutorServiceFactory, mSoundListHandler,
- mVibrationListHandler);
- mViewModel.addSoundRingtoneAsync(DEFAULT_URI, RingtoneManager.TYPE_NOTIFICATION,
- mockCallback1, mMainThreadExecutor);
- verify(mockCallback1, never()).onFailure(any());
- // We call addRingtoneAsync again to cancel the previous task and start a new one.
- // Cancelling an async task invokes the onFailure method in the callable.
- mViewModel.addSoundRingtoneAsync(DEFAULT_URI, RingtoneManager.TYPE_NOTIFICATION,
- mockCallback2, mMainThreadExecutor);
- verify(mockCallback1).onFailure(any());
- verify(mockCallback1, never()).onSuccess(any());
- verifyNoMoreInteractions(mockCallback2);
- }
-
- @Test
- public void testAddRingtoneAsync_whenAddRingtoneIsSuccessful_successCallbackIsInvoked()
- throws IOException {
- Uri expectedUri = DEFAULT_URI;
- FutureCallback<Uri> mockCallback = mock(FutureCallback.class);
-
- when(mMockRingtoneManager.addCustomExternalRingtone(DEFAULT_URI,
- RingtoneManager.TYPE_NOTIFICATION)).thenReturn(expectedUri);
-
- mViewModel.init(createPickerConfig(RingtoneManager.TYPE_RINGTONE), mSoundListConfig,
- mVibrationListConfig);
-
- mViewModel.addSoundRingtoneAsync(DEFAULT_URI, RingtoneManager.TYPE_NOTIFICATION,
- mockCallback, mMainThreadExecutor);
-
- verify(mockCallback).onSuccess(expectedUri);
- verify(mockCallback, never()).onFailure(any());
- }
-
- @Test
- public void testAddRingtoneAsync_whenAddRingtoneFailed_failureCallbackIsInvoked()
- throws IOException {
- FutureCallback<Uri> mockCallback = mock(FutureCallback.class);
-
- when(mMockRingtoneManager.addCustomExternalRingtone(any(), anyInt())).thenThrow(
- IOException.class);
-
- mViewModel.init(createPickerConfig(RingtoneManager.TYPE_RINGTONE), mSoundListConfig,
- mVibrationListConfig);
-
- mViewModel.addSoundRingtoneAsync(DEFAULT_URI, RingtoneManager.TYPE_NOTIFICATION,
- mockCallback, mMainThreadExecutor);
-
- verify(mockCallback).onFailure(any(IOException.class));
- verify(mockCallback, never()).onSuccess(any());
- }
-
- private Ringtone createMockRingtone() {
- Ringtone mockRingtone = mock(Ringtone.class);
- when(mockRingtone.getAudioAttributes()).thenReturn(
- audioAttributes(AudioAttributes.USAGE_NOTIFICATION_RINGTONE, 0));
-
- return mockRingtone;
- }
-
- private void verifyRingtonePlayCalledAndMockPlayingState(Ringtone ringtone) {
- verify(ringtone).play();
- when(ringtone.isPlaying()).thenReturn(true);
- }
-
- private static AudioAttributes audioAttributes(int audioUsage, int flags) {
- return new AudioAttributes.Builder()
- .setUsage(audioUsage)
- .setFlags(flags)
- .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
- .build();
- }
-
- private RingtonePickerViewModel.Config createPickerConfig(int ringtoneType,
- int audioAttributes) {
- return new RingtonePickerViewModel.Config("Phone ringtone", /* userId= */ 1,
- ringtoneType, /* showOkCancelButtons= */ true,
- audioAttributes, RingtonePickerViewModel.PickerType.RINGTONE_PICKER);
- }
-
- private RingtonePickerViewModel.Config createPickerConfig(int ringtoneType) {
- return new RingtonePickerViewModel.Config("Phone ringtone", /* userId= */ 1,
- ringtoneType, /* showOkCancelButtons= */ true,
- AudioAttributes.FLAG_AUDIBILITY_ENFORCED,
- RingtonePickerViewModel.PickerType.RINGTONE_PICKER);
- }
-
- private RingtoneListHandler.Config createRingtoneListConfig() {
- return new RingtoneListHandler.Config(/* hasDefaultItem= */ true,
- /* uriForDefaultItem= */ DEFAULT_URI, /* hasSilentItem= */ true,
- /* existingUri= */ Uri.parse(""));
- }
-}
diff --git a/packages/SystemUI/Android.bp b/packages/SystemUI/Android.bp
index 900a2f8..d1a3571 100644
--- a/packages/SystemUI/Android.bp
+++ b/packages/SystemUI/Android.bp
@@ -15,6 +15,7 @@
//
package {
+ default_team: "trendy_team_system_ui_please_use_a_more_specific_subteam_if_possible_",
default_applicable_licenses: ["frameworks_base_packages_SystemUI_license"],
}
diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml
index 0050676..12e8f57 100644
--- a/packages/SystemUI/AndroidManifest.xml
+++ b/packages/SystemUI/AndroidManifest.xml
@@ -262,6 +262,9 @@
<uses-permission android:name="android.permission.MODIFY_THEME_OVERLAY" />
+ <!-- Activity Manager -->
+ <uses-permission android:name="android.permission.SET_THEME_OVERLAY_CONTROLLER_READY" />
+
<!-- accessibility -->
<uses-permission android:name="android.permission.MODIFY_ACCESSIBILITY_DATA" />
<uses-permission android:name="android.permission.MANAGE_ACCESSIBILITY" />
@@ -475,6 +478,9 @@
<service android:name=".screenrecord.RecordingService"
android:foregroundServiceType="systemExempted"/>
+ <service android:name=".recordissue.IssueRecordingService"
+ android:foregroundServiceType="systemExempted"/>
+
<receiver android:name=".SysuiRestartReceiver"
android:exported="false">
<intent-filter>
@@ -992,7 +998,6 @@
android:theme="@style/Theme.EditWidgetsActivity"
android:excludeFromRecents="true"
android:autoRemoveFromRecents="true"
- android:showOnLockScreen="true"
android:launchMode="singleTop"
android:exported="false">
</activity>
diff --git a/packages/SystemUI/accessibility/accessibilitymenu/Android.bp b/packages/SystemUI/accessibility/accessibilitymenu/Android.bp
index 0df9bac..d674b6c 100644
--- a/packages/SystemUI/accessibility/accessibilitymenu/Android.bp
+++ b/packages/SystemUI/accessibility/accessibilitymenu/Android.bp
@@ -15,6 +15,7 @@
//
package {
+ default_team: "trendy_team_system_ui_please_use_a_more_specific_subteam_if_possible_",
default_applicable_licenses: ["Android-Apache-2.0"],
}
diff --git a/packages/SystemUI/accessibility/accessibilitymenu/tests/Android.bp b/packages/SystemUI/accessibility/accessibilitymenu/tests/Android.bp
index 3fc351c..64dcf6e 100644
--- a/packages/SystemUI/accessibility/accessibilitymenu/tests/Android.bp
+++ b/packages/SystemUI/accessibility/accessibilitymenu/tests/Android.bp
@@ -15,6 +15,7 @@
//
package {
+ default_team: "trendy_team_system_ui_please_use_a_more_specific_subteam_if_possible_",
default_applicable_licenses: ["Android-Apache-2.0"],
}
diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig
index 3db99f28..2ad7192 100644
--- a/packages/SystemUI/aconfig/systemui.aconfig
+++ b/packages/SystemUI/aconfig/systemui.aconfig
@@ -67,6 +67,16 @@
}
flag {
+ name: "nssl_falsing_fix"
+ namespace: "systemui"
+ description: "Minor touch changes to prevent falsing errors in NSSL"
+ bug: "316551193"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
+
+flag {
name: "refactor_get_current_user"
namespace: "systemui"
description: "KeyguardUpdateMonitor.getCurrentUser() was providing outdated results."
@@ -94,7 +104,7 @@
" standard background color is desired. This was the behavior before we discovered"
" a resources threading issue, which we worked around by tinting the notification"
" backgrounds and footer buttons."
- bug: "294347738"
+ bug: "294830092"
}
flag {
@@ -158,6 +168,9 @@
"Note that, even after this callback is called, we're waiting for all windows to finish "
" drawing."
bug: "295873557"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
}
flag {
@@ -323,13 +336,6 @@
}
flag {
- name: "screenshare_notification_hiding"
- namespace: "systemui"
- description: "Enable hiding of notifications during screenshare"
- bug: "312784809"
-}
-
-flag {
name: "run_fingerprint_detect_on_dismissible_keyguard"
namespace: "systemui"
description: "Run fingerprint detect instead of authenticate if the keyguard is dismissible."
@@ -365,9 +371,9 @@
}
flag {
- name: "enable_keyguard_compose"
+ name: "compose_lockscreen"
namespace: "systemui"
- description: "Enables the compose version of keyguard."
+ description: "Enables the compose version of lockscreen that runs standalone, outside of Flexiglass."
bug: "301968149"
}
@@ -384,3 +390,18 @@
description: "Enables on-screen contextual tip about how to take screenshot."
bug: "322891421"
}
+
+flag {
+ name: "enable_contextual_tips"
+ description: "Enables showing contextual tips."
+ namespace: "systemui"
+ bug: "322891421"
+}
+
+flag {
+ name: "shaderlib_loading_effect_refactor"
+ namespace: "systemui"
+ description: "Extend shader library to provide the common loading effects."
+ bug: "282007590"
+}
+
diff --git a/packages/SystemUI/animation/Android.bp b/packages/SystemUI/animation/Android.bp
index 872187a..c1125f0 100644
--- a/packages/SystemUI/animation/Android.bp
+++ b/packages/SystemUI/animation/Android.bp
@@ -13,6 +13,7 @@
// limitations under the License.
package {
+ default_team: "trendy_team_system_ui_please_use_a_more_specific_subteam_if_possible_",
// See: http://go/android-license-faq
// A large-scale-change added 'default_applicable_licenses' to import
// all of the 'license_kinds' from "frameworks_base_packages_SystemUI_license"
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityLaunchAnimator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityTransitionAnimator.kt
similarity index 79%
rename from packages/SystemUI/animation/src/com/android/systemui/animation/ActivityLaunchAnimator.kt
rename to packages/SystemUI/animation/src/com/android/systemui/animation/ActivityTransitionAnimator.kt
index 8194055..c489795 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityLaunchAnimator.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityTransitionAnimator.kt
@@ -45,20 +45,20 @@
import com.android.internal.policy.ScreenDecorationsUtils
import kotlin.math.roundToInt
-private const val TAG = "ActivityLaunchAnimator"
+private const val TAG = "ActivityTransitionAnimator"
/**
* A class that allows activities to be started in a seamless way from a view that is transforming
* nicely into the starting window.
*/
-class ActivityLaunchAnimator(
+class ActivityTransitionAnimator(
/** The animator used when animating a View into an app. */
- private val launchAnimator: LaunchAnimator = DEFAULT_LAUNCH_ANIMATOR,
+ private val transitionAnimator: TransitionAnimator = DEFAULT_TRANSITION_ANIMATOR,
/** The animator used when animating a Dialog into an app. */
// TODO(b/218989950): Remove this animator and instead set the duration of the dim fade out to
// TIMINGS.contentBeforeFadeOutDuration.
- private val dialogToAppAnimator: LaunchAnimator = DEFAULT_DIALOG_TO_APP_ANIMATOR,
+ private val dialogToAppAnimator: TransitionAnimator = DEFAULT_DIALOG_TO_APP_ANIMATOR,
/**
* Whether we should disable the WindowManager timeout. This should be set to true in tests
@@ -71,7 +71,7 @@
/** The timings when animating a View into an app. */
@JvmField
val TIMINGS =
- LaunchAnimator.Timings(
+ TransitionAnimator.Timings(
totalDuration = 500L,
contentBeforeFadeOutDelay = 0L,
contentBeforeFadeOutDuration = 150L,
@@ -89,7 +89,7 @@
/** The interpolators when animating a View or a dialog into an app. */
val INTERPOLATORS =
- LaunchAnimator.Interpolators(
+ TransitionAnimator.Interpolators(
positionInterpolator = Interpolators.EMPHASIZED,
positionXInterpolator = Interpolators.EMPHASIZED_COMPLEMENT,
contentBeforeFadeOutInterpolator = Interpolators.LINEAR_OUT_SLOW_IN,
@@ -97,10 +97,11 @@
)
// TODO(b/288507023): Remove this flag.
- @JvmField val DEBUG_LAUNCH_ANIMATION = Build.IS_DEBUGGABLE
+ @JvmField val DEBUG_TRANSITION_ANIMATION = Build.IS_DEBUGGABLE
- private val DEFAULT_LAUNCH_ANIMATOR = LaunchAnimator(TIMINGS, INTERPOLATORS)
- private val DEFAULT_DIALOG_TO_APP_ANIMATOR = LaunchAnimator(DIALOG_TIMINGS, INTERPOLATORS)
+ private val DEFAULT_TRANSITION_ANIMATOR = TransitionAnimator(TIMINGS, INTERPOLATORS)
+ private val DEFAULT_DIALOG_TO_APP_ANIMATOR =
+ TransitionAnimator(DIALOG_TIMINGS, INTERPOLATORS)
/** Durations & interpolators for the navigation bar fading in & out. */
private const val ANIMATION_DURATION_NAV_FADE_IN = 266L
@@ -112,13 +113,13 @@
private val NAV_FADE_OUT_INTERPOLATOR = PathInterpolator(0.2f, 0f, 1f, 1f)
/** The time we wait before timing out the remote animation after starting the intent. */
- private const val LAUNCH_TIMEOUT = 1_000L
+ private const val TRANSITION_TIMEOUT = 1_000L
/**
* The time we wait before we Log.wtf because the remote animation was neither started or
* cancelled by WM.
*/
- private const val LONG_LAUNCH_TIMEOUT = 5_000L
+ private const val LONG_TRANSITION_TIMEOUT = 5_000L
}
/**
@@ -133,20 +134,20 @@
/** Top-level listener that can be used to notify all registered [listeners]. */
private val lifecycleListener =
object : Listener {
- override fun onLaunchAnimationStart() {
- listeners.forEach { it.onLaunchAnimationStart() }
+ override fun onTransitionAnimationStart() {
+ listeners.forEach { it.onTransitionAnimationStart() }
}
- override fun onLaunchAnimationEnd() {
- listeners.forEach { it.onLaunchAnimationEnd() }
+ override fun onTransitionAnimationEnd() {
+ listeners.forEach { it.onTransitionAnimationEnd() }
}
- override fun onLaunchAnimationProgress(linearProgress: Float) {
- listeners.forEach { it.onLaunchAnimationProgress(linearProgress) }
+ override fun onTransitionAnimationProgress(linearProgress: Float) {
+ listeners.forEach { it.onTransitionAnimationProgress(linearProgress) }
}
- override fun onLaunchAnimationCancelled() {
- listeners.forEach { it.onLaunchAnimationCancelled() }
+ override fun onTransitionAnimationCancelled() {
+ listeners.forEach { it.onTransitionAnimationCancelled() }
}
}
@@ -154,7 +155,7 @@
* Start an intent and animate the opening window. The intent will be started by running
* [intentStarter], which should use the provided [RemoteAnimationAdapter] and return the launch
* result. [controller] is responsible from animating the view from which the intent was started
- * in [Controller.onLaunchAnimationProgress]. No animation will start if there is no window
+ * in [Controller.onTransitionAnimationProgress]. No animation will start if there is no window
* opening.
*
* If [controller] is null or [animate] is false, then the intent will be started and no
@@ -187,7 +188,7 @@
val callback =
this.callback
?: throw IllegalStateException(
- "ActivityLaunchAnimator.callback must be set before using this animator"
+ "ActivityTransitionAnimator.callback must be set before using this animator"
)
val runner = createRunner(controller)
val runnerDelegate = runner.delegate!!
@@ -255,11 +256,11 @@
private fun Controller.callOnIntentStartedOnMainThread(willAnimate: Boolean) {
if (Looper.myLooper() != Looper.getMainLooper()) {
- this.launchContainer.context.mainExecutor.execute {
+ this.transitionContainer.context.mainExecutor.execute {
callOnIntentStartedOnMainThread(willAnimate)
}
} else {
- if (DEBUG_LAUNCH_ANIMATION) {
+ if (DEBUG_TRANSITION_ANIMATION) {
Log.d(
TAG,
"Calling controller.onIntentStarted(willAnimate=$willAnimate) " +
@@ -292,7 +293,7 @@
}
}
- /** Add a [Listener] that can listen to launch animations. */
+ /** Add a [Listener] that can listen to transition animations. */
fun addListener(listener: Listener) {
listeners.add(listener)
}
@@ -306,14 +307,14 @@
@VisibleForTesting
fun createRunner(controller: Controller): Runner {
// Make sure we use the modified timings when animating a dialog into an app.
- val launchAnimator =
+ val transitionAnimator =
if (controller.isDialogLaunch) {
dialogToAppAnimator
} else {
- launchAnimator
+ transitionAnimator
}
- return Runner(controller, callback!!, launchAnimator, lifecycleListener)
+ return Runner(controller, callback!!, transitionAnimator, lifecycleListener)
}
interface PendingIntentStarter {
@@ -339,24 +340,24 @@
}
interface Listener {
- /** Called when an activity launch animation started. */
- fun onLaunchAnimationStart() {}
+ /** Called when an activity transition animation started. */
+ fun onTransitionAnimationStart() {}
/**
- * Called when an activity launch animation is finished. This will be called if and only if
- * [onLaunchAnimationStart] was called earlier.
+ * Called when an activity transition animation is finished. This will be called if and only
+ * if [onTransitionAnimationStart] was called earlier.
*/
- fun onLaunchAnimationEnd() {}
+ fun onTransitionAnimationEnd() {}
/**
- * The animation was cancelled. Note that [onLaunchAnimationEnd] will still be called after
- * this if the animation was already started, i.e. if [onLaunchAnimationStart] was called
- * before the cancellation.
+ * The animation was cancelled. Note that [onTransitionAnimationEnd] will still be called
+ * after this if the animation was already started, i.e. if [onTransitionAnimationStart] was
+ * called before the cancellation.
*/
- fun onLaunchAnimationCancelled() {}
+ fun onTransitionAnimationCancelled() {}
- /** Called when an activity launch animation made progress. */
- fun onLaunchAnimationProgress(linearProgress: Float) {}
+ /** Called when an activity transition animation made progress. */
+ fun onTransitionAnimationProgress(linearProgress: Float) {}
}
/**
@@ -364,7 +365,7 @@
*
* Note that all callbacks (onXXX methods) are all called on the main thread.
*/
- interface Controller : LaunchAnimator.Controller {
+ interface Controller : TransitionAnimator.Controller {
companion object {
/**
* Return a [Controller] that will animate and expand [view] into the opening window.
@@ -382,9 +383,10 @@
// issues.
if (view !is LaunchableView) {
throw IllegalArgumentException(
- "An ActivityLaunchAnimator.Controller was created from a View that does " +
- "not implement LaunchableView. This can lead to subtle bugs where the" +
- " visibility of the View we are launching from is not what we expected."
+ "An ActivityTransitionAnimator.Controller was created from a View that " +
+ "does not implement LaunchableView. This can lead to subtle bugs " +
+ "where the visibility of the View we are launching from is not what " +
+ "we expected."
)
}
@@ -410,11 +412,11 @@
get() = false
/**
- * Whether the expandable controller by this [Controller] is below the launching window that
- * is going to be animated.
+ * Whether the expandable controller by this [Controller] is below the window that is going
+ * to be animated.
*
- * This should be `false` when launching an app from the shade or status bar, given that
- * they are drawn above all apps. This is usually `true` when using this launcher in a
+ * This should be `false` when animating an app from or to the shade or status bar, given
+ * that they are drawn above all apps. This is usually `true` when using this animator in a
* normal app or a launcher, that are drawn below the animating activity/window.
*/
val isBelowAnimatingWindow: Boolean
@@ -427,14 +429,15 @@
fun onIntentStarted(willAnimate: Boolean) {}
/**
- * The animation was cancelled. Note that [onLaunchAnimationEnd] will still be called after
- * this if the animation was already started, i.e. if [onLaunchAnimationStart] was called
- * before the cancellation.
+ * The animation was cancelled. Note that [onTransitionAnimationEnd] will still be called
+ * after this if the animation was already started, i.e. if [onTransitionAnimationStart] was
+ * called before the cancellation.
*
- * If this launch animation affected the occlusion state of the keyguard, WM will provide us
- * with [newKeyguardOccludedState] so that we can set the occluded state appropriately.
+ * If this transition animation affected the occlusion state of the keyguard, WM will
+ * provide us with [newKeyguardOccludedState] so that we can set the occluded state
+ * appropriately.
*/
- fun onLaunchAnimationCancelled(newKeyguardOccludedState: Boolean? = null) {}
+ fun onTransitionAnimationCancelled(newKeyguardOccludedState: Boolean? = null) {}
}
/**
@@ -448,24 +451,24 @@
) : Listener {
var cancelled = false
- override fun onLaunchAnimationStart() {
- delegate?.onLaunchAnimationStart()
+ override fun onTransitionAnimationStart() {
+ delegate?.onTransitionAnimationStart()
}
- override fun onLaunchAnimationProgress(linearProgress: Float) {
- delegate?.onLaunchAnimationProgress(linearProgress)
+ override fun onTransitionAnimationProgress(linearProgress: Float) {
+ delegate?.onTransitionAnimationProgress(linearProgress)
}
- override fun onLaunchAnimationEnd() {
- delegate?.onLaunchAnimationEnd()
+ override fun onTransitionAnimationEnd() {
+ delegate?.onTransitionAnimationEnd()
if (!cancelled) {
onAnimationComplete.invoke()
}
}
- override fun onLaunchAnimationCancelled() {
+ override fun onTransitionAnimationCancelled() {
cancelled = true
- delegate?.onLaunchAnimationCancelled()
+ delegate?.onTransitionAnimationCancelled()
onAnimationComplete.invoke()
}
}
@@ -474,12 +477,12 @@
inner class Runner(
controller: Controller,
callback: Callback,
- /** The animator to use to animate the window launch. */
- launchAnimator: LaunchAnimator = DEFAULT_LAUNCH_ANIMATOR,
+ /** The animator to use to animate the window transition. */
+ transitionAnimator: TransitionAnimator = DEFAULT_TRANSITION_ANIMATOR,
/** Listener for animation lifecycle events. */
listener: Listener? = null
) : IRemoteAnimationRunner.Stub() {
- private val context = controller.launchContainer.context
+ private val context = controller.transitionContainer.context
// This is being passed across IPC boundaries and cycles (through PendingIntentRecords,
// etc.) are possible. So we need to make sure we drop any references that might
@@ -492,7 +495,7 @@
controller,
callback,
DelegatingAnimationCompletionListener(listener, this::dispose),
- launchAnimator,
+ transitionAnimator,
disableWmTimeout
)
}
@@ -542,8 +545,8 @@
private val callback: Callback,
/** Listener for animation lifecycle events. */
private val listener: Listener? = null,
- /** The animator to use to animate the window launch. */
- private val launchAnimator: LaunchAnimator = DEFAULT_LAUNCH_ANIMATOR,
+ /** The animator to use to animate the window transition. */
+ private val transitionAnimator: TransitionAnimator = DEFAULT_TRANSITION_ANIMATOR,
/**
* Whether we should disable the WindowManager timeout. This should be set to true in tests
@@ -552,10 +555,10 @@
// TODO(b/301385865): Remove this flag.
disableWmTimeout: Boolean = false,
) : RemoteAnimationDelegate<IRemoteAnimationFinishedCallback> {
- private val launchContainer = controller.launchContainer
- private val context = launchContainer.context
+ private val transitionContainer = controller.transitionContainer
+ private val context = transitionContainer.context
private val transactionApplierView =
- controller.openingWindowSyncView ?: controller.launchContainer
+ controller.openingWindowSyncView ?: controller.transitionContainer
private val transactionApplier = SyncRtSurfaceTransactionApplier(transactionApplierView)
private val timeoutHandler =
if (!disableWmTimeout) {
@@ -570,11 +573,11 @@
private var windowCropF = RectF()
private var timedOut = false
private var cancelled = false
- private var animation: LaunchAnimator.Animation? = null
+ private var animation: TransitionAnimator.Animation? = null
/**
- * A timeout to cancel the launch animation if the remote animation is not started or
- * cancelled within [LAUNCH_TIMEOUT] milliseconds after the intent was started.
+ * A timeout to cancel the transition animation if the remote animation is not started or
+ * cancelled within [TRANSITION_TIMEOUT] milliseconds after the intent was started.
*
* Note that this is important to keep this a Runnable (and not a Kotlin lambda), otherwise
* it will be automatically converted when posted and we wouldn't be able to remove it after
@@ -584,21 +587,22 @@
/**
* A long timeout to Log.wtf (signaling a bug in WM) when the remote animation wasn't
- * started or cancelled within [LONG_LAUNCH_TIMEOUT] milliseconds after the intent was
+ * started or cancelled within [LONG_TRANSITION_TIMEOUT] milliseconds after the intent was
* started.
*/
private var onLongTimeout = Runnable {
Log.wtf(
TAG,
- "The remote animation was neither cancelled or started within $LONG_LAUNCH_TIMEOUT"
+ "The remote animation was neither cancelled or started within " +
+ "$LONG_TRANSITION_TIMEOUT"
)
}
@UiThread
internal fun postTimeouts() {
if (timeoutHandler != null) {
- timeoutHandler.postDelayed(onTimeout, LAUNCH_TIMEOUT)
- timeoutHandler.postDelayed(onLongTimeout, LONG_LAUNCH_TIMEOUT)
+ timeoutHandler.postDelayed(onTimeout, TRANSITION_TIMEOUT)
+ timeoutHandler.postDelayed(onLongTimeout, LONG_TRANSITION_TIMEOUT)
}
}
@@ -660,7 +664,7 @@
nonApps: Array<out RemoteAnimationTarget>?,
iCallback: IRemoteAnimationFinishedCallback?
) {
- if (LaunchAnimator.DEBUG) {
+ if (TransitionAnimator.DEBUG) {
Log.d(TAG, "Remote animation started")
}
@@ -669,14 +673,14 @@
Log.i(TAG, "Aborting the animation as no window is opening")
iCallback?.invoke()
- if (DEBUG_LAUNCH_ANIMATION) {
+ if (DEBUG_TRANSITION_ANIMATION) {
Log.d(
TAG,
- "Calling controller.onLaunchAnimationCancelled() [no window opening]"
+ "Calling controller.onTransitionAnimationCancelled() [no window opening]"
)
}
- controller.onLaunchAnimationCancelled()
- listener?.onLaunchAnimationCancelled()
+ controller.onTransitionAnimationCancelled()
+ listener?.onTransitionAnimationCancelled()
return
}
@@ -687,7 +691,7 @@
val windowBounds = window.screenSpaceBounds
val endState =
- LaunchAnimator.State(
+ TransitionAnimator.State(
top = windowBounds.top,
bottom = windowBounds.bottom,
left = windowBounds.left,
@@ -699,7 +703,7 @@
// TODO(b/184121838): We should somehow get the top and bottom radius of the window
// instead of recomputing isExpandingFullyAbove here.
val isExpandingFullyAbove =
- launchAnimator.isExpandingFullyAbove(controller.launchContainer, endState)
+ transitionAnimator.isExpandingFullyAbove(controller.transitionContainer, endState)
val endRadius =
if (isExpandingFullyAbove) {
// Most of the time, expanding fully above the root view means expanding in full
@@ -718,35 +722,37 @@
val delegate = this.controller
val controller =
object : Controller by delegate {
- override fun onLaunchAnimationStart(isExpandingFullyAbove: Boolean) {
- listener?.onLaunchAnimationStart()
+ override fun onTransitionAnimationStart(isExpandingFullyAbove: Boolean) {
+ listener?.onTransitionAnimationStart()
- if (DEBUG_LAUNCH_ANIMATION) {
+ if (DEBUG_TRANSITION_ANIMATION) {
Log.d(
TAG,
- "Calling controller.onLaunchAnimationStart(isExpandingFullyAbove=" +
- "$isExpandingFullyAbove) [controller=$delegate]"
+ "Calling controller.onTransitionAnimationStart(" +
+ "isExpandingFullyAbove=$isExpandingFullyAbove) " +
+ "[controller=$delegate]"
)
}
- delegate.onLaunchAnimationStart(isExpandingFullyAbove)
+ delegate.onTransitionAnimationStart(isExpandingFullyAbove)
}
- override fun onLaunchAnimationEnd(isExpandingFullyAbove: Boolean) {
- listener?.onLaunchAnimationEnd()
+ override fun onTransitionAnimationEnd(isExpandingFullyAbove: Boolean) {
+ listener?.onTransitionAnimationEnd()
iCallback?.invoke()
- if (DEBUG_LAUNCH_ANIMATION) {
+ if (DEBUG_TRANSITION_ANIMATION) {
Log.d(
TAG,
- "Calling controller.onLaunchAnimationEnd(isExpandingFullyAbove=" +
- "$isExpandingFullyAbove) [controller=$delegate]"
+ "Calling controller.onTransitionAnimationEnd(" +
+ "isExpandingFullyAbove=$isExpandingFullyAbove) " +
+ "[controller=$delegate]"
)
}
- delegate.onLaunchAnimationEnd(isExpandingFullyAbove)
+ delegate.onTransitionAnimationEnd(isExpandingFullyAbove)
}
- override fun onLaunchAnimationProgress(
- state: LaunchAnimator.State,
+ override fun onTransitionAnimationProgress(
+ state: TransitionAnimator.State,
progress: Float,
linearProgress: Float
) {
@@ -757,13 +763,13 @@
}
navigationBar?.let { applyStateToNavigationBar(it, state, linearProgress) }
- listener?.onLaunchAnimationProgress(linearProgress)
- delegate.onLaunchAnimationProgress(state, progress, linearProgress)
+ listener?.onTransitionAnimationProgress(linearProgress)
+ delegate.onTransitionAnimationProgress(state, progress, linearProgress)
}
}
animation =
- launchAnimator.startAnimation(
+ transitionAnimator.startAnimation(
controller,
endState,
windowBackgroundColor,
@@ -774,7 +780,7 @@
private fun applyStateToWindow(
window: RemoteAnimationTarget,
- state: LaunchAnimator.State,
+ state: TransitionAnimator.State,
linearProgress: Float,
) {
if (transactionApplierView.viewRootImpl == null || !window.leash.isValid) {
@@ -825,7 +831,7 @@
val alpha =
if (controller.isBelowAnimatingWindow) {
val windowProgress =
- LaunchAnimator.getProgress(
+ TransitionAnimator.getProgress(
TIMINGS,
linearProgress,
TIMINGS.contentAfterFadeInDelay,
@@ -857,7 +863,7 @@
private fun applyStateToNavigationBar(
navigationBar: RemoteAnimationTarget,
- state: LaunchAnimator.State,
+ state: TransitionAnimator.State,
linearProgress: Float
) {
if (transactionApplierView.viewRootImpl == null || !navigationBar.leash.isValid) {
@@ -868,7 +874,7 @@
}
val fadeInProgress =
- LaunchAnimator.getProgress(
+ TransitionAnimator.getProgress(
TIMINGS,
linearProgress,
ANIMATION_DELAY_NAV_FADE_IN,
@@ -890,7 +896,7 @@
.withVisibility(true)
} else {
val fadeOutProgress =
- LaunchAnimator.getProgress(
+ TransitionAnimator.getProgress(
TIMINGS,
linearProgress,
0,
@@ -903,7 +909,7 @@
}
private fun onAnimationTimedOut() {
- // The remote animation was cancelled by WM, so we already cancelled the launch
+ // The remote animation was cancelled by WM, so we already cancelled the transition
// animation.
if (cancelled) {
return
@@ -912,18 +918,21 @@
Log.w(TAG, "Remote animation timed out")
timedOut = true
- if (DEBUG_LAUNCH_ANIMATION) {
- Log.d(TAG, "Calling controller.onLaunchAnimationCancelled() [animation timed out]")
+ if (DEBUG_TRANSITION_ANIMATION) {
+ Log.d(
+ TAG,
+ "Calling controller.onTransitionAnimationCancelled() [animation timed out]"
+ )
}
- controller.onLaunchAnimationCancelled()
- listener?.onLaunchAnimationCancelled()
+ controller.onTransitionAnimationCancelled()
+ listener?.onTransitionAnimationCancelled()
}
@UiThread
override fun onAnimationCancelled() {
removeTimeouts()
- // The short timeout happened, so we already cancelled the launch animation.
+ // The short timeout happened, so we already cancelled the transition animation.
if (timedOut) {
return
}
@@ -933,14 +942,15 @@
animation?.cancel()
- if (DEBUG_LAUNCH_ANIMATION) {
+ if (DEBUG_TRANSITION_ANIMATION) {
Log.d(
TAG,
- "Calling controller.onLaunchAnimationCancelled() [remote animation cancelled]",
+ "Calling controller.onTransitionAnimationCancelled() [remote animation " +
+ "cancelled]",
)
}
- controller.onLaunchAnimationCancelled()
- listener?.onLaunchAnimationCancelled()
+ controller.onTransitionAnimationCancelled()
+ listener?.onTransitionAnimationCancelled()
}
private fun IRemoteAnimationFinishedCallback.invoke() {
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/DelegateLaunchAnimatorController.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/DelegateLaunchAnimatorController.kt
index b879ba0..a53ab62 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/animation/DelegateLaunchAnimatorController.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/DelegateLaunchAnimatorController.kt
@@ -17,10 +17,10 @@
package com.android.systemui.animation
/**
- * A base class to easily create an implementation of [ActivityLaunchAnimator.Controller] which
+ * A base class to easily create an implementation of [ActivityTransitionAnimator.Controller] which
* delegates most of its call to [delegate]. This is mostly useful for Java code which can't easily
* create such a delegated class.
*/
open class DelegateLaunchAnimatorController(
- protected val delegate: ActivityLaunchAnimator.Controller
-) : ActivityLaunchAnimator.Controller by delegate
+ protected val delegate: ActivityTransitionAnimator.Controller
+) : ActivityTransitionAnimator.Controller by delegate
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/DialogLaunchAnimator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/DialogLaunchAnimator.kt
index 168039e..ed7f31c 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/animation/DialogLaunchAnimator.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/DialogLaunchAnimator.kt
@@ -58,17 +58,18 @@
private val callback: Callback,
private val interactionJankMonitor: InteractionJankMonitor,
private val featureFlags: AnimationFeatureFlags,
- private val launchAnimator: LaunchAnimator = LaunchAnimator(TIMINGS, INTERPOLATORS),
+ private val transitionAnimator: TransitionAnimator = TransitionAnimator(TIMINGS, INTERPOLATORS),
private val isForTesting: Boolean = false,
) {
private companion object {
- private val TIMINGS = ActivityLaunchAnimator.TIMINGS
+ private val TIMINGS = ActivityTransitionAnimator.TIMINGS
// We use the same interpolator for X and Y axis to make sure the dialog does not move out
// of the screen bounds during the animation.
private val INTERPOLATORS =
- ActivityLaunchAnimator.INTERPOLATORS.copy(
- positionXInterpolator = ActivityLaunchAnimator.INTERPOLATORS.positionInterpolator
+ ActivityTransitionAnimator.INTERPOLATORS.copy(
+ positionXInterpolator =
+ ActivityTransitionAnimator.INTERPOLATORS.positionInterpolator
)
}
@@ -108,21 +109,21 @@
fun stopDrawingInOverlay()
/**
- * Create the [LaunchAnimator.Controller] that will be called to animate the source
+ * Create the [TransitionAnimator.Controller] that will be called to animate the source
* controlled by this [Controller] during the dialog launch animation.
*
* At the end of this animation, the source should *not* be visible anymore (until the
* dialog is closed and is animated back into the source).
*/
- fun createLaunchController(): LaunchAnimator.Controller
+ fun createTransitionController(): TransitionAnimator.Controller
/**
- * Create the [LaunchAnimator.Controller] that will be called to animate the source
+ * Create the [TransitionAnimator.Controller] that will be called to animate the source
* controlled by this [Controller] during the dialog exit animation.
*
* At the end of this animation, the source should be visible again.
*/
- fun createExitController(): LaunchAnimator.Controller
+ fun createExitController(): TransitionAnimator.Controller
/**
* Whether we should animate the dialog back into the source when it is dismissed. If this
@@ -270,7 +271,7 @@
val animatedDialog =
AnimatedDialog(
- launchAnimator = launchAnimator,
+ transitionAnimator = transitionAnimator,
callback = callback,
interactionJankMonitor = interactionJankMonitor,
controller = controller,
@@ -319,9 +320,9 @@
}
/**
- * Create an [ActivityLaunchAnimator.Controller] that can be used to launch an activity from the
- * dialog that contains [View]. Note that the dialog must have been shown using this animator,
- * otherwise this method will return null.
+ * Create an [ActivityTransitionAnimator.Controller] that can be used to launch an activity from
+ * the dialog that contains [View]. Note that the dialog must have been shown using this
+ * animator, otherwise this method will return null.
*
* The returned controller will take care of dismissing the dialog at the right time after the
* activity started, when the dialog to app animation is done (or when it is cancelled). If this
@@ -333,7 +334,7 @@
fun createActivityLaunchController(
view: View,
cujType: Int? = null,
- ): ActivityLaunchAnimator.Controller? {
+ ): ActivityTransitionAnimator.Controller? {
val animatedDialog =
openedDialogs.firstOrNull {
it.dialog.window?.decorView?.viewRootImpl == view.viewRootImpl
@@ -343,7 +344,7 @@
}
/**
- * Create an [ActivityLaunchAnimator.Controller] that can be used to launch an activity from
+ * Create an [ActivityTransitionAnimator.Controller] that can be used to launch an activity from
* [dialog]. Note that the dialog must have been shown using this animator, otherwise this
* method will return null.
*
@@ -357,7 +358,7 @@
fun createActivityLaunchController(
dialog: Dialog,
cujType: Int? = null,
- ): ActivityLaunchAnimator.Controller? {
+ ): ActivityTransitionAnimator.Controller? {
val animatedDialog = openedDialogs.firstOrNull { it.dialog == dialog } ?: return null
return createActivityLaunchController(animatedDialog, cujType)
}
@@ -365,7 +366,7 @@
private fun createActivityLaunchController(
animatedDialog: AnimatedDialog,
cujType: Int? = null
- ): ActivityLaunchAnimator.Controller? {
+ ): ActivityTransitionAnimator.Controller? {
// At this point, we know that the intent of the caller is to dismiss the dialog to show
// an app, so we disable the exit animation into the source because we will never want to
// run it anyways.
@@ -384,12 +385,12 @@
val dialogContentWithBackground = animatedDialog.dialogContentWithBackground ?: return null
val controller =
- ActivityLaunchAnimator.Controller.fromView(dialogContentWithBackground, cujType)
+ ActivityTransitionAnimator.Controller.fromView(dialogContentWithBackground, cujType)
?: return null
// Wrap the controller into one that will instantly dismiss the dialog when the animation is
// done or dismiss it normally (fading it out) if the animation is cancelled.
- return object : ActivityLaunchAnimator.Controller by controller {
+ return object : ActivityTransitionAnimator.Controller by controller {
override val isDialogLaunch = true
override fun onIntentStarted(willAnimate: Boolean) {
@@ -400,14 +401,14 @@
}
}
- override fun onLaunchAnimationCancelled(newKeyguardOccludedState: Boolean?) {
- controller.onLaunchAnimationCancelled()
+ override fun onTransitionAnimationCancelled(newKeyguardOccludedState: Boolean?) {
+ controller.onTransitionAnimationCancelled()
enableDialogDismiss()
dialog.dismiss()
}
- override fun onLaunchAnimationStart(isExpandingFullyAbove: Boolean) {
- controller.onLaunchAnimationStart(isExpandingFullyAbove)
+ override fun onTransitionAnimationStart(isExpandingFullyAbove: Boolean) {
+ controller.onTransitionAnimationStart(isExpandingFullyAbove)
// Make sure the dialog is not dismissed during the animation.
disableDialogDismiss()
@@ -420,8 +421,8 @@
dialog.window?.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND)
}
- override fun onLaunchAnimationEnd(isExpandingFullyAbove: Boolean) {
- controller.onLaunchAnimationEnd(isExpandingFullyAbove)
+ override fun onTransitionAnimationEnd(isExpandingFullyAbove: Boolean) {
+ controller.onTransitionAnimationEnd(isExpandingFullyAbove)
// Hide the dialog then dismiss it to instantly dismiss it without playing the
// animation.
@@ -492,7 +493,7 @@
data class DialogCuj(@CujType val cujType: Int, val tag: String? = null)
private class AnimatedDialog(
- private val launchAnimator: LaunchAnimator,
+ private val transitionAnimator: TransitionAnimator,
private val callback: DialogLaunchAnimator.Callback,
private val interactionJankMonitor: InteractionJankMonitor,
@@ -892,7 +893,7 @@
// Create 2 controllers to animate both the dialog and the source.
val startController =
if (isLaunching) {
- controller.createLaunchController()
+ controller.createTransitionController()
} else {
GhostedViewLaunchAnimatorController(dialogContentWithBackground!!)
}
@@ -902,34 +903,34 @@
} else {
controller.createExitController()
}
- startController.launchContainer = decorView
- endController.launchContainer = decorView
+ startController.transitionContainer = decorView
+ endController.transitionContainer = decorView
val endState = endController.createAnimatorState()
val controller =
- object : LaunchAnimator.Controller {
- override var launchContainer: ViewGroup
- get() = startController.launchContainer
+ object : TransitionAnimator.Controller {
+ override var transitionContainer: ViewGroup
+ get() = startController.transitionContainer
set(value) {
- startController.launchContainer = value
- endController.launchContainer = value
+ startController.transitionContainer = value
+ endController.transitionContainer = value
}
- override fun createAnimatorState(): LaunchAnimator.State {
+ override fun createAnimatorState(): TransitionAnimator.State {
return startController.createAnimatorState()
}
- override fun onLaunchAnimationStart(isExpandingFullyAbove: Boolean) {
+ override fun onTransitionAnimationStart(isExpandingFullyAbove: Boolean) {
// During launch, onLaunchAnimationStart will be used to remove the temporary
// touch surface ghost so it is important to call this before calling
// onLaunchAnimationStart on the controller (which will create its own ghost).
onLaunchAnimationStart()
- startController.onLaunchAnimationStart(isExpandingFullyAbove)
- endController.onLaunchAnimationStart(isExpandingFullyAbove)
+ startController.onTransitionAnimationStart(isExpandingFullyAbove)
+ endController.onTransitionAnimationStart(isExpandingFullyAbove)
}
- override fun onLaunchAnimationEnd(isExpandingFullyAbove: Boolean) {
+ override fun onTransitionAnimationEnd(isExpandingFullyAbove: Boolean) {
// onLaunchAnimationEnd is called by an Animator at the end of the animation,
// on a Choreographer animation tick. The following calls will move the animated
// content from the dialog overlay back to its original position, and this
@@ -943,23 +944,23 @@
// that the move of the content back to its original window will be reflected in
// the next frame right after [onLaunchAnimationEnd] is called.
dialog.context.mainExecutor.execute {
- startController.onLaunchAnimationEnd(isExpandingFullyAbove)
- endController.onLaunchAnimationEnd(isExpandingFullyAbove)
+ startController.onTransitionAnimationEnd(isExpandingFullyAbove)
+ endController.onTransitionAnimationEnd(isExpandingFullyAbove)
onLaunchAnimationEnd()
}
}
- override fun onLaunchAnimationProgress(
- state: LaunchAnimator.State,
+ override fun onTransitionAnimationProgress(
+ state: TransitionAnimator.State,
progress: Float,
linearProgress: Float
) {
- startController.onLaunchAnimationProgress(state, progress, linearProgress)
+ startController.onTransitionAnimationProgress(state, progress, linearProgress)
// The end view is visible only iff the starting view is not visible.
state.visible = !state.visible
- endController.onLaunchAnimationProgress(state, progress, linearProgress)
+ endController.onTransitionAnimationProgress(state, progress, linearProgress)
// If the dialog content is complex, its dimension might change during the
// launch animation. The animation end position might also change during the
@@ -973,7 +974,7 @@
}
}
- launchAnimator.startAnimation(controller, endState, originalDialogBackgroundColor)
+ transitionAnimator.startAnimation(controller, endState, originalDialogBackgroundColor)
}
private fun shouldAnimateDialogIntoSource(): Boolean {
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/Expandable.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/Expandable.kt
index c49a487..2ba5948 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/animation/Expandable.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/Expandable.kt
@@ -21,14 +21,14 @@
/** A piece of UI that can be expanded into a Dialog or an Activity. */
interface Expandable {
/**
- * Create an [ActivityLaunchAnimator.Controller] that can be used to expand this [Expandable]
- * into an Activity, or return `null` if this [Expandable] should not be animated (e.g. if it is
- * currently not attached or visible).
+ * Create an [ActivityTransitionAnimator.Controller] that can be used to expand this
+ * [Expandable] into an Activity, or return `null` if this [Expandable] should not be animated
+ * (e.g. if it is currently not attached or visible).
*
* @param cujType the CUJ type from the [com.android.internal.jank.InteractionJankMonitor]
* associated to the launch that will use this controller.
*/
- fun activityLaunchController(cujType: Int? = null): ActivityLaunchAnimator.Controller?
+ fun activityLaunchController(cujType: Int? = null): ActivityTransitionAnimator.Controller?
/**
* Create a [DialogLaunchAnimator.Controller] that can be used to expand this [Expandable] into
@@ -49,8 +49,8 @@
return object : Expandable {
override fun activityLaunchController(
cujType: Int?,
- ): ActivityLaunchAnimator.Controller? {
- return ActivityLaunchAnimator.Controller.fromView(view, cujType)
+ ): ActivityTransitionAnimator.Controller? {
+ return ActivityTransitionAnimator.Controller.fromView(view, cujType)
}
override fun dialogLaunchController(
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/GhostedViewLaunchAnimatorController.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/GhostedViewLaunchAnimatorController.kt
index efdbfdb..f7148d7 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/animation/GhostedViewLaunchAnimatorController.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/GhostedViewLaunchAnimatorController.kt
@@ -42,15 +42,15 @@
private const val TAG = "GhostedViewLaunchAnimatorController"
/**
- * A base implementation of [ActivityLaunchAnimator.Controller] which creates a [ghost][GhostView]
- * of [ghostedView] as well as an expandable background view, which are drawn and animated instead
- * of the ghosted view.
+ * A base implementation of [ActivityTransitionAnimator.Controller] which creates a
+ * [ghost][GhostView] of [ghostedView] as well as an expandable background view, which are drawn and
+ * animated instead of the ghosted view.
*
* Important: [ghostedView] must be attached to a [ViewGroup] when calling this function and during
* the animation. It must also implement [LaunchableView], otherwise an exception will be thrown
* during this controller instantiation.
*
- * Note: Avoid instantiating this directly and call [ActivityLaunchAnimator.Controller.fromView]
+ * Note: Avoid instantiating this directly and call [ActivityTransitionAnimator.Controller.fromView]
* whenever possible instead.
*/
open class GhostedViewLaunchAnimatorController
@@ -63,14 +63,14 @@
private val cujType: Int? = null,
private var interactionJankMonitor: InteractionJankMonitor =
InteractionJankMonitor.getInstance(),
-) : ActivityLaunchAnimator.Controller {
+) : ActivityTransitionAnimator.Controller {
/** The container to which we will add the ghost view and expanding background. */
- override var launchContainer = ghostedView.rootView as ViewGroup
- private val launchContainerOverlay: ViewGroupOverlay
- get() = launchContainer.overlay
+ override var transitionContainer = ghostedView.rootView as ViewGroup
+ private val transitionContainerOverlay: ViewGroupOverlay
+ get() = transitionContainer.overlay
- private val launchContainerLocation = IntArray(2)
+ private val transitionContainerLocation = IntArray(2)
/** The ghost view that is drawn and animated instead of the ghosted view. */
private var ghostView: GhostView? = null
@@ -78,8 +78,8 @@
private val ghostViewMatrix = Matrix()
/**
- * The expanding background view that will be added to [launchContainer] (below [ghostView]) and
- * animate.
+ * The expanding background view that will be added to [transitionContainer] (below [ghostView])
+ * and animate.
*/
private var backgroundView: FrameLayout? = null
@@ -92,7 +92,7 @@
private var startBackgroundAlpha: Int = 0xFF
private val ghostedViewLocation = IntArray(2)
- private val ghostedViewState = LaunchAnimator.State()
+ private val ghostedViewState = TransitionAnimator.State()
/**
* The background of the [ghostedView]. This background will be used to draw the background of
@@ -175,9 +175,9 @@
return radius * ghostedView.scaleX
}
- override fun createAnimatorState(): LaunchAnimator.State {
+ override fun createAnimatorState(): TransitionAnimator.State {
val state =
- LaunchAnimator.State(
+ TransitionAnimator.State(
topCornerRadius = getCurrentTopCornerRadius(),
bottomCornerRadius = getCurrentBottomCornerRadius()
)
@@ -185,7 +185,7 @@
return state
}
- fun fillGhostedViewState(state: LaunchAnimator.State) {
+ fun fillGhostedViewState(state: TransitionAnimator.State) {
// For the animation we are interested in the area that has a non transparent background,
// so we have to take the optical insets into account.
ghostedView.getLocationOnScreen(ghostedViewLocation)
@@ -200,7 +200,7 @@
insets.right
}
- override fun onLaunchAnimationStart(isExpandingFullyAbove: Boolean) {
+ override fun onTransitionAnimationStart(isExpandingFullyAbove: Boolean) {
if (ghostedView.parent !is ViewGroup) {
// This should usually not happen, but let's make sure we don't crash if the view was
// detached right before we started the animation.
@@ -209,7 +209,7 @@
}
backgroundView =
- FrameLayout(launchContainer.context).also { launchContainerOverlay.add(it) }
+ FrameLayout(transitionContainer.context).also { transitionContainerOverlay.add(it) }
// We wrap the ghosted view background and use it to draw the expandable background. Its
// alpha will be set to 0 as soon as we start drawing the expanding background.
@@ -225,7 +225,7 @@
// Create a ghost of the view that will be moving and fading out. This allows to fade out
// the content before fading out the background.
- ghostView = GhostView.addGhost(ghostedView, launchContainer)
+ ghostView = GhostView.addGhost(ghostedView, transitionContainer)
// [GhostView.addGhost], the result of which is our [ghostView], creates a [GhostView], and
// adds it first to a [FrameLayout] container. It then adds _that_ container to an
@@ -244,8 +244,8 @@
cujType?.let { interactionJankMonitor.begin(ghostedView, it) }
}
- override fun onLaunchAnimationProgress(
- state: LaunchAnimator.State,
+ override fun onTransitionAnimationProgress(
+ state: TransitionAnimator.State,
progress: Float,
linearProgress: Float
) {
@@ -287,15 +287,15 @@
if (ghostedView.parent is ViewGroup) {
// Recalculate the matrix in case the ghosted view moved. We ensure that the ghosted
// view is still attached to a ViewGroup, otherwise calculateMatrix will throw.
- GhostView.calculateMatrix(ghostedView, launchContainer, ghostViewMatrix)
+ GhostView.calculateMatrix(ghostedView, transitionContainer, ghostViewMatrix)
}
- launchContainer.getLocationOnScreen(launchContainerLocation)
+ transitionContainer.getLocationOnScreen(transitionContainerLocation)
ghostViewMatrix.postScale(
scale,
scale,
- ghostedViewState.centerX - launchContainerLocation[0],
- ghostedViewState.centerY - launchContainerLocation[1]
+ ghostedViewState.centerX - transitionContainerLocation[0],
+ ghostedViewState.centerY - transitionContainerLocation[1]
)
ghostViewMatrix.postTranslate(
(leftChange + rightChange) / 2f,
@@ -310,10 +310,10 @@
val rightWithInsets = state.right + insets.right
val bottomWithInsets = state.bottom + insets.bottom
- backgroundView.top = topWithInsets - launchContainerLocation[1]
- backgroundView.bottom = bottomWithInsets - launchContainerLocation[1]
- backgroundView.left = leftWithInsets - launchContainerLocation[0]
- backgroundView.right = rightWithInsets - launchContainerLocation[0]
+ backgroundView.top = topWithInsets - transitionContainerLocation[1]
+ backgroundView.bottom = bottomWithInsets - transitionContainerLocation[1]
+ backgroundView.left = leftWithInsets - transitionContainerLocation[0]
+ backgroundView.right = rightWithInsets - transitionContainerLocation[0]
val backgroundDrawable = backgroundDrawable!!
backgroundDrawable.wrapped?.let {
@@ -321,7 +321,7 @@
}
}
- override fun onLaunchAnimationEnd(isExpandingFullyAbove: Boolean) {
+ override fun onTransitionAnimationEnd(isExpandingFullyAbove: Boolean) {
if (ghostView == null) {
// We didn't actually run the animation.
return
@@ -332,7 +332,7 @@
backgroundDrawable?.wrapped?.alpha = startBackgroundAlpha
GhostView.removeGhost(ghostedView)
- backgroundView?.let { launchContainerOverlay.remove(it) }
+ backgroundView?.let { transitionContainerOverlay.remove(it) }
if (ghostedView is LaunchableView) {
// Restore the ghosted view visibility.
@@ -353,7 +353,7 @@
/**
* Return the first [GradientDrawable] found in [drawable], or null if none is found. If
- * [drawable] is a [LayerDrawable], this will return the first layer that is a
+ * [drawable] is a [LayerDrawable], this will return the first layer that has a
* [GradientDrawable].
*/
fun findGradientDrawable(drawable: Drawable): GradientDrawable? {
@@ -367,8 +367,8 @@
if (drawable is LayerDrawable) {
for (i in 0 until drawable.numberOfLayers) {
- val maybeGradient = drawable.getDrawable(i)
- if (maybeGradient is GradientDrawable) {
+ val maybeGradient = findGradientDrawable(drawable.getDrawable(i))
+ if (maybeGradient != null) {
return maybeGradient
}
}
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/LaunchAnimator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/TransitionAnimator.kt
similarity index 79%
rename from packages/SystemUI/animation/src/com/android/systemui/animation/LaunchAnimator.kt
rename to packages/SystemUI/animation/src/com/android/systemui/animation/TransitionAnimator.kt
index d6eba2e..5e4276c 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/animation/LaunchAnimator.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/TransitionAnimator.kt
@@ -31,17 +31,18 @@
import com.android.app.animation.Interpolators.LINEAR
import kotlin.math.roundToInt
-private const val TAG = "LaunchAnimator"
+private const val TAG = "TransitionAnimator"
-/** A base class to animate a window launch (activity or dialog) from a view . */
-class LaunchAnimator(private val timings: Timings, private val interpolators: Interpolators) {
+/** A base class to animate a window (activity or dialog) launch to or return from a view . */
+class TransitionAnimator(private val timings: Timings, private val interpolators: Interpolators) {
companion object {
internal const val DEBUG = false
private val SRC_MODE = PorterDuffXfermode(PorterDuff.Mode.SRC)
/**
- * Given the [linearProgress] of a launch animation, return the linear progress of the
- * sub-animation starting [delay] ms after the launch animation and that lasts [duration].
+ * Given the [linearProgress] of a transition animation, return the linear progress of the
+ * sub-animation starting [delay] ms after the transition animation and that lasts
+ * [duration].
*/
@JvmStatic
fun getProgress(
@@ -58,7 +59,7 @@
}
}
- private val launchContainerLocation = IntArray(2)
+ private val transitionContainerLocation = IntArray(2)
private val cornerRadii = FloatArray(8)
/**
@@ -73,7 +74,7 @@
*
* This will be used to:
* - Get the associated [Context].
- * - Compute whether we are expanding fully above the launch container.
+ * - Compute whether we are expanding fully above the transition container.
* - Get to overlay to which we initially put the window background layer, until the opening
* window is made visible (see [openingWindowSyncView]).
*
@@ -81,7 +82,7 @@
* inside a different location, for instance to ensure correct layering during the
* animation.
*/
- var launchContainer: ViewGroup
+ var transitionContainer: ViewGroup
/**
* The [View] with which the opening app window should be synchronized with once it starts
@@ -90,7 +91,7 @@
* We will also move the window background layer to this view's overlay once the opening
* window is visible.
*
- * If null, this will default to [launchContainer].
+ * If null, this will default to [transitionContainer].
*/
val openingWindowSyncView: View?
get() = null
@@ -99,7 +100,7 @@
* Return the [State] of the view that will be animated. We will animate from this state to
* the final window state.
*
- * Note: This state will be mutated and passed to [onLaunchAnimationProgress] during the
+ * Note: This state will be mutated and passed to [onTransitionAnimationProgress] during the
* animation.
*/
fun createAnimatorState(): State
@@ -107,22 +108,22 @@
/**
* The animation started. This is typically used to initialize any additional resource
* needed for the animation. [isExpandingFullyAbove] will be true if the window is expanding
- * fully above the [launchContainer].
+ * fully above the [transitionContainer].
*/
- fun onLaunchAnimationStart(isExpandingFullyAbove: Boolean) {}
+ fun onTransitionAnimationStart(isExpandingFullyAbove: Boolean) {}
/** The animation made progress and the expandable view [state] should be updated. */
- fun onLaunchAnimationProgress(state: State, progress: Float, linearProgress: Float) {}
+ fun onTransitionAnimationProgress(state: State, progress: Float, linearProgress: Float) {}
/**
- * The animation ended. This will be called *if and only if* [onLaunchAnimationStart] was
- * called previously. This is typically used to clean up the resources initialized when the
- * animation was started.
+ * The animation ended. This will be called *if and only if* [onTransitionAnimationStart]
+ * was called previously. This is typically used to clean up the resources initialized when
+ * the animation was started.
*/
- fun onLaunchAnimationEnd(isExpandingFullyAbove: Boolean) {}
+ fun onTransitionAnimationEnd(isExpandingFullyAbove: Boolean) {}
}
- /** The state of an expandable view during a [LaunchAnimator] animation. */
+ /** The state of an expandable view during a [TransitionAnimator] animation. */
open class State(
/** The position of the view in screen space coordinates. */
var top: Int = 0,
@@ -198,13 +199,13 @@
)
/**
- * Start a launch animation controlled by [controller] towards [endState]. An intermediary layer
- * with [windowBackgroundColor] will fade in then (optionally) fade out above the expanding
- * view, and should be the same background color as the opening (or closing) window.
+ * Start a transition animation controlled by [controller] towards [endState]. An intermediary
+ * layer with [windowBackgroundColor] will fade in then (optionally) fade out above the
+ * expanding view, and should be the same background color as the opening (or closing) window.
*
* If [fadeOutWindowBackgroundLayer] is true, then this intermediary layer will fade out during
* the second half of the animation, and will have SRC blending mode (ultimately punching a hole
- * in the [launch container][Controller.launchContainer]) iff [drawHole] is true.
+ * in the [transition container][Controller.transitionContainer]) iff [drawHole] is true.
*/
fun startAnimation(
controller: Controller,
@@ -251,13 +252,13 @@
}
}
- val launchContainer = controller.launchContainer
- val isExpandingFullyAbove = isExpandingFullyAbove(launchContainer, endState)
+ val transitionContainer = controller.transitionContainer
+ val isExpandingFullyAbove = isExpandingFullyAbove(transitionContainer, endState)
// We add an extra layer with the same color as the dialog/app splash screen background
// color, which is usually the same color of the app background. We first fade in this layer
// to hide the expanding view, then we fade it out with SRC mode to draw a hole in the
- // launch container and reveal the opening window.
+ // transition container and reveal the opening window.
val windowBackgroundLayer =
GradientDrawable().apply {
setColor(windowBackgroundColor)
@@ -275,9 +276,9 @@
val openingWindowSyncViewOverlay = openingWindowSyncView?.overlay
val moveBackgroundLayerWhenAppIsVisible =
openingWindowSyncView != null &&
- openingWindowSyncView.viewRootImpl != controller.launchContainer.viewRootImpl
+ openingWindowSyncView.viewRootImpl != controller.transitionContainer.viewRootImpl
- val launchContainerOverlay = launchContainer.overlay
+ val transitionContainerOverlay = transitionContainer.overlay
var cancelled = false
var movedBackgroundLayer = false
@@ -287,20 +288,20 @@
if (DEBUG) {
Log.d(TAG, "Animation started")
}
- controller.onLaunchAnimationStart(isExpandingFullyAbove)
+ controller.onTransitionAnimationStart(isExpandingFullyAbove)
- // Add the drawable to the launch container overlay. Overlays always draw
+ // Add the drawable to the transition container overlay. Overlays always draw
// drawables after views, so we know that it will be drawn above any view added
// by the controller.
- launchContainerOverlay.add(windowBackgroundLayer)
+ transitionContainerOverlay.add(windowBackgroundLayer)
}
override fun onAnimationEnd(animation: Animator) {
if (DEBUG) {
Log.d(TAG, "Animation ended")
}
- controller.onLaunchAnimationEnd(isExpandingFullyAbove)
- launchContainerOverlay.remove(windowBackgroundLayer)
+ controller.onTransitionAnimationEnd(isExpandingFullyAbove)
+ transitionContainerOverlay.remove(windowBackgroundLayer)
if (moveBackgroundLayerWhenAppIsVisible) {
openingWindowSyncViewOverlay?.remove(windowBackgroundLayer)
@@ -353,17 +354,21 @@
// in its new container.
movedBackgroundLayer = true
- launchContainerOverlay.remove(windowBackgroundLayer)
+ transitionContainerOverlay.remove(windowBackgroundLayer)
openingWindowSyncViewOverlay!!.add(windowBackgroundLayer)
- ViewRootSync.synchronizeNextDraw(launchContainer, openingWindowSyncView, then = {})
+ ViewRootSync.synchronizeNextDraw(
+ transitionContainer,
+ openingWindowSyncView,
+ then = {}
+ )
}
val container =
if (movedBackgroundLayer) {
openingWindowSyncView!!
} else {
- controller.launchContainer
+ controller.transitionContainer
}
applyStateToWindowBackgroundLayer(
@@ -374,7 +379,7 @@
fadeOutWindowBackgroundLayer,
drawHole
)
- controller.onLaunchAnimationProgress(state, progress, linearProgress)
+ controller.onTransitionAnimationProgress(state, progress, linearProgress)
}
animator.start()
@@ -386,30 +391,30 @@
}
}
- /** Return whether we are expanding fully above the [launchContainer]. */
- internal fun isExpandingFullyAbove(launchContainer: View, endState: State): Boolean {
- launchContainer.getLocationOnScreen(launchContainerLocation)
- return endState.top <= launchContainerLocation[1] &&
- endState.bottom >= launchContainerLocation[1] + launchContainer.height &&
- endState.left <= launchContainerLocation[0] &&
- endState.right >= launchContainerLocation[0] + launchContainer.width
+ /** Return whether we are expanding fully above the [transitionContainer]. */
+ internal fun isExpandingFullyAbove(transitionContainer: View, endState: State): Boolean {
+ transitionContainer.getLocationOnScreen(transitionContainerLocation)
+ return endState.top <= transitionContainerLocation[1] &&
+ endState.bottom >= transitionContainerLocation[1] + transitionContainer.height &&
+ endState.left <= transitionContainerLocation[0] &&
+ endState.right >= transitionContainerLocation[0] + transitionContainer.width
}
private fun applyStateToWindowBackgroundLayer(
drawable: GradientDrawable,
state: State,
linearProgress: Float,
- launchContainer: View,
+ transitionContainer: View,
fadeOutWindowBackgroundLayer: Boolean,
drawHole: Boolean
) {
// Update position.
- launchContainer.getLocationOnScreen(launchContainerLocation)
+ transitionContainer.getLocationOnScreen(transitionContainerLocation)
drawable.setBounds(
- state.left - launchContainerLocation[0],
- state.top - launchContainerLocation[1],
- state.right - launchContainerLocation[0],
- state.bottom - launchContainerLocation[1]
+ state.left - transitionContainerLocation[0],
+ state.top - transitionContainerLocation[1],
+ state.right - transitionContainerLocation[0],
+ state.bottom - transitionContainerLocation[1]
)
// Update radius.
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/ViewDialogLaunchAnimatorController.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/ViewDialogLaunchAnimatorController.kt
index 1290f00..e2a29ab 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/animation/ViewDialogLaunchAnimatorController.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/ViewDialogLaunchAnimatorController.kt
@@ -68,19 +68,19 @@
}
}
- override fun createLaunchController(): LaunchAnimator.Controller {
+ override fun createTransitionController(): TransitionAnimator.Controller {
val delegate = GhostedViewLaunchAnimatorController(source)
- return object : LaunchAnimator.Controller by delegate {
- override fun onLaunchAnimationStart(isExpandingFullyAbove: Boolean) {
+ return object : TransitionAnimator.Controller by delegate {
+ override fun onTransitionAnimationStart(isExpandingFullyAbove: Boolean) {
// Remove the temporary ghost added by [startDrawingInOverlayOf]. Another
// ghost (that ghosts only the source content, and not its background) will
// be added right after this by the delegate and will be animated.
GhostView.removeGhost(source)
- delegate.onLaunchAnimationStart(isExpandingFullyAbove)
+ delegate.onTransitionAnimationStart(isExpandingFullyAbove)
}
- override fun onLaunchAnimationEnd(isExpandingFullyAbove: Boolean) {
- delegate.onLaunchAnimationEnd(isExpandingFullyAbove)
+ override fun onTransitionAnimationEnd(isExpandingFullyAbove: Boolean) {
+ delegate.onTransitionAnimationEnd(isExpandingFullyAbove)
// At this point the view visibility is restored by the delegate, so we delay the
// visibility changes again and make it invisible while the dialog is shown.
@@ -94,7 +94,7 @@
}
}
- override fun createExitController(): LaunchAnimator.Controller {
+ override fun createExitController(): TransitionAnimator.Controller {
return GhostedViewLaunchAnimatorController(source)
}
diff --git a/packages/SystemUI/checks/Android.bp b/packages/SystemUI/checks/Android.bp
index d3f66e3..4cbc18c 100644
--- a/packages/SystemUI/checks/Android.bp
+++ b/packages/SystemUI/checks/Android.bp
@@ -13,6 +13,7 @@
// limitations under the License.
package {
+ default_team: "trendy_team_system_ui_please_use_a_more_specific_subteam_if_possible_",
// See: http://go/android-license-faq
// A large-scale-change added 'default_applicable_licenses' to import
// all of the 'license_kinds' from "frameworks_base_license"
diff --git a/packages/SystemUI/common/Android.bp b/packages/SystemUI/common/Android.bp
index 482776a..6fc13d7 100644
--- a/packages/SystemUI/common/Android.bp
+++ b/packages/SystemUI/common/Android.bp
@@ -13,6 +13,7 @@
// limitations under the License.
package {
+ default_team: "trendy_team_system_ui_please_use_a_more_specific_subteam_if_possible_",
// See: http://go/android-license-faq
// A large-scale-change added 'default_applicable_licenses' to import
// all of the 'license_kinds' from "frameworks_base_packages_SystemUI_license"
diff --git a/packages/SystemUI/compose/core/Android.bp b/packages/SystemUI/compose/core/Android.bp
index 9a4347d..4f7a43e 100644
--- a/packages/SystemUI/compose/core/Android.bp
+++ b/packages/SystemUI/compose/core/Android.bp
@@ -13,6 +13,7 @@
// limitations under the License.
package {
+ default_team: "trendy_team_system_ui_please_use_a_more_specific_subteam_if_possible_",
// See: http://go/android-license-faq
// A large-scale-change added 'default_applicable_licenses' to import
// all of the 'license_kinds' from "frameworks_base_packages_SystemUI_license"
diff --git a/packages/SystemUI/compose/core/src/com/android/compose/animation/Expandable.kt b/packages/SystemUI/compose/core/src/com/android/compose/animation/Expandable.kt
index ac1ef15..8eb2f2e 100644
--- a/packages/SystemUI/compose/core/src/com/android/compose/animation/Expandable.kt
+++ b/packages/SystemUI/compose/core/src/com/android/compose/animation/Expandable.kt
@@ -75,7 +75,7 @@
import androidx.lifecycle.setViewTreeLifecycleOwner
import androidx.lifecycle.setViewTreeViewModelStoreOwner
import com.android.systemui.animation.Expandable
-import com.android.systemui.animation.LaunchAnimator
+import com.android.systemui.animation.TransitionAnimator
import kotlin.math.max
import kotlin.math.min
@@ -301,7 +301,7 @@
private fun AnimatedContentInOverlay(
color: Color,
sizeInOriginalLayout: Size,
- animatorState: State<LaunchAnimator.State?>,
+ animatorState: State<TransitionAnimator.State?>,
overlay: ViewGroupOverlay,
controller: ExpandableControllerImpl,
content: @Composable (Expandable) -> Unit,
@@ -407,7 +407,7 @@
internal fun measureAndLayoutComposeViewInOverlay(
view: View,
- state: LaunchAnimator.State,
+ state: TransitionAnimator.State,
) {
val exactWidth = state.width
val exactHeight = state.height
@@ -449,7 +449,7 @@
}
private fun ContentDrawScope.drawBackground(
- animatorState: LaunchAnimator.State,
+ animatorState: TransitionAnimator.State,
color: Color,
border: BorderStroke?,
) {
diff --git a/packages/SystemUI/compose/core/src/com/android/compose/animation/ExpandableController.kt b/packages/SystemUI/compose/core/src/com/android/compose/animation/ExpandableController.kt
index 0e7694e..84e5725 100644
--- a/packages/SystemUI/compose/core/src/com/android/compose/animation/ExpandableController.kt
+++ b/packages/SystemUI/compose/core/src/com/android/compose/animation/ExpandableController.kt
@@ -40,11 +40,11 @@
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.LayoutDirection
import com.android.internal.jank.InteractionJankMonitor
-import com.android.systemui.animation.ActivityLaunchAnimator
+import com.android.systemui.animation.ActivityTransitionAnimator
import com.android.systemui.animation.DialogCuj
import com.android.systemui.animation.DialogLaunchAnimator
import com.android.systemui.animation.Expandable
-import com.android.systemui.animation.LaunchAnimator
+import com.android.systemui.animation.TransitionAnimator
import kotlin.math.roundToInt
/** A controller that can control animated launches from an [Expandable]. */
@@ -70,7 +70,7 @@
val layoutDirection = LocalLayoutDirection.current
// The current animation state, if we are currently animating a dialog or activity.
- val animatorState = remember { mutableStateOf<LaunchAnimator.State?>(null) }
+ val animatorState = remember { mutableStateOf<TransitionAnimator.State?>(null) }
// Whether a dialog controlled by this ExpandableController is currently showing.
val isDialogShowing = remember { mutableStateOf(false) }
@@ -123,7 +123,7 @@
internal val borderStroke: BorderStroke?,
internal val composeViewRoot: View,
internal val density: Density,
- internal val animatorState: MutableState<LaunchAnimator.State?>,
+ internal val animatorState: MutableState<TransitionAnimator.State?>,
internal val isDialogShowing: MutableState<Boolean>,
internal val overlay: MutableState<ViewGroupOverlay?>,
internal val currentComposeViewInOverlay: MutableState<View?>,
@@ -135,7 +135,7 @@
object : Expandable {
override fun activityLaunchController(
cujType: Int?,
- ): ActivityLaunchAnimator.Controller? {
+ ): ActivityTransitionAnimator.Controller? {
if (!isComposed.value) {
return null
}
@@ -153,32 +153,32 @@
}
/**
- * Create a [LaunchAnimator.Controller] that is going to be used to drive an activity or dialog
- * animation. This controller will:
+ * Create a [TransitionAnimator.Controller] that is going to be used to drive an activity or
+ * dialog animation. This controller will:
* 1. Compute the start/end animation state using [boundsInComposeViewRoot] and the location of
* composeViewRoot on the screen.
* 2. Update [animatorState] with the current animation state if we are animating, or null
* otherwise.
*/
- private fun launchController(): LaunchAnimator.Controller {
- return object : LaunchAnimator.Controller {
+ private fun transitionController(): TransitionAnimator.Controller {
+ return object : TransitionAnimator.Controller {
private val rootLocationOnScreen = intArrayOf(0, 0)
- override var launchContainer: ViewGroup = composeViewRoot.rootView as ViewGroup
+ override var transitionContainer: ViewGroup = composeViewRoot.rootView as ViewGroup
- override fun onLaunchAnimationEnd(isExpandingFullyAbove: Boolean) {
+ override fun onTransitionAnimationEnd(isExpandingFullyAbove: Boolean) {
animatorState.value = null
}
- override fun onLaunchAnimationProgress(
- state: LaunchAnimator.State,
+ override fun onTransitionAnimationProgress(
+ state: TransitionAnimator.State,
progress: Float,
linearProgress: Float
) {
// We copy state given that it's always the same object that is mutated by
- // ActivityLaunchAnimator.
+ // ActivityTransitionAnimator.
animatorState.value =
- LaunchAnimator.State(
+ TransitionAnimator.State(
state.top,
state.bottom,
state.left,
@@ -195,7 +195,7 @@
}
}
- override fun createAnimatorState(): LaunchAnimator.State {
+ override fun createAnimatorState(): TransitionAnimator.State {
val boundsInRoot = boundsInComposeViewRoot.value
val outline =
shape.createOutline(
@@ -236,7 +236,7 @@
}
val rootLocation = rootLocationOnScreen()
- return LaunchAnimator.State(
+ return TransitionAnimator.State(
top = rootLocation.y.roundToInt(),
bottom = (rootLocation.y + boundsInRoot.height).roundToInt(),
left = rootLocation.x.roundToInt(),
@@ -256,19 +256,20 @@
}
}
- /** Create an [ActivityLaunchAnimator.Controller] that can be used to animate activities. */
- private fun activityController(cujType: Int?): ActivityLaunchAnimator.Controller {
- val delegate = launchController()
- return object : ActivityLaunchAnimator.Controller, LaunchAnimator.Controller by delegate {
- override fun onLaunchAnimationStart(isExpandingFullyAbove: Boolean) {
- delegate.onLaunchAnimationStart(isExpandingFullyAbove)
+ /** Create an [ActivityTransitionAnimator.Controller] that can be used to animate activities. */
+ private fun activityController(cujType: Int?): ActivityTransitionAnimator.Controller {
+ val delegate = transitionController()
+ return object :
+ ActivityTransitionAnimator.Controller, TransitionAnimator.Controller by delegate {
+ override fun onTransitionAnimationStart(isExpandingFullyAbove: Boolean) {
+ delegate.onTransitionAnimationStart(isExpandingFullyAbove)
overlay.value = composeViewRoot.rootView.overlay as ViewGroupOverlay
cujType?.let { InteractionJankMonitor.getInstance().begin(composeViewRoot, it) }
}
- override fun onLaunchAnimationEnd(isExpandingFullyAbove: Boolean) {
+ override fun onTransitionAnimationEnd(isExpandingFullyAbove: Boolean) {
cujType?.let { InteractionJankMonitor.getInstance().end(it) }
- delegate.onLaunchAnimationEnd(isExpandingFullyAbove)
+ delegate.onTransitionAnimationEnd(isExpandingFullyAbove)
overlay.value = null
}
}
@@ -293,11 +294,11 @@
}
}
- override fun createLaunchController(): LaunchAnimator.Controller {
- val delegate = launchController()
- return object : LaunchAnimator.Controller by delegate {
- override fun onLaunchAnimationEnd(isExpandingFullyAbove: Boolean) {
- delegate.onLaunchAnimationEnd(isExpandingFullyAbove)
+ override fun createTransitionController(): TransitionAnimator.Controller {
+ val delegate = transitionController()
+ return object : TransitionAnimator.Controller by delegate {
+ override fun onTransitionAnimationEnd(isExpandingFullyAbove: Boolean) {
+ delegate.onTransitionAnimationEnd(isExpandingFullyAbove)
// Make sure we don't draw this expandable when the dialog is showing.
isDialogShowing.value = true
@@ -305,11 +306,11 @@
}
}
- override fun createExitController(): LaunchAnimator.Controller {
- val delegate = launchController()
- return object : LaunchAnimator.Controller by delegate {
- override fun onLaunchAnimationEnd(isExpandingFullyAbove: Boolean) {
- delegate.onLaunchAnimationEnd(isExpandingFullyAbove)
+ override fun createExitController(): TransitionAnimator.Controller {
+ val delegate = transitionController()
+ return object : TransitionAnimator.Controller by delegate {
+ override fun onTransitionAnimationEnd(isExpandingFullyAbove: Boolean) {
+ delegate.onTransitionAnimationEnd(isExpandingFullyAbove)
isDialogShowing.value = false
}
}
diff --git a/packages/SystemUI/compose/core/src/com/android/compose/ui/graphics/painter/DrawablePainter.kt b/packages/SystemUI/compose/core/src/com/android/compose/ui/graphics/painter/DrawablePainter.kt
new file mode 100644
index 0000000..846abf7e
--- /dev/null
+++ b/packages/SystemUI/compose/core/src/com/android/compose/ui/graphics/painter/DrawablePainter.kt
@@ -0,0 +1,177 @@
+/*
+ * 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.compose.ui.graphics.painter
+
+import android.graphics.drawable.Animatable
+import android.graphics.drawable.ColorDrawable
+import android.graphics.drawable.Drawable
+import android.os.Build
+import android.os.Handler
+import android.os.Looper
+import android.view.View
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.RememberObserver
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.geometry.Size
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.ColorFilter
+import androidx.compose.ui.graphics.asAndroidColorFilter
+import androidx.compose.ui.graphics.drawscope.DrawScope
+import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
+import androidx.compose.ui.graphics.nativeCanvas
+import androidx.compose.ui.graphics.painter.ColorPainter
+import androidx.compose.ui.graphics.painter.Painter
+import androidx.compose.ui.graphics.withSave
+import androidx.compose.ui.unit.LayoutDirection
+import kotlin.math.roundToInt
+
+/**
+ * *************************************************************************************************
+ * This file was forked from
+ * https://github.com/google/accompanist/blob/main/drawablepainter/src/main/java/com/google/accompanist/drawablepainter/DrawablePainter.kt
+ */
+private val MAIN_HANDLER by lazy(LazyThreadSafetyMode.NONE) { Handler(Looper.getMainLooper()) }
+
+/**
+ * A [Painter] which draws an Android [Drawable] and supports [Animatable] drawables. Instances
+ * should be remembered to be able to start and stop [Animatable] animations.
+ *
+ * Instances are usually retrieved from [rememberDrawablePainter].
+ */
+public class DrawablePainter(public val drawable: Drawable) : Painter(), RememberObserver {
+ private var drawInvalidateTick by mutableStateOf(0)
+ private var drawableIntrinsicSize by mutableStateOf(drawable.intrinsicSize)
+
+ private val callback: Drawable.Callback by lazy {
+ object : Drawable.Callback {
+ override fun invalidateDrawable(d: Drawable) {
+ // Update the tick so that we get re-drawn
+ drawInvalidateTick++
+ // Update our intrinsic size too
+ drawableIntrinsicSize = drawable.intrinsicSize
+ }
+
+ override fun scheduleDrawable(d: Drawable, what: Runnable, time: Long) {
+ MAIN_HANDLER.postAtTime(what, time)
+ }
+
+ override fun unscheduleDrawable(d: Drawable, what: Runnable) {
+ MAIN_HANDLER.removeCallbacks(what)
+ }
+ }
+ }
+
+ init {
+ if (drawable.intrinsicWidth >= 0 && drawable.intrinsicHeight >= 0) {
+ // Update the drawable's bounds to match the intrinsic size
+ drawable.setBounds(0, 0, drawable.intrinsicWidth, drawable.intrinsicHeight)
+ }
+ }
+
+ override fun onRemembered() {
+ drawable.callback = callback
+ drawable.setVisible(true, true)
+ if (drawable is Animatable) drawable.start()
+ }
+
+ override fun onAbandoned(): Unit = onForgotten()
+
+ override fun onForgotten() {
+ if (drawable is Animatable) drawable.stop()
+ drawable.setVisible(false, false)
+ drawable.callback = null
+ }
+
+ override fun applyAlpha(alpha: Float): Boolean {
+ drawable.alpha = (alpha * 255).roundToInt().coerceIn(0, 255)
+ return true
+ }
+
+ override fun applyColorFilter(colorFilter: ColorFilter?): Boolean {
+ drawable.colorFilter = colorFilter?.asAndroidColorFilter()
+ return true
+ }
+
+ override fun applyLayoutDirection(layoutDirection: LayoutDirection): Boolean {
+ if (Build.VERSION.SDK_INT >= 23) {
+ return drawable.setLayoutDirection(
+ when (layoutDirection) {
+ LayoutDirection.Ltr -> View.LAYOUT_DIRECTION_LTR
+ LayoutDirection.Rtl -> View.LAYOUT_DIRECTION_RTL
+ }
+ )
+ }
+ return false
+ }
+
+ override val intrinsicSize: Size
+ get() = drawableIntrinsicSize
+
+ override fun DrawScope.onDraw() {
+ drawIntoCanvas { canvas ->
+ // Reading this ensures that we invalidate when invalidateDrawable() is called
+ drawInvalidateTick
+
+ // Update the Drawable's bounds
+ drawable.setBounds(0, 0, size.width.roundToInt(), size.height.roundToInt())
+
+ canvas.withSave { drawable.draw(canvas.nativeCanvas) }
+ }
+ }
+}
+
+/**
+ * Remembers [Drawable] wrapped up as a [Painter]. This function attempts to un-wrap the drawable
+ * contents and use Compose primitives where possible.
+ *
+ * If the provided [drawable] is `null`, an empty no-op painter is returned.
+ *
+ * This function tries to dispatch lifecycle events to [drawable] as much as possible from within
+ * Compose.
+ *
+ * @sample com.google.accompanist.sample.drawablepainter.BasicSample
+ */
+@Composable
+public fun rememberDrawablePainter(drawable: Drawable?): Painter =
+ remember(drawable) {
+ when (drawable) {
+ null -> EmptyPainter
+ is ColorDrawable -> ColorPainter(Color(drawable.color))
+ // Since the DrawablePainter will be remembered and it implements RememberObserver, it
+ // will receive the necessary events
+ else -> DrawablePainter(drawable.mutate())
+ }
+ }
+
+private val Drawable.intrinsicSize: Size
+ get() =
+ when {
+ // Only return a finite size if the drawable has an intrinsic size
+ intrinsicWidth >= 0 && intrinsicHeight >= 0 -> {
+ Size(width = intrinsicWidth.toFloat(), height = intrinsicHeight.toFloat())
+ }
+ else -> Size.Unspecified
+ }
+
+internal object EmptyPainter : Painter() {
+ override val intrinsicSize: Size
+ get() = Size.Unspecified
+ override fun DrawScope.onDraw() {}
+}
diff --git a/packages/SystemUI/compose/core/tests/Android.bp b/packages/SystemUI/compose/core/tests/Android.bp
index 8e9c586..6e7a142 100644
--- a/packages/SystemUI/compose/core/tests/Android.bp
+++ b/packages/SystemUI/compose/core/tests/Android.bp
@@ -13,6 +13,7 @@
// limitations under the License.
package {
+ default_team: "trendy_team_system_ui_please_use_a_more_specific_subteam_if_possible_",
// See: http://go/android-license-faq
// A large-scale-change added 'default_applicable_licenses' to import
// all of the 'license_kinds' from "frameworks_base_packages_SystemUI_license"
diff --git a/packages/SystemUI/compose/facade/disabled/src/com/android/systemui/compose/ComposeFacade.kt b/packages/SystemUI/compose/facade/disabled/src/com/android/systemui/compose/ComposeFacade.kt
index 3c32594..9a34d6f 100644
--- a/packages/SystemUI/compose/facade/disabled/src/com/android/systemui/compose/ComposeFacade.kt
+++ b/packages/SystemUI/compose/facade/disabled/src/com/android/systemui/compose/ComposeFacade.kt
@@ -17,7 +17,6 @@
package com.android.systemui.compose
-import android.app.Dialog
import android.content.Context
import android.view.View
import android.view.WindowInsets
@@ -28,12 +27,13 @@
import com.android.systemui.communal.ui.viewmodel.BaseCommunalViewModel
import com.android.systemui.communal.widgets.WidgetConfigurator
import com.android.systemui.keyboard.stickykeys.ui.viewmodel.StickyKeysIndicatorViewModel
+import com.android.systemui.keyguard.shared.model.LockscreenSceneBlueprint
+import com.android.systemui.keyguard.ui.viewmodel.LockscreenContentViewModel
import com.android.systemui.people.ui.viewmodel.PeopleViewModel
import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel
import com.android.systemui.scene.shared.model.Scene
import com.android.systemui.scene.shared.model.SceneKey
import com.android.systemui.scene.ui.viewmodel.SceneContainerViewModel
-import com.android.systemui.statusbar.phone.SystemUIDialogFactory
import com.android.systemui.volume.panel.ui.viewmodel.VolumePanelViewModel
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.StateFlow
@@ -90,10 +90,10 @@
throwComposeUnavailableError()
}
- override fun createStickyKeysDialog(
- dialogFactory: SystemUIDialogFactory,
+ override fun createStickyKeysIndicatorContent(
+ context: Context,
viewModel: StickyKeysIndicatorViewModel
- ): Dialog {
+ ): View {
throwComposeUnavailableError()
}
@@ -114,6 +114,12 @@
dialogFactory: BouncerDialogFactory,
): View = throwComposeUnavailableError()
+ override fun createLockscreen(
+ context: Context,
+ viewModel: LockscreenContentViewModel,
+ blueprints: Set<@JvmSuppressWildcards LockscreenSceneBlueprint>,
+ ): View = throwComposeUnavailableError()
+
private fun throwComposeUnavailableError(): Nothing {
error(
"Compose is not available. Make sure to check isComposeAvailable() before calling any" +
diff --git a/packages/SystemUI/compose/facade/disabled/src/com/android/systemui/scene/LockscreenSceneModule.kt b/packages/SystemUI/compose/facade/disabled/src/com/android/systemui/scene/LockscreenSceneModule.kt
index 725aef2..fc3912e 100644
--- a/packages/SystemUI/compose/facade/disabled/src/com/android/systemui/scene/LockscreenSceneModule.kt
+++ b/packages/SystemUI/compose/facade/disabled/src/com/android/systemui/scene/LockscreenSceneModule.kt
@@ -16,6 +16,16 @@
package com.android.systemui.scene
+import com.android.systemui.keyguard.shared.model.LockscreenSceneBlueprint
import dagger.Module
+import dagger.Provides
-@Module interface LockscreenSceneModule
+@Module
+interface LockscreenSceneModule {
+ companion object {
+ @Provides
+ fun providesLockscreenBlueprints(): Set<LockscreenSceneBlueprint> {
+ return emptySet()
+ }
+ }
+}
diff --git a/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/compose/ComposeFacade.kt b/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/compose/ComposeFacade.kt
index afb860e..51d2a03 100644
--- a/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/compose/ComposeFacade.kt
+++ b/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/compose/ComposeFacade.kt
@@ -16,31 +16,37 @@
package com.android.systemui.compose
-import android.app.Dialog
import android.content.Context
import android.graphics.Point
import android.view.View
import android.view.WindowInsets
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.lifecycle.LifecycleOwner
import com.android.compose.theme.PlatformTheme
import com.android.compose.ui.platform.DensityAwareComposeView
+import com.android.internal.policy.ScreenDecorationsUtils
import com.android.systemui.bouncer.ui.BouncerDialogFactory
import com.android.systemui.bouncer.ui.composable.BouncerContent
import com.android.systemui.bouncer.ui.viewmodel.BouncerViewModel
import com.android.systemui.common.ui.compose.windowinsets.CutoutLocation
import com.android.systemui.common.ui.compose.windowinsets.DisplayCutout
-import com.android.systemui.common.ui.compose.windowinsets.DisplayCutoutProvider
+import com.android.systemui.common.ui.compose.windowinsets.ScreenDecorProvider
import com.android.systemui.communal.ui.compose.CommunalContainer
import com.android.systemui.communal.ui.compose.CommunalHub
import com.android.systemui.communal.ui.viewmodel.BaseCommunalViewModel
import com.android.systemui.communal.widgets.WidgetConfigurator
-import com.android.systemui.keyboard.stickykeys.ui.view.StickyKeysIndicator
+import com.android.systemui.keyboard.stickykeys.ui.view.createStickyKeyIndicatorView
import com.android.systemui.keyboard.stickykeys.ui.viewmodel.StickyKeysIndicatorViewModel
+import com.android.systemui.keyguard.shared.model.LockscreenSceneBlueprint
+import com.android.systemui.keyguard.ui.composable.LockscreenContent
+import com.android.systemui.keyguard.ui.composable.blueprint.ComposableLockscreenSceneBlueprint
+import com.android.systemui.keyguard.ui.viewmodel.LockscreenContentViewModel
import com.android.systemui.people.ui.compose.PeopleScreen
import com.android.systemui.people.ui.viewmodel.PeopleViewModel
import com.android.systemui.qs.footer.ui.compose.FooterActions
@@ -50,8 +56,6 @@
import com.android.systemui.scene.ui.composable.ComposableScene
import com.android.systemui.scene.ui.composable.SceneContainer
import com.android.systemui.scene.ui.viewmodel.SceneContainerViewModel
-import com.android.systemui.statusbar.phone.SystemUIDialogFactory
-import com.android.systemui.statusbar.phone.create
import com.android.systemui.volume.panel.ui.composable.VolumePanelRoot
import com.android.systemui.volume.panel.ui.viewmodel.VolumePanelViewModel
import kotlinx.coroutines.CoroutineScope
@@ -126,8 +130,9 @@
return ComposeView(context).apply {
setContent {
PlatformTheme {
- DisplayCutoutProvider(
- displayCutout = displayCutoutFromWindowInsets(scope, context, windowInsets)
+ ScreenDecorProvider(
+ displayCutout = displayCutoutFromWindowInsets(scope, context, windowInsets),
+ screenCornerRadius = ScreenDecorationsUtils.getWindowCornerRadius(context)
) {
SceneContainer(
viewModel = viewModel,
@@ -140,11 +145,11 @@
}
}
- override fun createStickyKeysDialog(
- dialogFactory: SystemUIDialogFactory,
+ override fun createStickyKeysIndicatorContent(
+ context: Context,
viewModel: StickyKeysIndicatorViewModel
- ): Dialog {
- return dialogFactory.create { StickyKeysIndicator(viewModel) }
+ ): View {
+ return createStickyKeyIndicatorView(context, viewModel)
}
override fun createCommunalView(
@@ -214,4 +219,19 @@
setContent { PlatformTheme { BouncerContent(viewModel, dialogFactory) } }
}
}
+
+ override fun createLockscreen(
+ context: Context,
+ viewModel: LockscreenContentViewModel,
+ blueprints: Set<@JvmSuppressWildcards LockscreenSceneBlueprint>,
+ ): View {
+ val sceneBlueprints =
+ blueprints.mapNotNull { it as? ComposableLockscreenSceneBlueprint }.toSet()
+ return ComposeView(context).apply {
+ setContent {
+ LockscreenContent(viewModel = viewModel, blueprints = sceneBlueprints)
+ .Content(modifier = Modifier.fillMaxSize())
+ }
+ }
+ }
}
diff --git a/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/scene/LockscreenSceneModule.kt b/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/scene/LockscreenSceneModule.kt
index cbf2496..f5dc154 100644
--- a/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/scene/LockscreenSceneModule.kt
+++ b/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/scene/LockscreenSceneModule.kt
@@ -20,8 +20,10 @@
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.keyguard.KeyguardViewConfigurator
import com.android.systemui.keyguard.qualifiers.KeyguardRootView
+import com.android.systemui.keyguard.shared.model.LockscreenSceneBlueprint
import com.android.systemui.keyguard.ui.composable.LockscreenScene
import com.android.systemui.keyguard.ui.composable.LockscreenSceneBlueprintModule
+import com.android.systemui.keyguard.ui.composable.blueprint.ComposableLockscreenSceneBlueprint
import com.android.systemui.scene.shared.model.Scene
import dagger.Binds
import dagger.Module
@@ -51,5 +53,12 @@
): () -> View {
return { configurator.get().getKeyguardRootView() }
}
+
+ @Provides
+ fun providesLockscreenBlueprints(
+ blueprints: Set<@JvmSuppressWildcards ComposableLockscreenSceneBlueprint>
+ ): Set<LockscreenSceneBlueprint> {
+ return blueprints
+ }
}
}
diff --git a/packages/SystemUI/compose/features/Android.bp b/packages/SystemUI/compose/features/Android.bp
index 5ab2235..c12084d 100644
--- a/packages/SystemUI/compose/features/Android.bp
+++ b/packages/SystemUI/compose/features/Android.bp
@@ -13,6 +13,7 @@
// limitations under the License.
package {
+ default_team: "trendy_team_system_ui_please_use_a_more_specific_subteam_if_possible_",
// See: http://go/android-license-faq
// A large-scale-change added 'default_applicable_licenses' to import
// all of the 'license_kinds' from "frameworks_base_packages_SystemUI_license"
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/common/ui/compose/Color.kt b/packages/SystemUI/compose/features/src/com/android/systemui/common/ui/compose/Color.kt
new file mode 100644
index 0000000..64b9f2d
--- /dev/null
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/common/ui/compose/Color.kt
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.common.ui.compose
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.ReadOnlyComposable
+import androidx.compose.ui.graphics.Color
+import com.android.compose.theme.colorAttr
+
+/** Resolves [com.android.systemui.common.shared.model.Color] into [Color] */
+@Composable
+@ReadOnlyComposable
+fun com.android.systemui.common.shared.model.Color.toColor(): Color {
+ return when (this) {
+ is com.android.systemui.common.shared.model.Color.Attribute -> colorAttr(attribute)
+ is com.android.systemui.common.shared.model.Color.Loaded -> Color(color)
+ }
+}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/common/ui/compose/windowinsets/DisplayCutoutProvider.kt b/packages/SystemUI/compose/features/src/com/android/systemui/common/ui/compose/windowinsets/ScreenDecorProvider.kt
similarity index 70%
rename from packages/SystemUI/compose/features/src/com/android/systemui/common/ui/compose/windowinsets/DisplayCutoutProvider.kt
rename to packages/SystemUI/compose/features/src/com/android/systemui/common/ui/compose/windowinsets/ScreenDecorProvider.kt
index ed393c0..76bd4ec 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/common/ui/compose/windowinsets/DisplayCutoutProvider.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/common/ui/compose/windowinsets/ScreenDecorProvider.kt
@@ -21,17 +21,28 @@
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.staticCompositionLocalOf
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.unit.dp
import kotlinx.coroutines.flow.StateFlow
/** The bounds and [CutoutLocation] of the current display. */
val LocalDisplayCutout = staticCompositionLocalOf { DisplayCutout() }
+/** The corner radius in px of the current display. */
+val LocalScreenCornerRadius = staticCompositionLocalOf { 0.dp }
+
@Composable
-fun DisplayCutoutProvider(
+fun ScreenDecorProvider(
displayCutout: StateFlow<DisplayCutout>,
+ screenCornerRadius: Float,
content: @Composable () -> Unit,
) {
val cutout by displayCutout.collectAsState()
-
- CompositionLocalProvider(LocalDisplayCutout provides cutout) { content() }
+ val screenCornerRadiusDp = with(LocalDensity.current) { screenCornerRadius.toDp() }
+ CompositionLocalProvider(
+ LocalScreenCornerRadius provides screenCornerRadiusDp,
+ LocalDisplayCutout provides cutout
+ ) {
+ content()
+ }
}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyboard/stickykeys/ui/view/StickyKeysIndicator.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyboard/stickykeys/ui/view/StickyKeysIndicator.kt
index 68e57b5..dd86646 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/keyboard/stickykeys/ui/view/StickyKeysIndicator.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyboard/stickykeys/ui/view/StickyKeysIndicator.kt
@@ -16,23 +16,42 @@
package com.android.systemui.keyboard.stickykeys.ui.view
+import android.content.Context
+import android.view.View
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.key
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
+import com.android.compose.theme.PlatformTheme
import com.android.systemui.keyboard.stickykeys.shared.model.Locked
import com.android.systemui.keyboard.stickykeys.shared.model.ModifierKey
import com.android.systemui.keyboard.stickykeys.ui.viewmodel.StickyKeysIndicatorViewModel
+fun createStickyKeyIndicatorView(context: Context, viewModel: StickyKeysIndicatorViewModel): View {
+ return ComposeView(context).apply {
+ setContent {
+ PlatformTheme {
+ val defaultContentColor = MaterialTheme.colorScheme.onSurfaceVariant
+ CompositionLocalProvider(LocalContentColor provides defaultContentColor) {
+ StickyKeysIndicator(viewModel)
+ }
+ }
+ }
+ }
+}
+
@Composable
fun StickyKeysIndicator(viewModel: StickyKeysIndicatorViewModel) {
val stickyKeys by viewModel.indicatorContent.collectAsState(emptyMap())
@@ -53,7 +72,7 @@
stickyKeys.forEach { (key, isLocked) ->
key(key) {
Text(
- text = key.text,
+ text = key.displayedText,
fontWeight = if (isLocked.locked) FontWeight.Bold else FontWeight.Normal
)
}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenContent.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenContent.kt
index 2cb0034..b5499b7 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenContent.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenContent.kt
@@ -25,7 +25,7 @@
import com.android.compose.animation.scene.SceneKey
import com.android.compose.animation.scene.SceneTransitionLayout
import com.android.compose.animation.scene.transitions
-import com.android.systemui.keyguard.ui.composable.blueprint.LockscreenSceneBlueprint
+import com.android.systemui.keyguard.ui.composable.blueprint.ComposableLockscreenSceneBlueprint
import com.android.systemui.keyguard.ui.viewmodel.LockscreenContentViewModel
import javax.inject.Inject
@@ -39,10 +39,10 @@
@Inject
constructor(
private val viewModel: LockscreenContentViewModel,
- private val blueprints: Set<@JvmSuppressWildcards LockscreenSceneBlueprint>,
+ private val blueprints: Set<@JvmSuppressWildcards ComposableLockscreenSceneBlueprint>,
) {
- private val sceneKeyByBlueprint: Map<LockscreenSceneBlueprint, SceneKey> by lazy {
+ private val sceneKeyByBlueprint: Map<ComposableLockscreenSceneBlueprint, SceneKey> by lazy {
blueprints.associateWith { blueprint -> SceneKey(blueprint.id) }
}
private val sceneKeyByBlueprintId: Map<String, SceneKey> by lazy {
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenScene.kt
index ff53ff2..378a1e4 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenScene.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenScene.kt
@@ -34,6 +34,7 @@
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
@@ -43,23 +44,21 @@
@Inject
constructor(
@Application private val applicationScope: CoroutineScope,
- private val viewModel: LockscreenSceneViewModel,
+ viewModel: LockscreenSceneViewModel,
private val lockscreenContent: Lazy<LockscreenContent>,
) : ComposableScene {
override val key = SceneKey.Lockscreen
override val destinationScenes: StateFlow<Map<UserAction, SceneModel>> =
- viewModel.upDestinationSceneKey
- .map { pageKey ->
- destinationScenes(up = pageKey, left = viewModel.leftDestinationSceneKey)
- }
+ combine(viewModel.upDestinationSceneKey, viewModel.leftDestinationSceneKey, ::Pair)
+ .map { (upKey, leftKey) -> destinationScenes(up = upKey, left = leftKey) }
.stateIn(
scope = applicationScope,
started = SharingStarted.Eagerly,
initialValue =
destinationScenes(
up = viewModel.upDestinationSceneKey.value,
- left = viewModel.leftDestinationSceneKey,
+ left = viewModel.leftDestinationSceneKey.value,
)
)
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/CommunalBlueprint.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/CommunalBlueprint.kt
index 86124c6..6b210af 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/CommunalBlueprint.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/CommunalBlueprint.kt
@@ -36,7 +36,7 @@
@Inject
constructor(
private val viewModel: LockscreenContentViewModel,
-) : LockscreenSceneBlueprint {
+) : ComposableLockscreenSceneBlueprint {
override val id: String = "communal"
@@ -59,5 +59,5 @@
@Module
interface CommunalBlueprintModule {
- @Binds @IntoSet fun blueprint(blueprint: CommunalBlueprint): LockscreenSceneBlueprint
+ @Binds @IntoSet fun blueprint(blueprint: CommunalBlueprint): ComposableLockscreenSceneBlueprint
}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/LockscreenSceneBlueprint.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/ComposableLockscreenSceneBlueprint.kt
similarity index 87%
rename from packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/LockscreenSceneBlueprint.kt
rename to packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/ComposableLockscreenSceneBlueprint.kt
index 6d9cba4..cb73983 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/LockscreenSceneBlueprint.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/ComposableLockscreenSceneBlueprint.kt
@@ -19,13 +19,10 @@
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.android.compose.animation.scene.SceneScope
+import com.android.systemui.keyguard.shared.model.LockscreenSceneBlueprint
/** Defines interface for classes that can render the content for a specific blueprint/layout. */
-interface LockscreenSceneBlueprint {
-
- /** The ID that uniquely identifies this blueprint across all other blueprints. */
- val id: String
-
+interface ComposableLockscreenSceneBlueprint : LockscreenSceneBlueprint {
/** Renders the content of this blueprint. */
@Composable fun SceneScope.Content(modifier: Modifier)
}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/DefaultBlueprint.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/DefaultBlueprint.kt
index bf02d8a..a07ab4a 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/DefaultBlueprint.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/DefaultBlueprint.kt
@@ -20,10 +20,12 @@
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.Layout
import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.unit.IntRect
import com.android.compose.animation.scene.SceneScope
import com.android.compose.modifiers.padding
@@ -38,6 +40,7 @@
import com.android.systemui.keyguard.ui.composable.section.SmartSpaceSection
import com.android.systemui.keyguard.ui.composable.section.StatusBarSection
import com.android.systemui.keyguard.ui.viewmodel.LockscreenContentViewModel
+import com.android.systemui.res.R
import dagger.Binds
import dagger.Module
import dagger.multibindings.IntoSet
@@ -61,7 +64,7 @@
private val bottomAreaSection: BottomAreaSection,
private val settingsMenuSection: SettingsMenuSection,
private val clockInteractor: KeyguardClockInteractor,
-) : LockscreenSceneBlueprint {
+) : ComposableLockscreenSceneBlueprint {
override val id: String = "default"
@@ -84,6 +87,7 @@
with(statusBarSection) { StatusBar(modifier = Modifier.fillMaxWidth()) }
with(clockSection) {
SmallClock(
+ burnInParams = burnIn.parameters,
onTopChanged = burnIn.onSmallClockTopChanged,
modifier = Modifier.fillMaxWidth(),
)
@@ -95,7 +99,13 @@
modifier =
Modifier.fillMaxWidth()
.padding(
- top = { viewModel.getSmartSpacePaddingTop(resources) }
+ top = { viewModel.getSmartSpacePaddingTop(resources) },
+ )
+ .padding(
+ bottom =
+ dimensionResource(
+ R.dimen.keyguard_status_view_bottom_margin
+ ),
),
)
}
@@ -214,5 +224,5 @@
@Module
interface DefaultBlueprintModule {
- @Binds @IntoSet fun blueprint(blueprint: DefaultBlueprint): LockscreenSceneBlueprint
+ @Binds @IntoSet fun blueprint(blueprint: DefaultBlueprint): ComposableLockscreenSceneBlueprint
}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/ShortcutsBesideUdfpsBlueprint.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/ShortcutsBesideUdfpsBlueprint.kt
index d0aa444..b035e42 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/ShortcutsBesideUdfpsBlueprint.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/ShortcutsBesideUdfpsBlueprint.kt
@@ -20,10 +20,12 @@
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.Layout
import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.unit.IntRect
import com.android.compose.animation.scene.SceneScope
import com.android.compose.modifiers.padding
@@ -38,6 +40,7 @@
import com.android.systemui.keyguard.ui.composable.section.SmartSpaceSection
import com.android.systemui.keyguard.ui.composable.section.StatusBarSection
import com.android.systemui.keyguard.ui.viewmodel.LockscreenContentViewModel
+import com.android.systemui.res.R
import dagger.Binds
import dagger.Module
import dagger.multibindings.IntoSet
@@ -61,7 +64,7 @@
private val bottomAreaSection: BottomAreaSection,
private val settingsMenuSection: SettingsMenuSection,
private val clockInteractor: KeyguardClockInteractor,
-) : LockscreenSceneBlueprint {
+) : ComposableLockscreenSceneBlueprint {
override val id: String = "shortcuts-besides-udfps"
@@ -86,6 +89,7 @@
SmallClock(
onTopChanged = burnIn.onSmallClockTopChanged,
modifier = Modifier.fillMaxWidth(),
+ burnInParams = burnIn.parameters,
)
}
with(smartSpaceSection) {
@@ -96,6 +100,12 @@
Modifier.fillMaxWidth()
.padding(
top = { viewModel.getSmartSpacePaddingTop(resources) }
+ )
+ .padding(
+ bottom =
+ dimensionResource(
+ R.dimen.keyguard_status_view_bottom_margin
+ )
),
)
}
@@ -222,5 +232,5 @@
interface ShortcutsBesideUdfpsBlueprintModule {
@Binds
@IntoSet
- fun blueprint(blueprint: ShortcutsBesideUdfpsBlueprint): LockscreenSceneBlueprint
+ fun blueprint(blueprint: ShortcutsBesideUdfpsBlueprint): ComposableLockscreenSceneBlueprint
}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/SplitShadeBlueprint.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/SplitShadeBlueprint.kt
index 616a7b4..660fc5a 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/SplitShadeBlueprint.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/SplitShadeBlueprint.kt
@@ -72,7 +72,7 @@
private val settingsMenuSection: SettingsMenuSection,
private val clockInteractor: KeyguardClockInteractor,
private val largeScreenHeaderHelper: LargeScreenHeaderHelper,
-) : LockscreenSceneBlueprint {
+) : ComposableLockscreenSceneBlueprint {
override val id: String = "split-shade"
@@ -109,7 +109,14 @@
.padding(
top = {
viewModel.getSmartSpacePaddingTop(resources)
- }
+ },
+ )
+ .padding(
+ bottom =
+ dimensionResource(
+ R.dimen
+ .keyguard_status_view_bottom_margin
+ )
),
)
}
@@ -237,5 +244,7 @@
@Module
interface SplitShadeBlueprintModule {
- @Binds @IntoSet fun blueprint(blueprint: SplitShadeBlueprint): LockscreenSceneBlueprint
+ @Binds
+ @IntoSet
+ fun blueprint(blueprint: SplitShadeBlueprint): ComposableLockscreenSceneBlueprint
}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/ClockSection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/ClockSection.kt
index 8f21879..fa07baf 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/ClockSection.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/ClockSection.kt
@@ -33,7 +33,10 @@
import com.android.keyguard.KeyguardClockSwitch
import com.android.systemui.customization.R as customizationR
import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor
+import com.android.systemui.keyguard.ui.composable.modifier.burnInAware
import com.android.systemui.keyguard.ui.composable.modifier.onTopPlacementChanged
+import com.android.systemui.keyguard.ui.viewmodel.AodBurnInViewModel
+import com.android.systemui.keyguard.ui.viewmodel.BurnInParameters
import com.android.systemui.keyguard.ui.viewmodel.KeyguardClockViewModel
import javax.inject.Inject
@@ -42,10 +45,12 @@
constructor(
private val viewModel: KeyguardClockViewModel,
private val clockInteractor: KeyguardClockInteractor,
+ private val aodBurnInViewModel: AodBurnInViewModel,
) {
@Composable
fun SceneScope.SmallClock(
+ burnInParams: BurnInParameters,
onTopChanged: (top: Float?) -> Unit,
modifier: Modifier = Modifier,
) {
@@ -89,7 +94,11 @@
dimensionResource(customizationR.dimen.clock_padding_start)
)
.padding(top = { viewModel.getSmallClockTopMargin(view.context) })
- .onTopPlacementChanged(onTopChanged),
+ .onTopPlacementChanged(onTopChanged)
+ .burnInAware(
+ viewModel = aodBurnInViewModel,
+ params = burnInParams,
+ ),
update = {
val newClockView = checkNotNull(currentClock).smallClock.view
it.removeAllViews()
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/NotificationSection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/NotificationSection.kt
index 5e27d82..f387021 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/NotificationSection.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/NotificationSection.kt
@@ -45,7 +45,7 @@
class NotificationSection
@Inject
constructor(
- @Application context: Context,
+ @Application private val context: Context,
private val viewModel: NotificationsPlaceholderViewModel,
controller: NotificationStackScrollLayoutController,
sceneContainerFlags: SceneContainerFlags,
@@ -55,17 +55,22 @@
notificationStackAppearanceViewModel: NotificationStackAppearanceViewModel,
ambientState: AmbientState,
notificationStackSizeCalculator: NotificationStackSizeCalculator,
- @Main mainDispatcher: CoroutineDispatcher,
+ @Main private val mainDispatcher: CoroutineDispatcher,
) {
+
init {
if (!KeyguardShadeMigrationNssl.isUnexpectedlyInLegacyMode()) {
- // This scene container section moves the NSSL to the SharedNotificationContainer. This
- // also requires that SharedNotificationContainer gets moved to the SceneWindowRootView
- // by the SceneWindowRootViewBinder.
- // Prior to Scene Container, but when the KeyguardShadeMigrationNssl flag is enabled,
- // NSSL is moved into this container by the NotificationStackScrollLayoutSection.
- (stackScrollLayout.parent as? ViewGroup)?.removeView(stackScrollLayout)
- sharedNotificationContainer.addNotificationStackScrollLayout(stackScrollLayout)
+ // This scene container section moves the NSSL to the SharedNotificationContainer.
+ // This also requires that SharedNotificationContainer gets moved to the
+ // SceneWindowRootView by the SceneWindowRootViewBinder. Prior to Scene Container,
+ // but when the KeyguardShadeMigrationNssl flag is enabled, NSSL is moved into this
+ // container by the NotificationStackScrollLayoutSection.
+ // Ensure stackScrollLayout is a child of sharedNotificationContainer.
+
+ if (stackScrollLayout.parent != sharedNotificationContainer) {
+ (stackScrollLayout.parent as? ViewGroup)?.removeView(stackScrollLayout)
+ sharedNotificationContainer.addNotificationStackScrollLayout(stackScrollLayout)
+ }
SharedNotificationContainerBinder.bind(
sharedNotificationContainer,
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt
index 0e08a19..ef6ae2e 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt
@@ -19,6 +19,7 @@
import android.util.Log
import androidx.compose.foundation.background
+import androidx.compose.foundation.gestures.scrollBy
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
@@ -51,22 +52,29 @@
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.layout.LayoutCoordinates
import androidx.compose.ui.layout.boundsInWindow
+import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.layout.onPlaced
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.layout.positionInWindow
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.dp
+import androidx.compose.ui.util.lerp
import com.android.compose.animation.scene.ElementKey
import com.android.compose.animation.scene.NestedScrollBehavior
import com.android.compose.animation.scene.SceneScope
import com.android.compose.modifiers.height
+import com.android.systemui.common.ui.compose.windowinsets.LocalScreenCornerRadius
import com.android.systemui.notifications.ui.composable.Notifications.Form
+import com.android.systemui.notifications.ui.composable.Notifications.TransitionThresholds.EXPANSION_FOR_MAX_CORNER_RADIUS
+import com.android.systemui.notifications.ui.composable.Notifications.TransitionThresholds.EXPANSION_FOR_MAX_SCRIM_ALPHA
import com.android.systemui.scene.ui.composable.Gone
import com.android.systemui.scene.ui.composable.Shade
import com.android.systemui.shade.ui.composable.ShadeHeader
+import com.android.systemui.statusbar.notification.stack.ui.viewbinder.NotificationStackAppearanceViewBinder.SCRIM_CORNER_RADIUS
import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationsPlaceholderViewModel
import kotlin.math.roundToInt
@@ -77,6 +85,13 @@
val ShelfSpace = ElementKey("ShelfSpace")
}
+ // Expansion fraction thresholds (between 0-1f) at which the corresponding value should be
+ // at its maximum, given they are at their minimum value at expansion = 0f.
+ object TransitionThresholds {
+ const val EXPANSION_FOR_MAX_CORNER_RADIUS = 0.1f
+ const val EXPANSION_FOR_MAX_SCRIM_ALPHA = 0.3f
+ }
+
enum class Form {
HunFromTop,
Stack,
@@ -125,19 +140,21 @@
modifier: Modifier = Modifier,
) {
val density = LocalDensity.current
- val cornerRadius by viewModel.cornerRadiusDp.collectAsState()
+ val screenCornerRadius = LocalScreenCornerRadius.current
+ val scrollState = rememberScrollState()
+ val syntheticScroll = viewModel.syntheticScroll.collectAsState(0f)
val expansionFraction by viewModel.expandFraction.collectAsState(0f)
val navBarHeight =
with(density) { WindowInsets.systemBars.asPaddingValues().calculateBottomPadding().toPx() }
- val statusBarHeight =
- with(density) { WindowInsets.systemBars.asPaddingValues().calculateTopPadding().toPx() }
- val displayCutoutHeight =
- with(density) { WindowInsets.displayCutout.asPaddingValues().calculateTopPadding().toPx() }
+ val statusBarHeight = WindowInsets.systemBars.asPaddingValues().calculateTopPadding()
+ val displayCutoutHeight = WindowInsets.displayCutout.asPaddingValues().calculateTopPadding()
val screenHeight =
- with(density) { LocalConfiguration.current.screenHeightDp.dp.toPx() } +
- navBarHeight +
- maxOf(statusBarHeight, displayCutoutHeight)
+ with(density) {
+ (LocalConfiguration.current.screenHeightDp.dp +
+ maxOf(statusBarHeight, displayCutoutHeight))
+ .toPx()
+ } + navBarHeight
val contentHeight = viewModel.intrinsicContentHeight.collectAsState()
@@ -166,31 +183,75 @@
// if contentHeight drops below minimum visible scrim height while scrim is
// expanded, reset scrim offset.
- LaunchedEffect(contentHeight, screenHeight, maxScrimTop, scrimOffset) {
+ LaunchedEffect(contentHeight, scrimOffset) {
snapshotFlow { contentHeight.value < minVisibleScrimHeight() && scrimOffset.value < 0f }
.collect { shouldCollapse -> if (shouldCollapse) scrimOffset.value = 0f }
}
- Box(modifier = modifier.element(Notifications.Elements.NotificationScrim)) {
+ // if we receive scroll delta from NSSL, offset the scrim and placeholder accordingly.
+ LaunchedEffect(syntheticScroll, scrimOffset, scrollState) {
+ snapshotFlow { syntheticScroll.value }
+ .collect { delta ->
+ val minOffset = minScrimOffset()
+ if (scrimOffset.value > minOffset) {
+ val remainingDelta = (minOffset - (scrimOffset.value - delta)).coerceAtLeast(0f)
+ scrimOffset.value = (scrimOffset.value - delta).coerceAtLeast(minOffset)
+ if (remainingDelta > 0f) {
+ scrollState.scrollBy(remainingDelta)
+ }
+ } else {
+ scrollState.scrollTo(delta.roundToInt())
+ }
+ }
+ }
+
+ Box(
+ modifier =
+ modifier
+ .element(Notifications.Elements.NotificationScrim)
+ .offset {
+ // if scrim is expanded while transitioning to Gone scene, increase the offset
+ // in step with the transition so that it is 0 when it completes.
+ if (
+ scrimOffset.value < 0 &&
+ layoutState.isTransitioning(from = Shade, to = Gone)
+ ) {
+ IntOffset(x = 0, y = (scrimOffset.value * expansionFraction).roundToInt())
+ } else {
+ IntOffset(x = 0, y = scrimOffset.value.roundToInt())
+ }
+ }
+ .graphicsLayer {
+ shape =
+ calculateCornerRadius(
+ screenCornerRadius,
+ { expansionFraction },
+ layoutState.isTransitioningBetween(Gone, Shade)
+ )
+ .let {
+ RoundedCornerShape(
+ topStart = it,
+ topEnd = it,
+ )
+ }
+ clip = true
+ }
+ ) {
+ // Creates a cutout in the background scrim in the shape of the notifications scrim.
+ // Only visible when notif scrim alpha < 1, during shade expansion.
Spacer(
modifier =
- Modifier.fillMaxSize()
- .graphicsLayer {
- shape = RoundedCornerShape(cornerRadius.dp)
- clip = true
- }
- .drawBehind { drawRect(Color.Black, blendMode = BlendMode.DstOut) }
+ Modifier.fillMaxSize().drawBehind {
+ drawRect(Color.Black, blendMode = BlendMode.DstOut)
+ }
)
Box(
modifier =
Modifier.fillMaxSize()
- .offset { IntOffset(0, scrimOffset.value.roundToInt()) }
.graphicsLayer {
- shape = RoundedCornerShape(cornerRadius.dp)
- clip = true
alpha =
if (layoutState.isTransitioningBetween(Gone, Shade)) {
- (expansionFraction / 0.3f).coerceAtMost(1f)
+ (expansionFraction / EXPANSION_FOR_MAX_SCRIM_ALPHA).coerceAtMost(1f)
} else 1f
}
.background(MaterialTheme.colorScheme.surface)
@@ -219,7 +280,7 @@
)
}
)
- .verticalScroll(rememberScrollState())
+ .verticalScroll(scrollState)
.fillMaxWidth()
.height { (contentHeight.value + navBarHeight).roundToInt() },
)
@@ -278,10 +339,10 @@
.onSizeChanged { size: IntSize ->
debugLog(viewModel) { "STACK onSizeChanged: size=$size" }
}
- .onPlaced { coordinates: LayoutCoordinates ->
+ .onGloballyPositioned { coordinates: LayoutCoordinates ->
viewModel.onContentTopChanged(coordinates.positionInWindow().y)
debugLog(viewModel) {
- "STACK onPlaced:" +
+ "STACK onGloballyPositioned:" +
" size=${coordinates.size}" +
" position=${coordinates.positionInWindow()}" +
" bounds=${coordinates.boundsInWindow()}"
@@ -310,6 +371,23 @@
}
}
+private fun calculateCornerRadius(
+ screenCornerRadius: Dp,
+ expansionFraction: () -> Float,
+ transitioning: Boolean,
+): Dp {
+ return if (transitioning) {
+ lerp(
+ start = screenCornerRadius.value,
+ stop = SCRIM_CORNER_RADIUS,
+ fraction = (expansionFraction() / EXPANSION_FOR_MAX_CORNER_RADIUS).coerceAtMost(1f),
+ )
+ .dp
+ } else {
+ SCRIM_CORNER_RADIUS.dp
+ }
+}
+
private inline fun debugLog(
viewModel: NotificationsPlaceholderViewModel,
msg: () -> Any,
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettings.kt b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettings.kt
index c027c49..de8f2ec 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettings.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettings.kt
@@ -18,21 +18,21 @@
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
+import androidx.compose.ui.layout.layout
import androidx.compose.ui.platform.LocalContext
-import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import com.android.compose.animation.scene.ElementKey
+import com.android.compose.animation.scene.MovableElementScenePicker
import com.android.compose.animation.scene.SceneScope
import com.android.compose.animation.scene.TransitionState
+import com.android.compose.modifiers.thenIf
import com.android.compose.theme.colorAttr
import com.android.systemui.qs.ui.adapter.QSSceneAdapter
import com.android.systemui.qs.ui.adapter.QSSceneAdapter.State.Companion.Collapsing
@@ -44,9 +44,16 @@
import com.android.systemui.scene.ui.composable.Shade
object QuickSettings {
+ private val SCENES =
+ setOf(
+ QuickSettingsSceneKey,
+ Shade,
+ )
+
object Elements {
// TODO RENAME
- val Content = ElementKey("QuickSettingsContent")
+ val Content =
+ ElementKey("QuickSettingsContent", scenePicker = MovableElementScenePicker(SCENES))
val CollapsedGrid = ElementKey("QuickSettingsCollapsedGrid")
val FooterActions = ElementKey("QuickSettingsFooterActions")
}
@@ -86,14 +93,22 @@
*/
@Composable
fun SceneScope.QuickSettings(
- modifier: Modifier = Modifier,
qsSceneAdapter: QSSceneAdapter,
+ heightProvider: () -> Int,
+ modifier: Modifier = Modifier,
) {
val contentState = stateForQuickSettingsContent()
MovableElement(
key = QuickSettings.Elements.Content,
- modifier = modifier.fillMaxWidth().defaultMinSize(minHeight = 300.dp)
+ modifier =
+ modifier.fillMaxWidth().layout { measurable, constraints ->
+ val placeable = measurable.measure(constraints)
+ // Use the height of the correct view based on the scene it is being composed in
+ val height = heightProvider()
+
+ layout(placeable.width, height) { placeable.placeRelative(0, 0) }
+ }
) {
content { QuickSettingsContent(qsSceneAdapter = qsSceneAdapter, contentState) }
}
@@ -118,15 +133,7 @@
qsView?.let { view ->
Box(
modifier =
- modifier
- .fillMaxWidth()
- .then(
- if (isCustomizing) {
- Modifier.fillMaxHeight()
- } else {
- Modifier.wrapContentHeight()
- }
- )
+ modifier.fillMaxWidth().thenIf(isCustomizing) { Modifier.fillMaxHeight() }
) {
AndroidView(
modifier = Modifier.fillMaxWidth().background(colorAttr(R.attr.underSurface)),
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt
index 5531f9c..1cbc992 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt
@@ -36,7 +36,6 @@
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.rememberScrollState
-import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass
@@ -47,11 +46,6 @@
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.graphicsLayer
-import androidx.compose.ui.layout.LayoutCoordinates
-import androidx.compose.ui.layout.boundsInWindow
-import androidx.compose.ui.layout.onPlaced
-import androidx.compose.ui.layout.positionInWindow
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.unit.dp
@@ -62,7 +56,6 @@
import com.android.systemui.compose.modifiers.sysuiResTag
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
-import com.android.systemui.notifications.ui.composable.Notifications
import com.android.systemui.qs.footer.ui.compose.FooterActions
import com.android.systemui.qs.ui.viewmodel.QuickSettingsSceneViewModel
import com.android.systemui.scene.shared.model.SceneKey
@@ -122,8 +115,6 @@
statusBarIconController: StatusBarIconController,
modifier: Modifier = Modifier,
) {
- val cornerRadius by viewModel.notifications.cornerRadiusDp.collectAsState()
-
// TODO(b/280887232): implement the real UI.
Box(modifier = modifier.fillMaxSize()) {
val isCustomizing by viewModel.qsSceneAdapter.isCustomizing.collectAsState()
@@ -155,9 +146,9 @@
// a background that extends to the edges.
Spacer(
modifier =
- Modifier.element(Shade.Elements.ScrimBackground)
+ Modifier.element(Shade.Elements.BackgroundScrim)
.fillMaxSize()
- .background(MaterialTheme.colorScheme.scrim, shape = Shade.Shapes.Scrim)
+ .background(MaterialTheme.colorScheme.scrim)
)
Column(
horizontalAlignment = Alignment.CenterHorizontally,
@@ -222,8 +213,9 @@
Spacer(modifier = Modifier.height(16.dp))
// This view has its own horizontal padding
QuickSettings(
- modifier = Modifier.sysuiResTag("expanded_qs_scroll_view"),
viewModel.qsSceneAdapter,
+ { viewModel.qsSceneAdapter.qsHeight },
+ modifier = Modifier.sysuiResTag("expanded_qs_scroll_view"),
)
}
}
@@ -242,32 +234,5 @@
}
}
}
- // Scrim with height 0 aligned to bottom of the screen to facilitate shared element
- // transition from Shade scene.
- Box(
- modifier =
- Modifier.element(Notifications.Elements.NotificationScrim)
- .fillMaxWidth()
- .height(0.dp)
- .graphicsLayer {
- shape = RoundedCornerShape(cornerRadius.dp)
- clip = true
- alpha = 1f
- }
- .background(MaterialTheme.colorScheme.surface)
- .align(Alignment.BottomCenter)
- .onPlaced { coordinates: LayoutCoordinates ->
- viewModel.notifications.onContentTopChanged(
- coordinates.positionInWindow().y
- )
- val boundsInWindow = coordinates.boundsInWindow()
- viewModel.notifications.onBoundsChanged(
- left = boundsInWindow.left,
- top = boundsInWindow.top,
- right = boundsInWindow.right,
- bottom = boundsInWindow.bottom,
- )
- }
- )
}
}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/GoneScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/GoneScene.kt
index 770d654..736ee1f 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/GoneScene.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/GoneScene.kt
@@ -16,18 +16,18 @@
package com.android.systemui.scene.ui.composable
-import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.android.compose.animation.scene.SceneScope
import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.notifications.ui.composable.Notifications
import com.android.systemui.scene.shared.model.Direction
import com.android.systemui.scene.shared.model.Edge
import com.android.systemui.scene.shared.model.SceneKey
import com.android.systemui.scene.shared.model.SceneModel
import com.android.systemui.scene.shared.model.UserAction
+import com.android.systemui.shade.ui.composable.Shade
import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationsPlaceholderViewModel
import javax.inject.Inject
import kotlinx.coroutines.flow.MutableStateFlow
@@ -63,6 +63,6 @@
override fun SceneScope.Content(
modifier: Modifier,
) {
- Box(modifier = Modifier.fillMaxSize().element(Notifications.Elements.NotificationScrim))
+ Spacer(modifier.fillMaxSize())
}
}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt
index 9f9e1f5..da1b417 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt
@@ -40,6 +40,7 @@
import com.android.compose.animation.scene.Swipe
import com.android.compose.animation.scene.SwipeDirection
import com.android.compose.animation.scene.UserAction as SceneTransitionUserAction
+import com.android.compose.animation.scene.UserActionResult
import com.android.compose.animation.scene.observableTransitionState
import com.android.compose.animation.scene.updateSceneTransitionLayoutState
import com.android.systemui.ribbon.ui.composable.BottomRightCornerRibbon
@@ -168,7 +169,7 @@
private fun toTransitionModels(
userAction: UserAction,
sceneModel: SceneModel,
-): Pair<SceneTransitionUserAction, SceneTransitionSceneKey> {
+): Pair<SceneTransitionUserAction, UserActionResult> {
return userAction.toTransitionUserAction() to sceneModel.key.toTransitionSceneKey()
}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromGoneToShadeTransition.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromGoneToShadeTransition.kt
index 0c2c519..1223ace 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromGoneToShadeTransition.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromGoneToShadeTransition.kt
@@ -3,6 +3,7 @@
import androidx.compose.animation.core.tween
import com.android.compose.animation.scene.Edge
import com.android.compose.animation.scene.TransitionBuilder
+import com.android.systemui.notifications.ui.composable.Notifications
import com.android.systemui.qs.ui.composable.QuickSettings
import com.android.systemui.shade.ui.composable.ShadeHeader
@@ -10,5 +11,6 @@
spec = tween(durationMillis = 500)
fractionRange(start = .58f) { fade(ShadeHeader.Elements.CollapsedContent) }
- translate(QuickSettings.Elements.Content, Edge.Top, true)
+ translate(QuickSettings.Elements.Content, y = -ShadeHeader.Dimensions.CollapsedHeight * .66f)
+ translate(Notifications.Elements.NotificationScrim, Edge.Top, false)
}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromLockscreenToShadeTransition.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromLockscreenToShadeTransition.kt
index ebc343d..2d5cf5c 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromLockscreenToShadeTransition.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromLockscreenToShadeTransition.kt
@@ -10,9 +10,8 @@
fun TransitionBuilder.lockscreenToShadeTransition() {
spec = tween(durationMillis = 500)
- translate(Shade.Elements.Scrim, Edge.Top, startsOutsideLayoutBounds = false)
fractionRange(end = 0.5f) {
- fade(Shade.Elements.ScrimBackground)
+ fade(Shade.Elements.BackgroundScrim)
translate(
QuickSettings.Elements.CollapsedGrid,
Edge.Top,
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt
index 497fe87..cac35cb 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt
@@ -38,6 +38,7 @@
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.unit.dp
import com.android.compose.animation.scene.ElementKey
+import com.android.compose.animation.scene.LowestZIndexScenePicker
import com.android.compose.animation.scene.SceneScope
import com.android.systemui.battery.BatteryMeterViewController
import com.android.systemui.dagger.SysUISingleton
@@ -74,8 +75,8 @@
object Elements {
val QuickSettings = ElementKey("ShadeQuickSettings")
val MediaCarousel = ElementKey("ShadeMediaCarousel")
- val Scrim = ElementKey("ShadeScrim")
- val ScrimBackground = ElementKey("ShadeScrimBackground")
+ val BackgroundScrim =
+ ElementKey("ShadeBackgroundScrim", scenePicker = LowestZIndexScenePicker)
}
object Dimensions {
@@ -162,7 +163,9 @@
Box(
modifier =
- modifier.element(Shade.Elements.Scrim).background(MaterialTheme.colorScheme.scrim),
+ modifier
+ .element(Shade.Elements.BackgroundScrim)
+ .background(MaterialTheme.colorScheme.scrim),
)
Box {
Layout(
@@ -186,8 +189,8 @@
)
)
QuickSettings(
- modifier = Modifier.height(130.dp),
viewModel.qsSceneAdapter,
+ { viewModel.qsSceneAdapter.qqsHeight },
)
if (viewModel.isMediaVisible()) {
@@ -236,17 +239,7 @@
check(measurables[1].size == 1)
val quickSettingsPlaceable = measurables[0][0].measure(constraints)
-
- val notificationsMeasurable = measurables[1][0]
- val notificationsScrimMaxHeight =
- constraints.maxHeight - ShadeHeader.Dimensions.CollapsedHeight.roundToPx()
- val notificationsPlaceable =
- notificationsMeasurable.measure(
- constraints.copy(
- minHeight = notificationsScrimMaxHeight,
- maxHeight = notificationsScrimMaxHeight
- )
- )
+ val notificationsPlaceable = measurables[1][0].measure(constraints)
maxNotifScrimTop.value = quickSettingsPlaceable.height.toFloat()
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/ui/composable/VerticalVolumePanelContent.kt b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/ui/composable/VerticalVolumePanelContent.kt
index dcd22fe..a7ec93f 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/ui/composable/VerticalVolumePanelContent.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/ui/composable/VerticalVolumePanelContent.kt
@@ -19,6 +19,7 @@
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
+import androidx.compose.material3.Slider
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
@@ -33,6 +34,7 @@
modifier = modifier,
verticalArrangement = Arrangement.spacedBy(20.dp),
) {
+ Slider(0.5f, {})
for (component in components) {
AnimatedVisibility(component.isVisible) {
with(component.component as ComposeVolumePanelUiComponent) { Content(Modifier) }
diff --git a/packages/SystemUI/compose/features/tests/Android.bp b/packages/SystemUI/compose/features/tests/Android.bp
index c85cd7b..69b18c4 100644
--- a/packages/SystemUI/compose/features/tests/Android.bp
+++ b/packages/SystemUI/compose/features/tests/Android.bp
@@ -13,6 +13,7 @@
// limitations under the License.
package {
+ default_team: "trendy_team_system_ui_please_use_a_more_specific_subteam_if_possible_",
// See: http://go/android-license-faq
// A large-scale-change added 'default_applicable_licenses' to import
// all of the 'license_kinds' from "frameworks_base_packages_SystemUI_license"
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateSharedAsState.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateSharedAsState.kt
index b26194f..37d763b 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateSharedAsState.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateSharedAsState.kt
@@ -30,7 +30,8 @@
import androidx.compose.ui.graphics.lerp
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.lerp
-import com.android.compose.ui.util.lerp
+import androidx.compose.ui.util.fastCoerceIn
+import androidx.compose.ui.util.lerp
/**
* A [State] whose [value] is animated.
@@ -282,7 +283,7 @@
} else {
val progress =
if (canOverflow) transition.progress
- else transition.progress.coerceIn(0f, 1f)
+ else transition.progress.fastCoerceIn(0f, 1f)
lerp(fromValue, toValue, progress)
}
} else fromValue ?: toValue
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt
index a910bca..828e34d 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt
@@ -26,6 +26,7 @@
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.isUnspecified
import androidx.compose.ui.geometry.lerp
+import androidx.compose.ui.graphics.CompositingStrategy
import androidx.compose.ui.graphics.drawscope.ContentDrawScope
import androidx.compose.ui.graphics.drawscope.scale
import androidx.compose.ui.layout.IntermediateMeasureScope
@@ -38,6 +39,8 @@
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.round
+import androidx.compose.ui.util.fastCoerceIn
+import androidx.compose.ui.util.lerp
import com.android.compose.animation.scene.transformation.PropertyTransformation
import com.android.compose.animation.scene.transformation.SharedElementTransformation
import com.android.compose.ui.util.lerp
@@ -361,7 +364,7 @@
isSpecified = { true },
::lerp,
)
- .coerceIn(0f, 1f)
+ .fastCoerceIn(0f, 1f)
}
@OptIn(ExperimentalComposeUiApi::class)
@@ -473,7 +476,8 @@
placeable.place(offset)
} else {
placeable.placeWithLayer(offset) {
- this.alpha = elementAlpha(layoutImpl, element, scene)
+ alpha = elementAlpha(layoutImpl, element, scene)
+ compositingStrategy = CompositingStrategy.ModulateAlpha
}
}
}
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Key.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Key.kt
index 2596d4a..9770399 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Key.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Key.kt
@@ -44,7 +44,7 @@
class SceneKey(
debugName: String,
identity: Any = Object(),
-) : Key(debugName, identity), UserActionResult {
+) : Key(debugName, identity) {
@VisibleForTesting
// TODO(b/240432457): Make internal once PlatformComposeSceneTransitionLayoutTestsUtils can
// access internal members.
@@ -53,11 +53,6 @@
/** The unique [ElementKey] identifying this scene's root element. */
val rootElementKey = ElementKey(debugName, identity)
- // Implementation of [UserActionResult].
- override val toScene: SceneKey = this
- override val transitionKey: TransitionKey? = null
- override val distance: UserActionDistance? = null
-
override fun toString(): String {
return "SceneKey(debugName=$debugName)"
}
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt
index 529fc03..3ff869b 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt
@@ -283,21 +283,28 @@
}
onDragStart(drag.position, overSlop, pressed.size)
- onDrag(drag, overSlop)
- val successful =
- when (orientation) {
- Orientation.Horizontal ->
- horizontalDrag(drag.id) {
- onDrag(it, it.positionChange().x)
- it.consume()
- }
- Orientation.Vertical ->
- verticalDrag(drag.id) {
- onDrag(it, it.positionChange().y)
- it.consume()
- }
- }
+ val successful: Boolean
+ try {
+ onDrag(drag, overSlop)
+
+ successful =
+ when (orientation) {
+ Orientation.Horizontal ->
+ horizontalDrag(drag.id) {
+ onDrag(it, it.positionChange().x)
+ it.consume()
+ }
+ Orientation.Vertical ->
+ verticalDrag(drag.id) {
+ onDrag(it, it.positionChange().y)
+ it.consume()
+ }
+ }
+ } catch (t: Throwable) {
+ onDragCancel()
+ throw t
+ }
if (successful) {
onDragEnd()
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneGestureHandler.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneGestureHandler.kt
index 23af5ac..c8fbad4 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneGestureHandler.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneGestureHandler.kt
@@ -46,7 +46,7 @@
val draggable: DraggableHandler = SceneDraggableHandler(this)
private var _swipeTransition: SwipeTransition? = null
- internal var swipeTransition: SwipeTransition
+ private var swipeTransition: SwipeTransition
get() = _swipeTransition ?: error("SwipeTransition needs to be initialized")
set(value) {
_swipeTransition = value
@@ -92,10 +92,6 @@
/** The [Swipes] associated to the current gesture. */
private var swipes: Swipes? = null
- /** The [UserActionResult] associated to up and down swipes. */
- private var upOrLeftResult: UserActionResult? = null
- private var downOrRightResult: UserActionResult? = null
-
/**
* Whether we should immediately intercept a gesture.
*
@@ -128,7 +124,7 @@
// This [transition] was already driving the animation: simply take over it.
// Stop animating and start from where the current offset.
swipeTransition.cancelOffsetAnimation()
- updateSwipesResults(swipeTransition._fromScene)
+ swipes!!.updateSwipesResults(swipeTransition._fromScene)
return
}
@@ -144,16 +140,24 @@
}
val fromScene = layoutImpl.scene(transitionState.currentScene)
- updateSwipes(fromScene, startedPosition, pointersDown)
+ val newSwipes = computeSwipes(fromScene, startedPosition, pointersDown)
+ swipes = newSwipes
+ val result = newSwipes.findUserActionResult(fromScene, overSlop, true)
- val result =
- findUserActionResult(fromScene, directionOffset = overSlop, updateSwipesResults = true)
- ?: return
- updateTransition(SwipeTransition(fromScene, result), force = true)
- }
+ // As we were unable to locate a valid target scene, the initial SwipeTransition cannot be
+ // defined.
+ if (result == null) return
- private fun updateSwipes(fromScene: Scene, startedPosition: Offset?, pointersDown: Int) {
- this.swipes = computeSwipes(fromScene, startedPosition, pointersDown)
+ val newSwipeTransition =
+ SwipeTransition(
+ fromScene = fromScene,
+ result = result,
+ swipes = newSwipes,
+ layoutImpl = layoutImpl,
+ orientation = orientation
+ )
+
+ updateTransition(newSwipeTransition, force = true)
}
private fun computeSwipes(
@@ -210,13 +214,6 @@
}
}
- private fun Scene.getAbsoluteDistance(distance: UserActionDistance?): Float {
- val targetSize = this.targetSize
- return with(distance ?: DefaultSwipeDistance) {
- layoutImpl.density.absoluteDistance(targetSize, orientation)
- }
- }
-
internal fun onDrag(delta: Float) {
if (delta == 0f || !isDrivingTransition) return
swipeTransition.dragOffset += delta
@@ -226,15 +223,17 @@
val isNewFromScene = fromScene.key != swipeTransition.fromScene
val result =
- findUserActionResult(
- fromScene,
- swipeTransition.dragOffset,
- updateSwipesResults = isNewFromScene,
+ swipes!!.findUserActionResult(
+ fromScene = fromScene,
+ directionOffset = swipeTransition.dragOffset,
+ updateSwipesResults = isNewFromScene
)
- ?: run {
- onDragStopped(delta, true)
- return
- }
+
+ if (result == null) {
+ onDragStopped(velocity = delta, canChangeScene = true)
+ return
+ }
+
swipeTransition.dragOffset += acceleratedOffset
if (
@@ -242,25 +241,20 @@
result.toScene != swipeTransition.toScene ||
result.transitionKey != swipeTransition.key
) {
- updateTransition(
- SwipeTransition(fromScene, result).apply {
- this.dragOffset = swipeTransition.dragOffset
- }
- )
+ val newSwipeTransition =
+ SwipeTransition(
+ fromScene = fromScene,
+ result = result,
+ swipes = swipes!!,
+ layoutImpl = layoutImpl,
+ orientation = orientation
+ )
+ .apply { dragOffset = swipeTransition.dragOffset }
+
+ updateTransition(newSwipeTransition)
}
}
- private fun updateSwipesResults(fromScene: Scene) {
- val (upOrLeftResult, downOrRightResult) =
- computeSwipesResults(
- fromScene,
- this.swipes ?: error("updateSwipes() should be called before updateSwipesResults()")
- )
-
- this.upOrLeftResult = upOrLeftResult
- this.downOrRightResult = downOrRightResult
- }
-
private fun computeSwipesResults(
fromScene: Scene,
swipes: Swipes
@@ -295,74 +289,20 @@
// If the swipe was not committed, don't do anything.
if (swipeTransition._currentScene != toScene) {
- return Pair(fromScene, 0f)
+ return fromScene to 0f
}
// If the offset is past the distance then let's change fromScene so that the user can swipe
// to the next screen or go back to the previous one.
val offset = swipeTransition.dragOffset
- return if (offset <= -absoluteDistance && upOrLeftResult?.toScene == toScene.key) {
- Pair(toScene, absoluteDistance)
- } else if (offset >= absoluteDistance && downOrRightResult?.toScene == toScene.key) {
- Pair(toScene, -absoluteDistance)
+ return if (offset <= -absoluteDistance && swipes!!.upOrLeftResult?.toScene == toScene.key) {
+ toScene to absoluteDistance
+ } else if (
+ offset >= absoluteDistance && swipes!!.downOrRightResult?.toScene == toScene.key
+ ) {
+ toScene to -absoluteDistance
} else {
- Pair(fromScene, 0f)
- }
- }
-
- /**
- * Returns the [UserActionResult] from [fromScene] in the direction of [directionOffset].
- *
- * @param fromScene the scene from which we look for the target
- * @param directionOffset signed float that indicates the direction. Positive is down or right
- * negative is up or left.
- * @param updateSwipesResults whether the target scenes should be updated to the current values
- * held in the Scenes map. Usually we don't want to update them while doing a drag, because
- * this could change the target scene (jump cutting) to a different scene, when some system
- * state changed the targets the background. However, an update is needed any time we
- * calculate the targets for a new fromScene.
- * @return null when there are no targets in either direction. If one direction is null and you
- * drag into the null direction this function will return the opposite direction, assuming
- * that the users intention is to start the drag into the other direction eventually. If
- * [directionOffset] is 0f and both direction are available, it will default to
- * [upOrLeftResult].
- */
- private fun findUserActionResult(
- fromScene: Scene,
- directionOffset: Float,
- updateSwipesResults: Boolean,
- ): UserActionResult? {
- if (updateSwipesResults) updateSwipesResults(fromScene)
-
- return when {
- upOrLeftResult == null && downOrRightResult == null -> null
- (directionOffset < 0f && upOrLeftResult != null) || downOrRightResult == null ->
- upOrLeftResult
- else -> downOrRightResult
- }
- }
-
- /**
- * A strict version of [findUserActionResult] that will return null when there is no Scene in
- * [directionOffset] direction
- */
- private fun findUserActionResultStrict(directionOffset: Float): UserActionResult? {
- return when {
- directionOffset > 0f -> upOrLeftResult
- directionOffset < 0f -> downOrRightResult
- else -> null
- }
- }
-
- private fun computeAbsoluteDistance(
- fromScene: Scene,
- result: UserActionResult,
- ): Float {
- return if (result == upOrLeftResult) {
- -fromScene.getAbsoluteDistance(result.distance)
- } else {
- check(result == downOrRightResult)
- fromScene.getAbsoluteDistance(result.distance)
+ fromScene to 0f
}
}
@@ -430,19 +370,24 @@
if (startFromIdlePosition) {
// If there is a target scene, we start the overscroll animation.
- val result =
- findUserActionResultStrict(velocity)
- ?: run {
- // We will not animate
- layoutState.finishTransition(swipeTransition, idleScene = fromScene.key)
- return
- }
+ val result = swipes!!.findUserActionResultStrict(velocity)
+ if (result == null) {
+ // We will not animate
+ layoutState.finishTransition(swipeTransition, idleScene = fromScene.key)
+ return
+ }
- updateTransition(
- SwipeTransition(fromScene, result).apply {
- _currentScene = swipeTransition._currentScene
- }
- )
+ val newSwipeTransition =
+ SwipeTransition(
+ fromScene = fromScene,
+ result = result,
+ swipes = swipes!!,
+ layoutImpl = layoutImpl,
+ orientation = orientation
+ )
+ .apply { _currentScene = swipeTransition._currentScene }
+
+ updateTransition(newSwipeTransition)
animateTo(targetScene = fromScene, targetOffset = 0f)
} else {
// We were between two scenes: animate to the initial scene.
@@ -486,134 +431,220 @@
}
}
- private fun SwipeTransition(fromScene: Scene, result: UserActionResult): SwipeTransition {
- return SwipeTransition(
- result.transitionKey,
- fromScene,
- layoutImpl.scene(result.toScene),
- computeAbsoluteDistance(fromScene, result),
- )
- }
-
- internal class SwipeTransition(
- val key: TransitionKey?,
- val _fromScene: Scene,
- val _toScene: Scene,
- /**
- * The signed distance between [fromScene] and [toScene]. It is negative if [fromScene] is
- * above or to the left of [toScene].
- */
- val distance: Float,
- ) : TransitionState.Transition(_fromScene.key, _toScene.key) {
- var _currentScene by mutableStateOf(_fromScene)
- override val currentScene: SceneKey
- get() = _currentScene.key
-
- override val progress: Float
- get() {
- val offset = if (isAnimatingOffset) offsetAnimatable.value else dragOffset
- return offset / distance
- }
-
- override val isInitiatedByUserInput = true
-
- /** The current offset caused by the drag gesture. */
- var dragOffset by mutableFloatStateOf(0f)
-
- /**
- * Whether the offset is animated (the user lifted their finger) or if it is driven by
- * gesture.
- */
- var isAnimatingOffset by mutableStateOf(false)
-
- // If we are not animating offset, it means the offset is being driven by the user's finger.
- override val isUserInputOngoing: Boolean
- get() = !isAnimatingOffset
-
- /** The animatable used to animate the offset once the user lifted its finger. */
- val offsetAnimatable = Animatable(0f, OffsetVisibilityThreshold)
-
- /** Job to check that there is at most one offset animation in progress. */
- private var offsetAnimationJob: Job? = null
-
- /** The spec to use when animating this transition to either [fromScene] or [toScene]. */
- lateinit var swipeSpec: SpringSpec<Float>
-
- /** Ends any previous [offsetAnimationJob] and runs the new [job]. */
- private fun startOffsetAnimation(job: () -> Job) {
- cancelOffsetAnimation()
- offsetAnimationJob = job()
- }
-
- /** Cancel any ongoing offset animation. */
- // TODO(b/317063114) This should be a suspended function to avoid multiple jobs running at
- // the same time.
- fun cancelOffsetAnimation() {
- offsetAnimationJob?.cancel()
- finishOffsetAnimation()
- }
-
- fun finishOffsetAnimation() {
- if (isAnimatingOffset) {
- isAnimatingOffset = false
- dragOffset = offsetAnimatable.value
- }
- }
-
- fun animateOffset(
- // TODO(b/317063114) The CoroutineScope should be removed.
- coroutineScope: CoroutineScope,
- initialVelocity: Float,
- targetOffset: Float,
- onAnimationCompleted: () -> Unit,
- ) {
- startOffsetAnimation {
- coroutineScope.launch {
- animateOffset(targetOffset, initialVelocity)
- onAnimationCompleted()
- }
- }
- }
-
- private suspend fun animateOffset(targetOffset: Float, initialVelocity: Float) {
- if (!isAnimatingOffset) {
- offsetAnimatable.snapTo(dragOffset)
- }
- isAnimatingOffset = true
-
- offsetAnimatable.animateTo(
- targetValue = targetOffset,
- animationSpec = swipeSpec,
- initialVelocity = initialVelocity,
- )
-
- finishOffsetAnimation()
- }
- }
-
companion object {
private const val TAG = "SceneGestureHandler"
}
+}
- private object DefaultSwipeDistance : UserActionDistance {
- override fun Density.absoluteDistance(
- fromSceneSize: IntSize,
- orientation: Orientation,
- ): Float {
- return when (orientation) {
- Orientation.Horizontal -> fromSceneSize.width
- Orientation.Vertical -> fromSceneSize.height
- }.toFloat()
+private fun SwipeTransition(
+ fromScene: Scene,
+ result: UserActionResult,
+ swipes: Swipes,
+ layoutImpl: SceneTransitionLayoutImpl,
+ orientation: Orientation,
+): SwipeTransition {
+ val upOrLeftResult = swipes.upOrLeftResult
+ val downOrRightResult = swipes.downOrRightResult
+ val userActionDistance = result.distance ?: DefaultSwipeDistance
+ val absoluteDistance =
+ with(userActionDistance) {
+ layoutImpl.density.absoluteDistance(fromScene.targetSize, orientation)
+ }
+
+ return SwipeTransition(
+ key = result.transitionKey,
+ _fromScene = fromScene,
+ _toScene = layoutImpl.scene(result.toScene),
+ distance =
+ when (result) {
+ upOrLeftResult -> -absoluteDistance
+ downOrRightResult -> absoluteDistance
+ else -> error("Unknown result $result ($upOrLeftResult $downOrRightResult)")
+ },
+ )
+}
+
+private class SwipeTransition(
+ val key: TransitionKey?,
+ val _fromScene: Scene,
+ val _toScene: Scene,
+ /**
+ * The signed distance between [fromScene] and [toScene]. It is negative if [fromScene] is above
+ * or to the left of [toScene]
+ */
+ val distance: Float,
+) : TransitionState.Transition(_fromScene.key, _toScene.key) {
+ var _currentScene by mutableStateOf(_fromScene)
+ override val currentScene: SceneKey
+ get() = _currentScene.key
+
+ override val progress: Float
+ get() {
+ val offset = if (isAnimatingOffset) offsetAnimatable.value else dragOffset
+ return offset / distance
+ }
+
+ override val isInitiatedByUserInput = true
+
+ /** The current offset caused by the drag gesture. */
+ var dragOffset by mutableFloatStateOf(0f)
+
+ /**
+ * Whether the offset is animated (the user lifted their finger) or if it is driven by gesture.
+ */
+ var isAnimatingOffset by mutableStateOf(false)
+
+ // If we are not animating offset, it means the offset is being driven by the user's finger.
+ override val isUserInputOngoing: Boolean
+ get() = !isAnimatingOffset
+
+ /** The animatable used to animate the offset once the user lifted its finger. */
+ val offsetAnimatable = Animatable(0f, OffsetVisibilityThreshold)
+
+ /** Job to check that there is at most one offset animation in progress. */
+ private var offsetAnimationJob: Job? = null
+
+ /** The spec to use when animating this transition to either [fromScene] or [toScene]. */
+ lateinit var swipeSpec: SpringSpec<Float>
+
+ /** Ends any previous [offsetAnimationJob] and runs the new [job]. */
+ private fun startOffsetAnimation(job: () -> Job) {
+ cancelOffsetAnimation()
+ offsetAnimationJob = job()
+ }
+
+ /** Cancel any ongoing offset animation. */
+ // TODO(b/317063114) This should be a suspended function to avoid multiple jobs running at
+ // the same time.
+ fun cancelOffsetAnimation() {
+ offsetAnimationJob?.cancel()
+ finishOffsetAnimation()
+ }
+
+ fun finishOffsetAnimation() {
+ if (isAnimatingOffset) {
+ isAnimatingOffset = false
+ dragOffset = offsetAnimatable.value
}
}
- /** The [Swipe] associated to a given fromScene, startedPosition and pointersDown. */
- private class Swipes(
- val upOrLeft: Swipe?,
- val downOrRight: Swipe?,
- val upOrLeftNoSource: Swipe?,
- val downOrRightNoSource: Swipe?,
- )
+ fun animateOffset(
+ // TODO(b/317063114) The CoroutineScope should be removed.
+ coroutineScope: CoroutineScope,
+ initialVelocity: Float,
+ targetOffset: Float,
+ onAnimationCompleted: () -> Unit,
+ ) {
+ startOffsetAnimation {
+ coroutineScope.launch {
+ animateOffset(targetOffset, initialVelocity)
+ onAnimationCompleted()
+ }
+ }
+ }
+
+ private suspend fun animateOffset(targetOffset: Float, initialVelocity: Float) {
+ if (!isAnimatingOffset) {
+ offsetAnimatable.snapTo(dragOffset)
+ }
+ isAnimatingOffset = true
+
+ offsetAnimatable.animateTo(
+ targetValue = targetOffset,
+ animationSpec = swipeSpec,
+ initialVelocity = initialVelocity,
+ )
+
+ finishOffsetAnimation()
+ }
+}
+
+private object DefaultSwipeDistance : UserActionDistance {
+ override fun Density.absoluteDistance(
+ fromSceneSize: IntSize,
+ orientation: Orientation,
+ ): Float {
+ return when (orientation) {
+ Orientation.Horizontal -> fromSceneSize.width
+ Orientation.Vertical -> fromSceneSize.height
+ }.toFloat()
+ }
+}
+
+/** The [Swipe] associated to a given fromScene, startedPosition and pointersDown. */
+private class Swipes(
+ val upOrLeft: Swipe?,
+ val downOrRight: Swipe?,
+ val upOrLeftNoSource: Swipe?,
+ val downOrRightNoSource: Swipe?,
+) {
+ /** The [UserActionResult] associated to up and down swipes. */
+ var upOrLeftResult: UserActionResult? = null
+ var downOrRightResult: UserActionResult? = null
+
+ fun computeSwipesResults(fromScene: Scene): Pair<UserActionResult?, UserActionResult?> {
+ val userActions = fromScene.userActions
+ fun result(swipe: Swipe?): UserActionResult? {
+ return userActions[swipe ?: return null]
+ }
+
+ val upOrLeftResult = result(upOrLeft) ?: result(upOrLeftNoSource)
+ val downOrRightResult = result(downOrRight) ?: result(downOrRightNoSource)
+ return upOrLeftResult to downOrRightResult
+ }
+
+ fun updateSwipesResults(fromScene: Scene) {
+ val (upOrLeftResult, downOrRightResult) = computeSwipesResults(fromScene)
+
+ this.upOrLeftResult = upOrLeftResult
+ this.downOrRightResult = downOrRightResult
+ }
+
+ /**
+ * Returns the [UserActionResult] from [fromScene] in the direction of [directionOffset].
+ *
+ * @param fromScene the scene from which we look for the target
+ * @param directionOffset signed float that indicates the direction. Positive is down or right
+ * negative is up or left.
+ * @param updateSwipesResults whether the target scenes should be updated to the current values
+ * held in the Scenes map. Usually we don't want to update them while doing a drag, because
+ * this could change the target scene (jump cutting) to a different scene, when some system
+ * state changed the targets the background. However, an update is needed any time we
+ * calculate the targets for a new fromScene.
+ * @return null when there are no targets in either direction. If one direction is null and you
+ * drag into the null direction this function will return the opposite direction, assuming
+ * that the users intention is to start the drag into the other direction eventually. If
+ * [directionOffset] is 0f and both direction are available, it will default to
+ * [upOrLeftResult].
+ */
+ fun findUserActionResult(
+ fromScene: Scene,
+ directionOffset: Float,
+ updateSwipesResults: Boolean,
+ ): UserActionResult? {
+ if (updateSwipesResults) {
+ updateSwipesResults(fromScene)
+ }
+
+ return when {
+ upOrLeftResult == null && downOrRightResult == null -> null
+ (directionOffset < 0f && upOrLeftResult != null) || downOrRightResult == null ->
+ upOrLeftResult
+ else -> downOrRightResult
+ }
+ }
+
+ /**
+ * A strict version of [findUserActionResult] that will return null when there is no Scene in
+ * [directionOffset] direction
+ */
+ fun findUserActionResultStrict(directionOffset: Float): UserActionResult? {
+ return when {
+ directionOffset > 0f -> upOrLeftResult
+ directionOffset < 0f -> downOrRightResult
+ else -> null
+ }
+ }
}
private class SceneDraggableHandler(
@@ -701,27 +732,19 @@
gestureHandler.shouldImmediatelyIntercept(startedPosition = null)
if (!canInterceptSwipeTransition) return@PriorityNestedScrollConnection false
- val swipeTransition = gestureHandler.swipeTransition
- val progress = swipeTransition.progress
val threshold = layoutImpl.transitionInterceptionThreshold
- fun isProgressCloseTo(value: Float) = (progress - value).absoluteValue <= threshold
-
- // The transition is always between 0 and 1. If it is close to either of these
- // intervals, we want to go directly to the TransitionState.Idle.
- // The progress value can go beyond this range in the case of overscroll.
- val shouldSnapToIdle = isProgressCloseTo(0f) || isProgressCloseTo(1f)
- if (shouldSnapToIdle) {
- swipeTransition.cancelOffsetAnimation()
- layoutState.finishTransition(swipeTransition, swipeTransition.currentScene)
+ val hasSnappedToIdle = layoutState.snapToIdleIfClose(threshold)
+ if (hasSnappedToIdle) {
+ // If the current swipe transition is closed to 0f or 1f, then we want to
+ // interrupt the transition (snapping it to Idle) and scroll the list.
+ return@PriorityNestedScrollConnection false
}
- // Start only if we cannot consume this event
- val canStart = !shouldSnapToIdle
- if (canStart) {
- isIntercepting = true
- }
-
- canStart
+ // If the current swipe transition is *not* closed to 0f or 1f, then we want the
+ // scroll events to intercept the current transition to continue the scene
+ // transition.
+ isIntercepting = true
+ true
},
canStartPostScroll = { offsetAvailable, offsetBeforeStart ->
val behavior: NestedScrollBehavior =
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt
index d904c8b..e1f8a09 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt
@@ -332,7 +332,11 @@
@Stable @ElementDsl interface MovableElementContentScope : BaseSceneScope, ElementBoxScope
/** An action performed by the user. */
-sealed interface UserAction
+sealed interface UserAction {
+ infix fun to(scene: SceneKey): Pair<UserAction, UserActionResult> {
+ return this to UserActionResult(toScene = scene)
+ }
+}
/** The user navigated back, either using a gesture or by triggering a KEYCODE_BACK event. */
data object Back : UserAction
@@ -385,65 +389,26 @@
): SwipeSource?
}
-/**
- * The result of performing a [UserAction].
- *
- * Note: [UserActionResult] is implemented by [SceneKey], so you can also use scene keys directly
- * when defining your [UserActionResult]s.
- *
- * ```
- * SceneTransitionLayout(...) {
- * scene(
- * Scenes.Foo,
- * userActions =
- * mapOf(
- * Swipe.Right to Scene.Bar,
- * Swipe.Down to Scene.Doe,
- * )
- * )
- * ) { ... }
- * }
- * ```
- */
-interface UserActionResult {
+/** The result of performing a [UserAction]. */
+class UserActionResult(
/** The scene we should be transitioning to during the [UserAction]. */
- val toScene: SceneKey
-
- /** The key of the transition that should be used. */
- val transitionKey: TransitionKey?
+ val toScene: SceneKey,
/**
* The distance the action takes to animate from 0% to 100%.
*
* If `null`, a default distance will be used that depends on the [UserAction] performed.
*/
- val distance: UserActionDistance?
-}
+ val distance: UserActionDistance? = null,
-/** Create a [UserActionResult] to [toScene] with the given [distance] and [transitionKey]. */
-fun UserActionResult(
- toScene: SceneKey,
- distance: UserActionDistance? = null,
- transitionKey: TransitionKey? = null,
-): UserActionResult {
- return object : UserActionResult {
- override val toScene: SceneKey = toScene
- override val transitionKey: TransitionKey? = transitionKey
- override val distance: UserActionDistance? = distance
- }
-}
-
-/** Create a [UserActionResult] to [toScene] with the given fixed [distance] and [transitionKey]. */
-fun UserActionResult(
- toScene: SceneKey,
- distance: Dp,
- transitionKey: TransitionKey? = null,
-): UserActionResult {
- return UserActionResult(
- toScene = toScene,
- distance = FixedDistance(distance),
- transitionKey = transitionKey,
- )
+ /** The key of the transition that should be used. */
+ val transitionKey: TransitionKey? = null,
+) {
+ constructor(
+ toScene: SceneKey,
+ distance: Dp,
+ transitionKey: TransitionKey? = null,
+ ) : this(toScene, FixedDistance(distance), transitionKey)
}
interface UserActionDistance {
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt
index aee6f9e..a8da551 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt
@@ -24,6 +24,11 @@
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
+import androidx.compose.ui.util.fastFilter
+import androidx.compose.ui.util.fastForEach
+import com.android.compose.animation.scene.transition.link.LinkedTransition
+import com.android.compose.animation.scene.transition.link.StateLink
+import kotlin.math.absoluteValue
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.channels.Channel
@@ -100,8 +105,9 @@
fun MutableSceneTransitionLayoutState(
initialScene: SceneKey,
transitions: SceneTransitions = SceneTransitions.Empty,
+ stateLinks: List<StateLink> = emptyList(),
): MutableSceneTransitionLayoutState {
- return MutableSceneTransitionLayoutStateImpl(initialScene, transitions)
+ return MutableSceneTransitionLayoutStateImpl(initialScene, transitions, stateLinks)
}
/**
@@ -120,9 +126,12 @@
currentScene: SceneKey,
onChangeScene: (SceneKey) -> Unit,
transitions: SceneTransitions = SceneTransitions.Empty,
+ stateLinks: List<StateLink> = emptyList(),
): SceneTransitionLayoutState {
- return remember { HoistedSceneTransitionLayoutScene(currentScene, transitions, onChangeScene) }
- .apply { update(currentScene, onChangeScene, transitions) }
+ return remember {
+ HoistedSceneTransitionLayoutScene(currentScene, transitions, onChangeScene, stateLinks)
+ }
+ .apply { update(currentScene, onChangeScene, transitions, stateLinks) }
}
@Stable
@@ -183,8 +192,10 @@
}
}
-internal abstract class BaseSceneTransitionLayoutState(initialScene: SceneKey) :
- SceneTransitionLayoutState {
+internal abstract class BaseSceneTransitionLayoutState(
+ initialScene: SceneKey,
+ protected var stateLinks: List<StateLink>,
+) : SceneTransitionLayoutState {
override var transitionState: TransitionState by
mutableStateOf(TransitionState.Idle(initialScene))
protected set
@@ -195,6 +206,8 @@
*/
internal var transformationSpec: TransformationSpecImpl = TransformationSpec.Empty
+ private val activeTransitionLinks = mutableMapOf<StateLink, LinkedTransition>()
+
/**
* Called when the [current scene][TransitionState.currentScene] should be changed to [scene].
*
@@ -223,19 +236,94 @@
transitions
.transitionSpec(transition.fromScene, transition.toScene, key = transitionKey)
.transformationSpec()
-
+ cancelActiveTransitionLinks()
+ setupTransitionLinks(transition)
transitionState = transition
}
+ private fun cancelActiveTransitionLinks() {
+ for ((link, linkedTransition) in activeTransitionLinks) {
+ link.target.finishTransition(linkedTransition, linkedTransition.currentScene)
+ }
+ activeTransitionLinks.clear()
+ }
+
+ private fun setupTransitionLinks(transitionState: TransitionState) {
+ if (transitionState !is TransitionState.Transition) return
+ stateLinks.fastForEach { stateLink ->
+ val matchingLinks =
+ stateLink.transitionLinks.fastFilter { it.isMatchingLink(transitionState) }
+ if (matchingLinks.isEmpty()) return@fastForEach
+ if (matchingLinks.size > 1) error("More than one link matched.")
+
+ val targetCurrentScene = stateLink.target.transitionState.currentScene
+ val matchingLink = matchingLinks[0]
+
+ if (!matchingLink.targetIsInValidState(targetCurrentScene)) return@fastForEach
+
+ val linkedTransition =
+ LinkedTransition(
+ originalTransition = transitionState,
+ fromScene = targetCurrentScene,
+ toScene = matchingLink.targetTo,
+ )
+
+ stateLink.target.startTransition(linkedTransition, matchingLink.targetTransitionKey)
+ activeTransitionLinks[stateLink] = linkedTransition
+ }
+ }
+
/**
* Notify that [transition] was finished and that we should settle to [idleScene]. This will do
* nothing if [transition] was interrupted since it was started.
*/
internal fun finishTransition(transition: TransitionState.Transition, idleScene: SceneKey) {
+ resolveActiveTransitionLinks(idleScene)
if (transitionState == transition) {
transitionState = TransitionState.Idle(idleScene)
}
}
+
+ private fun resolveActiveTransitionLinks(idleScene: SceneKey) {
+ val previousTransition = this.transitionState as? TransitionState.Transition ?: return
+ for ((link, linkedTransition) in activeTransitionLinks) {
+ if (previousTransition.fromScene == idleScene) {
+ // The transition ended by arriving at the fromScene, move link to Idle(fromScene).
+ link.target.finishTransition(linkedTransition, linkedTransition.fromScene)
+ } else if (previousTransition.toScene == idleScene) {
+ // The transition ended by arriving at the toScene, move link to Idle(toScene).
+ link.target.finishTransition(linkedTransition, linkedTransition.toScene)
+ } else {
+ // The transition was interrupted by something else, we reset to initial state.
+ link.target.finishTransition(linkedTransition, linkedTransition.fromScene)
+ }
+ }
+ activeTransitionLinks.clear()
+ }
+
+ /**
+ * Check if a transition is in progress. If the progress value is near 0 or 1, immediately snap
+ * to the closest scene.
+ *
+ * @return true if snapped to the closest scene.
+ */
+ internal fun snapToIdleIfClose(threshold: Float): Boolean {
+ val transition = currentTransition ?: return false
+ val progress = transition.progress
+ fun isProgressCloseTo(value: Float) = (progress - value).absoluteValue <= threshold
+
+ return when {
+ isProgressCloseTo(0f) -> {
+ finishTransition(transition, transition.fromScene)
+ true
+ }
+ isProgressCloseTo(1f) -> {
+ finishTransition(transition, transition.toScene)
+ true
+ }
+ else -> false
+ }
+ }
}
/**
@@ -246,7 +334,8 @@
initialScene: SceneKey,
override var transitions: SceneTransitions,
private var changeScene: (SceneKey) -> Unit,
-) : BaseSceneTransitionLayoutState(initialScene) {
+ stateLinks: List<StateLink> = emptyList(),
+) : BaseSceneTransitionLayoutState(initialScene, stateLinks) {
private val targetSceneChannel = Channel<SceneKey>(Channel.CONFLATED)
override fun CoroutineScope.onChangeScene(scene: SceneKey) = changeScene(scene)
@@ -256,10 +345,12 @@
currentScene: SceneKey,
onChangeScene: (SceneKey) -> Unit,
transitions: SceneTransitions,
+ stateLinks: List<StateLink>,
) {
SideEffect {
this.changeScene = onChangeScene
this.transitions = transitions
+ this.stateLinks = stateLinks
targetSceneChannel.trySend(currentScene)
}
@@ -283,7 +374,8 @@
internal class MutableSceneTransitionLayoutStateImpl(
initialScene: SceneKey,
override var transitions: SceneTransitions,
-) : MutableSceneTransitionLayoutState, BaseSceneTransitionLayoutState(initialScene) {
+ stateLinks: List<StateLink> = emptyList(),
+) : MutableSceneTransitionLayoutState, BaseSceneTransitionLayoutState(initialScene, stateLinks) {
override fun setTargetScene(
targetScene: SceneKey,
coroutineScope: CoroutineScope,
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Transformation.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Transformation.kt
index 04254fb..603f7ba 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Transformation.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Transformation.kt
@@ -16,6 +16,9 @@
package com.android.compose.animation.scene.transformation
+import androidx.compose.ui.util.fastCoerceAtLeast
+import androidx.compose.ui.util.fastCoerceAtMost
+import androidx.compose.ui.util.fastCoerceIn
import com.android.compose.animation.scene.Element
import com.android.compose.animation.scene.ElementMatcher
import com.android.compose.animation.scene.Scene
@@ -106,10 +109,10 @@
fun progress(transitionProgress: Float): Float {
return when {
start.isSpecified() && end.isSpecified() ->
- ((transitionProgress - start) / (end - start)).coerceIn(0f, 1f)
+ ((transitionProgress - start) / (end - start)).fastCoerceIn(0f, 1f)
!start.isSpecified() && !end.isSpecified() -> transitionProgress
- end.isSpecified() -> (transitionProgress / end).coerceAtMost(1f)
- else -> ((transitionProgress - start) / (1f - start)).coerceAtLeast(0f)
+ end.isSpecified() -> (transitionProgress / end).fastCoerceAtMost(1f)
+ else -> ((transitionProgress - start) / (1f - start)).fastCoerceAtLeast(0f)
}
}
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transition/link/LinkedTransition.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transition/link/LinkedTransition.kt
new file mode 100644
index 0000000..33b57b2
--- /dev/null
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transition/link/LinkedTransition.kt
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2024 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.compose.animation.scene.transition.link
+
+import com.android.compose.animation.scene.SceneKey
+import com.android.compose.animation.scene.TransitionState
+
+/** A linked transition which is driven by a [originalTransition]. */
+internal class LinkedTransition(
+ private val originalTransition: TransitionState.Transition,
+ fromScene: SceneKey,
+ toScene: SceneKey,
+) : TransitionState.Transition(fromScene, toScene) {
+
+ override val currentScene: SceneKey
+ get() {
+ return when (originalTransition.currentScene) {
+ originalTransition.fromScene -> fromScene
+ originalTransition.toScene -> toScene
+ else -> error("Original currentScene is neither FromScene nor ToScene")
+ }
+ }
+
+ override val isInitiatedByUserInput: Boolean
+ get() = originalTransition.isInitiatedByUserInput
+
+ override val isUserInputOngoing: Boolean
+ get() = originalTransition.isUserInputOngoing
+
+ override val progress: Float
+ get() = originalTransition.progress
+}
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transition/link/StateLink.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transition/link/StateLink.kt
new file mode 100644
index 0000000..6c29946
--- /dev/null
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transition/link/StateLink.kt
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2024 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.compose.animation.scene.transition.link
+
+import com.android.compose.animation.scene.BaseSceneTransitionLayoutState
+import com.android.compose.animation.scene.SceneKey
+import com.android.compose.animation.scene.SceneTransitionLayoutState
+import com.android.compose.animation.scene.TransitionKey
+import com.android.compose.animation.scene.TransitionState
+
+/** A link between a source (implicit) and [target] `SceneTransitionLayoutState`. */
+class StateLink(target: SceneTransitionLayoutState, val transitionLinks: List<TransitionLink>) {
+
+ internal val target = target as BaseSceneTransitionLayoutState
+
+ /**
+ * Links two transitions (source and target) together.
+ *
+ * `null` can be passed to indicate that any SceneKey should match. e.g. passing `null`, `null`,
+ * `null`, `SceneA` means that any transition at the source will trigger a transition in the
+ * target to `SceneA` from any current scene.
+ */
+ class TransitionLink(
+ val sourceFrom: SceneKey?,
+ val sourceTo: SceneKey?,
+ val targetFrom: SceneKey?,
+ val targetTo: SceneKey,
+ val targetTransitionKey: TransitionKey? = null,
+ ) {
+ init {
+ if (
+ (sourceFrom != null && sourceFrom == sourceTo) ||
+ (targetFrom != null && targetFrom == targetTo)
+ )
+ error("From and To can't be the same")
+ }
+
+ internal fun isMatchingLink(transition: TransitionState.Transition): Boolean {
+ return (sourceFrom == null || sourceFrom == transition.fromScene) &&
+ (sourceTo == null || sourceTo == transition.toScene)
+ }
+
+ internal fun targetIsInValidState(targetCurrentScene: SceneKey): Boolean {
+ return (targetFrom == null || targetFrom == targetCurrentScene) &&
+ targetTo != targetCurrentScene
+ }
+ }
+}
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/ui/util/MathHelpers.kt b/packages/SystemUI/compose/scene/src/com/android/compose/ui/util/MathHelpers.kt
index 13747b7..e78ab29 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/ui/util/MathHelpers.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/ui/util/MathHelpers.kt
@@ -20,24 +20,8 @@
import androidx.compose.ui.geometry.isSpecified
import androidx.compose.ui.geometry.lerp
import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.util.lerp
import com.android.compose.animation.scene.Scale
-import kotlin.math.roundToInt
-import kotlin.math.roundToLong
-
-/** Linearly interpolate between [start] and [stop] with [fraction] fraction between them. */
-fun lerp(start: Float, stop: Float, fraction: Float): Float {
- return (1 - fraction) * start + fraction * stop
-}
-
-/** Linearly interpolate between [start] and [stop] with [fraction] fraction between them. */
-fun lerp(start: Int, stop: Int, fraction: Float): Int {
- return start + ((stop - start) * fraction.toDouble()).roundToInt()
-}
-
-/** Linearly interpolate between [start] and [stop] with [fraction] fraction between them. */
-fun lerp(start: Long, stop: Long, fraction: Float): Long {
- return start + ((stop - start) * fraction.toDouble()).roundToLong()
-}
/** Linearly interpolate between [start] and [stop] with [fraction] fraction between them. */
fun lerp(start: IntSize, stop: IntSize, fraction: Float): IntSize {
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/AnimatedSharedAsStateTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/AnimatedSharedAsStateTest.kt
index a116501..e8854cf 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/AnimatedSharedAsStateTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/AnimatedSharedAsStateTest.kt
@@ -30,8 +30,8 @@
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.lerp
+import androidx.compose.ui.util.lerp
import androidx.test.ext.junit.runners.AndroidJUnit4
-import com.android.compose.ui.util.lerp
import com.google.common.truth.Truth.assertThat
import org.junit.Assert.assertThrows
import org.junit.Rule
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/MultiPointerDraggableTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/MultiPointerDraggableTest.kt
new file mode 100644
index 0000000..cd99d05
--- /dev/null
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/MultiPointerDraggableTest.kt
@@ -0,0 +1,104 @@
+/*
+ * Copyright (C) 2024 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.compose.animation.scene
+
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.size
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.geometry.Size
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.platform.LocalViewConfiguration
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onRoot
+import androidx.compose.ui.test.performTouchInput
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.google.common.truth.Truth.assertThat
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class MultiPointerDraggableTest {
+ @get:Rule val rule = createComposeRule()
+
+ @Test
+ fun cancellingPointerCallsOnDragStopped() {
+ val size = 200f
+ val middle = Offset(size / 2f, size / 2f)
+
+ var enabled by mutableStateOf(false)
+ var started = false
+ var dragged = false
+ var stopped = false
+
+ var touchSlop = 0f
+ rule.setContent {
+ touchSlop = LocalViewConfiguration.current.touchSlop
+ Box(
+ Modifier.size(with(LocalDensity.current) { Size(size, size).toDpSize() })
+ .multiPointerDraggable(
+ orientation = Orientation.Vertical,
+ enabled = { enabled },
+ startDragImmediately = { false },
+ onDragStarted = { _, _, _ -> started = true },
+ onDragDelta = { _ -> dragged = true },
+ onDragStopped = { stopped = true },
+ )
+ )
+ }
+
+ fun startDraggingDown() {
+ rule.onRoot().performTouchInput {
+ down(middle)
+ moveBy(Offset(0f, touchSlop))
+ }
+ }
+
+ fun releaseFinger() {
+ rule.onRoot().performTouchInput { up() }
+ }
+
+ // Swiping down does nothing because enabled is false.
+ startDraggingDown()
+ assertThat(started).isFalse()
+ assertThat(dragged).isFalse()
+ assertThat(stopped).isFalse()
+ releaseFinger()
+
+ // Enable the draggable and swipe down. This should both call onDragStarted() and
+ // onDragDelta().
+ enabled = true
+ rule.waitForIdle()
+ startDraggingDown()
+ assertThat(started).isTrue()
+ assertThat(dragged).isTrue()
+ assertThat(stopped).isFalse()
+
+ // Disable the pointer input. This should call onDragStopped() even if didn't release the
+ // finger yet.
+ enabled = false
+ rule.waitForIdle()
+ assertThat(started).isTrue()
+ assertThat(dragged).isTrue()
+ assertThat(stopped).isTrue()
+ }
+}
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneGestureHandlerTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneGestureHandlerTest.kt
index 2dc94a4..c91d298 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneGestureHandlerTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneGestureHandlerTest.kt
@@ -54,12 +54,8 @@
private val layoutState =
MutableSceneTransitionLayoutStateImpl(SceneA, EmptyTestTransitions)
- val mutableUserActionsA: MutableMap<UserAction, SceneKey> =
- mutableMapOf(Swipe.Up to SceneB, Swipe.Down to SceneC)
-
- val mutableUserActionsB: MutableMap<UserAction, SceneKey> =
- mutableMapOf(Swipe.Up to SceneC, Swipe.Down to SceneA)
-
+ val mutableUserActionsA = mutableMapOf(Swipe.Up to SceneB, Swipe.Down to SceneC)
+ val mutableUserActionsB = mutableMapOf(Swipe.Up to SceneC, Swipe.Down to SceneA)
private val scenesBuilder: SceneTransitionLayoutScope.() -> Unit = {
scene(
key = SceneA,
@@ -131,6 +127,9 @@
val progress: Float
get() = (transitionState as Transition).progress
+ val isUserInputOngoing: Boolean
+ get() = (transitionState as Transition).isUserInputOngoing
+
fun advanceUntilIdle() {
testScope.testScheduler.advanceUntilIdle()
}
@@ -507,7 +506,7 @@
onDragStarted(overSlop = up(fractionOfScreen = 0.1f))
assertTransition(fromScene = SceneA, toScene = SceneB, progress = 0.1f)
- mutableUserActionsA[Swipe.Up] = SceneC
+ mutableUserActionsA[Swipe.Up] = UserActionResult(SceneC)
onDelta(pixels = up(fractionOfScreen = 0.1f))
// target stays B even though UserActions changed
assertTransition(fromScene = SceneA, toScene = SceneB, progress = 0.2f)
@@ -524,7 +523,7 @@
onDragStarted(overSlop = up(fractionOfScreen = 0.1f))
assertTransition(fromScene = SceneA, toScene = SceneB, progress = 0.1f)
- mutableUserActionsA[Swipe.Up] = SceneC
+ mutableUserActionsA[Swipe.Up] = UserActionResult(SceneC)
onDelta(pixels = up(fractionOfScreen = 0.1f))
onDragStopped(velocity = down(fractionOfScreen = 0.1f))
@@ -542,12 +541,11 @@
onDragStopped(velocity = velocityThreshold)
assertTransition(currentScene = SceneC)
- assertThat(sceneGestureHandler.isDrivingTransition).isTrue()
- assertThat(sceneGestureHandler.swipeTransition.isAnimatingOffset).isTrue()
+ assertThat(isUserInputOngoing).isFalse()
// Start a new gesture while the offset is animating
onDragStartedImmediately()
- assertThat(sceneGestureHandler.swipeTransition.isAnimatingOffset).isFalse()
+ assertThat(isUserInputOngoing).isTrue()
}
@Test
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutStateTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutStateTest.kt
index c61917d..f81a7f2 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutStateTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutStateTest.kt
@@ -18,10 +18,14 @@
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.compose.animation.scene.TestScenes.SceneA
+import com.android.compose.animation.scene.TestScenes.SceneB
+import com.android.compose.animation.scene.TestScenes.SceneC
+import com.android.compose.animation.scene.TestScenes.SceneD
+import com.android.compose.animation.scene.transition.link.StateLink
import com.android.compose.test.runMonotonicClockTest
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.CoroutineStart
-import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import org.junit.Rule
import org.junit.Test
@@ -31,93 +35,241 @@
class SceneTransitionLayoutStateTest {
@get:Rule val rule = createComposeRule()
+ class TestableTransition(
+ fromScene: SceneKey,
+ toScene: SceneKey,
+ ) : TransitionState.Transition(fromScene, toScene) {
+ override var currentScene: SceneKey = fromScene
+ override var progress: Float = 0.0f
+ override var isInitiatedByUserInput: Boolean = false
+ override var isUserInputOngoing: Boolean = false
+ }
+
@Test
fun isTransitioningTo_idle() {
- val state = MutableSceneTransitionLayoutStateImpl(TestScenes.SceneA, SceneTransitions.Empty)
+ val state = MutableSceneTransitionLayoutStateImpl(SceneA, SceneTransitions.Empty)
assertThat(state.isTransitioning()).isFalse()
- assertThat(state.isTransitioning(from = TestScenes.SceneA)).isFalse()
- assertThat(state.isTransitioning(to = TestScenes.SceneB)).isFalse()
- assertThat(state.isTransitioning(from = TestScenes.SceneA, to = TestScenes.SceneB))
- .isFalse()
+ assertThat(state.isTransitioning(from = SceneA)).isFalse()
+ assertThat(state.isTransitioning(to = SceneB)).isFalse()
+ assertThat(state.isTransitioning(from = SceneA, to = SceneB)).isFalse()
}
@Test
fun isTransitioningTo_transition() {
- val state = MutableSceneTransitionLayoutStateImpl(TestScenes.SceneA, SceneTransitions.Empty)
- state.startTransition(
- transition(from = TestScenes.SceneA, to = TestScenes.SceneB),
- transitionKey = null
- )
+ val state = MutableSceneTransitionLayoutStateImpl(SceneA, SceneTransitions.Empty)
+ state.startTransition(transition(from = SceneA, to = SceneB), transitionKey = null)
assertThat(state.isTransitioning()).isTrue()
- assertThat(state.isTransitioning(from = TestScenes.SceneA)).isTrue()
- assertThat(state.isTransitioning(from = TestScenes.SceneB)).isFalse()
- assertThat(state.isTransitioning(to = TestScenes.SceneB)).isTrue()
- assertThat(state.isTransitioning(to = TestScenes.SceneA)).isFalse()
- assertThat(state.isTransitioning(from = TestScenes.SceneA, to = TestScenes.SceneB)).isTrue()
+ assertThat(state.isTransitioning(from = SceneA)).isTrue()
+ assertThat(state.isTransitioning(from = SceneB)).isFalse()
+ assertThat(state.isTransitioning(to = SceneB)).isTrue()
+ assertThat(state.isTransitioning(to = SceneA)).isFalse()
+ assertThat(state.isTransitioning(from = SceneA, to = SceneB)).isTrue()
}
@Test
fun setTargetScene_idleToSameScene() = runMonotonicClockTest {
- val state = MutableSceneTransitionLayoutState(TestScenes.SceneA)
- assertThat(state.setTargetScene(TestScenes.SceneA, coroutineScope = this)).isNull()
+ val state = MutableSceneTransitionLayoutState(SceneA)
+ assertThat(state.setTargetScene(SceneA, coroutineScope = this)).isNull()
}
@Test
fun setTargetScene_idleToDifferentScene() = runMonotonicClockTest {
- val state = MutableSceneTransitionLayoutState(TestScenes.SceneA)
- val transition = state.setTargetScene(TestScenes.SceneB, coroutineScope = this)
+ val state = MutableSceneTransitionLayoutState(SceneA)
+ val transition = state.setTargetScene(SceneB, coroutineScope = this)
assertThat(transition).isNotNull()
assertThat(state.transitionState).isEqualTo(transition)
testScheduler.advanceUntilIdle()
- assertThat(state.transitionState).isEqualTo(TransitionState.Idle(TestScenes.SceneB))
+ assertThat(state.transitionState).isEqualTo(TransitionState.Idle(SceneB))
}
@Test
fun setTargetScene_transitionToSameScene() = runMonotonicClockTest {
- val state = MutableSceneTransitionLayoutState(TestScenes.SceneA)
- assertThat(state.setTargetScene(TestScenes.SceneB, coroutineScope = this)).isNotNull()
- assertThat(state.setTargetScene(TestScenes.SceneB, coroutineScope = this)).isNull()
+ val state = MutableSceneTransitionLayoutState(SceneA)
+ assertThat(state.setTargetScene(SceneB, coroutineScope = this)).isNotNull()
+ assertThat(state.setTargetScene(SceneB, coroutineScope = this)).isNull()
testScheduler.advanceUntilIdle()
- assertThat(state.transitionState).isEqualTo(TransitionState.Idle(TestScenes.SceneB))
+ assertThat(state.transitionState).isEqualTo(TransitionState.Idle(SceneB))
}
@Test
fun setTargetScene_transitionToDifferentScene() = runMonotonicClockTest {
- val state = MutableSceneTransitionLayoutState(TestScenes.SceneA)
- assertThat(state.setTargetScene(TestScenes.SceneB, coroutineScope = this)).isNotNull()
- assertThat(state.setTargetScene(TestScenes.SceneC, coroutineScope = this)).isNotNull()
+ val state = MutableSceneTransitionLayoutState(SceneA)
+ assertThat(state.setTargetScene(SceneB, coroutineScope = this)).isNotNull()
+ assertThat(state.setTargetScene(SceneC, coroutineScope = this)).isNotNull()
testScheduler.advanceUntilIdle()
- assertThat(state.transitionState).isEqualTo(TransitionState.Idle(TestScenes.SceneC))
+ assertThat(state.transitionState).isEqualTo(TransitionState.Idle(SceneC))
}
@Test
fun setTargetScene_transitionToOriginalScene() = runMonotonicClockTest {
- val state = MutableSceneTransitionLayoutState(TestScenes.SceneA)
- assertThat(state.setTargetScene(TestScenes.SceneB, coroutineScope = this)).isNotNull()
+ val state = MutableSceneTransitionLayoutState(SceneA)
+ assertThat(state.setTargetScene(SceneB, coroutineScope = this)).isNotNull()
// Progress is 0f, so we don't animate at all and directly snap back to A.
- assertThat(state.setTargetScene(TestScenes.SceneA, coroutineScope = this)).isNull()
- assertThat(state.transitionState).isEqualTo(TransitionState.Idle(TestScenes.SceneA))
+ assertThat(state.setTargetScene(SceneA, coroutineScope = this)).isNull()
+ assertThat(state.transitionState).isEqualTo(TransitionState.Idle(SceneA))
}
@Test
fun setTargetScene_coroutineScopeCancelled() = runMonotonicClockTest {
- val state = MutableSceneTransitionLayoutState(TestScenes.SceneA)
+ val state = MutableSceneTransitionLayoutState(SceneA)
lateinit var transition: TransitionState.Transition
val job =
launch(start = CoroutineStart.UNDISPATCHED) {
- transition = state.setTargetScene(TestScenes.SceneB, coroutineScope = this)!!
+ transition = state.setTargetScene(SceneB, coroutineScope = this)!!
}
assertThat(state.transitionState).isEqualTo(transition)
// Cancelling the scope/job still sets the state to Idle(targetScene).
job.cancel()
testScheduler.advanceUntilIdle()
- assertThat(state.transitionState).isEqualTo(TransitionState.Idle(TestScenes.SceneB))
+ assertThat(state.transitionState).isEqualTo(TransitionState.Idle(SceneB))
+ }
+
+ private fun setupLinkedStates(
+ parentInitialScene: SceneKey = SceneC,
+ childInitialScene: SceneKey = SceneA,
+ sourceFrom: SceneKey? = SceneA,
+ sourceTo: SceneKey? = SceneB,
+ targetFrom: SceneKey? = SceneC,
+ targetTo: SceneKey = SceneD
+ ): Pair<BaseSceneTransitionLayoutState, BaseSceneTransitionLayoutState> {
+ val parentState = MutableSceneTransitionLayoutState(parentInitialScene)
+ val link =
+ listOf(
+ StateLink(
+ parentState,
+ listOf(StateLink.TransitionLink(sourceFrom, sourceTo, targetFrom, targetTo))
+ )
+ )
+ val childState = MutableSceneTransitionLayoutState(childInitialScene, stateLinks = link)
+ return Pair(
+ parentState as BaseSceneTransitionLayoutState,
+ childState as BaseSceneTransitionLayoutState
+ )
+ }
+
+ @Test
+ fun linkedTransition_startsLinkAndFinishesLinkInToState() {
+ val (parentState, childState) = setupLinkedStates()
+
+ val childTransition = TestableTransition(SceneA, SceneB)
+
+ childState.startTransition(childTransition, null)
+ assertThat(childState.isTransitioning(SceneA, SceneB)).isTrue()
+ assertThat(parentState.isTransitioning(SceneC, SceneD)).isTrue()
+
+ childState.finishTransition(childTransition, SceneB)
+ assertThat(childState.transitionState).isEqualTo(TransitionState.Idle(SceneB))
+ assertThat(parentState.transitionState).isEqualTo(TransitionState.Idle(SceneD))
+ }
+
+ @Test
+ fun linkedTransition_transitiveLink() {
+ val parentParentState =
+ MutableSceneTransitionLayoutState(SceneB) as BaseSceneTransitionLayoutState
+ val parentLink =
+ listOf(
+ StateLink(
+ parentParentState,
+ listOf(StateLink.TransitionLink(SceneC, SceneD, SceneB, SceneC))
+ )
+ )
+ val parentState =
+ MutableSceneTransitionLayoutState(SceneC, stateLinks = parentLink)
+ as BaseSceneTransitionLayoutState
+ val link =
+ listOf(
+ StateLink(
+ parentState,
+ listOf(StateLink.TransitionLink(SceneA, SceneB, SceneC, SceneD))
+ )
+ )
+ val childState =
+ MutableSceneTransitionLayoutState(SceneA, stateLinks = link)
+ as BaseSceneTransitionLayoutState
+
+ val childTransition = TestableTransition(SceneA, SceneB)
+
+ childState.startTransition(childTransition, null)
+ assertThat(childState.isTransitioning(SceneA, SceneB)).isTrue()
+ assertThat(parentState.isTransitioning(SceneC, SceneD)).isTrue()
+ assertThat(parentParentState.isTransitioning(SceneB, SceneC)).isTrue()
+
+ childState.finishTransition(childTransition, SceneB)
+ assertThat(childState.transitionState).isEqualTo(TransitionState.Idle(SceneB))
+ assertThat(parentState.transitionState).isEqualTo(TransitionState.Idle(SceneD))
+ assertThat(parentParentState.transitionState).isEqualTo(TransitionState.Idle(SceneC))
+ }
+
+ @Test
+ fun linkedTransition_linkProgressIsEqual() {
+ val (parentState, childState) = setupLinkedStates()
+
+ val childTransition = TestableTransition(SceneA, SceneB)
+
+ childState.startTransition(childTransition, null)
+ assertThat(parentState.currentTransition?.progress).isEqualTo(0f)
+
+ childTransition.progress = .5f
+ assertThat(parentState.currentTransition?.progress).isEqualTo(.5f)
+ }
+
+ @Test
+ fun linkedTransition_reverseTransitionIsNotLinked() {
+ val (parentState, childState) = setupLinkedStates()
+
+ val childTransition = TestableTransition(SceneB, SceneA)
+
+ childState.startTransition(childTransition, null)
+ assertThat(childState.isTransitioning(SceneB, SceneA)).isTrue()
+ assertThat(parentState.transitionState).isEqualTo(TransitionState.Idle(SceneC))
+
+ childState.finishTransition(childTransition, SceneB)
+ assertThat(childState.transitionState).isEqualTo(TransitionState.Idle(SceneB))
+ assertThat(parentState.transitionState).isEqualTo(TransitionState.Idle(SceneC))
+ }
+
+ @Test
+ fun linkedTransition_startsLinkAndFinishesLinkInFromState() {
+ val (parentState, childState) = setupLinkedStates()
+
+ val childTransition = TestableTransition(SceneA, SceneB)
+ childState.startTransition(childTransition, null)
+
+ childState.finishTransition(childTransition, SceneA)
+ assertThat(childState.transitionState).isEqualTo(TransitionState.Idle(SceneA))
+ assertThat(parentState.transitionState).isEqualTo(TransitionState.Idle(SceneC))
+ }
+
+ @Test
+ fun linkedTransition_startsLinkAndFinishesLinkInUnknownState() {
+ val (parentState, childState) = setupLinkedStates()
+
+ val childTransition = TestableTransition(SceneA, SceneB)
+ childState.startTransition(childTransition, null)
+
+ childState.finishTransition(childTransition, SceneD)
+ assertThat(childState.transitionState).isEqualTo(TransitionState.Idle(SceneD))
+ assertThat(parentState.transitionState).isEqualTo(TransitionState.Idle(SceneC))
+ }
+
+ @Test
+ fun linkedTransition_startsLinkButLinkedStateIsTakenOver() {
+ val (parentState, childState) = setupLinkedStates()
+
+ val childTransition = TestableTransition(SceneA, SceneB)
+ val parentTransition = TestableTransition(SceneC, SceneA)
+ childState.startTransition(childTransition, null)
+ parentState.startTransition(parentTransition, null)
+
+ childState.finishTransition(childTransition, SceneB)
+ assertThat(childState.transitionState).isEqualTo(TransitionState.Idle(SceneB))
+ assertThat(parentState.transitionState).isEqualTo(parentTransition)
}
@Test
@@ -125,11 +277,11 @@
val transitionkey = TransitionKey(debugName = "foo")
val state =
MutableSceneTransitionLayoutState(
- TestScenes.SceneA,
+ SceneA,
transitions =
transitions {
- from(TestScenes.SceneA, to = TestScenes.SceneB) { fade(TestElements.Foo) }
- from(TestScenes.SceneA, to = TestScenes.SceneB, key = transitionkey) {
+ from(SceneA, to = SceneB) { fade(TestElements.Foo) }
+ from(SceneA, to = SceneB, key = transitionkey) {
fade(TestElements.Foo)
fade(TestElements.Bar)
}
@@ -138,19 +290,19 @@
as MutableSceneTransitionLayoutStateImpl
// Default transition from A to B.
- assertThat(state.setTargetScene(TestScenes.SceneB, coroutineScope = this)).isNotNull()
+ assertThat(state.setTargetScene(SceneB, coroutineScope = this)).isNotNull()
assertThat(state.transformationSpec.transformations).hasSize(1)
// Go back to A.
- state.setTargetScene(TestScenes.SceneA, coroutineScope = this)
+ state.setTargetScene(SceneA, coroutineScope = this)
testScheduler.advanceUntilIdle()
assertThat(state.currentTransition).isNull()
- assertThat(state.transitionState.currentScene).isEqualTo(TestScenes.SceneA)
+ assertThat(state.transitionState.currentScene).isEqualTo(SceneA)
// Specific transition from A to B.
assertThat(
state.setTargetScene(
- TestScenes.SceneB,
+ SceneB,
coroutineScope = this,
transitionKey = transitionkey,
)
@@ -158,4 +310,83 @@
.isNotNull()
assertThat(state.transformationSpec.transformations).hasSize(2)
}
+
+ @Test
+ fun snapToIdleIfClose_snapToStart() = runMonotonicClockTest {
+ val state = MutableSceneTransitionLayoutStateImpl(TestScenes.SceneA, SceneTransitions.Empty)
+ state.startTransition(
+ transition(from = TestScenes.SceneA, to = TestScenes.SceneB, progress = { 0.2f }),
+ transitionKey = null
+ )
+ assertThat(state.isTransitioning()).isTrue()
+
+ // Ignore the request if the progress is not close to 0 or 1, using the threshold.
+ assertThat(state.snapToIdleIfClose(threshold = 0.1f)).isFalse()
+ assertThat(state.isTransitioning()).isTrue()
+
+ // Go to the initial scene if it is close to 0.
+ assertThat(state.snapToIdleIfClose(threshold = 0.2f)).isTrue()
+ assertThat(state.isTransitioning()).isFalse()
+ assertThat(state.transitionState).isEqualTo(TransitionState.Idle(TestScenes.SceneA))
+ }
+
+ @Test
+ fun snapToIdleIfClose_snapToEnd() = runMonotonicClockTest {
+ val state = MutableSceneTransitionLayoutStateImpl(TestScenes.SceneA, SceneTransitions.Empty)
+ state.startTransition(
+ transition(from = TestScenes.SceneA, to = TestScenes.SceneB, progress = { 0.8f }),
+ transitionKey = null
+ )
+ assertThat(state.isTransitioning()).isTrue()
+
+ // Ignore the request if the progress is not close to 0 or 1, using the threshold.
+ assertThat(state.snapToIdleIfClose(threshold = 0.1f)).isFalse()
+ assertThat(state.isTransitioning()).isTrue()
+
+ // Go to the final scene if it is close to 1.
+ assertThat(state.snapToIdleIfClose(threshold = 0.2f)).isTrue()
+ assertThat(state.isTransitioning()).isFalse()
+ assertThat(state.transitionState).isEqualTo(TransitionState.Idle(TestScenes.SceneB))
+ }
+
+ @Test
+ fun linkedTransition_fuzzyLinksAreMatchedAndStarted() {
+ val (parentState, childState) = setupLinkedStates(SceneC, SceneA, null, null, null, SceneD)
+ val childTransition = TestableTransition(SceneA, SceneB)
+
+ childState.startTransition(childTransition, null)
+ assertThat(childState.isTransitioning(SceneA, SceneB)).isTrue()
+ assertThat(parentState.isTransitioning(SceneC, SceneD)).isTrue()
+
+ childState.finishTransition(childTransition, SceneB)
+ assertThat(childState.transitionState).isEqualTo(TransitionState.Idle(SceneB))
+ assertThat(parentState.transitionState).isEqualTo(TransitionState.Idle(SceneD))
+ }
+
+ @Test
+ fun linkedTransition_fuzzyLinksAreMatchedAndResetToProperPreviousScene() {
+ val (parentState, childState) =
+ setupLinkedStates(SceneC, SceneA, SceneA, null, null, SceneD)
+
+ val childTransition = TestableTransition(SceneA, SceneB)
+
+ childState.startTransition(childTransition, null)
+ assertThat(childState.isTransitioning(SceneA, SceneB)).isTrue()
+ assertThat(parentState.isTransitioning(SceneC, SceneD)).isTrue()
+
+ childState.finishTransition(childTransition, SceneA)
+ assertThat(childState.transitionState).isEqualTo(TransitionState.Idle(SceneA))
+ assertThat(parentState.transitionState).isEqualTo(TransitionState.Idle(SceneC))
+ }
+
+ @Test
+ fun linkedTransition_fuzzyLinksAreNotMatched() {
+ val (parentState, childState) =
+ setupLinkedStates(SceneC, SceneA, SceneB, null, SceneC, SceneD)
+ val childTransition = TestableTransition(SceneA, SceneB)
+
+ childState.startTransition(childTransition, null)
+ assertThat(childState.isTransitioning(SceneA, SceneB)).isTrue()
+ assertThat(parentState.isTransitioning(SceneC, SceneD)).isFalse()
+ }
}
diff --git a/packages/SystemUI/customization/Android.bp b/packages/SystemUI/customization/Android.bp
index 81b5bd4..c399abc 100644
--- a/packages/SystemUI/customization/Android.bp
+++ b/packages/SystemUI/customization/Android.bp
@@ -13,6 +13,7 @@
// limitations under the License.
package {
+ default_team: "trendy_team_system_ui_please_use_a_more_specific_subteam_if_possible_",
// See: http://go/android-license-faq
// A large-scale-change added 'default_applicable_licenses' to import
// all of the 'license_kinds' from "frameworks_base_packages_SystemUI_license"
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockController.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockController.kt
index 001e3a5..54c7a08 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockController.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockController.kt
@@ -49,7 +49,7 @@
* existing lockscreen clock.
*/
class DefaultClockController(
- ctx: Context,
+ private val ctx: Context,
private val layoutInflater: LayoutInflater,
private val resources: Resources,
private val settings: ClockSettings?,
@@ -121,7 +121,11 @@
protected var targetRegion: Rect? = null
override val config = ClockFaceConfig()
- override val layout = DefaultClockFaceLayout(view)
+ override val layout =
+ DefaultClockFaceLayout(view).apply {
+ views[0].id =
+ resources.getIdentifier("lockscreen_clock_view", "id", ctx.packageName)
+ }
override var animations: DefaultClockAnimations = DefaultClockAnimations(view, 0f, 0f)
internal set
@@ -188,7 +192,11 @@
seedColor: Int?,
messageBuffer: MessageBuffer?,
) : DefaultClockFaceController(view, seedColor, messageBuffer) {
- override val layout = DefaultClockFaceLayout(view)
+ override val layout =
+ DefaultClockFaceLayout(view).apply {
+ views[0].id =
+ resources.getIdentifier("lockscreen_clock_view_large", "id", ctx.packageName)
+ }
override val config =
ClockFaceConfig(hasCustomPositionUpdatedAnimation = hasStepClockAnimation)
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/notifications/data/repository/NotificationSettingsRepository.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/notifications/data/repository/NotificationSettingsRepository.kt
index 4b21105..e39d7ed 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/notifications/data/repository/NotificationSettingsRepository.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/notifications/data/repository/NotificationSettingsRepository.kt
@@ -20,8 +20,10 @@
import com.android.systemui.shared.settings.data.repository.SecureSettingsRepository
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.withContext
@@ -32,6 +34,12 @@
private val backgroundDispatcher: CoroutineDispatcher,
private val secureSettingsRepository: SecureSettingsRepository,
) {
+ val isNotificationHistoryEnabled: Flow<Boolean> =
+ secureSettingsRepository
+ .intSetting(name = Settings.Secure.NOTIFICATION_HISTORY_ENABLED)
+ .map { it == 1 }
+ .distinctUntilChanged()
+
/** The current state of the notification setting. */
val isShowNotificationsOnLockScreenEnabled: StateFlow<Boolean> =
secureSettingsRepository
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/notifications/domain/interactor/NotificationSettingsInteractor.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/notifications/domain/interactor/NotificationSettingsInteractor.kt
index 9ec6ec8..04e8090 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/notifications/domain/interactor/NotificationSettingsInteractor.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/notifications/domain/interactor/NotificationSettingsInteractor.kt
@@ -23,6 +23,8 @@
class NotificationSettingsInteractor(
private val repository: NotificationSettingsRepository,
) {
+ val isNotificationHistoryEnabled = repository.isNotificationHistoryEnabled
+
/** Should notifications be visible on the lockscreen? */
val isShowNotificationsOnLockScreenEnabled: StateFlow<Boolean> =
repository.isShowNotificationsOnLockScreenEnabled
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/settings/data/repository/SecureSettingsRepository.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/settings/data/repository/SecureSettingsRepository.kt
index 754d5dc..2a87452 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/settings/data/repository/SecureSettingsRepository.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/settings/data/repository/SecureSettingsRepository.kt
@@ -27,7 +27,11 @@
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.withContext
-/** Defines interface for classes that can provide access to data from [Settings.Secure]. */
+/**
+ * Defines interface for classes that can provide access to data from [Settings.Secure].
+ * This repository doesn't guarantee to provide value across different users. For that
+ * see: [UserAwareSecureSettingsRepository]
+ */
interface SecureSettingsRepository {
/** Returns a [Flow] tracking the value of a setting as an [Int]. */
diff --git a/packages/SystemUI/log/Android.bp b/packages/SystemUI/log/Android.bp
index 2be22a6..2f1d354 100644
--- a/packages/SystemUI/log/Android.bp
+++ b/packages/SystemUI/log/Android.bp
@@ -13,6 +13,7 @@
// limitations under the License.
package {
+ default_team: "trendy_team_system_ui_please_use_a_more_specific_subteam_if_possible_",
// See: http://go/android-license-faq
// A large-scale-change added 'default_applicable_licenses' to import
// all of the 'license_kinds' from "frameworks_base_packages_SystemUI_license"
diff --git a/packages/SystemUI/monet/Android.bp b/packages/SystemUI/monet/Android.bp
index 507ea25..98f7ace 100644
--- a/packages/SystemUI/monet/Android.bp
+++ b/packages/SystemUI/monet/Android.bp
@@ -14,6 +14,7 @@
// limitations under the License.
//
package {
+ default_team: "trendy_team_system_ui_please_use_a_more_specific_subteam_if_possible_",
default_applicable_licenses: ["Android-Apache-2.0"],
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardPasswordViewControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardPasswordViewControllerTest.kt
index 1519021..c6327ff 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardPasswordViewControllerTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardPasswordViewControllerTest.kt
@@ -24,11 +24,13 @@
import androidx.test.filters.SmallTest
import com.android.internal.util.LatencyTracker
import com.android.internal.widget.LockPatternUtils
+import com.android.keyguard.domain.interactor.KeyguardKeyboardInteractor
import com.android.systemui.Flags as AconfigFlags
import com.android.systemui.SysuiTestCase
import com.android.systemui.classifier.FalsingCollector
import com.android.systemui.flags.FakeFeatureFlags
import com.android.systemui.flags.Flags
+import com.android.systemui.keyboard.data.repository.FakeKeyboardRepository
import com.android.systemui.res.R
import com.android.systemui.statusbar.policy.DevicePostureController
import com.android.systemui.user.domain.interactor.SelectedUserInteractor
@@ -90,6 +92,8 @@
whenever(keyguardPasswordView.findViewById<ImageView>(R.id.switch_ime_button))
.thenReturn(mock(ImageView::class.java))
`when`(keyguardPasswordView.resources).thenReturn(context.resources)
+
+ val keyguardKeyboardInteractor = KeyguardKeyboardInteractor(FakeKeyboardRepository())
val fakeFeatureFlags = FakeFeatureFlags()
fakeFeatureFlags.set(Flags.LOCKSCREEN_ENABLE_LANDSCAPE, false)
mSetFlagsRule.enableFlags(AconfigFlags.FLAG_REVAMPED_BOUNCER_MESSAGES)
@@ -111,6 +115,7 @@
postureController,
fakeFeatureFlags,
mSelectedUserInteractor,
+ keyguardKeyboardInteractor,
)
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardPinBasedInputViewControllerTest.java b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardPinBasedInputViewControllerTest.java
index c4bcb53..f86342c 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardPinBasedInputViewControllerTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardPinBasedInputViewControllerTest.java
@@ -31,6 +31,7 @@
import com.android.internal.util.LatencyTracker;
import com.android.internal.widget.LockPatternUtils;
import com.android.keyguard.KeyguardSecurityModel.SecurityMode;
+import com.android.keyguard.domain.interactor.KeyguardKeyboardInteractor;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.classifier.FalsingCollector;
import com.android.systemui.classifier.FalsingCollectorFake;
@@ -102,13 +103,15 @@
.thenReturn(mOkButton);
when(mPinBasedInputView.getResources()).thenReturn(getContext().getResources());
+ KeyguardKeyboardInteractor keyguardKeyboardInteractor =
+ new KeyguardKeyboardInteractor(new FakeKeyboardRepository());
FakeFeatureFlags featureFlags = new FakeFeatureFlags();
mSetFlagsRule.enableFlags(com.android.systemui.Flags.FLAG_REVAMPED_BOUNCER_MESSAGES);
mKeyguardPinViewController = new KeyguardPinBasedInputViewController(mPinBasedInputView,
mKeyguardUpdateMonitor, mSecurityMode, mLockPatternUtils, mKeyguardSecurityCallback,
mKeyguardMessageAreaControllerFactory, mLatencyTracker, mLiftToactivateListener,
mEmergencyButtonController, mFalsingCollector, featureFlags,
- mSelectedUserInteractor, new FakeKeyboardRepository()) {
+ mSelectedUserInteractor, keyguardKeyboardInteractor) {
@Override
public void onResume(int reason) {
super.onResume(reason);
diff --git a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt
index c82688c..e8a43ac 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt
@@ -36,6 +36,7 @@
import com.android.internal.widget.LockPatternUtils
import com.android.keyguard.KeyguardSecurityContainer.UserSwitcherViewMode.UserSwitcherCallback
import com.android.keyguard.KeyguardSecurityModel.SecurityMode
+import com.android.keyguard.domain.interactor.KeyguardKeyboardInteractor
import com.android.systemui.Flags as AConfigFlags
import com.android.systemui.SysuiTestCase
import com.android.systemui.biometrics.FaceAuthAccessibilityDelegate
@@ -51,8 +52,9 @@
import com.android.systemui.deviceentry.domain.interactor.deviceEntryInteractor
import com.android.systemui.flags.FakeFeatureFlags
import com.android.systemui.flags.Flags
+import com.android.systemui.keyboard.data.repository.FakeKeyboardRepository
import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
-import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractorFactory
+import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.testScope
import com.android.systemui.log.SessionTracker
@@ -202,6 +204,7 @@
whenever(keyguardStateController.canDismissLockScreen()).thenReturn(true)
whenever(deviceProvisionedController.isUserSetup(anyInt())).thenReturn(true)
+ val keyguardKeyboardInteractor = KeyguardKeyboardInteractor(FakeKeyboardRepository())
featureFlags = FakeFeatureFlags()
featureFlags.set(Flags.REFACTOR_KEYGUARD_DISMISS_INTENT, false)
featureFlags.set(Flags.LOCKSCREEN_ENABLE_LANDSCAPE, false)
@@ -232,13 +235,12 @@
postureController,
featureFlags,
mSelectedUserInteractor,
+ keyguardKeyboardInteractor,
)
kosmos = testKosmos()
sceneInteractor = kosmos.sceneInteractor
- keyguardTransitionInteractor =
- KeyguardTransitionInteractorFactory.create(kosmos.testScope.backgroundScope)
- .keyguardTransitionInteractor
+ keyguardTransitionInteractor = kosmos.keyguardTransitionInteractor
sceneTransitionStateFlow =
MutableStateFlow(ObservableTransitionState.Idle(SceneKey.Lockscreen))
sceneInteractor.setTransitionState(sceneTransitionStateFlow)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt
index f7743e2..259f349 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt
@@ -37,7 +37,7 @@
import com.android.keyguard.KeyguardUpdateMonitor
import com.android.systemui.Flags
import com.android.systemui.SysuiTestCase
-import com.android.systemui.animation.ActivityLaunchAnimator
+import com.android.systemui.animation.ActivityTransitionAnimator
import com.android.systemui.biometrics.domain.interactor.UdfpsOverlayInteractor
import com.android.systemui.biometrics.shared.model.UdfpsOverlayParams
import com.android.systemui.biometrics.ui.viewmodel.DefaultUdfpsTouchOverlayViewModel
@@ -106,7 +106,7 @@
@Mock private lateinit var udfpsController: UdfpsController
@Mock private lateinit var udfpsView: UdfpsView
@Mock private lateinit var mUdfpsKeyguardViewLegacy: UdfpsKeyguardViewLegacy
- @Mock private lateinit var activityLaunchAnimator: ActivityLaunchAnimator
+ @Mock private lateinit var mActivityTransitionAnimator: ActivityTransitionAnimator
@Mock private lateinit var primaryBouncerInteractor: PrimaryBouncerInteractor
@Mock private lateinit var alternateBouncerInteractor: AlternateBouncerInteractor
@Mock private lateinit var mSelectedUserInteractor: SelectedUserInteractor
@@ -167,7 +167,7 @@
reason,
controllerCallback,
onTouch,
- activityLaunchAnimator,
+ mActivityTransitionAnimator,
primaryBouncerInteractor,
alternateBouncerInteractor,
isDebuggable,
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsControllerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsControllerTest.java
index 90c3c14..529403a 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsControllerTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsControllerTest.java
@@ -75,7 +75,7 @@
import com.android.keyguard.KeyguardUpdateMonitor;
import com.android.systemui.Flags;
import com.android.systemui.SysuiTestCase;
-import com.android.systemui.animation.ActivityLaunchAnimator;
+import com.android.systemui.animation.ActivityTransitionAnimator;
import com.android.systemui.biometrics.domain.interactor.UdfpsOverlayInteractor;
import com.android.systemui.biometrics.shared.model.UdfpsOverlayParams;
import com.android.systemui.biometrics.udfps.InteractionEvent;
@@ -203,7 +203,7 @@
@Mock
private SystemUIDialogManager mSystemUIDialogManager;
@Mock
- private ActivityLaunchAnimator mActivityLaunchAnimator;
+ private ActivityTransitionAnimator mActivityTransitionAnimator;
@Mock
private PrimaryBouncerInteractor mPrimaryBouncerInteractor;
@Mock
@@ -331,7 +331,7 @@
mUnlockedScreenOffAnimationController,
mSystemUIDialogManager,
mLatencyTracker,
- mActivityLaunchAnimator,
+ mActivityTransitionAnimator,
mBiometricExecutor,
mPrimaryBouncerInteractor,
mShadeInteractor,
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsKeyguardViewLegacyControllerBaseTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsKeyguardViewLegacyControllerBaseTest.java
index 7d9c2f9..324534f 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsKeyguardViewLegacyControllerBaseTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsKeyguardViewLegacyControllerBaseTest.java
@@ -26,7 +26,7 @@
import com.android.keyguard.KeyguardUpdateMonitor;
import com.android.systemui.Flags;
import com.android.systemui.SysuiTestCase;
-import com.android.systemui.animation.ActivityLaunchAnimator;
+import com.android.systemui.animation.ActivityTransitionAnimator;
import com.android.systemui.biometrics.domain.interactor.UdfpsOverlayInteractor;
import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor;
import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor;
@@ -70,7 +70,7 @@
protected @Mock UnlockedScreenOffAnimationController mUnlockedScreenOffAnimationController;
protected @Mock SystemUIDialogManager mDialogManager;
protected @Mock UdfpsController mUdfpsController;
- protected @Mock ActivityLaunchAnimator mActivityLaunchAnimator;
+ protected @Mock ActivityTransitionAnimator mActivityTransitionAnimator;
protected @Mock PrimaryBouncerInteractor mPrimaryBouncerInteractor;
protected @Mock ShadeInteractor mShadeInteractor;
protected @Mock AlternateBouncerInteractor mAlternateBouncerInteractor;
@@ -148,7 +148,7 @@
mUnlockedScreenOffAnimationController,
mDialogManager,
mUdfpsController,
- mActivityLaunchAnimator,
+ mActivityTransitionAnimator,
mPrimaryBouncerInteractor,
mAlternateBouncerInteractor,
mUdfpsKeyguardAccessibilityDelegate,
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsKeyguardViewLegacyControllerWithCoroutinesTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsKeyguardViewLegacyControllerWithCoroutinesTest.kt
index 0e257bc..55cfcc2 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsKeyguardViewLegacyControllerWithCoroutinesTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsKeyguardViewLegacyControllerWithCoroutinesTest.kt
@@ -16,44 +16,27 @@
package com.android.systemui.biometrics
-import android.os.Handler
import android.testing.TestableLooper
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
-import com.android.keyguard.KeyguardSecurityModel
import com.android.systemui.biometrics.UdfpsKeyguardViewLegacy.ANIMATE_APPEAR_ON_SCREEN_OFF
import com.android.systemui.biometrics.UdfpsKeyguardViewLegacy.ANIMATION_BETWEEN_AOD_AND_LOCKSCREEN
-import com.android.systemui.biometrics.data.repository.FakeFingerprintPropertyRepository
-import com.android.systemui.biometrics.domain.interactor.UdfpsOverlayInteractor
-import com.android.systemui.bouncer.data.repository.KeyguardBouncerRepository
-import com.android.systemui.bouncer.data.repository.KeyguardBouncerRepositoryImpl
-import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor
-import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerCallbackInteractor
-import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor
+import com.android.systemui.biometrics.domain.interactor.udfpsOverlayInteractor
+import com.android.systemui.bouncer.data.repository.fakeKeyguardBouncerRepository
+import com.android.systemui.bouncer.domain.interactor.alternateBouncerInteractor
+import com.android.systemui.bouncer.domain.interactor.primaryBouncerInteractor
import com.android.systemui.bouncer.shared.constants.KeyguardBouncerConstants
-import com.android.systemui.bouncer.ui.BouncerView
-import com.android.systemui.classifier.FalsingCollector
import com.android.systemui.coroutines.collectLastValue
-import com.android.systemui.deviceentry.domain.interactor.DeviceEntryFaceAuthInteractor
-import com.android.systemui.keyguard.DismissCallbackRegistry
-import com.android.systemui.keyguard.data.repository.BiometricSettingsRepository
-import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
-import com.android.systemui.keyguard.data.repository.FakeTrustRepository
-import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractorFactory
+import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
+import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor
import com.android.systemui.keyguard.shared.model.KeyguardState
import com.android.systemui.keyguard.shared.model.TransitionState
import com.android.systemui.keyguard.shared.model.TransitionStep
-import com.android.systemui.log.table.TableLogBuffer
-import com.android.systemui.plugins.statusbar.StatusBarStateController
+import com.android.systemui.kosmos.testScope
import com.android.systemui.statusbar.StatusBarState
-import com.android.systemui.statusbar.policy.KeyguardStateController
-import com.android.systemui.user.domain.interactor.SelectedUserInteractor
+import com.android.systemui.testKosmos
import com.android.systemui.util.mockito.whenever
-import com.android.systemui.util.time.FakeSystemClock
-import com.android.systemui.util.time.SystemClock
import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.test.StandardTestDispatcher
-import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertFalse
@@ -64,9 +47,7 @@
import org.mockito.ArgumentMatchers.any
import org.mockito.ArgumentMatchers.anyInt
import org.mockito.ArgumentMatchers.eq
-import org.mockito.Mock
import org.mockito.Mockito
-import org.mockito.Mockito.mock
import org.mockito.Mockito.never
import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations
@@ -77,70 +58,24 @@
@kotlinx.coroutines.ExperimentalCoroutinesApi
class UdfpsKeyguardViewLegacyControllerWithCoroutinesTest :
UdfpsKeyguardViewLegacyControllerBaseTest() {
- private val testDispatcher = StandardTestDispatcher()
- private val testScope = TestScope(testDispatcher)
+ private val kosmos = testKosmos()
+ private val testScope = kosmos.testScope
- private lateinit var keyguardBouncerRepository: KeyguardBouncerRepository
- private lateinit var transitionRepository: FakeKeyguardTransitionRepository
-
- @Mock private lateinit var bouncerLogger: TableLogBuffer
+ private val keyguardBouncerRepository = kosmos.fakeKeyguardBouncerRepository
+ private val transitionRepository = kosmos.fakeKeyguardTransitionRepository
@Before
override fun setUp() {
allowTestableLooperAsMainThread() // repeatWhenAttached requires the main thread
MockitoAnnotations.initMocks(this)
- keyguardBouncerRepository =
- KeyguardBouncerRepositoryImpl(
- FakeSystemClock(),
- testScope.backgroundScope,
- bouncerLogger,
- )
- transitionRepository = FakeKeyguardTransitionRepository()
super.setUp()
}
override fun createUdfpsKeyguardViewController(): UdfpsKeyguardViewControllerLegacy {
- mPrimaryBouncerInteractor =
- PrimaryBouncerInteractor(
- keyguardBouncerRepository,
- mock(BouncerView::class.java),
- mock(Handler::class.java),
- mKeyguardStateController,
- mock(KeyguardSecurityModel::class.java),
- mock(PrimaryBouncerCallbackInteractor::class.java),
- mock(FalsingCollector::class.java),
- mock(DismissCallbackRegistry::class.java),
- context,
- mKeyguardUpdateMonitor,
- FakeTrustRepository(),
- testScope.backgroundScope,
- mSelectedUserInteractor,
- mock(DeviceEntryFaceAuthInteractor::class.java),
- )
- mAlternateBouncerInteractor =
- AlternateBouncerInteractor(
- mock(StatusBarStateController::class.java),
- mock(KeyguardStateController::class.java),
- keyguardBouncerRepository,
- FakeFingerprintPropertyRepository(),
- mock(BiometricSettingsRepository::class.java),
- mock(SystemClock::class.java),
- mKeyguardUpdateMonitor,
- testScope.backgroundScope,
- )
- mKeyguardTransitionInteractor =
- KeyguardTransitionInteractorFactory.create(
- scope = testScope.backgroundScope,
- repository = transitionRepository,
- )
- .keyguardTransitionInteractor
- mUdfpsOverlayInteractor =
- UdfpsOverlayInteractor(
- context,
- mock(AuthController::class.java),
- mock(SelectedUserInteractor::class.java),
- testScope.backgroundScope,
- )
+ mPrimaryBouncerInteractor = kosmos.primaryBouncerInteractor
+ mAlternateBouncerInteractor = kosmos.alternateBouncerInteractor
+ mKeyguardTransitionInteractor = kosmos.keyguardTransitionInteractor
+ mUdfpsOverlayInteractor = kosmos.udfpsOverlayInteractor
return createUdfpsKeyguardViewController(/* useModernBouncer */ true)
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/common/data/repository/PackageChangeRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/common/data/repository/PackageChangeRepositoryTest.kt
index 6380ace..2386957 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/common/data/repository/PackageChangeRepositoryTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/common/data/repository/PackageChangeRepositoryTest.kt
@@ -23,7 +23,7 @@
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
-import com.android.systemui.common.data.shared.model.PackageChangeModel
+import com.android.systemui.common.shared.model.PackageChangeModel
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.coroutines.collectValues
import com.android.systemui.kosmos.applicationCoroutineScope
@@ -32,6 +32,7 @@
import com.android.systemui.log.logcatLogBuffer
import com.android.systemui.testKosmos
import com.android.systemui.util.mockito.whenever
+import com.android.systemui.util.time.fakeSystemClock
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.test.runTest
import org.junit.Before
@@ -67,7 +68,8 @@
scope = applicationCoroutineScope,
context = context,
bgHandler = handler,
- logger = PackageUpdateLogger(logcatLogBuffer())
+ logger = PackageUpdateLogger(logcatLogBuffer()),
+ systemClock = fakeSystemClock,
)
updateMonitor
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/common/data/repository/PackageUpdateMonitorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/common/data/repository/PackageUpdateMonitorTest.kt
index d610925..35d9d3f 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/common/data/repository/PackageUpdateMonitorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/common/data/repository/PackageUpdateMonitorTest.kt
@@ -23,7 +23,7 @@
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
-import com.android.systemui.common.data.shared.model.PackageChangeModel
+import com.android.systemui.common.shared.model.PackageChangeModel
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.coroutines.collectValues
import com.android.systemui.kosmos.applicationCoroutineScope
@@ -32,6 +32,7 @@
import com.android.systemui.log.logcatLogBuffer
import com.android.systemui.testKosmos
import com.android.systemui.util.mockito.whenever
+import com.android.systemui.util.time.fakeSystemClock
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableSharedFlow
@@ -71,8 +72,11 @@
bgHandler = handler,
context = context,
scope = applicationCoroutineScope,
- logger = PackageUpdateLogger(logcatLogBuffer())
+ logger = PackageUpdateLogger(logcatLogBuffer()),
+ systemClock = fakeSystemClock,
)
+
+ fakeSystemClock.setCurrentTimeMillis(0)
}
@Test
@@ -96,11 +100,16 @@
val packageChange by collectLastValue(monitor.packageChanged)
assertThat(packageChange).isNull()
+ fakeSystemClock.setCurrentTimeMillis(100)
monitor.onPackageAdded(TEST_PACKAGE, 123)
assertThat(packageChange)
.isEqualTo(
- PackageChangeModel.Installed(packageName = TEST_PACKAGE, packageUid = 123)
+ PackageChangeModel.Installed(
+ packageName = TEST_PACKAGE,
+ packageUid = 123,
+ timeMillis = 100,
+ )
)
}
}
@@ -112,11 +121,16 @@
val packageChange by collectLastValue(monitor.packageChanged)
assertThat(packageChange).isNull()
+ fakeSystemClock.setCurrentTimeMillis(200)
monitor.onPackageRemoved(TEST_PACKAGE, 123)
assertThat(packageChange)
.isEqualTo(
- PackageChangeModel.Uninstalled(packageName = TEST_PACKAGE, packageUid = 123)
+ PackageChangeModel.Uninstalled(
+ packageName = TEST_PACKAGE,
+ packageUid = 123,
+ timeMillis = 200,
+ )
)
}
}
@@ -128,11 +142,16 @@
val packageChange by collectLastValue(monitor.packageChanged)
assertThat(packageChange).isNull()
+ fakeSystemClock.setCurrentTimeMillis(100)
monitor.onPackageChanged(TEST_PACKAGE, 123, emptyArray())
assertThat(packageChange)
.isEqualTo(
- PackageChangeModel.Changed(packageName = TEST_PACKAGE, packageUid = 123)
+ PackageChangeModel.Changed(
+ packageName = TEST_PACKAGE,
+ packageUid = 123,
+ timeMillis = 100,
+ )
)
}
}
@@ -144,13 +163,15 @@
val packageChange by collectLastValue(monitor.packageChanged)
assertThat(packageChange).isNull()
+ fakeSystemClock.setCurrentTimeMillis(100)
monitor.onPackageUpdateStarted(TEST_PACKAGE, 123)
assertThat(packageChange)
.isEqualTo(
PackageChangeModel.UpdateStarted(
packageName = TEST_PACKAGE,
- packageUid = 123
+ packageUid = 123,
+ timeMillis = 100,
)
)
}
@@ -163,13 +184,15 @@
val packageChange by collectLastValue(monitor.packageChanged)
assertThat(packageChange).isNull()
+ fakeSystemClock.setCurrentTimeMillis(100)
monitor.onPackageUpdateFinished(TEST_PACKAGE, 123)
assertThat(packageChange)
.isEqualTo(
PackageChangeModel.UpdateFinished(
packageName = TEST_PACKAGE,
- packageUid = 123
+ packageUid = 123,
+ timeMillis = 100,
)
)
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/common/domain/interactor/PackageChangeInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/common/domain/interactor/PackageChangeInteractorTest.kt
new file mode 100644
index 0000000..a164e7c
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/common/domain/interactor/PackageChangeInteractorTest.kt
@@ -0,0 +1,175 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.common.domain.interactor
+
+import android.content.pm.UserInfo
+import android.os.UserHandle
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.common.data.repository.fakePackageChangeRepository
+import com.android.systemui.common.data.repository.packageChangeRepository
+import com.android.systemui.common.shared.model.PackageChangeModel
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.coroutines.collectValues
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.testKosmos
+import com.android.systemui.user.data.repository.fakeUserRepository
+import com.android.systemui.user.domain.interactor.selectedUserInteractor
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class PackageChangeInteractorTest : SysuiTestCase() {
+ private val kosmos = testKosmos()
+
+ private lateinit var underTest: PackageChangeInteractor
+
+ @Before
+ fun setUp() =
+ with(kosmos) {
+ underTest =
+ PackageChangeInteractor(
+ packageChangeRepository = packageChangeRepository,
+ userInteractor = selectedUserInteractor,
+ )
+ fakeUserRepository.setUserInfos(listOf(MAIN_USER, SECONDARY_USER))
+ }
+
+ @Test
+ fun packageChanges() =
+ with(kosmos) {
+ testScope.runTest {
+ val packageChange by collectLastValue(underTest.packageChanged(MAIN_USER_HANDLE))
+ assertThat(packageChange).isNull()
+
+ // Even if secondary user is active, we should still receive changes for the
+ // primary user.
+ setUser(SECONDARY_USER)
+
+ fakePackageChangeRepository.notifyChange(
+ PackageChangeModel.Installed(
+ packageName = "pkg",
+ packageUid = UserHandle.getUid(MAIN_USER.id, /* appId = */ 10),
+ )
+ )
+
+ assertThat(packageChange).isInstanceOf(PackageChangeModel.Installed::class.java)
+ assertThat(packageChange?.packageName).isEqualTo("pkg")
+ }
+ }
+
+ @Test
+ fun packageChanges_ignoresUpdatesFromOtherUsers() =
+ with(kosmos) {
+ testScope.runTest {
+ val packageChange by collectLastValue(underTest.packageChanged(MAIN_USER_HANDLE))
+ assertThat(packageChange).isNull()
+
+ setUser(SECONDARY_USER)
+ fakePackageChangeRepository.notifyChange(
+ PackageChangeModel.Installed(
+ packageName = "pkg",
+ packageUid = UserHandle.getUid(SECONDARY_USER.id, /* appId = */ 10),
+ )
+ )
+
+ assertThat(packageChange).isNull()
+ }
+ }
+
+ @Test
+ fun packageChanges_forCurrentUser() =
+ with(kosmos) {
+ testScope.runTest {
+ val packageChanges by collectValues(underTest.packageChanged(UserHandle.CURRENT))
+ assertThat(packageChanges).isEmpty()
+
+ setUser(SECONDARY_USER)
+ fakePackageChangeRepository.notifyChange(
+ PackageChangeModel.Installed(
+ packageName = "first",
+ packageUid = UserHandle.getUid(SECONDARY_USER.id, /* appId = */ 10),
+ )
+ )
+ fakePackageChangeRepository.notifyChange(
+ PackageChangeModel.Installed(
+ packageName = "second",
+ packageUid = UserHandle.getUid(MAIN_USER.id, /* appId = */ 10),
+ )
+ )
+ setUser(MAIN_USER)
+ fakePackageChangeRepository.notifyChange(
+ PackageChangeModel.Installed(
+ packageName = "third",
+ packageUid = UserHandle.getUid(MAIN_USER.id, /* appId = */ 10),
+ )
+ )
+
+ assertThat(packageChanges.map { it.packageName })
+ .containsExactly("first", "third")
+ .inOrder()
+ }
+ }
+
+ @Test
+ fun packageChanges_forSpecificPackageName() =
+ with(kosmos) {
+ testScope.runTest {
+ val packageChange by
+ collectLastValue(underTest.packageChanged(MAIN_USER_HANDLE, "mypkg"))
+ assertThat(packageChange).isNull()
+
+ fakePackageChangeRepository.notifyChange(
+ PackageChangeModel.Installed(
+ packageName = "other",
+ packageUid = UserHandle.getUid(MAIN_USER.id, /* appId = */ 10),
+ )
+ )
+ assertThat(packageChange).isNull()
+
+ fakePackageChangeRepository.notifyChange(
+ PackageChangeModel.Installed(
+ packageName = "mypkg",
+ packageUid = UserHandle.getUid(MAIN_USER.id, /* appId = */ 10),
+ )
+ )
+ assertThat(packageChange).isInstanceOf(PackageChangeModel.Installed::class.java)
+ assertThat(packageChange?.packageName).isEqualTo("mypkg")
+ }
+ }
+
+ private suspend fun TestScope.setUser(user: UserInfo) {
+ kosmos.fakeUserRepository.setSelectedUserInfo(user)
+ runCurrent()
+ }
+
+ private companion object {
+ val MAIN_USER_HANDLE = UserHandle.of(1)
+ val MAIN_USER = UserInfo(MAIN_USER_HANDLE.identifier, "main", UserInfo.FLAG_MAIN)
+ val SECONDARY_USER_HANDLE = UserHandle.of(2)
+ val SECONDARY_USER = UserInfo(SECONDARY_USER_HANDLE.identifier, "secondary", 0)
+ }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalMediaRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalMediaRepositoryImplTest.kt
index 1642e52..45f98be 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalMediaRepositoryImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalMediaRepositoryImplTest.kt
@@ -72,7 +72,7 @@
testScope.runTest {
val mediaModel = collectLastValue(underTest.mediaModel)
runCurrent()
- assertThat(mediaModel()?.hasAnyMediaOrRecommendation).isFalse()
+ assertThat(mediaModel()?.hasActiveMediaOrRecommendation).isFalse()
}
@Test
@@ -84,16 +84,16 @@
// Initial value is false.
val mediaModel = collectLastValue(underTest.mediaModel)
runCurrent()
- assertThat(mediaModel()?.hasAnyMediaOrRecommendation).isFalse()
+ assertThat(mediaModel()?.hasActiveMediaOrRecommendation).isFalse()
// Change to media available and notify the listener.
- whenever(mediaDataManager.hasAnyMediaOrRecommendation()).thenReturn(true)
+ whenever(mediaDataManager.hasActiveMediaOrRecommendation()).thenReturn(true)
whenever(mediaData.createdTimestampMillis).thenReturn(1234L)
mediaDataListenerCaptor.value.onMediaDataLoaded("key", null, mediaData)
runCurrent()
// Media active now returns true.
- assertThat(mediaModel()?.hasAnyMediaOrRecommendation).isTrue()
+ assertThat(mediaModel()?.hasActiveMediaOrRecommendation).isTrue()
assertThat(mediaModel()?.createdTimestampMillis).isEqualTo(1234L)
}
@@ -104,20 +104,20 @@
verify(mediaDataManager).addListener(mediaDataListenerCaptor.capture())
// Change to media available and notify the listener.
- whenever(mediaDataManager.hasAnyMediaOrRecommendation()).thenReturn(true)
+ whenever(mediaDataManager.hasActiveMediaOrRecommendation()).thenReturn(true)
mediaDataListenerCaptor.value.onMediaDataLoaded("key", null, mediaData)
runCurrent()
// Media active now returns true.
val mediaModel = collectLastValue(underTest.mediaModel)
- assertThat(mediaModel()?.hasAnyMediaOrRecommendation).isTrue()
+ assertThat(mediaModel()?.hasActiveMediaOrRecommendation).isTrue()
// Change to media unavailable and notify the listener.
- whenever(mediaDataManager.hasAnyMediaOrRecommendation()).thenReturn(false)
+ whenever(mediaDataManager.hasActiveMediaOrRecommendation()).thenReturn(false)
mediaDataListenerCaptor.value.onMediaDataRemoved("key")
runCurrent()
// Media active now returns false.
- assertThat(mediaModel()?.hasAnyMediaOrRecommendation).isFalse()
+ assertThat(mediaModel()?.hasActiveMediaOrRecommendation).isFalse()
}
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalRepositoryImplTest.kt
index bd9ca30..b4e2eab 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalRepositoryImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalRepositoryImplTest.kt
@@ -16,26 +16,18 @@
package com.android.systemui.communal.data.repository
-import android.content.pm.UserInfo
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
-import com.android.systemui.Flags.FLAG_COMMUNAL_HUB
import com.android.systemui.SysuiTestCase
import com.android.systemui.communal.shared.model.CommunalSceneKey
import com.android.systemui.communal.shared.model.ObservableCommunalTransitionState
import com.android.systemui.coroutines.collectLastValue
-import com.android.systemui.flags.Flags
-import com.android.systemui.flags.fakeFeatureFlagsClassic
-import com.android.systemui.kosmos.testDispatcher
import com.android.systemui.kosmos.testScope
import com.android.systemui.scene.data.repository.sceneContainerRepository
import com.android.systemui.scene.shared.flag.fakeSceneContainerFlags
import com.android.systemui.scene.shared.model.SceneKey
import com.android.systemui.scene.shared.model.SceneModel
import com.android.systemui.testKosmos
-import com.android.systemui.user.data.repository.FakeUserRepository
-import com.android.systemui.user.data.repository.fakeUserRepository
-import com.android.systemui.util.settings.FakeSettings
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.runTest
@@ -48,37 +40,20 @@
class CommunalRepositoryImplTest : SysuiTestCase() {
private lateinit var underTest: CommunalRepositoryImpl
- private lateinit var secureSettings: FakeSettings
- private lateinit var userRepository: FakeUserRepository
-
private val kosmos = testKosmos()
private val testScope = kosmos.testScope
private val sceneContainerRepository = kosmos.sceneContainerRepository
@Before
fun setUp() {
- secureSettings = FakeSettings()
- userRepository = kosmos.fakeUserRepository
-
- val listOfUserInfo = listOf(MAIN_USER_INFO)
- userRepository.setUserInfos(listOfUserInfo)
-
- kosmos.fakeFeatureFlagsClassic.apply { set(Flags.COMMUNAL_SERVICE_ENABLED, true) }
- mSetFlagsRule.enableFlags(FLAG_COMMUNAL_HUB)
-
underTest = createRepositoryImpl(false)
}
private fun createRepositoryImpl(sceneContainerEnabled: Boolean): CommunalRepositoryImpl {
return CommunalRepositoryImpl(
testScope.backgroundScope,
- testScope.backgroundScope,
- kosmos.testDispatcher,
- kosmos.fakeFeatureFlagsClassic,
kosmos.fakeSceneContainerFlags.apply { enabled = sceneContainerEnabled },
sceneContainerRepository,
- kosmos.fakeUserRepository,
- secureSettings,
)
}
@@ -159,29 +134,4 @@
assertThat(transitionState)
.isEqualTo(ObservableCommunalTransitionState.Idle(CommunalSceneKey.DEFAULT))
}
-
- @Test
- fun communalEnabledState_false_whenGlanceableHubSettingFalse() =
- testScope.runTest {
- userRepository.setSelectedUserInfo(MAIN_USER_INFO)
- secureSettings.putIntForUser(GLANCEABLE_HUB_ENABLED, 0, MAIN_USER_INFO.id)
-
- val communalEnabled by collectLastValue(underTest.communalEnabledState)
- assertThat(communalEnabled).isFalse()
- }
-
- @Test
- fun communalEnabledState_true_whenGlanceableHubSettingTrue() =
- testScope.runTest {
- userRepository.setSelectedUserInfo(MAIN_USER_INFO)
- secureSettings.putIntForUser(GLANCEABLE_HUB_ENABLED, 1, MAIN_USER_INFO.id)
-
- val communalEnabled by collectLastValue(underTest.communalEnabledState)
- assertThat(communalEnabled).isTrue()
- }
-
- companion object {
- private const val GLANCEABLE_HUB_ENABLED = "glanceable_hub_enabled"
- private val MAIN_USER_INFO = UserInfo(0, "primary", UserInfo.FLAG_MAIN)
- }
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalSettingsRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalSettingsRepositoryImplTest.kt
new file mode 100644
index 0000000..0aca16d
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalSettingsRepositoryImplTest.kt
@@ -0,0 +1,153 @@
+/*
+ * Copyright (C) 2024 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.communal.data.repository
+
+import android.app.admin.DevicePolicyManager
+import android.app.admin.DevicePolicyManager.KEYGUARD_DISABLE_FEATURES_NONE
+import android.app.admin.DevicePolicyManager.KEYGUARD_DISABLE_WIDGETS_ALL
+import android.app.admin.devicePolicyManager
+import android.content.Intent
+import android.content.pm.UserInfo
+import android.platform.test.annotations.DisableFlags
+import android.platform.test.annotations.EnableFlags
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.Flags.FLAG_COMMUNAL_HUB
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.broadcast.broadcastDispatcher
+import com.android.systemui.communal.data.model.DisabledReason
+import com.android.systemui.communal.data.repository.CommunalSettingsRepositoryImpl.Companion.GLANCEABLE_HUB_ENABLED
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.flags.Flags.COMMUNAL_SERVICE_ENABLED
+import com.android.systemui.flags.fakeFeatureFlagsClassic
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.testKosmos
+import com.android.systemui.util.mockito.nullable
+import com.android.systemui.util.mockito.whenever
+import com.android.systemui.util.settings.fakeSettings
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.eq
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class CommunalSettingsRepositoryImplTest : SysuiTestCase() {
+ private val kosmos = testKosmos()
+ private val testScope = kosmos.testScope
+ private lateinit var underTest: CommunalSettingsRepository
+
+ @Before
+ fun setUp() {
+ kosmos.fakeFeatureFlagsClassic.set(COMMUNAL_SERVICE_ENABLED, true)
+ setKeyguardFeaturesDisabled(PRIMARY_USER, KEYGUARD_DISABLE_FEATURES_NONE)
+ setKeyguardFeaturesDisabled(SECONDARY_USER, KEYGUARD_DISABLE_FEATURES_NONE)
+ underTest = kosmos.communalSettingsRepository
+ }
+
+ @EnableFlags(FLAG_COMMUNAL_HUB)
+ @Test
+ fun secondaryUserIsInvalid() =
+ testScope.runTest {
+ val enabledState by collectLastValue(underTest.getEnabledState(SECONDARY_USER))
+
+ assertThat(enabledState?.enabled).isFalse()
+ assertThat(enabledState).containsExactly(DisabledReason.DISABLED_REASON_INVALID_USER)
+ }
+
+ @EnableFlags(FLAG_COMMUNAL_HUB)
+ @Test
+ fun classicFlagIsDisabled() =
+ testScope.runTest {
+ kosmos.fakeFeatureFlagsClassic.set(COMMUNAL_SERVICE_ENABLED, false)
+ val enabledState by collectLastValue(underTest.getEnabledState(PRIMARY_USER))
+ assertThat(enabledState?.enabled).isFalse()
+ assertThat(enabledState).containsExactly(DisabledReason.DISABLED_REASON_FLAG)
+ }
+
+ @DisableFlags(FLAG_COMMUNAL_HUB)
+ @Test
+ fun communalHubFlagIsDisabled() =
+ testScope.runTest {
+ val enabledState by collectLastValue(underTest.getEnabledState(PRIMARY_USER))
+ assertThat(enabledState?.enabled).isFalse()
+ assertThat(enabledState).containsExactly(DisabledReason.DISABLED_REASON_FLAG)
+ }
+
+ @EnableFlags(FLAG_COMMUNAL_HUB)
+ @Test
+ fun hubIsDisabledByUser() =
+ testScope.runTest {
+ kosmos.fakeSettings.putIntForUser(GLANCEABLE_HUB_ENABLED, 0, PRIMARY_USER.id)
+ val enabledState by collectLastValue(underTest.getEnabledState(PRIMARY_USER))
+ assertThat(enabledState?.enabled).isFalse()
+ assertThat(enabledState).containsExactly(DisabledReason.DISABLED_REASON_USER_SETTING)
+
+ kosmos.fakeSettings.putIntForUser(GLANCEABLE_HUB_ENABLED, 1, SECONDARY_USER.id)
+ assertThat(enabledState?.enabled).isFalse()
+
+ kosmos.fakeSettings.putIntForUser(GLANCEABLE_HUB_ENABLED, 1, PRIMARY_USER.id)
+ assertThat(enabledState?.enabled).isTrue()
+ }
+
+ @EnableFlags(FLAG_COMMUNAL_HUB)
+ @Test
+ fun hubIsDisabledByDevicePolicy() =
+ testScope.runTest {
+ val enabledState by collectLastValue(underTest.getEnabledState(PRIMARY_USER))
+ assertThat(enabledState?.enabled).isTrue()
+
+ setKeyguardFeaturesDisabled(PRIMARY_USER, KEYGUARD_DISABLE_WIDGETS_ALL)
+ assertThat(enabledState?.enabled).isFalse()
+ assertThat(enabledState).containsExactly(DisabledReason.DISABLED_REASON_DEVICE_POLICY)
+ }
+
+ @EnableFlags(FLAG_COMMUNAL_HUB)
+ @Test
+ fun hubIsDisabledByUserAndDevicePolicy() =
+ testScope.runTest {
+ val enabledState by collectLastValue(underTest.getEnabledState(PRIMARY_USER))
+ assertThat(enabledState?.enabled).isTrue()
+
+ kosmos.fakeSettings.putIntForUser(GLANCEABLE_HUB_ENABLED, 0, PRIMARY_USER.id)
+ setKeyguardFeaturesDisabled(PRIMARY_USER, KEYGUARD_DISABLE_WIDGETS_ALL)
+
+ assertThat(enabledState?.enabled).isFalse()
+ assertThat(enabledState)
+ .containsExactly(
+ DisabledReason.DISABLED_REASON_DEVICE_POLICY,
+ DisabledReason.DISABLED_REASON_USER_SETTING,
+ )
+ }
+
+ private fun setKeyguardFeaturesDisabled(user: UserInfo, disabledFlags: Int) {
+ whenever(kosmos.devicePolicyManager.getKeyguardDisabledFeatures(nullable(), eq(user.id)))
+ .thenReturn(disabledFlags)
+ kosmos.broadcastDispatcher.sendIntentToMatchingReceiversOnly(
+ context,
+ Intent(DevicePolicyManager.ACTION_DEVICE_POLICY_MANAGER_STATE_CHANGED),
+ )
+ }
+
+ private companion object {
+ val PRIMARY_USER =
+ UserInfo(/* id= */ 0, /* name= */ "primary user", /* flags= */ UserInfo.FLAG_MAIN)
+ val SECONDARY_USER = UserInfo(/* id= */ 1, /* name= */ "secondary user", /* flags= */ 0)
+ }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorCommunalDisabledTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorCommunalDisabledTest.kt
index 6a3fc2a..824733b 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorCommunalDisabledTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorCommunalDisabledTest.kt
@@ -19,6 +19,7 @@
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
+import com.android.systemui.Flags.FLAG_COMMUNAL_HUB
import com.android.systemui.SysuiTestCase
import com.android.systemui.communal.data.repository.FakeCommunalRepository
import com.android.systemui.communal.data.repository.FakeCommunalWidgetRepository
@@ -59,7 +60,7 @@
widgetRepository = kosmos.fakeCommunalWidgetRepository
keyguardRepository = kosmos.fakeKeyguardRepository
- communalRepository.setIsCommunalEnabled(false)
+ mSetFlagsRule.disableFlags(FLAG_COMMUNAL_HUB)
underTest = kosmos.communalInteractor
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt
index c5485c5..3ac19e4 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt
@@ -23,6 +23,7 @@
import android.widget.RemoteViews
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
+import com.android.systemui.Flags.FLAG_COMMUNAL_HUB
import com.android.systemui.SysuiTestCase
import com.android.systemui.communal.data.repository.FakeCommunalMediaRepository
import com.android.systemui.communal.data.repository.FakeCommunalPrefsRepository
@@ -41,6 +42,8 @@
import com.android.systemui.communal.shared.model.ObservableCommunalTransitionState
import com.android.systemui.communal.widgets.EditWidgetsActivityStarter
import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.flags.Flags
+import com.android.systemui.flags.fakeFeatureFlagsClassic
import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository
import com.android.systemui.kosmos.testScope
@@ -109,12 +112,19 @@
whenever(secondaryUser.isMain).thenReturn(false)
userRepository.setUserInfos(listOf(mainUser, secondaryUser))
+ kosmos.fakeFeatureFlagsClassic.set(Flags.COMMUNAL_SERVICE_ENABLED, true)
+ mSetFlagsRule.enableFlags(FLAG_COMMUNAL_HUB)
+
underTest = kosmos.communalInteractor
}
@Test
fun communalEnabled_true() =
- testScope.runTest { assertThat(underTest.isCommunalEnabled).isTrue() }
+ testScope.runTest {
+ userRepository.setSelectedUserInfo(mainUser)
+ runCurrent()
+ assertThat(underTest.isCommunalEnabled).isTrue()
+ }
@Test
fun isCommunalAvailable_storageUnlockedAndMainUser_true() =
@@ -125,7 +135,6 @@
keyguardRepository.setIsEncryptedOrLockdown(false)
userRepository.setSelectedUserInfo(mainUser)
keyguardRepository.setKeyguardShowing(true)
- communalRepository.setCommunalEnabledState(true)
assertThat(isAvailable).isTrue()
}
@@ -139,7 +148,6 @@
keyguardRepository.setIsEncryptedOrLockdown(true)
userRepository.setSelectedUserInfo(mainUser)
keyguardRepository.setKeyguardShowing(true)
- communalRepository.setCommunalEnabledState(true)
assertThat(isAvailable).isFalse()
}
@@ -153,7 +161,6 @@
keyguardRepository.setIsEncryptedOrLockdown(false)
userRepository.setSelectedUserInfo(secondaryUser)
keyguardRepository.setKeyguardShowing(true)
- communalRepository.setCommunalEnabledState(true)
assertThat(isAvailable).isFalse()
}
@@ -167,7 +174,6 @@
keyguardRepository.setIsEncryptedOrLockdown(false)
userRepository.setSelectedUserInfo(mainUser)
keyguardRepository.setDreaming(true)
- communalRepository.setCommunalEnabledState(true)
assertThat(isAvailable).isTrue()
}
@@ -175,13 +181,14 @@
@Test
fun isCommunalAvailable_communalDisabled_false() =
testScope.runTest {
+ mSetFlagsRule.disableFlags(FLAG_COMMUNAL_HUB)
+
val isAvailable by collectLastValue(underTest.isCommunalAvailable)
assertThat(isAvailable).isFalse()
keyguardRepository.setIsEncryptedOrLockdown(false)
userRepository.setSelectedUserInfo(mainUser)
keyguardRepository.setKeyguardShowing(true)
- communalRepository.setCommunalEnabledState(false)
assertThat(isAvailable).isFalse()
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalTutorialInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalTutorialInteractorTest.kt
index 6c87e0f..ceb7fac 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalTutorialInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalTutorialInteractorTest.kt
@@ -22,12 +22,15 @@
import android.provider.Settings.Secure.HUB_MODE_TUTORIAL_STARTED
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
+import com.android.systemui.Flags.FLAG_COMMUNAL_HUB
import com.android.systemui.SysuiTestCase
import com.android.systemui.communal.data.repository.FakeCommunalRepository
import com.android.systemui.communal.data.repository.FakeCommunalTutorialRepository
import com.android.systemui.communal.data.repository.fakeCommunalRepository
import com.android.systemui.communal.data.repository.fakeCommunalTutorialRepository
import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.flags.Flags
+import com.android.systemui.flags.fakeFeatureFlagsClassic
import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository
import com.android.systemui.kosmos.testScope
@@ -35,11 +38,13 @@
import com.android.systemui.user.data.repository.FakeUserRepository
import com.android.systemui.user.data.repository.fakeUserRepository
import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
+@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
@RunWith(AndroidJUnit4::class)
class CommunalTutorialInteractorTest : SysuiTestCase() {
@@ -62,6 +67,8 @@
userRepository = kosmos.fakeUserRepository
userRepository.setUserInfos(listOf(MAIN_USER_INFO))
+ kosmos.fakeFeatureFlagsClassic.set(Flags.COMMUNAL_SERVICE_ENABLED, true)
+ mSetFlagsRule.enableFlags(FLAG_COMMUNAL_HUB)
underTest = kosmos.communalTutorialInteractor
}
@@ -127,6 +134,7 @@
testScope.runTest {
val tutorialSettingState by
collectLastValue(communalTutorialRepository.tutorialSettingState)
+ userRepository.setSelectedUserInfo(MAIN_USER_INFO)
communalTutorialRepository.setTutorialSettingState(HUB_MODE_TUTORIAL_NOT_STARTED)
communalRepository.setIsCommunalHubShowing(true)
@@ -139,6 +147,7 @@
testScope.runTest {
val tutorialSettingState by
collectLastValue(communalTutorialRepository.tutorialSettingState)
+ userRepository.setSelectedUserInfo(MAIN_USER_INFO)
communalTutorialRepository.setTutorialSettingState(HUB_MODE_TUTORIAL_STARTED)
communalRepository.setIsCommunalHubShowing(true)
@@ -151,6 +160,7 @@
testScope.runTest {
val tutorialSettingState by
collectLastValue(communalTutorialRepository.tutorialSettingState)
+ userRepository.setSelectedUserInfo(MAIN_USER_INFO)
communalTutorialRepository.setTutorialSettingState(HUB_MODE_TUTORIAL_COMPLETED)
communalRepository.setIsCommunalHubShowing(true)
@@ -163,6 +173,7 @@
testScope.runTest {
val tutorialSettingState by
collectLastValue(communalTutorialRepository.tutorialSettingState)
+ userRepository.setSelectedUserInfo(MAIN_USER_INFO)
communalTutorialRepository.setTutorialSettingState(HUB_MODE_TUTORIAL_NOT_STARTED)
communalRepository.setIsCommunalHubShowing(false)
@@ -175,6 +186,7 @@
testScope.runTest {
val tutorialSettingState by
collectLastValue(communalTutorialRepository.tutorialSettingState)
+ userRepository.setSelectedUserInfo(MAIN_USER_INFO)
communalRepository.setIsCommunalHubShowing(true)
communalTutorialRepository.setTutorialSettingState(HUB_MODE_TUTORIAL_STARTED)
@@ -188,6 +200,7 @@
testScope.runTest {
val tutorialSettingState by
collectLastValue(communalTutorialRepository.tutorialSettingState)
+ userRepository.setSelectedUserInfo(MAIN_USER_INFO)
communalRepository.setIsCommunalHubShowing(true)
communalTutorialRepository.setTutorialSettingState(HUB_MODE_TUTORIAL_COMPLETED)
@@ -198,14 +211,11 @@
private suspend fun setCommunalAvailable(available: Boolean) {
if (available) {
- communalRepository.setIsCommunalEnabled(true)
- communalRepository.setCommunalEnabledState(true)
keyguardRepository.setIsEncryptedOrLockdown(false)
userRepository.setSelectedUserInfo(MAIN_USER_INFO)
keyguardRepository.setKeyguardShowing(true)
} else {
- communalRepository.setIsCommunalEnabled(false)
- communalRepository.setCommunalEnabledState(false)
+ keyguardRepository.setIsEncryptedOrLockdown(true)
}
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt
index 73d3091..f70b6a5 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt
@@ -22,12 +22,12 @@
import android.widget.RemoteViews
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
+import com.android.systemui.Flags.FLAG_COMMUNAL_HUB
import com.android.systemui.SysuiTestCase
import com.android.systemui.communal.data.repository.FakeCommunalMediaRepository
import com.android.systemui.communal.data.repository.FakeCommunalTutorialRepository
import com.android.systemui.communal.data.repository.FakeCommunalWidgetRepository
import com.android.systemui.communal.data.repository.fakeCommunalMediaRepository
-import com.android.systemui.communal.data.repository.fakeCommunalRepository
import com.android.systemui.communal.data.repository.fakeCommunalTutorialRepository
import com.android.systemui.communal.data.repository.fakeCommunalWidgetRepository
import com.android.systemui.communal.domain.interactor.communalInteractor
@@ -37,6 +37,8 @@
import com.android.systemui.communal.ui.viewmodel.CommunalViewModel
import com.android.systemui.communal.ui.viewmodel.CommunalViewModel.Companion.POPUP_AUTO_HIDE_TIMEOUT_MS
import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.flags.Flags.COMMUNAL_SERVICE_ENABLED
+import com.android.systemui.flags.fakeFeatureFlagsClassic
import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository
import com.android.systemui.kosmos.testScope
@@ -92,7 +94,8 @@
mediaRepository = kosmos.fakeCommunalMediaRepository
userRepository = kosmos.fakeUserRepository
- kosmos.fakeCommunalRepository.setCommunalEnabledState(true)
+ kosmos.fakeFeatureFlagsClassic.set(COMMUNAL_SERVICE_ENABLED, true)
+ mSetFlagsRule.enableFlags(FLAG_COMMUNAL_HUB)
underTest =
CommunalViewModel(
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartableTest.kt
index 032d76f..8488843 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartableTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartableTest.kt
@@ -19,12 +19,15 @@
import android.content.pm.UserInfo
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
+import com.android.systemui.Flags.FLAG_COMMUNAL_HUB
import com.android.systemui.SysuiTestCase
-import com.android.systemui.communal.data.repository.fakeCommunalRepository
+import com.android.systemui.communal.data.repository.CommunalSettingsRepositoryImpl.Companion.GLANCEABLE_HUB_ENABLED
import com.android.systemui.communal.data.repository.fakeCommunalWidgetRepository
import com.android.systemui.communal.domain.interactor.communalInteractor
import com.android.systemui.communal.shared.model.CommunalWidgetContentModel
import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.flags.Flags
+import com.android.systemui.flags.fakeFeatureFlagsClassic
import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository
import com.android.systemui.kosmos.applicationCoroutineScope
import com.android.systemui.kosmos.testDispatcher
@@ -33,6 +36,7 @@
import com.android.systemui.user.data.repository.fakeUserRepository
import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.whenever
+import com.android.systemui.util.settings.fakeSettings
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableSharedFlow
@@ -62,6 +66,8 @@
fun setUp() {
MockitoAnnotations.initMocks(this)
kosmos.fakeUserRepository.setUserInfos(listOf(MAIN_USER_INFO))
+ kosmos.fakeFeatureFlagsClassic.set(Flags.COMMUNAL_SERVICE_ENABLED, true)
+ mSetFlagsRule.enableFlags(FLAG_COMMUNAL_HUB)
appWidgetIdToRemove = MutableSharedFlow()
whenever(appWidgetHost.appWidgetIdToRemove).thenReturn(appWidgetIdToRemove)
@@ -169,7 +175,8 @@
fakeKeyguardRepository.setIsEncryptedOrLockdown(false)
fakeUserRepository.setSelectedUserInfo(MAIN_USER_INFO)
fakeKeyguardRepository.setKeyguardShowing(true)
- fakeCommunalRepository.setCommunalEnabledState(available)
+ val settingsValue = if (available) 1 else 0
+ fakeSettings.putIntForUser(GLANCEABLE_HUB_ENABLED, settingsValue, MAIN_USER_INFO.id)
}
private companion object {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/data/repository/DeviceEntryFaceAuthRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/data/repository/DeviceEntryFaceAuthRepositoryTest.kt
index 6808f5d..b611e0a 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/data/repository/DeviceEntryFaceAuthRepositoryTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/data/repository/DeviceEntryFaceAuthRepositoryTest.kt
@@ -37,16 +37,11 @@
import androidx.test.filters.SmallTest
import com.android.internal.logging.InstanceId.fakeInstanceId
import com.android.internal.logging.UiEventLogger
-import com.android.keyguard.KeyguardUpdateMonitor
import com.android.systemui.Flags as AConfigFlags
import com.android.systemui.SysuiTestCase
-import com.android.systemui.biometrics.data.repository.FakeDisplayStateRepository
-import com.android.systemui.biometrics.data.repository.FakeFacePropertyRepository
-import com.android.systemui.biometrics.data.repository.FakeFingerprintPropertyRepository
-import com.android.systemui.biometrics.domain.interactor.DisplayStateInteractor
-import com.android.systemui.biometrics.domain.interactor.DisplayStateInteractorImpl
-import com.android.systemui.bouncer.data.repository.FakeKeyguardBouncerRepository
-import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor
+import com.android.systemui.biometrics.domain.interactor.displayStateInteractor
+import com.android.systemui.bouncer.data.repository.fakeKeyguardBouncerRepository
+import com.android.systemui.bouncer.domain.interactor.alternateBouncerInteractor
import com.android.systemui.coroutines.FlowValue
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.coroutines.collectValues
@@ -59,52 +54,46 @@
import com.android.systemui.deviceentry.shared.model.FaceDetectionStatus
import com.android.systemui.deviceentry.shared.model.HelpFaceAuthenticationStatus
import com.android.systemui.deviceentry.shared.model.SuccessFaceAuthenticationStatus
-import com.android.systemui.display.data.repository.FakeDisplayRepository
import com.android.systemui.display.data.repository.display
+import com.android.systemui.display.data.repository.displayRepository
import com.android.systemui.dump.DumpManager
import com.android.systemui.flags.FakeFeatureFlags
import com.android.systemui.keyguard.data.repository.BiometricType
-import com.android.systemui.keyguard.data.repository.FakeBiometricSettingsRepository
-import com.android.systemui.keyguard.data.repository.FakeCommandQueue
-import com.android.systemui.keyguard.data.repository.FakeDeviceEntryFingerprintAuthRepository
-import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
-import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
-import com.android.systemui.keyguard.data.repository.FakeTrustRepository
-import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
-import com.android.systemui.keyguard.domain.interactor.KeyguardInteractorFactory
-import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
-import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractorFactory
+import com.android.systemui.keyguard.data.repository.fakeBiometricSettingsRepository
+import com.android.systemui.keyguard.data.repository.fakeCommandQueue
+import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFingerprintAuthRepository
+import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository
+import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
+import com.android.systemui.keyguard.data.repository.fakeTrustRepository
+import com.android.systemui.keyguard.domain.interactor.keyguardInteractor
+import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor
import com.android.systemui.keyguard.shared.model.KeyguardState
import com.android.systemui.keyguard.shared.model.TransitionState
import com.android.systemui.keyguard.shared.model.TransitionStep
+import com.android.systemui.kosmos.testDispatcher
+import com.android.systemui.kosmos.testScope
import com.android.systemui.log.FaceAuthenticationLogger
import com.android.systemui.log.SessionTracker
import com.android.systemui.log.logcatLogBuffer
import com.android.systemui.log.table.TableLogBuffer
-import com.android.systemui.plugins.statusbar.StatusBarStateController
-import com.android.systemui.power.data.repository.FakePowerRepository
-import com.android.systemui.power.domain.interactor.PowerInteractor
import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAsleepForTest
import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAwakeForTest
-import com.android.systemui.power.domain.interactor.PowerInteractorFactory
+import com.android.systemui.power.domain.interactor.powerInteractor
import com.android.systemui.res.R
-import com.android.systemui.statusbar.phone.FakeKeyguardStateController
+import com.android.systemui.statusbar.commandQueue
import com.android.systemui.statusbar.phone.KeyguardBypassController
+import com.android.systemui.testKosmos
import com.android.systemui.user.data.model.SelectionStatus
-import com.android.systemui.user.data.repository.FakeUserRepository
-import com.android.systemui.util.concurrency.FakeExecutor
+import com.android.systemui.user.data.repository.fakeUserRepository
import com.android.systemui.util.mockito.KotlinArgumentCaptor
import com.android.systemui.util.mockito.captureMany
import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.whenever
import com.android.systemui.util.time.FakeSystemClock
-import com.android.systemui.util.time.SystemClock
import com.google.common.truth.Truth.assertThat
import java.io.PrintWriter
import java.io.StringWriter
import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.test.StandardTestDispatcher
-import kotlinx.coroutines.test.TestDispatcher
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.advanceTimeBy
import kotlinx.coroutines.test.runCurrent
@@ -129,6 +118,7 @@
@SmallTest
@RunWith(AndroidJUnit4::class)
class DeviceEntryFaceAuthRepositoryTest : SysuiTestCase() {
+ private val kosmos = testKosmos().apply { this.commandQueue = this.fakeCommandQueue }
private lateinit var underTest: DeviceEntryFaceAuthRepositoryImpl
@Mock private lateinit var faceManager: FaceManager
@@ -136,7 +126,6 @@
@Mock private lateinit var sessionTracker: SessionTracker
@Mock private lateinit var uiEventLogger: UiEventLogger
@Mock private lateinit var dumpManager: DumpManager
- @Mock private lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor
@Captor
private lateinit var authenticationCallback: ArgumentCaptor<FaceManager.AuthenticationCallback>
@@ -151,11 +140,11 @@
@Captor
private lateinit var faceLockoutResetCallback: ArgumentCaptor<FaceManager.LockoutResetCallback>
- private lateinit var testDispatcher: TestDispatcher
+ private val testDispatcher = kosmos.testDispatcher
- private lateinit var keyguardTransitionRepository: FakeKeyguardTransitionRepository
- private lateinit var testScope: TestScope
- private lateinit var fakeUserRepository: FakeUserRepository
+ private val keyguardTransitionRepository = kosmos.fakeKeyguardTransitionRepository
+ private val testScope = kosmos.testScope
+ private val fakeUserRepository = kosmos.fakeUserRepository
private lateinit var authStatus: FlowValue<FaceAuthenticationStatus?>
private lateinit var detectStatus: FlowValue<FaceDetectionStatus?>
private lateinit var authRunning: FlowValue<Boolean?>
@@ -163,88 +152,30 @@
private lateinit var lockedOut: FlowValue<Boolean?>
private lateinit var canFaceAuthRun: FlowValue<Boolean?>
private lateinit var authenticated: FlowValue<Boolean?>
- private lateinit var biometricSettingsRepository: FakeBiometricSettingsRepository
- private lateinit var deviceEntryFingerprintAuthRepository:
- FakeDeviceEntryFingerprintAuthRepository
- private lateinit var trustRepository: FakeTrustRepository
- private lateinit var keyguardRepository: FakeKeyguardRepository
- private lateinit var powerRepository: FakePowerRepository
- private lateinit var powerInteractor: PowerInteractor
- private lateinit var keyguardInteractor: KeyguardInteractor
- private lateinit var alternateBouncerInteractor: AlternateBouncerInteractor
- private lateinit var displayStateInteractor: DisplayStateInteractor
- private lateinit var bouncerRepository: FakeKeyguardBouncerRepository
- private lateinit var displayRepository: FakeDisplayRepository
- private lateinit var fakeCommandQueue: FakeCommandQueue
+ private val biometricSettingsRepository = kosmos.fakeBiometricSettingsRepository
+ private val deviceEntryFingerprintAuthRepository =
+ kosmos.fakeDeviceEntryFingerprintAuthRepository
+ private val trustRepository = kosmos.fakeTrustRepository
+ private val keyguardRepository = kosmos.fakeKeyguardRepository
+ private val powerInteractor = kosmos.powerInteractor
+ private val keyguardInteractor = kosmos.keyguardInteractor
+ private val alternateBouncerInteractor = kosmos.alternateBouncerInteractor
+ private val displayStateInteractor = kosmos.displayStateInteractor
+ private val bouncerRepository = kosmos.fakeKeyguardBouncerRepository
+ private val displayRepository = kosmos.displayRepository
+ private val fakeCommandQueue = kosmos.fakeCommandQueue
+ private val keyguardTransitionInteractor = kosmos.keyguardTransitionInteractor
private lateinit var featureFlags: FakeFeatureFlags
- private lateinit var fakeFacePropertyRepository: FakeFacePropertyRepository
private var wasAuthCancelled = false
private var wasDetectCancelled = false
- private lateinit var keyguardTransitionInteractor: KeyguardTransitionInteractor
-
@Before
fun setup() {
MockitoAnnotations.initMocks(this)
- testDispatcher = StandardTestDispatcher()
- testScope = TestScope(testDispatcher)
- fakeUserRepository = FakeUserRepository()
fakeUserRepository.setUserInfos(listOf(primaryUser, secondaryUser))
- biometricSettingsRepository = FakeBiometricSettingsRepository()
- deviceEntryFingerprintAuthRepository = FakeDeviceEntryFingerprintAuthRepository()
- trustRepository = FakeTrustRepository()
featureFlags = FakeFeatureFlags()
- powerRepository = FakePowerRepository()
- powerInteractor =
- PowerInteractorFactory.create(
- repository = powerRepository,
- )
- .powerInteractor
-
- val withDeps =
- KeyguardInteractorFactory.create(
- featureFlags = featureFlags,
- powerInteractor = powerInteractor,
- )
- keyguardInteractor = withDeps.keyguardInteractor
- keyguardRepository = withDeps.repository
- bouncerRepository = withDeps.bouncerRepository
-
- keyguardTransitionRepository = FakeKeyguardTransitionRepository()
- keyguardTransitionInteractor =
- KeyguardTransitionInteractorFactory.create(
- scope = testScope.backgroundScope,
- repository = keyguardTransitionRepository,
- keyguardInteractor = keyguardInteractor,
- )
- .keyguardTransitionInteractor
-
- fakeCommandQueue = withDeps.commandQueue
-
- alternateBouncerInteractor =
- AlternateBouncerInteractor(
- bouncerRepository = bouncerRepository,
- fingerprintPropertyRepository = FakeFingerprintPropertyRepository(),
- biometricSettingsRepository = biometricSettingsRepository,
- systemClock = mock(SystemClock::class.java),
- keyguardStateController = FakeKeyguardStateController(),
- statusBarStateController = mock(StatusBarStateController::class.java),
- keyguardUpdateMonitor = keyguardUpdateMonitor,
- scope = testScope.backgroundScope,
- )
-
- displayRepository = FakeDisplayRepository()
- displayStateInteractor =
- DisplayStateInteractorImpl(
- applicationScope = testScope.backgroundScope,
- context = context,
- mainExecutor = FakeExecutor(FakeSystemClock()),
- displayStateRepository = FakeDisplayStateRepository(),
- displayRepository = displayRepository,
- )
-
bypassStateChangedListener =
KotlinArgumentCaptor(KeyguardBypassController.OnBypassStateChangedListener::class.java)
whenever(sessionTracker.getSessionId(SESSION_KEYGUARD)).thenReturn(keyguardSessionId)
@@ -282,7 +213,6 @@
testScope.backgroundScope
)
- fakeFacePropertyRepository = FakeFacePropertyRepository()
return DeviceEntryFaceAuthRepositoryImpl(
mContext,
fmOverride,
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/homecontrols/HomeControlsComponentInteractorKosmos.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/homecontrols/HomeControlsComponentInteractorKosmos.kt
index efccf7a..2c890f4 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/homecontrols/HomeControlsComponentInteractorKosmos.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/homecontrols/HomeControlsComponentInteractorKosmos.kt
@@ -15,27 +15,32 @@
*/
package com.android.systemui.dreams.homecontrols
+import android.service.dream.dreamManager
+import com.android.systemui.common.domain.interactor.packageChangeInteractor
import com.android.systemui.controls.dagger.ControlsComponent
import com.android.systemui.controls.management.ControlsListingController
-import com.android.systemui.controls.panels.AuthorizedPanelsRepository
+import com.android.systemui.controls.panels.authorizedPanelsRepository
import com.android.systemui.controls.panels.selectedComponentRepository
import com.android.systemui.dreams.homecontrols.domain.interactor.HomeControlsComponentInteractor
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.applicationCoroutineScope
import com.android.systemui.user.data.repository.fakeUserRepository
import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.time.fakeSystemClock
val Kosmos.homeControlsComponentInteractor by
Kosmos.Fixture {
HomeControlsComponentInteractor(
selectedComponentRepository = selectedComponentRepository,
- controlsComponent,
+ controlsComponent = controlsComponent,
authorizedPanelsRepository = authorizedPanelsRepository,
userRepository = fakeUserRepository,
bgScope = applicationCoroutineScope,
+ systemClock = fakeSystemClock,
+ dreamManager = dreamManager,
+ packageChangeInteractor = packageChangeInteractor,
)
}
val Kosmos.controlsComponent by Kosmos.Fixture<ControlsComponent> { mock() }
val Kosmos.controlsListingController by Kosmos.Fixture<ControlsListingController> { mock() }
-val Kosmos.authorizedPanelsRepository by Kosmos.Fixture<AuthorizedPanelsRepository> { mock() }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/homecontrols/HomeControlsComponentInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/homecontrols/HomeControlsComponentInteractorTest.kt
index ce74a90..298ce70 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/homecontrols/HomeControlsComponentInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/homecontrols/HomeControlsComponentInteractorTest.kt
@@ -20,84 +20,70 @@
import android.content.pm.ApplicationInfo
import android.content.pm.ServiceInfo
import android.content.pm.UserInfo
+import android.os.UserHandle
+import android.service.dream.dreamManager
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
+import com.android.systemui.common.data.repository.fakePackageChangeRepository
import com.android.systemui.controls.ControlsServiceInfo
-import com.android.systemui.controls.dagger.ControlsComponent
-import com.android.systemui.controls.management.ControlsListingController
-import com.android.systemui.controls.panels.AuthorizedPanelsRepository
-import com.android.systemui.controls.panels.FakeSelectedComponentRepository
import com.android.systemui.controls.panels.SelectedComponentRepository
+import com.android.systemui.controls.panels.authorizedPanelsRepository
import com.android.systemui.controls.panels.selectedComponentRepository
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.dreams.homecontrols.domain.interactor.HomeControlsComponentInteractor
-import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.dreams.homecontrols.domain.interactor.HomeControlsComponentInteractor.Companion.MAX_UPDATE_CORRELATION_DELAY
import com.android.systemui.kosmos.testScope
+import com.android.systemui.settings.fakeUserTracker
import com.android.systemui.testKosmos
-import com.android.systemui.user.data.repository.FakeUserRepository
import com.android.systemui.user.data.repository.fakeUserRepository
import com.android.systemui.util.mockito.whenever
import com.android.systemui.util.mockito.withArgCaptor
+import com.android.systemui.util.time.fakeSystemClock
import com.google.common.truth.Truth.assertThat
import java.util.Optional
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
+import org.mockito.Mockito.never
import org.mockito.Mockito.verify
-import org.mockito.MockitoAnnotations
+@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
@RunWith(AndroidJUnit4::class)
class HomeControlsComponentInteractorTest : SysuiTestCase() {
private val kosmos = testKosmos()
- private lateinit var controlsComponent: ControlsComponent
- private lateinit var controlsListingController: ControlsListingController
- private lateinit var authorizedPanelsRepository: AuthorizedPanelsRepository
private lateinit var underTest: HomeControlsComponentInteractor
- private lateinit var userRepository: FakeUserRepository
- private lateinit var selectedComponentRepository: FakeSelectedComponentRepository
@Before
- fun setUp() {
- MockitoAnnotations.initMocks(this)
- userRepository = kosmos.fakeUserRepository
- userRepository.setUserInfos(listOf(PRIMARY_USER, ANOTHER_USER))
+ fun setUp() =
+ with(kosmos) {
+ fakeSystemClock.setCurrentTimeMillis(0)
+ fakeUserRepository.setUserInfos(listOf(PRIMARY_USER, ANOTHER_USER))
+ whenever(controlsComponent.getControlsListingController())
+ .thenReturn(Optional.of(controlsListingController))
- controlsComponent = kosmos.controlsComponent
- authorizedPanelsRepository = kosmos.authorizedPanelsRepository
- controlsListingController = kosmos.controlsListingController
- selectedComponentRepository = kosmos.selectedComponentRepository
-
- selectedComponentRepository.setCurrentUserHandle(PRIMARY_USER.userHandle)
- whenever(controlsComponent.getControlsListingController())
- .thenReturn(Optional.of(controlsListingController))
-
- underTest =
- HomeControlsComponentInteractor(
- selectedComponentRepository,
- controlsComponent,
- authorizedPanelsRepository,
- userRepository,
- kosmos.applicationCoroutineScope,
- )
- }
+ underTest = homeControlsComponentInteractor
+ }
@Test
fun testPanelComponentReturnsComponentNameForSelectedItemByUser() =
with(kosmos) {
testScope.runTest {
- whenever(authorizedPanelsRepository.getAuthorizedPanels())
- .thenReturn(setOf(TEST_PACKAGE_PANEL))
- userRepository.setSelectedUserInfo(PRIMARY_USER)
+ setActiveUser(PRIMARY_USER)
+ authorizedPanelsRepository.addAuthorizedPanels(setOf(TEST_PACKAGE))
selectedComponentRepository.setSelectedComponent(TEST_SELECTED_COMPONENT_PANEL)
val actualValue by collectLastValue(underTest.panelComponent)
assertThat(actualValue).isNull()
runServicesUpdate()
- assertThat(actualValue).isEqualTo(TEST_COMPONENT_PANEL)
+ assertThat(actualValue).isEqualTo(TEST_COMPONENT)
}
}
@@ -105,16 +91,15 @@
fun testPanelComponentReturnsComponentNameAsInitialValueWithoutServiceUpdate() =
with(kosmos) {
testScope.runTest {
- whenever(authorizedPanelsRepository.getAuthorizedPanels())
- .thenReturn(setOf(TEST_PACKAGE_PANEL))
- userRepository.setSelectedUserInfo(PRIMARY_USER)
+ setActiveUser(PRIMARY_USER)
+ authorizedPanelsRepository.addAuthorizedPanels(setOf(TEST_PACKAGE))
selectedComponentRepository.setSelectedComponent(TEST_SELECTED_COMPONENT_PANEL)
whenever(controlsListingController.getCurrentServices())
.thenReturn(
- listOf(ControlsServiceInfo(TEST_COMPONENT_PANEL, "panel", hasPanel = true))
+ listOf(ControlsServiceInfo(TEST_COMPONENT, "panel", hasPanel = true))
)
val actualValue by collectLastValue(underTest.panelComponent)
- assertThat(actualValue).isEqualTo(TEST_COMPONENT_PANEL)
+ assertThat(actualValue).isEqualTo(TEST_COMPONENT)
}
}
@@ -122,9 +107,8 @@
fun testPanelComponentReturnsNullForHomeControlsThatDoesNotSupportPanel() =
with(kosmos) {
testScope.runTest {
- whenever(authorizedPanelsRepository.getAuthorizedPanels())
- .thenReturn(setOf(TEST_PACKAGE_PANEL))
- userRepository.setSelectedUserInfo(PRIMARY_USER)
+ setActiveUser(PRIMARY_USER)
+ authorizedPanelsRepository.addAuthorizedPanels(setOf(TEST_PACKAGE))
selectedComponentRepository.setSelectedComponent(TEST_SELECTED_COMPONENT_NON_PANEL)
val actualValue by collectLastValue(underTest.panelComponent)
assertThat(actualValue).isNull()
@@ -137,8 +121,8 @@
fun testPanelComponentReturnsNullWhenPanelIsUnauthorized() =
with(kosmos) {
testScope.runTest {
- whenever(authorizedPanelsRepository.getAuthorizedPanels()).thenReturn(setOf())
- userRepository.setSelectedUserInfo(PRIMARY_USER)
+ setActiveUser(PRIMARY_USER)
+ authorizedPanelsRepository.removeAuthorizedPanels(setOf(TEST_PACKAGE))
selectedComponentRepository.setSelectedComponent(TEST_SELECTED_COMPONENT_PANEL)
val actualValue by collectLastValue(underTest.panelComponent)
assertThat(actualValue).isNull()
@@ -151,17 +135,24 @@
fun testPanelComponentReturnsComponentNameForDifferentUsers() =
with(kosmos) {
testScope.runTest {
- whenever(authorizedPanelsRepository.getAuthorizedPanels())
- .thenReturn(setOf(TEST_PACKAGE_PANEL))
- userRepository.setSelectedUserInfo(ANOTHER_USER)
+ val actualValue by collectLastValue(underTest.panelComponent)
+
+ // Secondary user has non-panel selected.
+ setActiveUser(ANOTHER_USER)
selectedComponentRepository.setSelectedComponent(TEST_SELECTED_COMPONENT_NON_PANEL)
- selectedComponentRepository.setCurrentUserHandle(ANOTHER_USER.userHandle)
+
+ // Primary user has panel selected.
+ setActiveUser(PRIMARY_USER)
+ authorizedPanelsRepository.addAuthorizedPanels(setOf(TEST_PACKAGE))
selectedComponentRepository.setSelectedComponent(TEST_SELECTED_COMPONENT_PANEL)
- val actualValue by collectLastValue(underTest.panelComponent)
- assertThat(actualValue).isNull()
runServicesUpdate()
- assertThat(actualValue).isEqualTo(TEST_COMPONENT_PANEL)
+ assertThat(actualValue).isEqualTo(TEST_COMPONENT)
+
+ // Back to secondary user, should be null.
+ setActiveUser(ANOTHER_USER)
+ runServicesUpdate()
+ assertThat(actualValue).isNull()
}
}
@@ -169,24 +160,119 @@
fun testPanelComponentReturnsNullWhenControlsComponentReturnsNullForListingController() =
with(kosmos) {
testScope.runTest {
- whenever(authorizedPanelsRepository.getAuthorizedPanels())
- .thenReturn(setOf(TEST_PACKAGE_PANEL))
+ authorizedPanelsRepository.addAuthorizedPanels(setOf(TEST_PACKAGE))
whenever(controlsComponent.getControlsListingController())
.thenReturn(Optional.empty())
- userRepository.setSelectedUserInfo(PRIMARY_USER)
+ fakeUserRepository.setSelectedUserInfo(PRIMARY_USER)
selectedComponentRepository.setSelectedComponent(TEST_SELECTED_COMPONENT_PANEL)
val actualValue by collectLastValue(underTest.panelComponent)
assertThat(actualValue).isNull()
}
}
+ @Test
+ fun testMonitoringUpdatesAndRestart() =
+ with(kosmos) {
+ testScope.runTest {
+ setActiveUser(PRIMARY_USER)
+ authorizedPanelsRepository.addAuthorizedPanels(setOf(TEST_PACKAGE))
+ selectedComponentRepository.setSelectedComponent(TEST_SELECTED_COMPONENT_PANEL)
+ whenever(controlsListingController.getCurrentServices())
+ .thenReturn(
+ listOf(ControlsServiceInfo(TEST_COMPONENT, "panel", hasPanel = true))
+ )
+
+ val job = launch { underTest.monitorUpdatesAndRestart() }
+ val panelComponent by collectLastValue(underTest.panelComponent)
+
+ assertThat(panelComponent).isEqualTo(TEST_COMPONENT)
+ verify(dreamManager, never()).startDream()
+
+ fakeSystemClock.advanceTime(100)
+ // The package update is started.
+ fakePackageChangeRepository.notifyUpdateStarted(
+ TEST_PACKAGE,
+ UserHandle.of(PRIMARY_USER_ID),
+ )
+ fakeSystemClock.advanceTime(MAX_UPDATE_CORRELATION_DELAY.inWholeMilliseconds)
+ // Task fragment becomes empty as a result of the update.
+ underTest.onTaskFragmentEmpty()
+
+ runCurrent()
+ verify(dreamManager, never()).startDream()
+
+ fakeSystemClock.advanceTime(500)
+ // The package update is finished.
+ fakePackageChangeRepository.notifyUpdateFinished(
+ TEST_PACKAGE,
+ UserHandle.of(PRIMARY_USER_ID),
+ )
+
+ runCurrent()
+ verify(dreamManager).startDream()
+ job.cancel()
+ }
+ }
+
+ @Test
+ fun testMonitoringUpdatesAndRestart_dreamEndsAfterDelay() =
+ with(kosmos) {
+ testScope.runTest {
+ setActiveUser(PRIMARY_USER)
+ authorizedPanelsRepository.addAuthorizedPanels(setOf(TEST_PACKAGE))
+ selectedComponentRepository.setSelectedComponent(TEST_SELECTED_COMPONENT_PANEL)
+ whenever(controlsListingController.getCurrentServices())
+ .thenReturn(
+ listOf(ControlsServiceInfo(TEST_COMPONENT, "panel", hasPanel = true))
+ )
+
+ val job = launch { underTest.monitorUpdatesAndRestart() }
+ val panelComponent by collectLastValue(underTest.panelComponent)
+
+ assertThat(panelComponent).isEqualTo(TEST_COMPONENT)
+ verify(dreamManager, never()).startDream()
+
+ fakeSystemClock.advanceTime(100)
+ // The package update is started.
+ fakePackageChangeRepository.notifyUpdateStarted(
+ TEST_PACKAGE,
+ UserHandle.of(PRIMARY_USER_ID),
+ )
+ fakeSystemClock.advanceTime(MAX_UPDATE_CORRELATION_DELAY.inWholeMilliseconds + 100)
+ // Task fragment becomes empty as a result of the update.
+ underTest.onTaskFragmentEmpty()
+
+ runCurrent()
+ verify(dreamManager, never()).startDream()
+
+ fakeSystemClock.advanceTime(500)
+ // The package update is finished.
+ fakePackageChangeRepository.notifyUpdateFinished(
+ TEST_PACKAGE,
+ UserHandle.of(PRIMARY_USER_ID),
+ )
+
+ runCurrent()
+ verify(dreamManager, never()).startDream()
+ job.cancel()
+ }
+ }
+
private fun runServicesUpdate(hasPanelBoolean: Boolean = true) {
val listings =
- listOf(ControlsServiceInfo(TEST_COMPONENT_PANEL, "panel", hasPanel = hasPanelBoolean))
- val callback = withArgCaptor { verify(controlsListingController).addCallback(capture()) }
+ listOf(ControlsServiceInfo(TEST_COMPONENT, "panel", hasPanel = hasPanelBoolean))
+ val callback = withArgCaptor {
+ verify(kosmos.controlsListingController).addCallback(capture())
+ }
callback.onServicesUpdated(listings)
}
+ private suspend fun TestScope.setActiveUser(user: UserInfo) {
+ kosmos.fakeUserRepository.setSelectedUserInfo(user)
+ kosmos.fakeUserTracker.set(listOf(user), 0)
+ runCurrent()
+ }
+
private fun ControlsServiceInfo(
componentName: ComponentName,
label: CharSequence,
@@ -237,19 +323,9 @@
)
private const val TEST_PACKAGE = "pkg"
private val TEST_COMPONENT = ComponentName(TEST_PACKAGE, "service")
- private const val TEST_PACKAGE_PANEL = "pkg.panel"
- private val TEST_COMPONENT_PANEL = ComponentName(TEST_PACKAGE_PANEL, "service")
private val TEST_SELECTED_COMPONENT_PANEL =
- SelectedComponentRepository.SelectedComponent(
- TEST_PACKAGE_PANEL,
- TEST_COMPONENT_PANEL,
- true
- )
+ SelectedComponentRepository.SelectedComponent(TEST_PACKAGE, TEST_COMPONENT, true)
private val TEST_SELECTED_COMPONENT_NON_PANEL =
- SelectedComponentRepository.SelectedComponent(
- TEST_PACKAGE_PANEL,
- TEST_COMPONENT_PANEL,
- false
- )
+ SelectedComponentRepository.SelectedComponent(TEST_PACKAGE, TEST_COMPONENT, false)
}
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/homecontrols/HomeControlsDreamServiceTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/homecontrols/HomeControlsDreamServiceTest.kt
index d28b6bf..0a3aea7 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/homecontrols/HomeControlsDreamServiceTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/homecontrols/HomeControlsDreamServiceTest.kt
@@ -16,20 +16,19 @@
package com.android.systemui.dreams.homecontrols
import android.app.Activity
-import android.content.ComponentName
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
-import com.android.systemui.controls.dagger.ControlsComponent
-import com.android.systemui.controls.management.ControlsListingController
import com.android.systemui.controls.settings.FakeControlsSettingsRepository
-import com.android.systemui.dreams.homecontrols.domain.interactor.HomeControlsComponentInteractor
-import com.android.systemui.log.LogBuffer
+import com.android.systemui.kosmos.testDispatcher
+import com.android.systemui.kosmos.testScope
import com.android.systemui.log.core.FakeLogBuffer.Factory.Companion.create
+import com.android.systemui.log.logcatLogBuffer
import com.android.systemui.testKosmos
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.whenever
import java.util.Optional
+import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@@ -43,78 +42,60 @@
class HomeControlsDreamServiceTest : SysuiTestCase() {
private val kosmos = testKosmos()
+ private val testScope = kosmos.testScope
- private lateinit var controlsSettingsRepository: FakeControlsSettingsRepository
@Mock private lateinit var taskFragmentComponentFactory: TaskFragmentComponent.Factory
@Mock private lateinit var taskFragmentComponent: TaskFragmentComponent
@Mock private lateinit var activity: Activity
- private val logBuffer: LogBuffer = create()
private lateinit var underTest: HomeControlsDreamService
- private lateinit var homeControlsComponentInteractor: HomeControlsComponentInteractor
- private lateinit var fakeDreamActivityProvider: DreamActivityProvider
- private lateinit var controlsComponent: ControlsComponent
- private lateinit var controlsListingController: ControlsListingController
@Before
- fun setup() {
- MockitoAnnotations.initMocks(this)
- whenever(taskFragmentComponentFactory.create(any(), any(), any(), any()))
- .thenReturn(taskFragmentComponent)
+ fun setup() =
+ with(kosmos) {
+ MockitoAnnotations.initMocks(this@HomeControlsDreamServiceTest)
+ whenever(taskFragmentComponentFactory.create(any(), any(), any(), any()))
+ .thenReturn(taskFragmentComponent)
- controlsSettingsRepository = FakeControlsSettingsRepository()
- controlsSettingsRepository.setAllowActionOnTrivialControlsInLockscreen(true)
+ whenever(controlsComponent.getControlsListingController())
+ .thenReturn(Optional.of(controlsListingController))
- controlsComponent = kosmos.controlsComponent
- controlsListingController = kosmos.controlsListingController
+ underTest = buildService { activity }
+ }
- whenever(controlsComponent.getControlsListingController())
- .thenReturn(Optional.of(controlsListingController))
+ @Test
+ fun testOnAttachedToWindowCreatesTaskFragmentComponent() =
+ testScope.runTest {
+ underTest.onAttachedToWindow()
+ verify(taskFragmentComponentFactory).create(any(), any(), any(), any())
+ }
- homeControlsComponentInteractor = kosmos.homeControlsComponentInteractor
+ @Test
+ fun testOnDetachedFromWindowDestroyTaskFragmentComponent() =
+ testScope.runTest {
+ underTest.onAttachedToWindow()
+ underTest.onDetachedFromWindow()
+ verify(taskFragmentComponent).destroy()
+ }
- fakeDreamActivityProvider = DreamActivityProvider { activity }
- underTest =
- HomeControlsDreamService(
- controlsSettingsRepository,
- taskFragmentComponentFactory,
- homeControlsComponentInteractor,
- fakeDreamActivityProvider,
- logBuffer
+ @Test
+ fun testNotCreatingTaskFragmentComponentWhenActivityIsNull() =
+ testScope.runTest {
+ underTest = buildService { null }
+
+ underTest.onAttachedToWindow()
+ verify(taskFragmentComponentFactory, never()).create(any(), any(), any(), any())
+ }
+
+ private fun buildService(activityProvider: DreamActivityProvider): HomeControlsDreamService =
+ with(kosmos) {
+ return HomeControlsDreamService(
+ controlsSettingsRepository = FakeControlsSettingsRepository(),
+ taskFragmentFactory = taskFragmentComponentFactory,
+ homeControlsComponentInteractor = homeControlsComponentInteractor,
+ dreamActivityProvider = activityProvider,
+ bgDispatcher = testDispatcher,
+ logBuffer = logcatLogBuffer("HomeControlsDreamServiceTest")
)
- }
-
- @Test
- fun testOnAttachedToWindowCreatesTaskFragmentComponent() {
- underTest.onAttachedToWindow()
- verify(taskFragmentComponentFactory).create(any(), any(), any(), any())
- }
-
- @Test
- fun testOnDetachedFromWindowDestroyTaskFragmentComponent() {
- underTest.onAttachedToWindow()
- underTest.onDetachedFromWindow()
- verify(taskFragmentComponent).destroy()
- }
-
- @Test
- fun testNotCreatingTaskFragmentComponentWhenActivityIsNull() {
- fakeDreamActivityProvider = DreamActivityProvider { null }
- underTest =
- HomeControlsDreamService(
- controlsSettingsRepository,
- taskFragmentComponentFactory,
- homeControlsComponentInteractor,
- fakeDreamActivityProvider,
- logBuffer
- )
-
- underTest.onAttachedToWindow()
- verify(taskFragmentComponentFactory, never()).create(any(), any(), any(), any())
- }
-
- companion object {
- private const val TEST_PACKAGE_PANEL = "pkg.panel"
- private val TEST_COMPONENT_PANEL = ComponentName(TEST_PACKAGE_PANEL, "service")
- }
+ }
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/homecontrols/HomeControlsDreamStartableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/homecontrols/HomeControlsDreamStartableTest.kt
index 6610e70..87b1bbb 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/homecontrols/HomeControlsDreamStartableTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/homecontrols/HomeControlsDreamStartableTest.kt
@@ -32,6 +32,7 @@
import com.android.systemui.controls.management.ControlsListingController
import com.android.systemui.controls.panels.AuthorizedPanelsRepository
import com.android.systemui.controls.panels.SelectedComponentRepository
+import com.android.systemui.controls.panels.authorizedPanelsRepository
import com.android.systemui.controls.panels.selectedComponentRepository
import com.android.systemui.dreams.homecontrols.domain.interactor.HomeControlsComponentInteractor
import com.android.systemui.kosmos.applicationCoroutineScope
@@ -84,8 +85,7 @@
userRepository.setUserInfos(listOf(PRIMARY_USER))
- whenever(authorizedPanelsRepository.getAuthorizedPanels())
- .thenReturn(setOf(TEST_PACKAGE_PANEL))
+ authorizedPanelsRepository.addAuthorizedPanels(setOf(TEST_PACKAGE_PANEL))
whenever(controlsComponent.getControlsListingController())
.thenReturn(Optional.of(controlsListingController))
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/haptics/slider/SeekableSliderHapticPluginTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/haptics/slider/SeekableSliderHapticPluginTest.kt
index ea766f8..805b4a8 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/haptics/slider/SeekableSliderHapticPluginTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/haptics/slider/SeekableSliderHapticPluginTest.kt
@@ -26,7 +26,6 @@
import com.android.systemui.util.mockito.whenever
import com.android.systemui.util.time.fakeSystemClock
import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestScope
@@ -75,7 +74,7 @@
fun start_afterStop_startsTheTrackingAgain() = runOnStartedPlugin {
// WHEN the plugin is restarted
plugin.stop()
- plugin.start()
+ plugin.startInScope(CoroutineScope(UnconfinedTestDispatcher(testScheduler)))
// THEN the tracking begins again
assertThat(plugin.isTracking).isTrue()
@@ -131,22 +130,21 @@
private fun runOnStartedPlugin(test: suspend TestScope.() -> Unit) =
with(kosmos) {
testScope.runTest {
- createPlugin(this, UnconfinedTestDispatcher(testScheduler))
- // GIVEN that the plugin is started
- plugin.start()
+ val pluginScope = CoroutineScope(UnconfinedTestDispatcher(testScheduler))
+ createPlugin()
+ // GIVEN that the plugin is started in a test scope
+ plugin.startInScope(pluginScope)
// THEN run the test
test()
}
}
- private fun createPlugin(scope: CoroutineScope, dispatcher: CoroutineDispatcher) {
+ private fun createPlugin() {
plugin =
SeekableSliderHapticPlugin(
vibratorHelper,
kosmos.fakeSystemClock,
- dispatcher,
- scope,
)
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfigTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfigTest.kt
index a613ad8..0768340 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfigTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfigTest.kt
@@ -23,14 +23,14 @@
import android.service.quickaccesswallet.WalletCard
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
-import com.android.systemui.res.R
import com.android.systemui.SysuiTestCase
-import com.android.systemui.animation.ActivityLaunchAnimator
+import com.android.systemui.animation.ActivityTransitionAnimator
import com.android.systemui.animation.Expandable
import com.android.systemui.common.shared.model.ContentDescription
import com.android.systemui.common.shared.model.Icon
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.plugins.ActivityStarter
+import com.android.systemui.res.R
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.whenever
@@ -76,26 +76,28 @@
}
@Test
- fun affordance_keyguardShowing_hasWalletCard_visibleModel() = testScope.runTest {
- setUpState()
+ fun affordance_keyguardShowing_hasWalletCard_visibleModel() =
+ testScope.runTest {
+ setUpState()
- val latest by collectLastValue(underTest.lockScreenState)
+ val latest by collectLastValue(underTest.lockScreenState)
- val visibleModel = latest as KeyguardQuickAffordanceConfig.LockScreenState.Visible
- assertThat(visibleModel.icon)
- .isEqualTo(
- Icon.Loaded(
- drawable = ICON,
- contentDescription =
- ContentDescription.Resource(
- res = R.string.accessibility_wallet_button,
- ),
+ val visibleModel = latest as KeyguardQuickAffordanceConfig.LockScreenState.Visible
+ assertThat(visibleModel.icon)
+ .isEqualTo(
+ Icon.Loaded(
+ drawable = ICON,
+ contentDescription =
+ ContentDescription.Resource(
+ res = R.string.accessibility_wallet_button,
+ ),
+ )
)
- )
- }
+ }
@Test
- fun affordance_keyguardShowing_hasNonPaymentCard_modelIsNone() = testScope.runTest {
+ fun affordance_keyguardShowing_hasNonPaymentCard_modelIsNone() =
+ testScope.runTest {
setUpState(cardType = WalletCard.CARD_TYPE_NON_PAYMENT)
val latest by collectLastValue(underTest.lockScreenState)
@@ -104,54 +106,58 @@
}
@Test
- fun affordance_keyguardShowing_hasPaymentCard_visibleModel() = testScope.runTest {
- setUpState(cardType = WalletCard.CARD_TYPE_PAYMENT)
+ fun affordance_keyguardShowing_hasPaymentCard_visibleModel() =
+ testScope.runTest {
+ setUpState(cardType = WalletCard.CARD_TYPE_PAYMENT)
- val latest by collectLastValue(underTest.lockScreenState)
+ val latest by collectLastValue(underTest.lockScreenState)
- val visibleModel = latest as KeyguardQuickAffordanceConfig.LockScreenState.Visible
- assertThat(visibleModel.icon)
- .isEqualTo(
- Icon.Loaded(
- drawable = ICON,
- contentDescription =
- ContentDescription.Resource(
- res = R.string.accessibility_wallet_button,
- ),
+ val visibleModel = latest as KeyguardQuickAffordanceConfig.LockScreenState.Visible
+ assertThat(visibleModel.icon)
+ .isEqualTo(
+ Icon.Loaded(
+ drawable = ICON,
+ contentDescription =
+ ContentDescription.Resource(
+ res = R.string.accessibility_wallet_button,
+ ),
+ )
)
- )
- }
+ }
@Test
- fun affordance_walletFeatureNotEnabled_modelIsNone() = testScope.runTest {
- setUpState(isWalletFeatureAvailable = false)
+ fun affordance_walletFeatureNotEnabled_modelIsNone() =
+ testScope.runTest {
+ setUpState(isWalletFeatureAvailable = false)
- val latest by collectLastValue(underTest.lockScreenState)
+ val latest by collectLastValue(underTest.lockScreenState)
- assertThat(latest).isEqualTo(KeyguardQuickAffordanceConfig.LockScreenState.Hidden)
- }
+ assertThat(latest).isEqualTo(KeyguardQuickAffordanceConfig.LockScreenState.Hidden)
+ }
@Test
- fun affordance_queryNotSuccessful_modelIsNone() = testScope.runTest {
- setUpState(isWalletQuerySuccessful = false)
+ fun affordance_queryNotSuccessful_modelIsNone() =
+ testScope.runTest {
+ setUpState(isWalletQuerySuccessful = false)
- val latest by collectLastValue(underTest.lockScreenState)
+ val latest by collectLastValue(underTest.lockScreenState)
- assertThat(latest).isEqualTo(KeyguardQuickAffordanceConfig.LockScreenState.Hidden)
- }
+ assertThat(latest).isEqualTo(KeyguardQuickAffordanceConfig.LockScreenState.Hidden)
+ }
@Test
- fun affordance_noSelectedCard_modelIsNone() = testScope.runTest {
- setUpState(hasSelectedCard = false)
+ fun affordance_noSelectedCard_modelIsNone() =
+ testScope.runTest {
+ setUpState(hasSelectedCard = false)
- val latest by collectLastValue(underTest.lockScreenState)
+ val latest by collectLastValue(underTest.lockScreenState)
- assertThat(latest).isEqualTo(KeyguardQuickAffordanceConfig.LockScreenState.Hidden)
- }
+ assertThat(latest).isEqualTo(KeyguardQuickAffordanceConfig.LockScreenState.Hidden)
+ }
@Test
fun onQuickAffordanceTriggered() {
- val animationController: ActivityLaunchAnimator.Controller = mock()
+ val animationController: ActivityTransitionAnimator.Controller = mock()
val expandable: Expandable = mock {
whenever(this.activityLaunchController()).thenReturn(animationController)
}
@@ -167,42 +173,46 @@
}
@Test
- fun getPickerScreenState_default() = testScope.runTest {
- setUpState()
+ fun getPickerScreenState_default() =
+ testScope.runTest {
+ setUpState()
- assertThat(underTest.getPickerScreenState())
- .isEqualTo(KeyguardQuickAffordanceConfig.PickerScreenState.Default())
- }
+ assertThat(underTest.getPickerScreenState())
+ .isEqualTo(KeyguardQuickAffordanceConfig.PickerScreenState.Default())
+ }
@Test
- fun getPickerScreenState_unavailable() = testScope.runTest {
- setUpState(
- isWalletServiceAvailable = false,
- )
+ fun getPickerScreenState_unavailable() =
+ testScope.runTest {
+ setUpState(
+ isWalletServiceAvailable = false,
+ )
- assertThat(underTest.getPickerScreenState())
- .isEqualTo(KeyguardQuickAffordanceConfig.PickerScreenState.UnavailableOnDevice)
- }
+ assertThat(underTest.getPickerScreenState())
+ .isEqualTo(KeyguardQuickAffordanceConfig.PickerScreenState.UnavailableOnDevice)
+ }
@Test
- fun getPickerScreenState_disabledWhenTheFeatureIsNotEnabled() = testScope.runTest {
- setUpState(
- isWalletFeatureAvailable = false,
- )
+ fun getPickerScreenState_disabledWhenTheFeatureIsNotEnabled() =
+ testScope.runTest {
+ setUpState(
+ isWalletFeatureAvailable = false,
+ )
- assertThat(underTest.getPickerScreenState())
- .isInstanceOf(KeyguardQuickAffordanceConfig.PickerScreenState.Disabled::class.java)
- }
+ assertThat(underTest.getPickerScreenState())
+ .isInstanceOf(KeyguardQuickAffordanceConfig.PickerScreenState.Disabled::class.java)
+ }
@Test
- fun getPickerScreenState_disabledWhenThereIsNoCard() = testScope.runTest {
- setUpState(
- hasSelectedCard = false,
- )
+ fun getPickerScreenState_disabledWhenThereIsNoCard() =
+ testScope.runTest {
+ setUpState(
+ hasSelectedCard = false,
+ )
- assertThat(underTest.getPickerScreenState())
- .isInstanceOf(KeyguardQuickAffordanceConfig.PickerScreenState.Disabled::class.java)
- }
+ assertThat(underTest.getPickerScreenState())
+ .isInstanceOf(KeyguardQuickAffordanceConfig.PickerScreenState.Disabled::class.java)
+ }
private fun setUpState(
isWalletFeatureAvailable: Boolean = true,
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardLongPressInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardLongPressInteractorTest.kt
index 98f0211..9d34903 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardLongPressInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardLongPressInteractorTest.kt
@@ -18,23 +18,27 @@
package com.android.systemui.keyguard.domain.interactor
import android.content.Intent
+import android.view.accessibility.accessibilityManagerWrapper
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
-import com.android.internal.logging.UiEventLogger
+import com.android.internal.logging.testing.UiEventLoggerFake
+import com.android.internal.logging.uiEventLogger
import com.android.systemui.SysuiTestCase
import com.android.systemui.coroutines.collectLastValue
-import com.android.systemui.flags.FakeFeatureFlags
import com.android.systemui.flags.Flags
-import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
-import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
+import com.android.systemui.flags.fakeFeatureFlagsClassic
+import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository
+import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.kosmos.testScope
import com.android.systemui.res.R
import com.android.systemui.statusbar.policy.AccessibilityManagerWrapper
+import com.android.systemui.testKosmos
+import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.whenever
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.runBlocking
-import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.advanceTimeBy
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
@@ -43,7 +47,6 @@
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentMatchers.anyInt
-import org.mockito.Mock
import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations
@@ -51,27 +54,25 @@
@SmallTest
@RunWith(AndroidJUnit4::class)
class KeyguardLongPressInteractorTest : SysuiTestCase() {
-
- @Mock private lateinit var logger: UiEventLogger
- @Mock private lateinit var accessibilityManager: AccessibilityManagerWrapper
+ private val kosmos =
+ testKosmos().apply {
+ this.accessibilityManagerWrapper = mock<AccessibilityManagerWrapper>()
+ this.uiEventLogger = mock<UiEventLoggerFake>()
+ }
private lateinit var underTest: KeyguardLongPressInteractor
- private lateinit var testScope: TestScope
- private lateinit var keyguardRepository: FakeKeyguardRepository
- private lateinit var keyguardTransitionRepository: FakeKeyguardTransitionRepository
+ private val logger = kosmos.uiEventLogger
+ private val testScope = kosmos.testScope
+ private val keyguardRepository = kosmos.fakeKeyguardRepository
+ private val keyguardTransitionRepository = kosmos.fakeKeyguardTransitionRepository
@Before
fun setUp() {
MockitoAnnotations.initMocks(this)
overrideResource(R.bool.long_press_keyguard_customize_lockscreen_enabled, true)
- whenever(accessibilityManager.getRecommendedTimeoutMillis(anyInt(), anyInt())).thenAnswer {
- it.arguments[0]
- }
-
- testScope = TestScope()
- keyguardRepository = FakeKeyguardRepository()
- keyguardTransitionRepository = FakeKeyguardTransitionRepository()
+ whenever(kosmos.accessibilityManagerWrapper.getRecommendedTimeoutMillis(anyInt(), anyInt()))
+ .thenAnswer { it.arguments[0] }
runBlocking { createUnderTest() }
}
@@ -284,25 +285,22 @@
isRevampedWppFeatureEnabled: Boolean = true,
isOpenWppDirectlyEnabled: Boolean = false,
) {
+ // This needs to be re-created for each test outside of kosmos since the flag values are
+ // read during initialization to set up flows. Maybe there is a better way to handle that.
underTest =
KeyguardLongPressInteractor(
appContext = mContext,
scope = testScope.backgroundScope,
- transitionInteractor =
- KeyguardTransitionInteractorFactory.create(
- scope = testScope.backgroundScope,
- repository = keyguardTransitionRepository,
- )
- .keyguardTransitionInteractor,
+ transitionInteractor = kosmos.keyguardTransitionInteractor,
repository = keyguardRepository,
logger = logger,
featureFlags =
- FakeFeatureFlags().apply {
+ kosmos.fakeFeatureFlagsClassic.apply {
set(Flags.LOCK_SCREEN_LONG_PRESS_ENABLED, isLongPressFeatureEnabled)
set(Flags.LOCK_SCREEN_LONG_PRESS_DIRECT_TO_WPP, isOpenWppDirectlyEnabled)
},
broadcastDispatcher = fakeBroadcastDispatcher,
- accessibilityManager = accessibilityManager
+ accessibilityManager = kosmos.accessibilityManagerWrapper
)
setUpState()
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/LightRevealScrimInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/LightRevealScrimInteractorTest.kt
index ce43d4e..9b302ae 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/LightRevealScrimInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/LightRevealScrimInteractorTest.kt
@@ -19,23 +19,23 @@
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
-import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
+import com.android.systemui.keyguard.data.fakeLightRevealScrimRepository
import com.android.systemui.keyguard.data.repository.FakeLightRevealScrimRepository
+import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
import com.android.systemui.keyguard.shared.model.KeyguardState
import com.android.systemui.keyguard.shared.model.TransitionState
import com.android.systemui.keyguard.shared.model.TransitionStep
+import com.android.systemui.kosmos.testScope
import com.android.systemui.statusbar.LightRevealEffect
import com.android.systemui.statusbar.LightRevealScrim
-import com.android.systemui.util.mockito.mock
+import com.android.systemui.testKosmos
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
-import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertEquals
-import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mockito
@@ -43,29 +43,22 @@
import org.mockito.Mockito.never
import org.mockito.Mockito.reset
import org.mockito.Mockito.verify
-import org.mockito.MockitoAnnotations
@SmallTest
@OptIn(ExperimentalCoroutinesApi::class)
@RunWith(AndroidJUnit4::class)
class LightRevealScrimInteractorTest : SysuiTestCase() {
- private val fakeKeyguardTransitionRepository = FakeKeyguardTransitionRepository()
+ val kosmos =
+ testKosmos().apply {
+ this.fakeLightRevealScrimRepository = Mockito.spy(FakeLightRevealScrimRepository())
+ }
- private val fakeLightRevealScrimRepository by lazy {
- Mockito.spy(FakeLightRevealScrimRepository())
- }
+ private val fakeLightRevealScrimRepository = kosmos.fakeLightRevealScrimRepository
- private val testScope = TestScope()
+ private val fakeKeyguardTransitionRepository = kosmos.fakeKeyguardTransitionRepository
+ private val testScope = kosmos.testScope
- private val keyguardTransitionInteractor by lazy {
- KeyguardTransitionInteractorFactory.create(
- scope = testScope.backgroundScope,
- repository = fakeKeyguardTransitionRepository,
- )
- .keyguardTransitionInteractor
- }
-
- private lateinit var underTest: LightRevealScrimInteractor
+ private val underTest = kosmos.lightRevealScrimInteractor
private val reveal1 =
object : LightRevealEffect {
@@ -77,19 +70,6 @@
override fun setRevealAmountOnScrim(amount: Float, scrim: LightRevealScrim) {}
}
- @Before
- fun setUp() {
- MockitoAnnotations.initMocks(this)
- underTest =
- LightRevealScrimInteractor(
- keyguardTransitionInteractor,
- fakeLightRevealScrimRepository,
- testScope.backgroundScope,
- mock(),
- mock()
- )
- }
-
@Test
fun lightRevealEffect_doesNotChangeDuringKeyguardTransition() =
runTest(UnconfinedTestDispatcher()) {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AodBurnInViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AodBurnInViewModelTest.kt
index 0543bc2..d52696a 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AodBurnInViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AodBurnInViewModelTest.kt
@@ -20,6 +20,7 @@
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
+import com.android.systemui.Flags as AConfigFlags
import com.android.systemui.SysuiTestCase
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
@@ -68,6 +69,8 @@
@Before
fun setUp() {
+ mSetFlagsRule.disableFlags(AConfigFlags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
+
MockitoAnnotations.initMocks(this)
whenever(burnInInteractor.keyguardBurnIn).thenReturn(burnInFlow)
kosmos.burnInInteractor = burnInInteractor
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelTest.kt
index 6cc680b..c23ec22 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelTest.kt
@@ -30,6 +30,8 @@
import com.android.systemui.communal.shared.model.ObservableCommunalTransitionState
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.deviceentry.data.repository.fakeDeviceEntryRepository
+import com.android.systemui.flags.Flags
+import com.android.systemui.flags.fakeFeatureFlagsClassic
import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository
import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
import com.android.systemui.keyguard.domain.interactor.keyguardInteractor
@@ -57,7 +59,10 @@
@SmallTest
@RunWith(AndroidJUnit4::class)
class KeyguardRootViewModelTest : SysuiTestCase() {
- private val kosmos = testKosmos()
+ private val kosmos =
+ testKosmos().apply {
+ fakeFeatureFlagsClassic.apply { set(Flags.REFACTOR_KEYGUARD_DISMISS_INTENT, false) }
+ }
private val testScope = kosmos.testScope
private val keyguardTransitionRepository = kosmos.fakeKeyguardTransitionRepository
private val keyguardInteractor = kosmos.keyguardInteractor
@@ -111,6 +116,23 @@
}
@Test
+ fun iconContainer_isNotVisible_onKeyguard_dontShowWhenGoneToAodTransitionRunning() =
+ testScope.runTest {
+ val isVisible by collectLastValue(underTest.isNotifIconContainerVisible)
+ runCurrent()
+ keyguardTransitionRepository.sendTransitionSteps(
+ from = KeyguardState.GONE,
+ to = KeyguardState.AOD,
+ testScope,
+ )
+ whenever(screenOffAnimationController.shouldShowAodIconsWhenShade()).thenReturn(false)
+ runCurrent()
+
+ assertThat(isVisible?.value).isFalse()
+ assertThat(isVisible?.isAnimating).isFalse()
+ }
+
+ @Test
fun iconContainer_isVisible_bypassEnabled() =
testScope.runTest {
val isVisible by collectLastValue(underTest.isNotifIconContainerVisible)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneViewModelTest.kt
index 4595fbf..7261723 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneViewModelTest.kt
@@ -18,22 +18,28 @@
package com.android.systemui.keyguard.ui.viewmodel
+import android.content.pm.UserInfo
+import android.platform.test.annotations.DisableFlags
+import android.platform.test.annotations.EnableFlags
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
+import com.android.systemui.Flags.FLAG_COMMUNAL_HUB
import com.android.systemui.SysuiTestCase
import com.android.systemui.authentication.data.repository.fakeAuthenticationRepository
import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
-import com.android.systemui.communal.data.repository.fakeCommunalRepository
-import com.android.systemui.communal.domain.interactor.communalInteractor
+import com.android.systemui.communal.domain.interactor.communalSettingsInteractor
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.deviceentry.data.repository.fakeDeviceEntryRepository
import com.android.systemui.deviceentry.domain.interactor.deviceEntryInteractor
+import com.android.systemui.flags.Flags.COMMUNAL_SERVICE_ENABLED
+import com.android.systemui.flags.fakeFeatureFlagsClassic
import com.android.systemui.kosmos.testScope
import com.android.systemui.scene.domain.interactor.sceneInteractor
import com.android.systemui.scene.shared.model.SceneKey
import com.android.systemui.scene.shared.model.SceneModel
import com.android.systemui.statusbar.notification.stack.ui.viewmodel.notificationsPlaceholderViewModel
import com.android.systemui.testKosmos
+import com.android.systemui.user.data.repository.fakeUserRepository
import com.android.systemui.util.mockito.mock
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -49,9 +55,7 @@
private val testScope = kosmos.testScope
private val sceneInteractor by lazy { kosmos.sceneInteractor }
- private val underTest by lazy {
- createLockscreenSceneViewModel()
- }
+ private val underTest by lazy { createLockscreenSceneViewModel() }
@Test
fun upTransitionSceneKey_canSwipeToUnlock_gone() =
@@ -80,29 +84,37 @@
assertThat(upTransitionSceneKey).isEqualTo(SceneKey.Bouncer)
}
+ @EnableFlags(FLAG_COMMUNAL_HUB)
@Test
fun leftTransitionSceneKey_communalIsEnabled_communal() =
testScope.runTest {
- kosmos.fakeCommunalRepository.setIsCommunalEnabled(true)
- val underTest = createLockscreenSceneViewModel()
-
- assertThat(underTest.leftDestinationSceneKey).isEqualTo(SceneKey.Communal)
+ with(kosmos.fakeUserRepository) {
+ setUserInfos(listOf(PRIMARY_USER))
+ setSelectedUserInfo(PRIMARY_USER)
+ }
+ kosmos.fakeFeatureFlagsClassic.set(COMMUNAL_SERVICE_ENABLED, true)
+ val leftDestinationSceneKey by collectLastValue(underTest.leftDestinationSceneKey)
+ assertThat(leftDestinationSceneKey).isEqualTo(SceneKey.Communal)
}
+ @DisableFlags(FLAG_COMMUNAL_HUB)
@Test
fun leftTransitionSceneKey_communalIsDisabled_null() =
testScope.runTest {
- kosmos.fakeCommunalRepository.setIsCommunalEnabled(false)
- val underTest = createLockscreenSceneViewModel()
-
- assertThat(underTest.leftDestinationSceneKey).isNull()
+ with(kosmos.fakeUserRepository) {
+ setUserInfos(listOf(PRIMARY_USER))
+ setSelectedUserInfo(PRIMARY_USER)
+ }
+ kosmos.fakeFeatureFlagsClassic.set(COMMUNAL_SERVICE_ENABLED, false)
+ val leftDestinationSceneKey by collectLastValue(underTest.leftDestinationSceneKey)
+ assertThat(leftDestinationSceneKey).isNull()
}
private fun createLockscreenSceneViewModel(): LockscreenSceneViewModel {
return LockscreenSceneViewModel(
applicationScope = testScope.backgroundScope,
deviceEntryInteractor = kosmos.deviceEntryInteractor,
- communalInteractor = kosmos.communalInteractor,
+ communalSettingsInteractor = kosmos.communalSettingsInteractor,
longPress =
KeyguardLongPressViewModel(
interactor = mock(),
@@ -110,4 +122,9 @@
notifications = kosmos.notificationsPlaceholderViewModel,
)
}
+
+ private companion object {
+ val PRIMARY_USER =
+ UserInfo(/* id= */ 0, /* name= */ "primary user", /* flags= */ UserInfo.FLAG_MAIN)
+ }
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/SideFpsProgressBarViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/SideFpsProgressBarViewModelTest.kt
index 2fe4ef78..f400cb1 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/SideFpsProgressBarViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/SideFpsProgressBarViewModelTest.kt
@@ -25,9 +25,11 @@
import com.android.systemui.Flags
import com.android.systemui.SysuiTestCase
import com.android.systemui.biometrics.data.repository.fakeFingerprintPropertyRepository
+import com.android.systemui.biometrics.domain.interactor.biometricStatusInteractor
import com.android.systemui.biometrics.domain.interactor.displayStateInteractor
import com.android.systemui.biometrics.domain.interactor.sideFpsSensorInteractor
import com.android.systemui.biometrics.fakeFingerprintInteractiveToAuthProvider
+import com.android.systemui.biometrics.shared.model.AuthenticationReason
import com.android.systemui.biometrics.shared.model.FingerprintSensorType
import com.android.systemui.biometrics.shared.model.SensorStrength
import com.android.systemui.coroutines.collectLastValue
@@ -146,6 +148,7 @@
kosmos.fakeKeyguardRepository.setIsDozing(false)
kosmos.fakeDeviceEntryFingerprintAuthRepository.setAuthenticationStatus(
AcquiredFingerprintAuthenticationStatus(
+ AuthenticationReason.DeviceEntryAuthentication,
BiometricFingerprintConstants.FINGERPRINT_ACQUIRED_START
)
)
@@ -165,6 +168,7 @@
kosmos.fakeKeyguardRepository.setIsDozing(true)
kosmos.fakeDeviceEntryFingerprintAuthRepository.setAuthenticationStatus(
AcquiredFingerprintAuthenticationStatus(
+ AuthenticationReason.DeviceEntryAuthentication,
BiometricFingerprintConstants.FINGERPRINT_ACQUIRED_START
)
)
@@ -177,6 +181,7 @@
private fun createViewModel() =
SideFpsProgressBarViewModel(
kosmos.applicationContext,
+ kosmos.biometricStatusInteractor,
kosmos.deviceEntryFingerprintAuthInteractor,
kosmos.sideFpsSensorInteractor,
kosmos.dozeServiceHost,
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/data/repository/InstalledTilesComponentRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/data/repository/InstalledTilesComponentRepositoryImplTest.kt
index d9f24b3..9c0674d 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/data/repository/InstalledTilesComponentRepositoryImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/data/repository/InstalledTilesComponentRepositoryImplTest.kt
@@ -33,7 +33,7 @@
import com.android.systemui.SysuiTestCase
import com.android.systemui.common.data.repository.fakePackageChangeRepository
import com.android.systemui.common.data.repository.packageChangeRepository
-import com.android.systemui.common.data.shared.model.PackageChangeModel
+import com.android.systemui.common.shared.model.PackageChangeModel
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.kosmos.testScope
import com.android.systemui.testKosmos
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/flashlight/domain/FlashlightMapperTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/flashlight/domain/FlashlightMapperTest.kt
index b60f483..63fb67d 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/flashlight/domain/FlashlightMapperTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/flashlight/domain/FlashlightMapperTest.kt
@@ -50,7 +50,8 @@
@Test
fun mapsDisabledDataToInactiveState() {
- val tileState: QSTileState = mapper.map(qsTileConfig, FlashlightTileModel(false))
+ val tileState: QSTileState =
+ mapper.map(qsTileConfig, FlashlightTileModel.FlashlightAvailable(false))
val actualActivationState = tileState.activationState
@@ -59,7 +60,8 @@
@Test
fun mapsEnabledDataToActiveState() {
- val tileState: QSTileState = mapper.map(qsTileConfig, FlashlightTileModel(true))
+ val tileState: QSTileState =
+ mapper.map(qsTileConfig, FlashlightTileModel.FlashlightAvailable(true))
val actualActivationState = tileState.activationState
assertEquals(QSTileState.ActivationState.ACTIVE, actualActivationState)
@@ -67,7 +69,8 @@
@Test
fun mapsEnabledDataToOnIconState() {
- val tileState: QSTileState = mapper.map(qsTileConfig, FlashlightTileModel(true))
+ val tileState: QSTileState =
+ mapper.map(qsTileConfig, FlashlightTileModel.FlashlightAvailable(true))
val expectedIcon =
Icon.Loaded(context.getDrawable(R.drawable.qs_flashlight_icon_on)!!, null)
@@ -77,7 +80,8 @@
@Test
fun mapsDisabledDataToOffIconState() {
- val tileState: QSTileState = mapper.map(qsTileConfig, FlashlightTileModel(false))
+ val tileState: QSTileState =
+ mapper.map(qsTileConfig, FlashlightTileModel.FlashlightAvailable(false))
val expectedIcon =
Icon.Loaded(context.getDrawable(R.drawable.qs_flashlight_icon_off)!!, null)
@@ -86,11 +90,32 @@
}
@Test
- fun supportsOnlyClickAction() {
+ fun mapsUnavailableDataToOffIconState() {
+ val tileState: QSTileState =
+ mapper.map(qsTileConfig, FlashlightTileModel.FlashlightTemporarilyUnavailable)
+
+ val expectedIcon =
+ Icon.Loaded(context.getDrawable(R.drawable.qs_flashlight_icon_off)!!, null)
+ val actualIcon = tileState.icon()
+ assertThat(actualIcon).isEqualTo(expectedIcon)
+ }
+
+ @Test
+ fun supportClickActionWhenAvailable() {
val dontCare = true
- val tileState: QSTileState = mapper.map(qsTileConfig, FlashlightTileModel(dontCare))
+ val tileState: QSTileState =
+ mapper.map(qsTileConfig, FlashlightTileModel.FlashlightAvailable(dontCare))
val supportedActions = tileState.supportedActions
assertThat(supportedActions).containsExactly(QSTileState.UserAction.CLICK)
}
+
+ @Test
+ fun doesNotSupportClickActionWhenUnavailable() {
+ val tileState: QSTileState =
+ mapper.map(qsTileConfig, FlashlightTileModel.FlashlightTemporarilyUnavailable)
+
+ val supportedActions = tileState.supportedActions
+ assertThat(supportedActions).isEmpty()
+ }
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/flashlight/domain/interactor/FlashlightTileDataInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/flashlight/domain/interactor/FlashlightTileDataInteractorTest.kt
index a9e39354..c5a8c70 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/flashlight/domain/interactor/FlashlightTileDataInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/flashlight/domain/interactor/FlashlightTileDataInteractorTest.kt
@@ -70,8 +70,7 @@
}
@Test
- fun dataMatchesController() = runTest {
- controller.setFlashlight(false)
+ fun isEnabledDataMatchesControllerWhenAvailable() = runTest {
val flowValues: List<FlashlightTileModel> by
collectValues(underTest.tileData(TEST_USER, flowOf(DataUpdateTrigger.InitialRequest)))
@@ -81,8 +80,35 @@
controller.setFlashlight(false)
runCurrent()
- assertThat(flowValues.size).isEqualTo(3)
- assertThat(flowValues.map { it.isEnabled }).containsExactly(false, true, false).inOrder()
+ assertThat(flowValues.size).isEqualTo(4) // 2 from setup(), 2 from this test
+ assertThat(
+ flowValues.filterIsInstance<FlashlightTileModel.FlashlightAvailable>().map {
+ it.isEnabled
+ }
+ )
+ .containsExactly(false, false, true, false)
+ .inOrder()
+ }
+
+ /**
+ * Simulates the scenario of changes in flashlight tile availability when camera is initially
+ * closed, then opened, and closed again.
+ */
+ @Test
+ fun availabilityDataMatchesControllerAvailability() = runTest {
+ val flowValues: List<FlashlightTileModel> by
+ collectValues(underTest.tileData(TEST_USER, flowOf(DataUpdateTrigger.InitialRequest)))
+
+ runCurrent()
+ controller.onFlashlightAvailabilityChanged(false)
+ runCurrent()
+ controller.onFlashlightAvailabilityChanged(true)
+ runCurrent()
+
+ assertThat(flowValues.size).isEqualTo(4) // 2 from setup + 2 from this test
+ assertThat(flowValues.map { it is FlashlightTileModel.FlashlightAvailable })
+ .containsExactly(true, true, false, true)
+ .inOrder()
}
private companion object {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/flashlight/domain/interactor/FlashlightTileUserActionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/flashlight/domain/interactor/FlashlightTileUserActionInteractorTest.kt
index 28d43b3..1f19c98 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/flashlight/domain/interactor/FlashlightTileUserActionInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/flashlight/domain/interactor/FlashlightTileUserActionInteractorTest.kt
@@ -29,7 +29,9 @@
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.anyBoolean
import org.mockito.Mock
+import org.mockito.Mockito.never
import org.mockito.Mockito.verify
@SmallTest
@@ -51,7 +53,7 @@
assumeFalse(ActivityManager.isUserAMonkey())
val stateBeforeClick = false
- underTest.handleInput(click(FlashlightTileModel(stateBeforeClick)))
+ underTest.handleInput(click(FlashlightTileModel.FlashlightAvailable(stateBeforeClick)))
verify(controller).setFlashlight(!stateBeforeClick)
}
@@ -61,8 +63,17 @@
assumeFalse(ActivityManager.isUserAMonkey())
val stateBeforeClick = true
- underTest.handleInput(click(FlashlightTileModel(stateBeforeClick)))
+ underTest.handleInput(click(FlashlightTileModel.FlashlightAvailable(stateBeforeClick)))
verify(controller).setFlashlight(!stateBeforeClick)
}
+
+ @Test
+ fun handleClickWhenUnavailable() = runTest {
+ assumeFalse(ActivityManager.isUserAMonkey())
+
+ underTest.handleInput(click(FlashlightTileModel.FlashlightTemporarilyUnavailable))
+
+ verify(controller, never()).setFlashlight(anyBoolean())
+ }
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModelTest.kt
index 42200a3..51f8b11 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModelTest.kt
@@ -61,7 +61,7 @@
private val sceneInteractor by lazy { kosmos.sceneInteractor }
private val mobileIconsInteractor = FakeMobileIconsInteractor(FakeMobileMappingsProxy(), mock())
private val flags = FakeFeatureFlagsClassic().also { it.set(Flags.NEW_NETWORK_SLICE_UI, false) }
- private val qsFlexiglassAdapter = FakeQSSceneAdapter { mock() }
+ private val qsFlexiglassAdapter = FakeQSSceneAdapter({ mock() })
private val footerActionsViewModel = mock<FooterActionsViewModel>()
private val footerActionsViewModelFactory =
mock<FooterActionsViewModel.Factory> {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt
index d6d2509..006f429 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt
@@ -40,7 +40,7 @@
import com.android.systemui.bouncer.ui.viewmodel.bouncerViewModel
import com.android.systemui.classifier.domain.interactor.falsingInteractor
import com.android.systemui.classifier.falsingCollector
-import com.android.systemui.communal.domain.interactor.communalInteractor
+import com.android.systemui.communal.domain.interactor.communalSettingsInteractor
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.deviceentry.data.repository.fakeDeviceEntryRepository
import com.android.systemui.deviceentry.domain.interactor.deviceEntryInteractor
@@ -130,7 +130,7 @@
private val sceneInteractor by lazy { kosmos.sceneInteractor }
private val authenticationInteractor by lazy { kosmos.authenticationInteractor }
private val deviceEntryInteractor by lazy { kosmos.deviceEntryInteractor }
- private val communalInteractor by lazy { kosmos.communalInteractor }
+ private val communalSettingsInteractor by lazy { kosmos.communalSettingsInteractor }
private val transitionState by lazy {
MutableStateFlow<ObservableTransitionState>(
@@ -155,7 +155,7 @@
LockscreenSceneViewModel(
applicationScope = testScope.backgroundScope,
deviceEntryInteractor = deviceEntryInteractor,
- communalInteractor = communalInteractor,
+ communalSettingsInteractor = communalSettingsInteractor,
longPress =
KeyguardLongPressViewModel(
interactor = mock(),
@@ -265,6 +265,7 @@
authenticationInteractor = dagger.Lazy { kosmos.authenticationInteractor },
windowController = mock(),
deviceProvisioningInteractor = kosmos.deviceProvisioningInteractor,
+ centralSurfaces = mock(),
)
startable.start()
@@ -522,14 +523,18 @@
}
@Test
- fun factoryResetProtectionActive_isNotVisible() =
+ fun deviceProvisioningAndFactoryResetProtection() =
testScope.runTest {
val isVisible by collectLastValue(sceneContainerViewModel.isVisible)
- assertThat(isVisible).isTrue()
-
- kosmos.fakeDeviceProvisioningRepository.setFactoryResetProtectionActive(isActive = true)
-
+ kosmos.fakeDeviceProvisioningRepository.setDeviceProvisioned(false)
+ kosmos.fakeDeviceProvisioningRepository.setFactoryResetProtectionActive(true)
assertThat(isVisible).isFalse()
+
+ kosmos.fakeDeviceProvisioningRepository.setFactoryResetProtectionActive(false)
+ assertThat(isVisible).isFalse()
+
+ kosmos.fakeDeviceProvisioningRepository.setDeviceProvisioned(true)
+ assertThat(isVisible).isTrue()
}
/**
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt
index 1abbc92..34c5173 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt
@@ -18,6 +18,7 @@
package com.android.systemui.scene.domain.startable
+import android.app.StatusBarManager
import android.os.PowerManager
import android.platform.test.annotations.EnableFlags
import android.view.Display
@@ -48,7 +49,9 @@
import com.android.systemui.scene.shared.model.SceneKey
import com.android.systemui.scene.shared.model.SceneModel
import com.android.systemui.statusbar.NotificationShadeWindowController
+import com.android.systemui.statusbar.phone.CentralSurfaces
import com.android.systemui.statusbar.pipeline.mobile.data.repository.fakeMobileConnectionsRepository
+import com.android.systemui.statusbar.policy.data.repository.fakeDeviceProvisioningRepository
import com.android.systemui.statusbar.policy.domain.interactor.deviceProvisioningInteractor
import com.android.systemui.testKosmos
import com.android.systemui.util.mockito.mock
@@ -64,6 +67,7 @@
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentMatchers.anyBoolean
+import org.mockito.ArgumentMatchers.anyInt
import org.mockito.Mock
import org.mockito.Mockito.clearInvocations
import org.mockito.Mockito.never
@@ -77,6 +81,7 @@
class SceneContainerStartableTest : SysuiTestCase() {
@Mock private lateinit var windowController: NotificationShadeWindowController
+ @Mock private lateinit var centralSurfaces: CentralSurfaces
private val kosmos = testKosmos()
private val testScope = kosmos.testScope
@@ -114,6 +119,7 @@
authenticationInteractor = dagger.Lazy { authenticationInteractor },
windowController = windowController,
deviceProvisioningInteractor = kosmos.deviceProvisioningInteractor,
+ centralSurfaces = centralSurfaces,
)
}
@@ -164,6 +170,30 @@
}
@Test
+ fun hydrateVisibility_basedOnDeviceProvisioningAndFactoryResetProtection() =
+ testScope.runTest {
+ val isVisible by collectLastValue(sceneInteractor.isVisible)
+ prepareState(
+ isDeviceUnlocked = true,
+ initialSceneKey = SceneKey.Lockscreen,
+ isDeviceProvisioned = false,
+ isFrpActive = true,
+ )
+
+ underTest.start()
+ assertThat(isVisible).isFalse()
+
+ kosmos.fakeDeviceProvisioningRepository.setFactoryResetProtectionActive(false)
+ assertThat(isVisible).isFalse()
+
+ kosmos.fakeDeviceProvisioningRepository.setDeviceProvisioned(true)
+ assertThat(isVisible).isTrue()
+
+ kosmos.fakeDeviceProvisioningRepository.setFactoryResetProtectionActive(true)
+ assertThat(isVisible).isFalse()
+ }
+
+ @Test
fun startsInLockscreenScene() =
testScope.runTest {
val currentSceneKey by collectLastValue(sceneInteractor.desiredScene.map { it.key })
@@ -738,6 +768,227 @@
verify(windowController, times(2)).setNotificationShadeFocusable(false)
}
+ @Test
+ fun hydrateInteractionState_whileLocked() =
+ testScope.runTest {
+ val transitionStateFlow =
+ prepareState(
+ initialSceneKey = SceneKey.Lockscreen,
+ )
+ underTest.start()
+ runCurrent()
+ verify(centralSurfaces).setInteracting(StatusBarManager.WINDOW_STATUS_BAR, true)
+
+ clearInvocations(centralSurfaces)
+ emulateSceneTransition(
+ transitionStateFlow = transitionStateFlow,
+ toScene = SceneKey.Bouncer,
+ verifyBeforeTransition = {
+ verify(centralSurfaces, never()).setInteracting(anyInt(), anyBoolean())
+ },
+ verifyDuringTransition = {
+ verify(centralSurfaces, never()).setInteracting(anyInt(), anyBoolean())
+ },
+ verifyAfterTransition = {
+ verify(centralSurfaces)
+ .setInteracting(
+ StatusBarManager.WINDOW_STATUS_BAR,
+ false,
+ )
+ },
+ )
+
+ clearInvocations(centralSurfaces)
+ emulateSceneTransition(
+ transitionStateFlow = transitionStateFlow,
+ toScene = SceneKey.Lockscreen,
+ verifyBeforeTransition = {
+ verify(centralSurfaces, never()).setInteracting(anyInt(), anyBoolean())
+ },
+ verifyDuringTransition = {
+ verify(centralSurfaces, never()).setInteracting(anyInt(), anyBoolean())
+ },
+ verifyAfterTransition = {
+ verify(centralSurfaces)
+ .setInteracting(
+ StatusBarManager.WINDOW_STATUS_BAR,
+ true,
+ )
+ },
+ )
+
+ clearInvocations(centralSurfaces)
+ emulateSceneTransition(
+ transitionStateFlow = transitionStateFlow,
+ toScene = SceneKey.Shade,
+ verifyBeforeTransition = {
+ verify(centralSurfaces, never()).setInteracting(anyInt(), anyBoolean())
+ },
+ verifyDuringTransition = {
+ verify(centralSurfaces, never()).setInteracting(anyInt(), anyBoolean())
+ },
+ verifyAfterTransition = {
+ verify(centralSurfaces)
+ .setInteracting(
+ StatusBarManager.WINDOW_STATUS_BAR,
+ false,
+ )
+ },
+ )
+
+ clearInvocations(centralSurfaces)
+ emulateSceneTransition(
+ transitionStateFlow = transitionStateFlow,
+ toScene = SceneKey.Lockscreen,
+ verifyBeforeTransition = {
+ verify(centralSurfaces, never()).setInteracting(anyInt(), anyBoolean())
+ },
+ verifyDuringTransition = {
+ verify(centralSurfaces, never()).setInteracting(anyInt(), anyBoolean())
+ },
+ verifyAfterTransition = {
+ verify(centralSurfaces)
+ .setInteracting(
+ StatusBarManager.WINDOW_STATUS_BAR,
+ true,
+ )
+ },
+ )
+
+ clearInvocations(centralSurfaces)
+ emulateSceneTransition(
+ transitionStateFlow = transitionStateFlow,
+ toScene = SceneKey.QuickSettings,
+ verifyBeforeTransition = {
+ verify(centralSurfaces, never()).setInteracting(anyInt(), anyBoolean())
+ },
+ verifyDuringTransition = {
+ verify(centralSurfaces, never()).setInteracting(anyInt(), anyBoolean())
+ },
+ verifyAfterTransition = {
+ verify(centralSurfaces, never()).setInteracting(anyInt(), anyBoolean())
+ },
+ )
+ }
+
+ @Test
+ fun hydrateInteractionState_whileUnlocked() =
+ testScope.runTest {
+ val transitionStateFlow =
+ prepareState(
+ isDeviceUnlocked = true,
+ initialSceneKey = SceneKey.Gone,
+ )
+ underTest.start()
+ verify(centralSurfaces, never()).setInteracting(anyInt(), anyBoolean())
+
+ clearInvocations(centralSurfaces)
+ emulateSceneTransition(
+ transitionStateFlow = transitionStateFlow,
+ toScene = SceneKey.Bouncer,
+ verifyBeforeTransition = {
+ verify(centralSurfaces, never()).setInteracting(anyInt(), anyBoolean())
+ },
+ verifyDuringTransition = {
+ verify(centralSurfaces, never()).setInteracting(anyInt(), anyBoolean())
+ },
+ verifyAfterTransition = {
+ verify(centralSurfaces, never()).setInteracting(anyInt(), anyBoolean())
+ },
+ )
+
+ clearInvocations(centralSurfaces)
+ emulateSceneTransition(
+ transitionStateFlow = transitionStateFlow,
+ toScene = SceneKey.Lockscreen,
+ verifyBeforeTransition = {
+ verify(centralSurfaces, never()).setInteracting(anyInt(), anyBoolean())
+ },
+ verifyDuringTransition = {
+ verify(centralSurfaces, never()).setInteracting(anyInt(), anyBoolean())
+ },
+ verifyAfterTransition = {
+ verify(centralSurfaces, never()).setInteracting(anyInt(), anyBoolean())
+ },
+ )
+
+ clearInvocations(centralSurfaces)
+ emulateSceneTransition(
+ transitionStateFlow = transitionStateFlow,
+ toScene = SceneKey.Shade,
+ verifyBeforeTransition = {
+ verify(centralSurfaces, never()).setInteracting(anyInt(), anyBoolean())
+ },
+ verifyDuringTransition = {
+ verify(centralSurfaces, never()).setInteracting(anyInt(), anyBoolean())
+ },
+ verifyAfterTransition = {
+ verify(centralSurfaces, never()).setInteracting(anyInt(), anyBoolean())
+ },
+ )
+
+ clearInvocations(centralSurfaces)
+ emulateSceneTransition(
+ transitionStateFlow = transitionStateFlow,
+ toScene = SceneKey.Lockscreen,
+ verifyBeforeTransition = {
+ verify(centralSurfaces, never()).setInteracting(anyInt(), anyBoolean())
+ },
+ verifyDuringTransition = {
+ verify(centralSurfaces, never()).setInteracting(anyInt(), anyBoolean())
+ },
+ verifyAfterTransition = {
+ verify(centralSurfaces, never()).setInteracting(anyInt(), anyBoolean())
+ },
+ )
+
+ clearInvocations(centralSurfaces)
+ emulateSceneTransition(
+ transitionStateFlow = transitionStateFlow,
+ toScene = SceneKey.QuickSettings,
+ verifyBeforeTransition = {
+ verify(centralSurfaces, never()).setInteracting(anyInt(), anyBoolean())
+ },
+ verifyDuringTransition = {
+ verify(centralSurfaces, never()).setInteracting(anyInt(), anyBoolean())
+ },
+ verifyAfterTransition = {
+ verify(centralSurfaces, never()).setInteracting(anyInt(), anyBoolean())
+ },
+ )
+ }
+
+ private fun TestScope.emulateSceneTransition(
+ transitionStateFlow: MutableStateFlow<ObservableTransitionState>,
+ toScene: SceneKey,
+ verifyBeforeTransition: (() -> Unit)? = null,
+ verifyDuringTransition: (() -> Unit)? = null,
+ verifyAfterTransition: (() -> Unit)? = null,
+ ) {
+ val fromScene = sceneInteractor.desiredScene.value.key
+ sceneInteractor.changeScene(SceneModel(toScene), "reason")
+ runCurrent()
+ verifyBeforeTransition?.invoke()
+
+ transitionStateFlow.value =
+ ObservableTransitionState.Transition(
+ fromScene = fromScene,
+ toScene = toScene,
+ progress = flowOf(0.5f),
+ isInitiatedByUserInput = true,
+ isUserInputOngoing = flowOf(true),
+ )
+ runCurrent()
+ verifyDuringTransition?.invoke()
+
+ transitionStateFlow.value =
+ ObservableTransitionState.Idle(
+ scene = toScene,
+ )
+ runCurrent()
+ verifyAfterTransition?.invoke()
+ }
+
private fun TestScope.prepareState(
isDeviceUnlocked: Boolean = false,
isBypassEnabled: Boolean = false,
@@ -745,6 +996,8 @@
authenticationMethod: AuthenticationMethodModel? = null,
isLockscreenEnabled: Boolean = true,
startsAwake: Boolean = true,
+ isDeviceProvisioned: Boolean = true,
+ isFrpActive: Boolean = false,
): MutableStateFlow<ObservableTransitionState> {
if (authenticationMethod?.isSecure == true) {
assert(isLockscreenEnabled) {
@@ -781,6 +1034,10 @@
} else {
powerInteractor.setAsleepForTest()
}
+
+ kosmos.fakeDeviceProvisioningRepository.setDeviceProvisioned(isDeviceProvisioned)
+ kosmos.fakeDeviceProvisioningRepository.setFactoryResetProtectionActive(isFrpActive)
+
runCurrent()
return transitionStateFlow
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeBackActionInteractorImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeBackActionInteractorImplTest.kt
new file mode 100644
index 0000000..d3c6598
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeBackActionInteractorImplTest.kt
@@ -0,0 +1,108 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.shade.domain.interactor
+
+import android.content.applicationContext
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.deviceentry.data.repository.fakeDeviceEntryRepository
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.scene.domain.interactor.sceneInteractor
+import com.android.systemui.scene.shared.flag.fakeSceneContainerFlags
+import com.android.systemui.scene.shared.model.ObservableTransitionState
+import com.android.systemui.scene.shared.model.SceneKey
+import com.android.systemui.scene.shared.model.SceneModel
+import com.android.systemui.shared.recents.utilities.Utilities
+import com.android.systemui.testKosmos
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Assume
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class ShadeBackActionInteractorImplTest : SysuiTestCase() {
+ val kosmos = testKosmos().apply { fakeSceneContainerFlags.enabled = true }
+ val testScope = kosmos.testScope
+ val sceneInteractor = kosmos.sceneInteractor
+ val deviceEntryRepository = kosmos.fakeDeviceEntryRepository
+ val underTest = kosmos.shadeBackActionInteractor
+
+ @Before
+ fun ignoreSplitShade() {
+ Assume.assumeFalse(Utilities.isLargeScreen(kosmos.applicationContext))
+ }
+
+ @Test
+ fun animateCollapseQs_notOnQs() =
+ testScope.runTest {
+ setScene(SceneKey.Shade)
+ underTest.animateCollapseQs(true)
+ runCurrent()
+ assertThat(sceneInteractor.desiredScene.value.key).isEqualTo(SceneKey.Shade)
+ }
+
+ @Test
+ fun animateCollapseQs_fullyCollapse_entered() =
+ testScope.runTest {
+ enterDevice()
+ setScene(SceneKey.QuickSettings)
+ underTest.animateCollapseQs(true)
+ runCurrent()
+ assertThat(sceneInteractor.desiredScene.value.key).isEqualTo(SceneKey.Gone)
+ }
+
+ @Test
+ fun animateCollapseQs_fullyCollapse_locked() =
+ testScope.runTest {
+ deviceEntryRepository.setUnlocked(false)
+ setScene(SceneKey.QuickSettings)
+ underTest.animateCollapseQs(true)
+ runCurrent()
+ assertThat(sceneInteractor.desiredScene.value.key).isEqualTo(SceneKey.Lockscreen)
+ }
+
+ @Test
+ fun animateCollapseQs_notFullyCollapse() =
+ testScope.runTest {
+ setScene(SceneKey.QuickSettings)
+ underTest.animateCollapseQs(false)
+ runCurrent()
+ assertThat(sceneInteractor.desiredScene.value.key).isEqualTo(SceneKey.Shade)
+ }
+
+ private fun enterDevice() {
+ deviceEntryRepository.setUnlocked(true)
+ testScope.runCurrent()
+ setScene(SceneKey.Gone)
+ }
+
+ private fun setScene(key: SceneKey) {
+ sceneInteractor.changeScene(SceneModel(key), "test")
+ sceneInteractor.setTransitionState(
+ MutableStateFlow<ObservableTransitionState>(ObservableTransitionState.Idle(key))
+ )
+ testScope.runCurrent()
+ }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt
index 5ef095f..f1f5dc3 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt
@@ -82,7 +82,7 @@
scope = testScope.backgroundScope,
)
- private val qsFlexiglassAdapter = FakeQSSceneAdapter { mock() }
+ private val qsFlexiglassAdapter = FakeQSSceneAdapter({ mock() })
private lateinit var shadeHeaderViewModel: ShadeHeaderViewModel
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModelTest.kt
new file mode 100644
index 0000000..693de55
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModelTest.kt
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2024 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.notification.stack.ui.viewmodel
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.common.shared.model.NotificationContainerBounds
+import com.android.systemui.keyguard.domain.interactor.keyguardInteractor
+import com.android.systemui.statusbar.notification.stack.domain.interactor.notificationStackAppearanceInteractor
+import com.android.systemui.testKosmos
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class NotificationsPlaceholderViewModelTest : SysuiTestCase() {
+ private val kosmos = testKosmos()
+ private val underTest = kosmos.notificationsPlaceholderViewModel
+ @Test
+ fun onBoundsChanged_setsNotificationContainerBounds() {
+ underTest.onBoundsChanged(left = 5f, top = 5f, right = 5f, bottom = 5f)
+ assertThat(kosmos.keyguardInteractor.notificationContainerBounds.value)
+ .isEqualTo(NotificationContainerBounds(left = 5f, top = 5f, right = 5f, bottom = 5f))
+ assertThat(kosmos.notificationStackAppearanceInteractor.stackBounds.value)
+ .isEqualTo(NotificationContainerBounds(left = 5f, top = 5f, right = 5f, bottom = 5f))
+ }
+ @Test
+ fun onContentTopChanged_setsContentTop() {
+ underTest.onContentTopChanged(padding = 5f)
+ assertThat(kosmos.notificationStackAppearanceInteractor.contentTop.value).isEqualTo(5f)
+ }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ActivityStarterImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ActivityStarterImplTest.kt
index cc4ebd4..c01f1c7 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ActivityStarterImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ActivityStarterImplTest.kt
@@ -27,7 +27,7 @@
import com.android.keyguard.KeyguardUpdateMonitor
import com.android.systemui.ActivityIntentHelper
import com.android.systemui.SysuiTestCase
-import com.android.systemui.animation.ActivityLaunchAnimator
+import com.android.systemui.animation.ActivityTransitionAnimator
import com.android.systemui.animation.LaunchableView
import com.android.systemui.assist.AssistManager
import com.android.systemui.keyguard.KeyguardViewMediator
@@ -78,7 +78,7 @@
@Mock private lateinit var shadeController: ShadeController
@Mock private lateinit var shadeViewController: ShadeViewController
@Mock private lateinit var statusBarKeyguardViewManager: StatusBarKeyguardViewManager
- @Mock private lateinit var activityLaunchAnimator: ActivityLaunchAnimator
+ @Mock private lateinit var mActivityTransitionAnimator: ActivityTransitionAnimator
@Mock private lateinit var lockScreenUserManager: NotificationLockscreenUserManager
@Mock private lateinit var statusBarWindowController: StatusBarWindowController
@Mock private lateinit var notifShadeWindowController: NotificationShadeWindowController
@@ -109,7 +109,7 @@
shadeAnimationInteractor,
Lazy { statusBarKeyguardViewManager },
Lazy { notifShadeWindowController },
- activityLaunchAnimator,
+ mActivityTransitionAnimator,
context,
DISPLAY_ID,
lockScreenUserManager,
@@ -149,7 +149,7 @@
override fun setShouldBlockVisibilityChanges(block: Boolean) {}
}
parent.addView(view)
- val controller = ActivityLaunchAnimator.Controller.fromView(view)
+ val controller = ActivityTransitionAnimator.Controller.fromView(view)
whenever(pendingIntent.isActivity).thenReturn(true)
whenever(keyguardStateController.isShowing).thenReturn(true)
whenever(deviceProvisionedController.isDeviceProvisioned).thenReturn(true)
@@ -163,7 +163,7 @@
)
mainExecutor.runAllReady()
- verify(activityLaunchAnimator)
+ verify(mActivityTransitionAnimator)
.startPendingIntentWithAnimation(
nullable(),
eq(true),
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarContentInsetsProviderTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/StatusBarContentInsetsProviderTest.kt
similarity index 62%
rename from packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarContentInsetsProviderTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/StatusBarContentInsetsProviderTest.kt
index 84b2c4b..53b262b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarContentInsetsProviderTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/StatusBarContentInsetsProviderTest.kt
@@ -21,7 +21,11 @@
import android.graphics.Rect
import android.view.Display
import android.view.DisplayCutout
+import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
+import com.android.systemui.CameraProtectionInfo
+import com.android.systemui.SysUICutoutInformation
+import com.android.systemui.SysUICutoutProvider
import com.android.systemui.SysuiTestCase
import com.android.systemui.dump.DumpManager
import com.android.systemui.statusbar.commandline.CommandRegistry
@@ -38,30 +42,30 @@
import junit.framework.Assert.assertTrue
import org.junit.Before
import org.junit.Test
+import org.junit.runner.RunWith
import org.mockito.ArgumentMatchers.any
-import org.mockito.Mock
-import org.mockito.Mockito.`when`
-import org.mockito.MockitoAnnotations
+@RunWith(AndroidJUnit4::class)
@SmallTest
class StatusBarContentInsetsProviderTest : SysuiTestCase() {
- @Mock private lateinit var dc: DisplayCutout
- @Mock private lateinit var contextMock: Context
- @Mock private lateinit var display: Display
- private lateinit var configurationController: ConfigurationController
-
+ private val sysUICutout = mock<SysUICutoutInformation>()
+ private val dc = mock<DisplayCutout>()
+ private val contextMock = mock<Context>()
+ private val display = mock<Display>()
private val configuration = Configuration()
+ private lateinit var configurationController: ConfigurationController
+
@Before
fun setup() {
- MockitoAnnotations.initMocks(this)
- `when`(contextMock.display).thenReturn(display)
+ whenever(sysUICutout.cutout).thenReturn(dc)
+ whenever(contextMock.display).thenReturn(display)
context.ensureTestableResources()
- `when`(contextMock.resources).thenReturn(context.resources)
- `when`(contextMock.resources.configuration).thenReturn(configuration)
- `when`(contextMock.createConfigurationContext(any())).thenAnswer {
+ whenever(contextMock.resources).thenReturn(context.resources)
+ whenever(contextMock.resources.configuration).thenReturn(configuration)
+ whenever(contextMock.createConfigurationContext(any())).thenAnswer {
context.createConfigurationContext(it.arguments[0] as Configuration)
}
configurationController = ConfigurationControllerImpl(contextMock)
@@ -117,7 +121,7 @@
bounds = calculateInsetsForRotationWithRotatedResources(
currentRotation,
targetRotation,
- dc,
+ sysUICutout,
screenBounds,
sbHeightLandscape,
minLeftPadding,
@@ -161,7 +165,7 @@
}
@Test
- fun testCalculateInsetsForRotationWithRotatedResources_topLeftCutout() {
+ fun testCalculateInsetsForRotationWithRotatedResources_topLeftCutout_noCameraProtection() {
// GIVEN a device in portrait mode with width < height and a display cutout in the top-left
val screenBounds = Rect(0, 0, 1080, 2160)
val dcBounds = Rect(0, 0, 100, 100)
@@ -174,7 +178,7 @@
val dotWidth = 10
val statusBarContentHeight = 15
- `when`(dc.boundingRects).thenReturn(listOf(dcBounds))
+ whenever(dc.boundingRects).thenReturn(listOf(dcBounds))
// THEN rotations which share a short side should use the greater value between rounded
// corner padding and the display cutout's size
@@ -187,7 +191,7 @@
var bounds = calculateInsetsForRotationWithRotatedResources(
currentRotation,
targetRotation,
- dc,
+ sysUICutout,
screenBounds,
sbHeightPortrait,
minLeftPadding,
@@ -208,7 +212,7 @@
bounds = calculateInsetsForRotationWithRotatedResources(
currentRotation,
targetRotation,
- dc,
+ sysUICutout,
screenBounds,
sbHeightLandscape,
minLeftPadding,
@@ -231,7 +235,7 @@
bounds = calculateInsetsForRotationWithRotatedResources(
currentRotation,
targetRotation,
- dc,
+ sysUICutout,
screenBounds,
sbHeightPortrait,
minLeftPadding,
@@ -253,7 +257,335 @@
bounds = calculateInsetsForRotationWithRotatedResources(
currentRotation,
targetRotation,
- dc,
+ sysUICutout,
+ screenBounds,
+ sbHeightLandscape,
+ minLeftPadding,
+ minRightPadding,
+ isRtl,
+ dotWidth,
+ BOTTOM_ALIGNED_MARGIN_NONE,
+ statusBarContentHeight)
+
+ assertRects(expectedBounds, bounds, currentRotation, targetRotation)
+ }
+
+ @Test
+ fun testCalculateInsetsForRotationWithRotatedResources_topLeftCutout_withCameraProtection() {
+ // GIVEN a device in portrait mode with width < height and a display cutout in the top-left
+ val screenBounds = Rect(0, 0, 1080, 2160)
+ val dcBounds = Rect(0, 0, 100, 100)
+ val protectionBounds = Rect(10, 10, 110, 110)
+ val minLeftPadding = 20
+ val minRightPadding = 20
+ val sbHeightPortrait = 100
+ val sbHeightLandscape = 60
+ val currentRotation = ROTATION_NONE
+ val isRtl = false
+ val dotWidth = 10
+ val statusBarContentHeight = 15
+
+ val protectionInfo = mock<CameraProtectionInfo> {
+ whenever(this.cutoutBounds).thenReturn(protectionBounds)
+ }
+ whenever(sysUICutout.cameraProtection).thenReturn(protectionInfo)
+ whenever(dc.boundingRects).thenReturn(listOf(dcBounds))
+
+ // THEN rotations which share a short side should use the greater value between rounded
+ // corner padding, the display cutout's size, and the camera protections' size.
+ var targetRotation = ROTATION_NONE
+ var expectedBounds = Rect(protectionBounds.right,
+ 0,
+ screenBounds.right - minRightPadding,
+ sbHeightPortrait)
+
+ var bounds = calculateInsetsForRotationWithRotatedResources(
+ currentRotation,
+ targetRotation,
+ sysUICutout,
+ screenBounds,
+ sbHeightPortrait,
+ minLeftPadding,
+ minRightPadding,
+ isRtl,
+ dotWidth,
+ BOTTOM_ALIGNED_MARGIN_NONE,
+ statusBarContentHeight)
+
+ assertRects(expectedBounds, bounds, currentRotation, targetRotation)
+
+ targetRotation = ROTATION_LANDSCAPE
+ expectedBounds = Rect(protectionBounds.bottom,
+ 0,
+ screenBounds.height() - minRightPadding,
+ sbHeightLandscape)
+
+ bounds = calculateInsetsForRotationWithRotatedResources(
+ currentRotation,
+ targetRotation,
+ sysUICutout,
+ screenBounds,
+ sbHeightLandscape,
+ minLeftPadding,
+ minRightPadding,
+ isRtl,
+ dotWidth,
+ BOTTOM_ALIGNED_MARGIN_NONE,
+ statusBarContentHeight)
+
+ assertRects(expectedBounds, bounds, currentRotation, targetRotation)
+
+ // THEN the side that does NOT share a short side with the display cutout ignores the
+ // display cutout bounds
+ targetRotation = ROTATION_UPSIDE_DOWN
+ expectedBounds = Rect(minLeftPadding,
+ 0,
+ screenBounds.width() - minRightPadding,
+ sbHeightPortrait)
+
+ bounds = calculateInsetsForRotationWithRotatedResources(
+ currentRotation,
+ targetRotation,
+ sysUICutout,
+ screenBounds,
+ sbHeightPortrait,
+ minLeftPadding,
+ minRightPadding,
+ isRtl,
+ dotWidth,
+ BOTTOM_ALIGNED_MARGIN_NONE,
+ statusBarContentHeight)
+
+ assertRects(expectedBounds, bounds, currentRotation, targetRotation)
+
+ // Phone in portrait, seascape (rot_270) bounds
+ targetRotation = ROTATION_SEASCAPE
+ expectedBounds = Rect(minLeftPadding,
+ 0,
+ screenBounds.height() - protectionBounds.bottom - dotWidth,
+ sbHeightLandscape)
+
+ bounds = calculateInsetsForRotationWithRotatedResources(
+ currentRotation,
+ targetRotation,
+ sysUICutout,
+ screenBounds,
+ sbHeightLandscape,
+ minLeftPadding,
+ minRightPadding,
+ isRtl,
+ dotWidth,
+ BOTTOM_ALIGNED_MARGIN_NONE,
+ statusBarContentHeight)
+
+ assertRects(expectedBounds, bounds, currentRotation, targetRotation)
+ }
+
+ @Test
+ fun testCalculateInsetsForRotationWithRotatedResources_topRightCutout_noCameraProtection() {
+ // GIVEN a device in portrait mode with width < height and a display cutout in the top-left
+ val screenBounds = Rect(0, 0, 1000, 2000)
+ val dcBounds = Rect(900, 0, 1000, 100)
+ val minLeftPadding = 20
+ val minRightPadding = 20
+ val sbHeightPortrait = 100
+ val sbHeightLandscape = 60
+ val currentRotation = ROTATION_NONE
+ val isRtl = false
+ val dotWidth = 10
+ val statusBarContentHeight = 15
+
+ whenever(dc.boundingRects).thenReturn(listOf(dcBounds))
+
+ // THEN rotations which share a short side should use the greater value between rounded
+ // corner padding and the display cutout's size
+ var targetRotation = ROTATION_NONE
+ var expectedBounds = Rect(minLeftPadding,
+ 0,
+ dcBounds.left - dotWidth,
+ sbHeightPortrait)
+
+ var bounds = calculateInsetsForRotationWithRotatedResources(
+ currentRotation,
+ targetRotation,
+ sysUICutout,
+ screenBounds,
+ sbHeightPortrait,
+ minLeftPadding,
+ minRightPadding,
+ isRtl,
+ dotWidth,
+ BOTTOM_ALIGNED_MARGIN_NONE,
+ statusBarContentHeight)
+
+ assertRects(expectedBounds, bounds, currentRotation, targetRotation)
+
+ targetRotation = ROTATION_LANDSCAPE
+ expectedBounds = Rect(dcBounds.height(),
+ 0,
+ screenBounds.height() - minRightPadding,
+ sbHeightLandscape)
+
+ bounds = calculateInsetsForRotationWithRotatedResources(
+ currentRotation,
+ targetRotation,
+ sysUICutout,
+ screenBounds,
+ sbHeightLandscape,
+ minLeftPadding,
+ minRightPadding,
+ isRtl,
+ dotWidth,
+ BOTTOM_ALIGNED_MARGIN_NONE,
+ statusBarContentHeight)
+
+ assertRects(expectedBounds, bounds, currentRotation, targetRotation)
+
+ // THEN the side that does NOT share a short side with the display cutout ignores the
+ // display cutout bounds
+ targetRotation = ROTATION_UPSIDE_DOWN
+ expectedBounds = Rect(minLeftPadding,
+ 0,
+ screenBounds.width() - minRightPadding,
+ sbHeightPortrait)
+
+ bounds = calculateInsetsForRotationWithRotatedResources(
+ currentRotation,
+ targetRotation,
+ sysUICutout,
+ screenBounds,
+ sbHeightPortrait,
+ minLeftPadding,
+ minRightPadding,
+ isRtl,
+ dotWidth,
+ BOTTOM_ALIGNED_MARGIN_NONE,
+ statusBarContentHeight)
+
+ assertRects(expectedBounds, bounds, currentRotation, targetRotation)
+
+ // Phone in portrait, seascape (rot_270) bounds
+ targetRotation = ROTATION_SEASCAPE
+ expectedBounds = Rect(minLeftPadding,
+ 0,
+ screenBounds.height() - dcBounds.height() - dotWidth,
+ sbHeightLandscape)
+
+ bounds = calculateInsetsForRotationWithRotatedResources(
+ currentRotation,
+ targetRotation,
+ sysUICutout,
+ screenBounds,
+ sbHeightLandscape,
+ minLeftPadding,
+ minRightPadding,
+ isRtl,
+ dotWidth,
+ BOTTOM_ALIGNED_MARGIN_NONE,
+ statusBarContentHeight)
+
+ assertRects(expectedBounds, bounds, currentRotation, targetRotation)
+ }
+
+ @Test
+ fun testCalculateInsetsForRotationWithRotatedResources_topRightCutout_withCameraProtection() {
+ // GIVEN a device in portrait mode with width < height and a display cutout in the top-left
+ val screenBounds = Rect(0, 0, 1000, 2000)
+ val dcBounds = Rect(900, 0, 1000, 100)
+ val protectionBounds = Rect(890, 10, 990, 110)
+ val minLeftPadding = 20
+ val minRightPadding = 20
+ val sbHeightPortrait = 100
+ val sbHeightLandscape = 60
+ val currentRotation = ROTATION_NONE
+ val isRtl = false
+ val dotWidth = 10
+ val statusBarContentHeight = 15
+
+ val protectionInfo = mock<CameraProtectionInfo> {
+ whenever(this.cutoutBounds).thenReturn(protectionBounds)
+ }
+ whenever(sysUICutout.cameraProtection).thenReturn(protectionInfo)
+ whenever(dc.boundingRects).thenReturn(listOf(dcBounds))
+
+ // THEN rotations which share a short side should use the greater value between rounded
+ // corner padding, the display cutout's size, and the camera protections' size.
+ var targetRotation = ROTATION_NONE
+ var expectedBounds = Rect(minLeftPadding,
+ 0,
+ protectionBounds.left - dotWidth,
+ sbHeightPortrait)
+
+ var bounds = calculateInsetsForRotationWithRotatedResources(
+ currentRotation,
+ targetRotation,
+ sysUICutout,
+ screenBounds,
+ sbHeightPortrait,
+ minLeftPadding,
+ minRightPadding,
+ isRtl,
+ dotWidth,
+ BOTTOM_ALIGNED_MARGIN_NONE,
+ statusBarContentHeight)
+
+ assertRects(expectedBounds, bounds, currentRotation, targetRotation)
+
+ targetRotation = ROTATION_LANDSCAPE
+ expectedBounds = Rect(protectionBounds.bottom,
+ 0,
+ screenBounds.height() - minRightPadding,
+ sbHeightLandscape)
+
+ bounds = calculateInsetsForRotationWithRotatedResources(
+ currentRotation,
+ targetRotation,
+ sysUICutout,
+ screenBounds,
+ sbHeightLandscape,
+ minLeftPadding,
+ minRightPadding,
+ isRtl,
+ dotWidth,
+ BOTTOM_ALIGNED_MARGIN_NONE,
+ statusBarContentHeight)
+
+ assertRects(expectedBounds, bounds, currentRotation, targetRotation)
+
+ // THEN the side that does NOT share a short side with the display cutout ignores the
+ // display cutout bounds
+ targetRotation = ROTATION_UPSIDE_DOWN
+ expectedBounds = Rect(minLeftPadding,
+ 0,
+ screenBounds.width() - minRightPadding,
+ sbHeightPortrait)
+
+ bounds = calculateInsetsForRotationWithRotatedResources(
+ currentRotation,
+ targetRotation,
+ sysUICutout,
+ screenBounds,
+ sbHeightPortrait,
+ minLeftPadding,
+ minRightPadding,
+ isRtl,
+ dotWidth,
+ BOTTOM_ALIGNED_MARGIN_NONE,
+ statusBarContentHeight)
+
+ assertRects(expectedBounds, bounds, currentRotation, targetRotation)
+
+ // Phone in portrait, seascape (rot_270) bounds
+ targetRotation = ROTATION_SEASCAPE
+ expectedBounds = Rect(minLeftPadding,
+ 0,
+ screenBounds.height() - protectionBounds.bottom - dotWidth,
+ sbHeightLandscape)
+
+ bounds = calculateInsetsForRotationWithRotatedResources(
+ currentRotation,
+ targetRotation,
+ sysUICutout,
screenBounds,
sbHeightLandscape,
minLeftPadding,
@@ -273,7 +605,7 @@
val bounds = calculateInsetsForRotationWithRotatedResources(
currentRotation = ROTATION_NONE,
targetRotation = ROTATION_NONE,
- displayCutout = dc,
+ sysUICutout = sysUICutout,
maxBounds = Rect(0, 0, 1080, 2160),
statusBarHeight = 100,
minLeft = 0,
@@ -293,7 +625,7 @@
val bounds = calculateInsetsForRotationWithRotatedResources(
currentRotation = ROTATION_NONE,
targetRotation = ROTATION_NONE,
- displayCutout = dc,
+ sysUICutout = sysUICutout,
maxBounds = Rect(0, 0, 1080, 2160),
statusBarHeight = 100,
minLeft = 0,
@@ -321,6 +653,7 @@
val screenBounds = Rect(0, 0, 1080, 2160)
// cutout centered at the top
val dcBounds = Rect(490, 0, 590, 100)
+ val protectionBounds = Rect(480, 10, 600, 90)
val minLeftPadding = 20
val minRightPadding = 20
val sbHeightPortrait = 100
@@ -330,7 +663,11 @@
val dotWidth = 10
val statusBarContentHeight = 15
- `when`(dc.boundingRects).thenReturn(listOf(dcBounds))
+ val protectionInfo = mock<CameraProtectionInfo> {
+ whenever(this.cutoutBounds).thenReturn(protectionBounds)
+ }
+ whenever(sysUICutout.cameraProtection).thenReturn(protectionInfo)
+ whenever(dc.boundingRects).thenReturn(listOf(dcBounds))
// THEN only the landscape/seascape rotations should avoid the cutout area because of the
// potential letterboxing
@@ -343,7 +680,7 @@
var bounds = calculateInsetsForRotationWithRotatedResources(
currentRotation,
targetRotation,
- dc,
+ sysUICutout = sysUICutout,
screenBounds,
sbHeightPortrait,
minLeftPadding,
@@ -364,7 +701,7 @@
bounds = calculateInsetsForRotationWithRotatedResources(
currentRotation,
targetRotation,
- dc,
+ sysUICutout = sysUICutout,
screenBounds,
sbHeightLandscape,
minLeftPadding,
@@ -385,7 +722,7 @@
bounds = calculateInsetsForRotationWithRotatedResources(
currentRotation,
targetRotation,
- dc,
+ sysUICutout = sysUICutout,
screenBounds,
sbHeightPortrait,
minLeftPadding,
@@ -406,7 +743,7 @@
bounds = calculateInsetsForRotationWithRotatedResources(
currentRotation,
targetRotation,
- dc,
+ sysUICutout = sysUICutout,
screenBounds,
sbHeightLandscape,
minLeftPadding,
@@ -528,7 +865,7 @@
val dotWidth = 10
val statusBarContentHeight = 15
- `when`(dc.boundingRects).thenReturn(listOf(dcBounds))
+ whenever(dc.boundingRects).thenReturn(listOf(dcBounds))
// THEN left should be set to the display cutout width, and right should use the minRight
val targetRotation = ROTATION_NONE
@@ -540,7 +877,7 @@
val bounds = calculateInsetsForRotationWithRotatedResources(
currentRotation,
targetRotation,
- dc,
+ sysUICutout,
screenBounds,
sbHeightPortrait,
minLeftPadding,
@@ -557,7 +894,7 @@
fun testDisplayChanged_returnsUpdatedInsets() {
// GIVEN: get insets on the first display and switch to the second display
val provider = StatusBarContentInsetsProvider(contextMock, configurationController,
- mock<DumpManager>(), mock<CommandRegistry>())
+ mock<DumpManager>(), mock<CommandRegistry>(), mock<SysUICutoutProvider>())
configuration.windowConfiguration.setMaxBounds(Rect(0, 0, 1080, 2160))
val firstDisplayInsets = provider.getStatusBarContentAreaForRotation(ROTATION_NONE)
@@ -576,7 +913,7 @@
// GIVEN: get insets on the first display, switch to the second display,
// get insets and switch back
val provider = StatusBarContentInsetsProvider(contextMock, configurationController,
- mock<DumpManager>(), mock<CommandRegistry>())
+ mock<DumpManager>(), mock<CommandRegistry>(), mock<SysUICutoutProvider>())
configuration.windowConfiguration.setMaxBounds(Rect(0, 0, 1080, 2160))
val firstDisplayInsetsFirstCall = provider
@@ -602,7 +939,7 @@
configuration.windowConfiguration.setMaxBounds(0, 0, 100, 100)
configurationController.onConfigurationChanged(configuration)
val provider = StatusBarContentInsetsProvider(contextMock, configurationController,
- mock<DumpManager>(), mock<CommandRegistry>())
+ mock<DumpManager>(), mock<CommandRegistry>(), mock<SysUICutoutProvider>())
val listener = object : StatusBarContentInsetsChangedListener {
var triggered = false
@@ -624,7 +961,7 @@
fun onDensityOrFontScaleChanged_listenerNotified() {
configuration.densityDpi = 12
val provider = StatusBarContentInsetsProvider(contextMock, configurationController,
- mock<DumpManager>(), mock<CommandRegistry>())
+ mock<DumpManager>(), mock<CommandRegistry>(), mock<SysUICutoutProvider>())
val listener = object : StatusBarContentInsetsChangedListener {
var triggered = false
@@ -645,7 +982,7 @@
@Test
fun onThemeChanged_listenerNotified() {
val provider = StatusBarContentInsetsProvider(contextMock, configurationController,
- mock<DumpManager>(), mock<CommandRegistry>())
+ mock<DumpManager>(), mock<CommandRegistry>(), mock<SysUICutoutProvider>())
val listener = object : StatusBarContentInsetsChangedListener {
var triggered = false
diff --git a/packages/SystemUI/plugin/Android.bp b/packages/SystemUI/plugin/Android.bp
index 9063a02..682a68f 100644
--- a/packages/SystemUI/plugin/Android.bp
+++ b/packages/SystemUI/plugin/Android.bp
@@ -13,6 +13,7 @@
// limitations under the License.
package {
+ default_team: "trendy_team_system_ui_please_use_a_more_specific_subteam_if_possible_",
// See: http://go/android-license-faq
// A large-scale-change added 'default_applicable_licenses' to import
// all of the 'license_kinds' from "frameworks_base_packages_SystemUI_license"
diff --git a/packages/SystemUI/plugin/ExamplePlugin/Android.bp b/packages/SystemUI/plugin/ExamplePlugin/Android.bp
index 66951b5..f6fa6a5 100644
--- a/packages/SystemUI/plugin/ExamplePlugin/Android.bp
+++ b/packages/SystemUI/plugin/ExamplePlugin/Android.bp
@@ -1,4 +1,5 @@
package {
+ default_team: "trendy_team_system_ui_please_use_a_more_specific_subteam_if_possible_",
// See: http://go/android-license-faq
// A large-scale-change added 'default_applicable_licenses' to import
// all of the 'license_kinds' from "frameworks_base_packages_SystemUI_license"
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/ActivityStarter.java b/packages/SystemUI/plugin/src/com/android/systemui/plugins/ActivityStarter.java
index 6434209..1126ec3 100644
--- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/ActivityStarter.java
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/ActivityStarter.java
@@ -20,7 +20,7 @@
import android.os.UserHandle;
import android.view.View;
-import com.android.systemui.animation.ActivityLaunchAnimator;
+import com.android.systemui.animation.ActivityTransitionAnimator;
import com.android.systemui.plugins.annotations.ProvidesInterface;
/**
@@ -54,7 +54,7 @@
*/
void startPendingIntentDismissingKeyguard(PendingIntent intent,
Runnable intentSentUiThreadCallback,
- @Nullable ActivityLaunchAnimator.Controller animationController);
+ @Nullable ActivityTransitionAnimator.Controller animationController);
/**
* Similar to {@link #startPendingIntentDismissingKeyguard}, except that it supports launching
@@ -64,7 +64,7 @@
*/
void startPendingIntentMaybeDismissingKeyguard(PendingIntent intent,
@Nullable Runnable intentSentUiThreadCallback,
- @Nullable ActivityLaunchAnimator.Controller animationController);
+ @Nullable ActivityTransitionAnimator.Controller animationController);
/**
* The intent flag can be specified in startActivity().
@@ -72,26 +72,26 @@
void startActivity(Intent intent, boolean onlyProvisioned, boolean dismissShade, int flags);
void startActivity(Intent intent, boolean dismissShade);
default void startActivity(Intent intent, boolean dismissShade,
- @Nullable ActivityLaunchAnimator.Controller animationController) {
+ @Nullable ActivityTransitionAnimator.Controller animationController) {
startActivity(intent, dismissShade, animationController,
false /* showOverLockscreenWhenLocked */);
}
void startActivity(Intent intent, boolean dismissShade,
- @Nullable ActivityLaunchAnimator.Controller animationController,
+ @Nullable ActivityTransitionAnimator.Controller animationController,
boolean showOverLockscreenWhenLocked);
void startActivity(Intent intent, boolean dismissShade,
- @Nullable ActivityLaunchAnimator.Controller animationController,
+ @Nullable ActivityTransitionAnimator.Controller animationController,
boolean showOverLockscreenWhenLocked, UserHandle userHandle);
void startActivity(Intent intent, boolean onlyProvisioned, boolean dismissShade);
void startActivity(Intent intent, boolean dismissShade, Callback callback);
void postStartActivityDismissingKeyguard(Intent intent, int delay);
void postStartActivityDismissingKeyguard(Intent intent, int delay,
- @Nullable ActivityLaunchAnimator.Controller animationController);
+ @Nullable ActivityTransitionAnimator.Controller animationController);
/** Posts a start activity intent that dismisses keyguard. */
void postStartActivityDismissingKeyguard(Intent intent, int delay,
- @Nullable ActivityLaunchAnimator.Controller animationController,
+ @Nullable ActivityTransitionAnimator.Controller animationController,
@Nullable String customMessage);
void postStartActivityDismissingKeyguard(PendingIntent intent);
@@ -100,7 +100,7 @@
* animation controller that should be used for the activity launch animation.
*/
void postStartActivityDismissingKeyguard(PendingIntent intent,
- @Nullable ActivityLaunchAnimator.Controller animationController);
+ @Nullable ActivityTransitionAnimator.Controller animationController);
void postQSRunnableDismissingKeyguard(Runnable runnable);
@@ -123,7 +123,7 @@
boolean disallowEnterPictureInPictureWhileLaunching,
Callback callback,
int flags,
- @Nullable ActivityLaunchAnimator.Controller animationController,
+ @Nullable ActivityTransitionAnimator.Controller animationController,
UserHandle userHandle);
/** Execute a runnable after dismissing keyguard. */
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockProviderPlugin.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockProviderPlugin.kt
index 4436be7..fd7a7f3 100644
--- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockProviderPlugin.kt
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockProviderPlugin.kt
@@ -116,6 +116,8 @@
/** Custom constraints to apply to Lockscreen ConstraintLayout. */
fun applyConstraints(constraints: ConstraintSet): ConstraintSet
+
+ fun applyPreviewConstraints(constraints: ConstraintSet): ConstraintSet
}
/** A ClockFaceLayout that applies the default lockscreen layout to a single view */
@@ -131,6 +133,10 @@
}
return constraints
}
+
+ override fun applyPreviewConstraints(constraints: ConstraintSet): ConstraintSet {
+ return constraints
+ }
}
/** Events that should call when various rendering parameters change */
diff --git a/packages/SystemUI/plugin_core/Android.bp b/packages/SystemUI/plugin_core/Android.bp
index b7e1545..521c019 100644
--- a/packages/SystemUI/plugin_core/Android.bp
+++ b/packages/SystemUI/plugin_core/Android.bp
@@ -13,6 +13,7 @@
// limitations under the License.
package {
+ default_team: "trendy_team_system_ui_please_use_a_more_specific_subteam_if_possible_",
// See: http://go/android-license-faq
// A large-scale-change added 'default_applicable_licenses' to import
// all of the 'license_kinds' from "frameworks_base_packages_SystemUI_license"
diff --git a/packages/SystemUI/res-keyguard/drawable/bouncer_input_method_background.xml b/packages/SystemUI/res-keyguard/drawable/bouncer_input_method_background.xml
new file mode 100644
index 0000000..ad22894
--- /dev/null
+++ b/packages/SystemUI/res-keyguard/drawable/bouncer_input_method_background.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ ~ Copyright (C) 2024 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:state_focused="true">
+ <shape android:shape="oval">
+ <stroke android:width="3dp" android:color="@color/bouncer_password_focus_color"/>
+ </shape>
+ </item>
+</selector>
\ No newline at end of file
diff --git a/packages/SystemUI/res-keyguard/drawable/bouncer_password_view_background.xml b/packages/SystemUI/res-keyguard/drawable/bouncer_password_view_background.xml
new file mode 100644
index 0000000..8c2b036
--- /dev/null
+++ b/packages/SystemUI/res-keyguard/drawable/bouncer_password_view_background.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ ~ Copyright (C) 2024 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:state_focused="true">
+ <shape android:shape="rectangle">
+ <corners android:radius="16dp" />
+ <stroke android:width="3dp"
+ android:color="@color/bouncer_password_focus_color" />
+ <padding android:bottom="8dp" android:left="8dp" android:right="8dp" android:top="8dp"/>
+ </shape>
+ </item>
+ <item>
+ <inset android:insetLeft="-4dp"
+ android:insetRight="-4dp"
+ android:insetTop="-4dp">
+ <shape android:shape="rectangle">
+ <stroke android:width="3dp" android:color="@color/bouncer_password_focus_color"/>
+ </shape>
+ </inset>
+ </item>
+</selector>
\ No newline at end of file
diff --git a/packages/SystemUI/res-keyguard/drawable/bouncer_password_text_view_focused_background.xml b/packages/SystemUI/res-keyguard/drawable/bouncer_pin_view_focused_background.xml
similarity index 100%
rename from packages/SystemUI/res-keyguard/drawable/bouncer_password_text_view_focused_background.xml
rename to packages/SystemUI/res-keyguard/drawable/bouncer_pin_view_focused_background.xml
diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_password_view.xml b/packages/SystemUI/res-keyguard/layout/keyguard_password_view.xml
index 2fc1d2e..909d4fc 100644
--- a/packages/SystemUI/res-keyguard/layout/keyguard_password_view.xml
+++ b/packages/SystemUI/res-keyguard/layout/keyguard_password_view.xml
@@ -54,7 +54,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:contentDescription="@string/keyguard_accessibility_password"
- android:gravity="center_horizontal"
+ android:gravity="center"
android:singleLine="true"
android:textStyle="normal"
android:inputType="textPassword"
@@ -68,14 +68,14 @@
<ImageView android:id="@+id/switch_ime_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:layout_marginBottom="12dp"
android:src="@drawable/ic_lockscreen_ime"
android:contentDescription="@string/accessibility_ime_switch_button"
android:clickable="true"
- android:padding="8dip"
+ android:layout_marginRight="8dp"
+ android:padding="12dip"
android:tint="?android:attr/textColorPrimary"
android:layout_gravity="end|center_vertical"
- android:background="?android:attr/selectableItemBackground"
+ android:background="@drawable/bouncer_input_method_background"
android:visibility="gone"
/>
</FrameLayout>
diff --git a/packages/SystemUI/res-keyguard/values/dimens.xml b/packages/SystemUI/res-keyguard/values/dimens.xml
index ddad1e3..e853f02 100644
--- a/packages/SystemUI/res-keyguard/values/dimens.xml
+++ b/packages/SystemUI/res-keyguard/values/dimens.xml
@@ -28,8 +28,14 @@
<!-- Width for the keyguard pin input field -->
<dimen name="keyguard_pin_field_width">292dp</dimen>
- <!-- Width for the keyguard pin input field -->
- <dimen name="keyguard_pin_field_height">48dp</dimen>
+ <!-- height for the keyguard pin input field -->
+ <dimen name="keyguard_pin_field_height">56dp</dimen>
+
+ <!-- height for the keyguard password input field -->
+ <dimen name="keyguard_password_field_height">56dp</dimen>
+
+ <!-- width for the keyguard password input field -->
+ <dimen name="keyguard_password_field_width">276dp</dimen>
<!-- Height of the sliding KeyguardSecurityContainer
(includes 2x keyguard_security_view_top_margin) -->
diff --git a/packages/SystemUI/res-keyguard/values/styles.xml b/packages/SystemUI/res-keyguard/values/styles.xml
index 4789a22..c43e394 100644
--- a/packages/SystemUI/res-keyguard/values/styles.xml
+++ b/packages/SystemUI/res-keyguard/values/styles.xml
@@ -76,7 +76,7 @@
</style>
<style name="Widget.TextView.Password" parent="@android:style/Widget.TextView">
<item name="android:fontFamily">@*android:string/config_headlineFontFamily</item>
- <item name="android:background">@drawable/bouncer_password_text_view_focused_background</item>
+ <item name="android:background">@drawable/bouncer_pin_view_focused_background</item>
<item name="android:gravity">center</item>
<item name="android:textColor">?android:attr/textColorPrimary</item>
</style>
diff --git a/packages/SystemUI/res/color/brightness_slider_track.xml b/packages/SystemUI/res/color/brightness_slider_track.xml
new file mode 100644
index 0000000..6028769
--- /dev/null
+++ b/packages/SystemUI/res/color/brightness_slider_track.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ ~ Copyright (C) 2024 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:color="@android:color/system_neutral2_500" android:lStar="40" />
+</selector>
\ No newline at end of file
diff --git a/packages/SystemUI/res/drawable/brightness_progress_drawable.xml b/packages/SystemUI/res/drawable/brightness_progress_drawable.xml
index 95c7778c..cae9d6b 100644
--- a/packages/SystemUI/res/drawable/brightness_progress_drawable.xml
+++ b/packages/SystemUI/res/drawable/brightness_progress_drawable.xml
@@ -24,7 +24,7 @@
<shape>
<size android:height="@dimen/rounded_slider_track_width" />
<corners android:radius="@dimen/rounded_slider_track_corner_radius" />
- <solid android:color="?attr/shadeInactive" />
+ <solid android:color="@color/brightness_slider_track" />
</shape>
</inset>
</item>
diff --git a/packages/SystemUI/res/drawable/ic_satellite_not_connected.xml b/packages/SystemUI/res/drawable/ic_satellite_not_connected.xml
index dec9930..a80d3b4 100644
--- a/packages/SystemUI/res/drawable/ic_satellite_not_connected.xml
+++ b/packages/SystemUI/res/drawable/ic_satellite_not_connected.xml
@@ -20,6 +20,7 @@
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0"
+ android:alpha="0.3"
>
<path
android:pathData="M14.73,3.36L17.63,6.2C17.83,6.39 17.83,6.71 17.63,6.91L16.89,7.65C16.69,7.85 16.37,7.85 16.18,7.65L13.34,4.78C13.15,4.59 13.15,4.28 13.34,4.08L14.01,3.37C14.2,3.17 14.52,3.16 14.72,3.36H14.73ZM14.37,1C13.85,1 13.32,1.2 12.93,1.61L11.56,3.06C10.8,3.84 10.81,5.09 11.58,5.86L15.13,9.41C15.52,9.8 16.03,10 16.55,10C17.07,10 17.58,9.8 17.97,9.41L19.42,7.96C20.21,7.17 20.2,5.89 19.4,5.12L15.77,1.57C15.38,1.19 14.88,1 14.37,1Z"
diff --git a/packages/SystemUI/res/layout/biometric_prompt_constraint_layout.xml b/packages/SystemUI/res/layout/biometric_prompt_constraint_layout.xml
new file mode 100644
index 0000000..a877853
--- /dev/null
+++ b/packages/SystemUI/res/layout/biometric_prompt_constraint_layout.xml
@@ -0,0 +1,244 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+xmlns:app="http://schemas.android.com/apk/res-auto"
+xmlns:tools="http://schemas.android.com/tools"
+android:layout_width="match_parent"
+android:layout_height="match_parent">
+
+ <ImageView
+ android:id="@+id/logo"
+ android:layout_width="@dimen/biometric_auth_icon_size"
+ android:layout_height="@dimen/biometric_auth_icon_size"
+ android:layout_gravity="center"
+ android:scaleType="fitXY"
+ android:visibility="gone" />
+
+ <ImageView
+ android:id="@+id/background"
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ android:contentDescription="@string/biometric_dialog_empty_space_description"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent" />
+
+ <View
+ android:id="@+id/panel"
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ android:background="?android:attr/colorBackgroundFloating"
+ android:clickable="true"
+ android:clipToOutline="true"
+ android:importantForAccessibility="no"
+ android:paddingHorizontal="16dp"
+ android:paddingVertical="16dp"
+ android:visibility="visible"
+ app:layout_constraintBottom_toTopOf="@+id/bottomGuideline"
+ app:layout_constraintEnd_toStartOf="@+id/rightGuideline"
+ app:layout_constraintStart_toStartOf="@+id/leftGuideline"
+ app:layout_constraintTop_toTopOf="@+id/title" />
+
+ <com.android.systemui.biometrics.BiometricPromptLottieViewWrapper
+ android:id="@+id/biometric_icon"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintVertical_bias="0.8"
+ tools:srcCompat="@tools:sample/avatars" />
+
+ <com.android.systemui.biometrics.BiometricPromptLottieViewWrapper
+ android:id="@+id/biometric_icon_overlay"
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ android:layout_gravity="center"
+ android:contentDescription="@null"
+ android:scaleType="fitXY"
+ app:layout_constraintBottom_toBottomOf="@+id/biometric_icon"
+ app:layout_constraintEnd_toEndOf="@+id/biometric_icon"
+ app:layout_constraintHorizontal_bias="1.0"
+ app:layout_constraintStart_toStartOf="@+id/biometric_icon"
+ app:layout_constraintTop_toTopOf="@+id/biometric_icon"
+ app:layout_constraintVertical_bias="0.0" />
+
+ <TextView
+ android:id="@+id/title"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:gravity="@integer/biometric_dialog_text_gravity"
+ android:singleLine="true"
+ android:marqueeRepeatLimit="1"
+ android:ellipsize="marquee"
+ style="@style/TextAppearance.AuthCredential.Title"
+ app:layout_constraintBottom_toTopOf="@+id/subtitle"
+ app:layout_constraintEnd_toEndOf="@+id/panel"
+ app:layout_constraintStart_toStartOf="@+id/panel" />
+
+ <TextView
+ android:id="@+id/subtitle"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:gravity="@integer/biometric_dialog_text_gravity"
+ android:singleLine="true"
+ android:marqueeRepeatLimit="1"
+ android:ellipsize="marquee"
+ style="@style/TextAppearance.AuthCredential.Subtitle"
+ app:layout_constraintBottom_toTopOf="@+id/description"
+ app:layout_constraintEnd_toEndOf="@+id/panel"
+ app:layout_constraintStart_toStartOf="@+id/panel" />
+
+ <Space
+ android:id="@+id/space_above_content"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/biometric_prompt_space_above_content"
+ android:visibility="gone"
+ app:layout_constraintTop_toBottomOf="@+id/subtitle"
+ app:layout_constraintEnd_toEndOf="@+id/panel"
+ app:layout_constraintStart_toStartOf="@+id/panel"/>
+
+ <ScrollView
+ android:id="@+id/customized_view_container"
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ android:fillViewport="true"
+ android:fadeScrollbars="false"
+ android:gravity="center_vertical"
+ android:orientation="vertical"
+ android:paddingHorizontal="@dimen/biometric_prompt_content_container_padding_horizontal"
+ android:scrollbars="vertical"
+ android:visibility="gone"
+ app:layout_constraintTop_toBottomOf="@+id/space_above_content"
+ app:layout_constraintBottom_toTopOf="@+id/biometric_icon"
+ app:layout_constraintEnd_toEndOf="@+id/panel"
+ app:layout_constraintStart_toStartOf="@+id/panel"/>
+
+ <TextView
+ android:id="@+id/description"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="24dp"
+ android:scrollbars="vertical"
+ android:gravity="@integer/biometric_dialog_text_gravity"
+ style="@style/TextAppearance.AuthCredential.Description"
+ app:layout_constraintBottom_toTopOf="@+id/biometric_icon"
+ app:layout_constraintEnd_toEndOf="@+id/panel"
+ app:layout_constraintStart_toStartOf="@+id/panel" />
+
+ <TextView
+ android:id="@+id/indicator"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="16dp"
+ android:gravity="center_horizontal"
+ android:textColor="@color/biometric_dialog_gray"
+ android:textSize="12sp"
+ android:accessibilityLiveRegion="polite"
+ android:marqueeRepeatLimit="marquee_forever"
+ android:scrollHorizontally="true"
+ android:fadingEdge="horizontal"
+ app:layout_constraintEnd_toEndOf="@+id/panel"
+ app:layout_constraintHorizontal_bias="0.5"
+ app:layout_constraintStart_toStartOf="@+id/panel"
+ app:layout_constraintTop_toBottomOf="@+id/biometric_icon" />
+
+ <!-- Negative Button, reserved for app -->
+ <Button
+ android:id="@+id/button_negative"
+ style="@*android:style/Widget.DeviceDefault.Button.Borderless.Colored"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_vertical"
+ android:layout_marginBottom="8dp"
+ android:layout_marginLeft="8dp"
+ android:ellipsize="end"
+ android:maxLines="2"
+ android:visibility="invisible"
+ app:layout_constraintBottom_toBottomOf="@+id/panel"
+ app:layout_constraintStart_toStartOf="@+id/panel" />
+
+ <!-- Cancel Button, replaces negative button when biometric is accepted -->
+ <Button
+ android:id="@+id/button_cancel"
+ style="@*android:style/Widget.DeviceDefault.Button.Borderless.Colored"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_vertical"
+ android:layout_marginBottom="8dp"
+ android:layout_marginLeft="8dp"
+ android:text="@string/cancel"
+ android:visibility="invisible"
+ app:layout_constraintBottom_toBottomOf="@+id/panel"
+ app:layout_constraintStart_toStartOf="@+id/panel" />
+
+ <!-- "Use Credential" Button, replaces if device credential is allowed -->
+ <Button
+ android:id="@+id/button_use_credential"
+ style="@*android:style/Widget.DeviceDefault.Button.Borderless.Colored"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_vertical"
+ android:layout_marginBottom="8dp"
+ android:layout_marginLeft="8dp"
+ android:visibility="invisible"
+ app:layout_constraintBottom_toBottomOf="@+id/panel"
+ app:layout_constraintStart_toStartOf="@+id/panel" />
+
+ <!-- Positive Button -->
+ <Button
+ android:id="@+id/button_confirm"
+ style="@*android:style/Widget.DeviceDefault.Button.Colored"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_vertical"
+ android:layout_marginBottom="8dp"
+ android:layout_marginRight="8dp"
+ android:ellipsize="end"
+ android:maxLines="2"
+ android:text="@string/biometric_dialog_confirm"
+ android:visibility="invisible"
+ app:layout_constraintBottom_toBottomOf="@+id/panel"
+ app:layout_constraintEnd_toEndOf="@+id/panel"
+ tools:visibility="invisible" />
+
+ <!-- Try Again Button -->
+ <Button
+ android:id="@+id/button_try_again"
+ style="@*android:style/Widget.DeviceDefault.Button.Colored"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_vertical"
+ android:layout_marginBottom="8dp"
+ android:layout_marginRight="8dp"
+ android:ellipsize="end"
+ android:maxLines="2"
+ android:text="@string/biometric_dialog_try_again"
+ android:visibility="invisible"
+ app:layout_constraintBottom_toBottomOf="@+id/panel"
+ app:layout_constraintEnd_toEndOf="@+id/panel" />
+
+ <!-- Guidelines for setting panel border -->
+ <androidx.constraintlayout.widget.Guideline
+ android:id="@+id/leftGuideline"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ app:layout_constraintGuide_begin="@dimen/biometric_dialog_border_padding" />
+
+ <androidx.constraintlayout.widget.Guideline
+ android:id="@+id/rightGuideline"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ app:layout_constraintGuide_end="@dimen/biometric_dialog_border_padding" />
+
+ <androidx.constraintlayout.widget.Guideline
+ android:id="@+id/bottomGuideline"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ app:layout_constraintGuide_end="@dimen/biometric_dialog_border_padding" />
+
+</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/packages/SystemUI/res/layout/notification_snooze.xml b/packages/SystemUI/res/layout/notification_snooze.xml
index 6e541a7..ce09385 100644
--- a/packages/SystemUI/res/layout/notification_snooze.xml
+++ b/packages/SystemUI/res/layout/notification_snooze.xml
@@ -49,7 +49,7 @@
android:layout_centerVertical="true"
android:paddingTop="1dp"
android:importantForAccessibility="yes"
- android:tint="#9E9E9E" />
+ android:tint="?android:attr/textColorPrimary"/>
<TextView
android:id="@+id/undo"
diff --git a/packages/SystemUI/res/values-xxhdpi/dimens.xml b/packages/SystemUI/res/values-xxhdpi/dimens.xml
index 26c8437..9bff422 100644
--- a/packages/SystemUI/res/values-xxhdpi/dimens.xml
+++ b/packages/SystemUI/res/values-xxhdpi/dimens.xml
@@ -22,4 +22,8 @@
fraction of a pixel.-->
<fraction name="battery_subpixel_smoothing_left">33%</fraction>
<fraction name="battery_subpixel_smoothing_right">33%</fraction>
+
+ <!-- Biometrics fingerprint icon size for full resolution.-->
+ <dimen name="biometric_dialog_fingerprint_icon_width">120dp</dimen>
+ <dimen name="biometric_dialog_fingerprint_icon_height">120dp</dimen>
</resources>
diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml
index 7a83070..65c69f7 100644
--- a/packages/SystemUI/res/values/config.xml
+++ b/packages/SystemUI/res/values/config.xml
@@ -542,6 +542,9 @@
<string translatable="false" name="config_protectedCameraId"></string>
<!-- Physical ID for the camera of outer display that needs extra protection -->
<string translatable="false" name="config_protectedPhysicalCameraId"></string>
+ <!-- Unique ID of the outer display that contains the camera that needs protection. -->
+ <string translatable="false" name="config_protectedScreenUniqueId"></string>
+
<!-- Similar to config_frontBuiltInDisplayCutoutProtection but for inner display. -->
<string translatable="false" name="config_innerBuiltInDisplayCutoutProtection"></string>
@@ -550,6 +553,8 @@
<string translatable="false" name="config_protectedInnerCameraId"></string>
<!-- Physical ID for the camera of inner display that needs extra protection -->
<string translatable="false" name="config_protectedInnerPhysicalCameraId"></string>
+ <!-- Unique ID of the inner display that contains the camera that needs protection. -->
+ <string translatable="false" name="config_protectedInnerScreenUniqueId"></string>
<!-- Comma-separated list of packages to exclude from camera protection e.g.
"com.android.systemui,com.android.xyz" -->
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index 33bdca3..cc31754 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -174,7 +174,7 @@
<dimen name="status_bar_clock_size">14sp</dimen>
<!-- The starting padding for the clock in the status bar. -->
- <dimen name="status_bar_clock_starting_padding">7dp</dimen>
+ <dimen name="status_bar_clock_starting_padding">4dp</dimen>
<!-- The end padding for the clock in the status bar. -->
<dimen name="status_bar_clock_end_padding">0dp</dimen>
@@ -318,9 +318,6 @@
notification panel collapses -->
<dimen name="shelf_appear_translation">42dp</dimen>
- <!-- Vertical translation of pulsing notification animations -->
- <dimen name="pulsing_notification_appear_translation">10dp</dimen>
-
<!-- The amount the content shifts upwards when transforming into the shelf -->
<dimen name="shelf_transform_content_shift">32dp</dimen>
@@ -398,7 +395,7 @@
<dimen name="status_bar_icon_horizontal_margin">0sp</dimen>
<!-- the padding on the start of the statusbar -->
- <dimen name="status_bar_padding_start">8dp</dimen>
+ <dimen name="status_bar_padding_start">4dp</dimen>
<!-- the padding on the end of the statusbar -->
<dimen name="status_bar_padding_end">4dp</dimen>
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index 8971859..15688c5 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -305,6 +305,27 @@
<!-- A toast message shown when the screen recording cannot be started due to a generic error [CHAR LIMIT=NONE] -->
<string name="screenrecord_start_error">Error starting screen recording</string>
+ <!-- Notification title displayed for issue recording [CHAR LIMIT=50]-->
+ <string name="issuerecord_title">Issue Recorder</string>
+ <!-- Processing issue recoding data in the background [CHAR LIMIT=30]-->
+ <string name="issuerecord_background_processing_label">Processing issue recording</string>
+ <!-- Description of the screen recording notification channel [CHAR LIMIT=NONE]-->
+ <string name="issuerecord_channel_description">Ongoing notification for an issue collection session</string>
+
+ <!-- Notification text displayed when we are recording the screen [CHAR LIMIT=100]-->
+ <string name="issuerecord_ongoing_screen_only">Recording issue</string>
+ <!-- Label for notification action to share issue recording [CHAR LIMIT=35] -->
+ <string name="issuerecord_share_label">Share</string>
+ <!-- A toast message shown after successfully canceling a issue recording [CHAR LIMIT=NONE] -->
+ <!-- Notification text shown after saving a issue recording [CHAR LIMIT=100] -->
+ <string name="issuerecord_save_title">Issue recording saved</string>
+ <!-- Subtext for a notification shown after saving a issue recording to prompt the user to view it [CHAR_LIMIT=100] -->
+ <string name="issuerecord_save_text">Tap to view</string>
+ <!-- A toast message shown when there is an error saving a issue recording [CHAR LIMIT=NONE] -->
+ <string name="issuerecord_save_error">Error saving issue recording</string>
+ <!-- A toast message shown when the issue recording cannot be started due to a generic error [CHAR LIMIT=NONE] -->
+ <string name="issuerecord_start_error">Error starting issue recording</string>
+
<!-- Cling help message title when hiding the navigation bar entering immersive mode [CHAR LIMIT=none] -->
<string name="immersive_cling_title">Viewing full screen</string>
<!-- Cling help message description when hiding the navigation bar entering immersive mode [CHAR LIMIT=none] -->
@@ -1578,6 +1599,15 @@
<!-- Accessibility label for hotspot icon [CHAR LIMIT=NONE] -->
<string name="accessibility_status_bar_hotspot">Hotspot</string>
+ <!-- Accessibility label for no satellite connection [CHAR LIMIT=NONE] -->
+ <string name="accessibility_status_bar_satellite_no_connection">Satellite, no connection</string>
+ <!-- Accessibility label for poor satellite connection [CHAR LIMIT=NONE] -->
+ <string name="accessibility_status_bar_satellite_poor_connection">Satellite, poor connection</string>
+ <!-- Accessibility label for good satellite connection [CHAR LIMIT=NONE] -->
+ <string name="accessibility_status_bar_satellite_good_connection">Satellite, good connection</string>
+ <!-- Accessibility label for available satellite connection [CHAR LIMIT=NONE] -->
+ <string name="accessibility_status_bar_satellite_available">Satellite, connection available</string>
+
<!-- Accessibility label for managed profile icon (not shown on screen) [CHAR LIMIT=NONE] -->
<string name="accessibility_managed_profile">Work profile</string>
@@ -3316,6 +3346,6 @@
<string name="keyboard_backlight_value">Level %1$d of %2$d</string>
<!-- Label for home control panel [CHAR LIMIT=30] -->
<string name="home_controls_dream_label">Home Controls</string>
- <!-- Description for home control panel [CHAR LIMIT=50] -->
+ <!-- Description for home control panel [CHAR LIMIT=67] -->
<string name="home_controls_dream_description">Quickly access your home controls as a screensaver</string>
</resources>
diff --git a/packages/SystemUI/shared/Android.bp b/packages/SystemUI/shared/Android.bp
index 4e04af6..6e611fe 100644
--- a/packages/SystemUI/shared/Android.bp
+++ b/packages/SystemUI/shared/Android.bp
@@ -13,6 +13,7 @@
// limitations under the License.
package {
+ default_team: "trendy_team_system_ui_please_use_a_more_specific_subteam_if_possible_",
// See: http://go/android-license-faq
// A large-scale-change added 'default_applicable_licenses' to import
// all of the 'license_kinds' from "frameworks_base_packages_SystemUI_license"
@@ -54,7 +55,7 @@
"SystemUIUnfoldLib",
"SystemUISharedLib-Keyguard",
"WindowManager-Shell-shared",
- "tracinglib",
+ "tracinglib-platform",
"androidx.dynamicanimation_dynamicanimation",
"androidx.concurrent_concurrent-futures",
"androidx.lifecycle_lifecycle-runtime-ktx",
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardAbsKeyInputView.java b/packages/SystemUI/src/com/android/keyguard/KeyguardAbsKeyInputView.java
index dfeb1f3..d821f19 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardAbsKeyInputView.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardAbsKeyInputView.java
@@ -33,7 +33,7 @@
public abstract class KeyguardAbsKeyInputView extends KeyguardInputView {
protected View mEcaView;
- // To avoid accidental lockout due to events while the device in in the pocket, ignore
+ // To avoid accidental lockout due to events while the device in the pocket, ignore
// any passwords with length less than or equal to this length.
protected static final int MINIMUM_PASSWORD_LENGTH_BEFORE_REPORT = 3;
private KeyDownListener mKeyDownListener;
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardInputViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardInputViewController.java
index efd8f7f..458a21c 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardInputViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardInputViewController.java
@@ -30,6 +30,7 @@
import com.android.internal.util.LatencyTracker;
import com.android.internal.widget.LockPatternUtils;
import com.android.keyguard.KeyguardSecurityModel.SecurityMode;
+import com.android.keyguard.domain.interactor.KeyguardKeyboardInteractor;
import com.android.systemui.Flags;
import com.android.systemui.bouncer.domain.interactor.BouncerMessageInteractor;
import com.android.systemui.bouncer.ui.BouncerMessageView;
@@ -37,7 +38,6 @@
import com.android.systemui.classifier.FalsingCollector;
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.flags.FeatureFlags;
-import com.android.systemui.keyboard.data.repository.KeyboardRepository;
import com.android.systemui.log.BouncerLogger;
import com.android.systemui.res.R;
import com.android.systemui.statusbar.policy.DevicePostureController;
@@ -211,7 +211,7 @@
private final FeatureFlags mFeatureFlags;
private final SelectedUserInteractor mSelectedUserInteractor;
private final UiEventLogger mUiEventLogger;
- private final KeyboardRepository mKeyboardRepository;
+ private final KeyguardKeyboardInteractor mKeyguardKeyboardInteractor;
@Inject
public Factory(KeyguardUpdateMonitor keyguardUpdateMonitor,
@@ -226,7 +226,7 @@
KeyguardViewController keyguardViewController,
FeatureFlags featureFlags, SelectedUserInteractor selectedUserInteractor,
UiEventLogger uiEventLogger,
- KeyboardRepository keyboardRepository) {
+ KeyguardKeyboardInteractor keyguardKeyboardInteractor) {
mKeyguardUpdateMonitor = keyguardUpdateMonitor;
mLockPatternUtils = lockPatternUtils;
mLatencyTracker = latencyTracker;
@@ -243,7 +243,7 @@
mFeatureFlags = featureFlags;
mSelectedUserInteractor = selectedUserInteractor;
mUiEventLogger = uiEventLogger;
- mKeyboardRepository = keyboardRepository;
+ mKeyguardKeyboardInteractor = keyguardKeyboardInteractor;
}
/** Create a new {@link KeyguardInputViewController}. */
@@ -265,14 +265,15 @@
keyguardSecurityCallback, mMessageAreaControllerFactory, mLatencyTracker,
mInputMethodManager, emergencyButtonController, mMainExecutor, mResources,
mFalsingCollector, mKeyguardViewController,
- mDevicePostureController, mFeatureFlags, mSelectedUserInteractor);
+ mDevicePostureController, mFeatureFlags, mSelectedUserInteractor,
+ mKeyguardKeyboardInteractor);
} else if (keyguardInputView instanceof KeyguardPINView) {
return new KeyguardPinViewController((KeyguardPINView) keyguardInputView,
mKeyguardUpdateMonitor, securityMode, mLockPatternUtils,
keyguardSecurityCallback, mMessageAreaControllerFactory, mLatencyTracker,
mLiftToActivateListener, emergencyButtonController, mFalsingCollector,
mDevicePostureController, mFeatureFlags, mSelectedUserInteractor,
- mUiEventLogger, mKeyboardRepository
+ mUiEventLogger, mKeyguardKeyboardInteractor
);
} else if (keyguardInputView instanceof KeyguardSimPinView) {
return new KeyguardSimPinViewController((KeyguardSimPinView) keyguardInputView,
@@ -280,14 +281,15 @@
keyguardSecurityCallback, mMessageAreaControllerFactory, mLatencyTracker,
mLiftToActivateListener, mTelephonyManager, mFalsingCollector,
emergencyButtonController, mFeatureFlags, mSelectedUserInteractor,
- mKeyboardRepository);
+ mKeyguardKeyboardInteractor);
} else if (keyguardInputView instanceof KeyguardSimPukView) {
return new KeyguardSimPukViewController((KeyguardSimPukView) keyguardInputView,
mKeyguardUpdateMonitor, securityMode, mLockPatternUtils,
keyguardSecurityCallback, mMessageAreaControllerFactory, mLatencyTracker,
mLiftToActivateListener, mTelephonyManager, mFalsingCollector,
emergencyButtonController, mFeatureFlags, mSelectedUserInteractor,
- mKeyboardRepository);
+ mKeyguardKeyboardInteractor
+ );
}
throw new RuntimeException("Unable to find controller for " + keyguardInputView);
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPasswordViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPasswordViewController.java
index 3d8aaaf..7473e0c6 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardPasswordViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPasswordViewController.java
@@ -17,8 +17,10 @@
package com.android.keyguard;
import static com.android.systemui.flags.Flags.LOCKSCREEN_ENABLE_LANDSCAPE;
+import static com.android.systemui.util.kotlin.JavaAdapterKt.collectFlow;
import android.content.res.Resources;
+import android.graphics.drawable.Drawable;
import android.os.UserHandle;
import android.text.Editable;
import android.text.InputType;
@@ -27,6 +29,7 @@
import android.text.method.TextKeyListener;
import android.view.KeyEvent;
import android.view.View;
+import android.view.ViewGroup;
import android.view.ViewGroup.MarginLayoutParams;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputMethodInfo;
@@ -39,6 +42,8 @@
import com.android.internal.util.LatencyTracker;
import com.android.internal.widget.LockPatternUtils;
import com.android.keyguard.KeyguardSecurityModel.SecurityMode;
+import com.android.keyguard.domain.interactor.KeyguardKeyboardInteractor;
+import com.android.systemui.Flags;
import com.android.systemui.classifier.FalsingCollector;
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.flags.FeatureFlags;
@@ -52,6 +57,7 @@
public class KeyguardPasswordViewController
extends KeyguardAbsKeyInputViewController<KeyguardPasswordView> {
+ private final KeyguardKeyboardInteractor mKeyguardKeyboardInteractor;
private final KeyguardSecurityCallback mKeyguardSecurityCallback;
private final DevicePostureController mPostureController;
private final DevicePostureController.Callback mPostureCallback = posture ->
@@ -60,6 +66,8 @@
private final DelayableExecutor mMainExecutor;
private final KeyguardViewController mKeyguardViewController;
private final boolean mShowImeAtScreenOn;
+ private Drawable mDefaultPasswordFieldBackground;
+ private Drawable mFocusedPasswordFieldBackground;
private EditText mPasswordEntry;
private ImageView mSwitchImeButton;
private boolean mPaused;
@@ -121,7 +129,8 @@
KeyguardViewController keyguardViewController,
DevicePostureController postureController,
FeatureFlags featureFlags,
- SelectedUserInteractor selectedUserInteractor) {
+ SelectedUserInteractor selectedUserInteractor,
+ KeyguardKeyboardInteractor keyguardKeyboardInteractor) {
super(view, keyguardUpdateMonitor, securityMode, lockPatternUtils, keyguardSecurityCallback,
messageAreaControllerFactory, latencyTracker, falsingCollector,
emergencyButtonController, featureFlags, selectedUserInteractor);
@@ -130,11 +139,15 @@
mPostureController = postureController;
mMainExecutor = mainExecutor;
mKeyguardViewController = keyguardViewController;
+ mKeyguardKeyboardInteractor = keyguardKeyboardInteractor;
if (featureFlags.isEnabled(LOCKSCREEN_ENABLE_LANDSCAPE)) {
view.setIsLockScreenLandscapeEnabled();
}
mShowImeAtScreenOn = resources.getBoolean(R.bool.kg_show_ime_at_screen_on);
mPasswordEntry = mView.findViewById(mView.getPasswordTextViewId());
+ mDefaultPasswordFieldBackground = mPasswordEntry.getBackground();
+ mFocusedPasswordFieldBackground = getResources().getDrawable(
+ R.drawable.bouncer_password_view_background);
mSwitchImeButton = mView.findViewById(R.id.switch_ime_button);
}
@@ -175,6 +188,27 @@
// If there's more than one IME, enable the IME switcher button
updateSwitchImeButton();
+
+ if (Flags.pinInputFieldStyledFocusState()) {
+ collectFlow(mPasswordEntry,
+ mKeyguardKeyboardInteractor.isAnyKeyboardConnected(),
+ this::setPasswordFieldFocusBackground);
+
+ ViewGroup.LayoutParams layoutParams = mPasswordEntry.getLayoutParams();
+ layoutParams.height = (int) getResources()
+ .getDimension(R.dimen.keyguard_password_field_height);
+ layoutParams.width = (int) getResources()
+ .getDimension(R.dimen.keyguard_password_field_width);
+ }
+
+ }
+
+ private void setPasswordFieldFocusBackground(boolean isAnyKeyboardConnected) {
+ if (isAnyKeyboardConnected) {
+ mPasswordEntry.setBackground(mFocusedPasswordFieldBackground);
+ } else {
+ mPasswordEntry.setBackground(mDefaultPasswordFieldBackground);
+ }
}
@Override
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputViewController.java
index 60dd568..476497d 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputViewController.java
@@ -16,8 +16,8 @@
package com.android.keyguard;
-import static com.android.systemui.util.kotlin.JavaAdapterKt.collectFlow;
import static com.android.systemui.Flags.pinInputFieldStyledFocusState;
+import static com.android.systemui.util.kotlin.JavaAdapterKt.collectFlow;
import android.graphics.drawable.GradientDrawable;
import android.graphics.drawable.StateListDrawable;
@@ -32,9 +32,9 @@
import com.android.internal.util.LatencyTracker;
import com.android.internal.widget.LockPatternUtils;
import com.android.keyguard.KeyguardSecurityModel.SecurityMode;
+import com.android.keyguard.domain.interactor.KeyguardKeyboardInteractor;
import com.android.systemui.classifier.FalsingCollector;
import com.android.systemui.flags.FeatureFlags;
-import com.android.systemui.keyboard.data.repository.KeyboardRepository;
import com.android.systemui.res.R;
import com.android.systemui.user.domain.interactor.SelectedUserInteractor;
@@ -43,7 +43,7 @@
private final LiftToActivateListener mLiftToActivateListener;
private final FalsingCollector mFalsingCollector;
- private final KeyboardRepository mKeyboardRepository;
+ private final KeyguardKeyboardInteractor mKeyguardKeyboardInteractor;
protected PasswordTextView mPasswordEntry;
private final OnKeyListener mOnKeyListener = (v, keyCode, event) -> {
@@ -75,13 +75,13 @@
FalsingCollector falsingCollector,
FeatureFlags featureFlags,
SelectedUserInteractor selectedUserInteractor,
- KeyboardRepository keyboardRepository) {
+ KeyguardKeyboardInteractor keyguardKeyboardInteractor) {
super(view, keyguardUpdateMonitor, securityMode, lockPatternUtils, keyguardSecurityCallback,
messageAreaControllerFactory, latencyTracker, falsingCollector,
emergencyButtonController, featureFlags, selectedUserInteractor);
mLiftToActivateListener = liftToActivateListener;
mFalsingCollector = falsingCollector;
- mKeyboardRepository = keyboardRepository;
+ mKeyguardKeyboardInteractor = keyguardKeyboardInteractor;
mPasswordEntry = mView.findViewById(mView.getPasswordTextViewId());
}
@@ -132,7 +132,7 @@
okButton.setOnHoverListener(mLiftToActivateListener);
}
if (pinInputFieldStyledFocusState()) {
- collectFlow(mPasswordEntry, mKeyboardRepository.isAnyKeyboardConnected(),
+ collectFlow(mPasswordEntry, mKeyguardKeyboardInteractor.isAnyKeyboardConnected(),
this::setKeyboardBasedFocusOutline);
/**
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPinViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPinViewController.java
index b958f55..f4cda02 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardPinViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPinViewController.java
@@ -25,10 +25,10 @@
import com.android.internal.util.LatencyTracker;
import com.android.internal.widget.LockPatternUtils;
import com.android.keyguard.KeyguardSecurityModel.SecurityMode;
+import com.android.keyguard.domain.interactor.KeyguardKeyboardInteractor;
import com.android.systemui.classifier.FalsingCollector;
import com.android.systemui.flags.FeatureFlags;
import com.android.systemui.flags.Flags;
-import com.android.systemui.keyboard.data.repository.KeyboardRepository;
import com.android.systemui.res.R;
import com.android.systemui.statusbar.policy.DevicePostureController;
import com.android.systemui.user.domain.interactor.SelectedUserInteractor;
@@ -61,11 +61,11 @@
FalsingCollector falsingCollector,
DevicePostureController postureController, FeatureFlags featureFlags,
SelectedUserInteractor selectedUserInteractor, UiEventLogger uiEventLogger,
- KeyboardRepository keyboardRepository) {
+ KeyguardKeyboardInteractor keyguardKeyboardInteractor) {
super(view, keyguardUpdateMonitor, securityMode, lockPatternUtils, keyguardSecurityCallback,
messageAreaControllerFactory, latencyTracker, liftToActivateListener,
emergencyButtonController, falsingCollector, featureFlags, selectedUserInteractor,
- keyboardRepository);
+ keyguardKeyboardInteractor);
mKeyguardUpdateMonitor = keyguardUpdateMonitor;
mPostureController = postureController;
mLockPatternUtils = lockPatternUtils;
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSimPinViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSimPinViewController.java
index 1cdcbd0..558679e 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardSimPinViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSimPinViewController.java
@@ -42,9 +42,9 @@
import com.android.internal.util.LatencyTracker;
import com.android.internal.widget.LockPatternUtils;
import com.android.keyguard.KeyguardSecurityModel.SecurityMode;
+import com.android.keyguard.domain.interactor.KeyguardKeyboardInteractor;
import com.android.systemui.classifier.FalsingCollector;
import com.android.systemui.flags.FeatureFlags;
-import com.android.systemui.keyboard.data.repository.KeyboardRepository;
import com.android.systemui.res.R;
import com.android.systemui.user.domain.interactor.SelectedUserInteractor;
@@ -94,11 +94,12 @@
LatencyTracker latencyTracker, LiftToActivateListener liftToActivateListener,
TelephonyManager telephonyManager, FalsingCollector falsingCollector,
EmergencyButtonController emergencyButtonController, FeatureFlags featureFlags,
- SelectedUserInteractor selectedUserInteractor, KeyboardRepository keyboardRepository) {
+ SelectedUserInteractor selectedUserInteractor,
+ KeyguardKeyboardInteractor keyguardKeyboardInteractor) {
super(view, keyguardUpdateMonitor, securityMode, lockPatternUtils, keyguardSecurityCallback,
messageAreaControllerFactory, latencyTracker, liftToActivateListener,
emergencyButtonController, falsingCollector, featureFlags, selectedUserInteractor,
- keyboardRepository);
+ keyguardKeyboardInteractor);
mKeyguardUpdateMonitor = keyguardUpdateMonitor;
mTelephonyManager = telephonyManager;
mSimImageView = mView.findViewById(R.id.keyguard_sim);
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSimPukViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSimPukViewController.java
index f019d61..cb1c4b3 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardSimPukViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSimPukViewController.java
@@ -37,9 +37,9 @@
import com.android.internal.util.LatencyTracker;
import com.android.internal.widget.LockPatternUtils;
import com.android.keyguard.KeyguardSecurityModel.SecurityMode;
+import com.android.keyguard.domain.interactor.KeyguardKeyboardInteractor;
import com.android.systemui.classifier.FalsingCollector;
import com.android.systemui.flags.FeatureFlags;
-import com.android.systemui.keyboard.data.repository.KeyboardRepository;
import com.android.systemui.res.R;
import com.android.systemui.user.domain.interactor.SelectedUserInteractor;
@@ -91,11 +91,12 @@
LatencyTracker latencyTracker, LiftToActivateListener liftToActivateListener,
TelephonyManager telephonyManager, FalsingCollector falsingCollector,
EmergencyButtonController emergencyButtonController, FeatureFlags featureFlags,
- SelectedUserInteractor selectedUserInteractor, KeyboardRepository keyboardRepository) {
+ SelectedUserInteractor selectedUserInteractor,
+ KeyguardKeyboardInteractor keyguardKeyboardInteractor) {
super(view, keyguardUpdateMonitor, securityMode, lockPatternUtils, keyguardSecurityCallback,
messageAreaControllerFactory, latencyTracker, liftToActivateListener,
emergencyButtonController, falsingCollector, featureFlags, selectedUserInteractor,
- keyboardRepository);
+ keyguardKeyboardInteractor);
mKeyguardUpdateMonitor = keyguardUpdateMonitor;
mTelephonyManager = telephonyManager;
mSimImageView = mView.findViewById(R.id.keyguard_sim);
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardViewController.java
index bc12aee..ce03072 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardViewController.java
@@ -132,7 +132,7 @@
boolean shouldSubtleWindowAnimationsForUnlock();
/**
- * Starts the animation before we dismiss Keyguard, i.e. an disappearing animation on the
+ * Starts the animation before we dismiss Keyguard, i.e. a disappearing animation on the
* security view of the bouncer.
*
* @param finishRunnable the runnable to be run after the animation finished, or {@code null} if
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardVisibilityHelper.java b/packages/SystemUI/src/com/android/keyguard/KeyguardVisibilityHelper.java
index ef65144..9ebae90 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardVisibilityHelper.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardVisibilityHelper.java
@@ -109,8 +109,12 @@
animProps.setDelay(0).setDuration(160);
log("goingToFullShade && !keyguardFadingAway");
}
- PropertyAnimator.setProperty(
- mView, AnimatableProperty.ALPHA, 0f, animProps, true /* animate */);
+ if (KeyguardShadeMigrationNssl.isEnabled()) {
+ log("Using LockscreenToGoneTransition 1");
+ } else {
+ PropertyAnimator.setProperty(
+ mView, AnimatableProperty.ALPHA, 0f, animProps, true /* animate */);
+ }
} else if (oldStatusBarState == StatusBarState.SHADE_LOCKED && statusBarState == KEYGUARD) {
mView.setVisibility(View.VISIBLE);
mKeyguardViewVisibilityAnimating = true;
@@ -179,9 +183,13 @@
mView.setVisibility(View.VISIBLE);
}
} else {
- log("Direct set Visibility to GONE");
- mView.setVisibility(View.GONE);
- mView.setAlpha(1f);
+ if (KeyguardShadeMigrationNssl.isEnabled()) {
+ log("Using LockscreenToGoneTransition 2");
+ } else {
+ log("Direct set Visibility to GONE");
+ mView.setVisibility(View.GONE);
+ mView.setAlpha(1f);
+ }
}
mLastOccludedState = isOccluded;
diff --git a/packages/SystemUI/src/com/android/keyguard/domain/interactor/KeyguardKeyboardInteractor.kt b/packages/SystemUI/src/com/android/keyguard/domain/interactor/KeyguardKeyboardInteractor.kt
new file mode 100644
index 0000000..c39d3e6
--- /dev/null
+++ b/packages/SystemUI/src/com/android/keyguard/domain/interactor/KeyguardKeyboardInteractor.kt
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2024 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.keyguard.domain.interactor
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.keyboard.data.repository.KeyboardRepository
+import javax.inject.Inject
+import kotlinx.coroutines.flow.Flow
+
+@SysUISingleton
+class KeyguardKeyboardInteractor @Inject constructor(keyboardRepository: KeyboardRepository) {
+ val isAnyKeyboardConnected: Flow<Boolean> = keyboardRepository.isAnyKeyboardConnected
+}
diff --git a/packages/SystemUI/src/com/android/keyguard/mediator/ScreenOnCoordinator.kt b/packages/SystemUI/src/com/android/keyguard/mediator/ScreenOnCoordinator.kt
index 7a560e8..29df49b5 100644
--- a/packages/SystemUI/src/com/android/keyguard/mediator/ScreenOnCoordinator.kt
+++ b/packages/SystemUI/src/com/android/keyguard/mediator/ScreenOnCoordinator.kt
@@ -29,26 +29,28 @@
import javax.inject.Inject
/**
- * Coordinates screen on/turning on animations for the KeyguardViewMediator. Specifically for
- * screen on events, this will invoke the onDrawn Runnable after all tasks have completed. This
- * should route back to the [com.android.systemui.keyguard.KeyguardService], which informs
- * the system_server that keyguard has drawn.
+ * Coordinates screen on/turning on animations for the KeyguardViewMediator. Specifically for screen
+ * on events, this will invoke the onDrawn Runnable after all tasks have completed. This should
+ * route back to the [com.android.systemui.keyguard.KeyguardService], which informs the
+ * system_server that keyguard has drawn.
*/
@SysUISingleton
-class ScreenOnCoordinator @Inject constructor(
+class ScreenOnCoordinator
+@Inject
+constructor(
unfoldComponent: Optional<SysUIUnfoldComponent>,
- @Main private val mainHandler: Handler
+ @Main private val mainHandler: Handler,
) {
- private val unfoldLightRevealAnimation = unfoldComponent.map(
- SysUIUnfoldComponent::getUnfoldLightRevealOverlayAnimation).getOrNull()
- private val foldAodAnimationController = unfoldComponent.map(
- SysUIUnfoldComponent::getFoldAodAnimationController).getOrNull()
+ private val foldAodAnimationController =
+ unfoldComponent.map(SysUIUnfoldComponent::getFoldAodAnimationController).getOrNull()
+ private val fullScreenLightRevealAnimations =
+ unfoldComponent.map(SysUIUnfoldComponent::getFullScreenLightRevealAnimations).getOrNull()
private val pendingTasks = PendingTasksContainer()
/**
- * When turning on, registers tasks that may need to run before invoking [onDrawn].
- * This is called on a binder thread from [com.android.systemui.keyguard.KeyguardService].
+ * When turning on, registers tasks that may need to run before invoking [onDrawn]. This is
+ * called on a binder thread from [com.android.systemui.keyguard.KeyguardService].
*/
@BinderThread
fun onScreenTurningOn(onDrawn: Runnable) {
@@ -56,8 +58,10 @@
pendingTasks.reset()
- unfoldLightRevealAnimation?.onScreenTurningOn(pendingTasks.registerTask("unfold-reveal"))
foldAodAnimationController?.onScreenTurningOn(pendingTasks.registerTask("fold-to-aod"))
+ fullScreenLightRevealAnimations?.forEach {
+ it.onScreenTurningOn(pendingTasks.registerTask(it::class.java.simpleName))
+ }
pendingTasks.onTasksComplete {
if (Flags.enableBackgroundKeyguardOndrawnCallback()) {
@@ -71,8 +75,8 @@
}
/**
- * Called when screen is fully turned on and screen on blocker is removed.
- * This is called on a binder thread from [com.android.systemui.keyguard.KeyguardService].
+ * Called when screen is fully turned on and screen on blocker is removed. This is called on a
+ * binder thread from [com.android.systemui.keyguard.KeyguardService].
*/
@BinderThread
fun onScreenTurnedOn() {
diff --git a/packages/SystemUI/src/com/android/systemui/CameraAvailabilityListener.kt b/packages/SystemUI/src/com/android/systemui/CameraAvailabilityListener.kt
index 5f5cca8..e8499d3 100644
--- a/packages/SystemUI/src/com/android/systemui/CameraAvailabilityListener.kt
+++ b/packages/SystemUI/src/com/android/systemui/CameraAvailabilityListener.kt
@@ -17,15 +17,11 @@
package com.android.systemui
import android.content.Context
-import android.content.res.Resources
import android.graphics.Path
import android.graphics.Rect
-import android.graphics.RectF
import android.hardware.camera2.CameraManager
-import android.util.PathParser
import com.android.systemui.res.R
import java.util.concurrent.Executor
-import kotlin.math.roundToInt
/**
* Listens for usage of the Camera and controls the ScreenDecorations transition to show extra
@@ -163,89 +159,20 @@
}
companion object Factory {
- fun build(context: Context, executor: Executor): CameraAvailabilityListener {
+ fun build(
+ context: Context,
+ executor: Executor,
+ cameraProtectionLoader: CameraProtectionLoader
+ ): CameraAvailabilityListener {
val manager = context.getSystemService(Context.CAMERA_SERVICE) as CameraManager
val res = context.resources
- val cameraProtectionInfoList = loadCameraProtectionInfoList(res)
+ val cameraProtectionInfoList = cameraProtectionLoader.loadCameraProtectionInfoList()
val excluded = res.getString(R.string.config_cameraProtectionExcludedPackages)
return CameraAvailabilityListener(manager, cameraProtectionInfoList, excluded, executor)
}
-
- private fun pathFromString(pathString: String): Path {
- val spec = pathString.trim()
- val p: Path
- try {
- p = PathParser.createPathFromPathData(spec)
- } catch (e: Throwable) {
- throw IllegalArgumentException("Invalid protection path", e)
- }
-
- return p
- }
-
- private fun loadCameraProtectionInfoList(res: Resources): List<CameraProtectionInfo> {
- val list = mutableListOf<CameraProtectionInfo>()
- val front =
- loadCameraProtectionInfo(
- res,
- R.string.config_protectedCameraId,
- R.string.config_protectedPhysicalCameraId,
- R.string.config_frontBuiltInDisplayCutoutProtection
- )
- if (front != null) {
- list.add(front)
- }
- val inner =
- loadCameraProtectionInfo(
- res,
- R.string.config_protectedInnerCameraId,
- R.string.config_protectedInnerPhysicalCameraId,
- R.string.config_innerBuiltInDisplayCutoutProtection
- )
- if (inner != null) {
- list.add(inner)
- }
- return list
- }
-
- private fun loadCameraProtectionInfo(
- res: Resources,
- cameraIdRes: Int,
- physicalCameraIdRes: Int,
- pathRes: Int
- ): CameraProtectionInfo? {
- val logicalCameraId = res.getString(cameraIdRes)
- if (logicalCameraId.isNullOrEmpty()) {
- return null
- }
- val physicalCameraId = res.getString(physicalCameraIdRes)
- val protectionPath = pathFromString(res.getString(pathRes))
- val computed = RectF()
- protectionPath.computeBounds(computed)
- val protectionBounds =
- Rect(
- computed.left.roundToInt(),
- computed.top.roundToInt(),
- computed.right.roundToInt(),
- computed.bottom.roundToInt()
- )
- return CameraProtectionInfo(
- logicalCameraId,
- physicalCameraId,
- protectionPath,
- protectionBounds
- )
- }
}
- data class CameraProtectionInfo(
- val logicalCameraId: String,
- val physicalCameraId: String?,
- val cutoutProtectionPath: Path,
- val cutoutBounds: Rect,
- )
-
private data class OpenCameraInfo(
val logicalCameraId: String,
val packageId: String,
diff --git a/packages/SoundPicker2/src/com/android/soundpicker/RingtonePickerApplication.java b/packages/SystemUI/src/com/android/systemui/CameraProtectionInfo.kt
similarity index 61%
copy from packages/SoundPicker2/src/com/android/soundpicker/RingtonePickerApplication.java
copy to packages/SystemUI/src/com/android/systemui/CameraProtectionInfo.kt
index 48fd4fe..6314bd9 100644
--- a/packages/SoundPicker2/src/com/android/soundpicker/RingtonePickerApplication.java
+++ b/packages/SystemUI/src/com/android/systemui/CameraProtectionInfo.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2023 The Android Open Source Project
+ * Copyright (C) 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,15 +14,15 @@
* limitations under the License.
*/
-package com.android.soundpicker;
+package com.android.systemui
-import android.app.Application;
+import android.graphics.Path
+import android.graphics.Rect
-import dagger.hilt.android.HiltAndroidApp;
-
-/**
- * The main application class for the project.
- */
-@HiltAndroidApp(Application.class)
-public class RingtonePickerApplication extends Hilt_RingtonePickerApplication {
-}
+data class CameraProtectionInfo(
+ val logicalCameraId: String,
+ val physicalCameraId: String?,
+ val cutoutProtectionPath: Path,
+ val cutoutBounds: Rect,
+ val displayUniqueId: String?,
+)
diff --git a/packages/SystemUI/src/com/android/systemui/CameraProtectionLoader.kt b/packages/SystemUI/src/com/android/systemui/CameraProtectionLoader.kt
new file mode 100644
index 0000000..6cee28b
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/CameraProtectionLoader.kt
@@ -0,0 +1,98 @@
+/*
+ * Copyright (C) 2024 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.content.Context
+import android.graphics.Path
+import android.graphics.Rect
+import android.graphics.RectF
+import android.util.PathParser
+import com.android.systemui.res.R
+import javax.inject.Inject
+import kotlin.math.roundToInt
+
+interface CameraProtectionLoader {
+ fun loadCameraProtectionInfoList(): List<CameraProtectionInfo>
+}
+
+class CameraProtectionLoaderImpl @Inject constructor(private val context: Context) :
+ CameraProtectionLoader {
+
+ override fun loadCameraProtectionInfoList(): List<CameraProtectionInfo> {
+ val list = mutableListOf<CameraProtectionInfo>()
+ val front =
+ loadCameraProtectionInfo(
+ R.string.config_protectedCameraId,
+ R.string.config_protectedPhysicalCameraId,
+ R.string.config_frontBuiltInDisplayCutoutProtection,
+ R.string.config_protectedScreenUniqueId,
+ )
+ if (front != null) {
+ list.add(front)
+ }
+ val inner =
+ loadCameraProtectionInfo(
+ R.string.config_protectedInnerCameraId,
+ R.string.config_protectedInnerPhysicalCameraId,
+ R.string.config_innerBuiltInDisplayCutoutProtection,
+ R.string.config_protectedInnerScreenUniqueId,
+ )
+ if (inner != null) {
+ list.add(inner)
+ }
+ return list
+ }
+
+ private fun loadCameraProtectionInfo(
+ cameraIdRes: Int,
+ physicalCameraIdRes: Int,
+ pathRes: Int,
+ displayUniqueIdRes: Int,
+ ): CameraProtectionInfo? {
+ val logicalCameraId = context.getString(cameraIdRes)
+ if (logicalCameraId.isNullOrEmpty()) {
+ return null
+ }
+ val physicalCameraId = context.getString(physicalCameraIdRes)
+ val protectionPath = pathFromString(context.getString(pathRes))
+ val computed = RectF()
+ protectionPath.computeBounds(computed)
+ val protectionBounds =
+ Rect(
+ computed.left.roundToInt(),
+ computed.top.roundToInt(),
+ computed.right.roundToInt(),
+ computed.bottom.roundToInt()
+ )
+ val displayUniqueId = context.getString(displayUniqueIdRes)
+ return CameraProtectionInfo(
+ logicalCameraId,
+ physicalCameraId,
+ protectionPath,
+ protectionBounds,
+ displayUniqueId
+ )
+ }
+
+ private fun pathFromString(pathString: String): Path {
+ return try {
+ PathParser.createPathFromPathData(pathString.trim())
+ } catch (e: Throwable) {
+ throw IllegalArgumentException("Invalid protection path", e)
+ }
+ }
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/animation/ActivityLaunchAnimatorKosmos.kt b/packages/SystemUI/src/com/android/systemui/CameraProtectionModule.kt
similarity index 67%
copy from packages/SystemUI/tests/utils/src/com/android/systemui/animation/ActivityLaunchAnimatorKosmos.kt
copy to packages/SystemUI/src/com/android/systemui/CameraProtectionModule.kt
index 128f58b..58680a8 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/animation/ActivityLaunchAnimatorKosmos.kt
+++ b/packages/SystemUI/src/com/android/systemui/CameraProtectionModule.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2023 The Android Open Source Project
+ * Copyright (C) 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,8 +14,13 @@
* limitations under the License.
*/
-package com.android.systemui.animation
+package com.android.systemui
-import com.android.systemui.kosmos.Kosmos
+import dagger.Binds
+import dagger.Module
-val Kosmos.activityLaunchAnimator by Kosmos.Fixture { ActivityLaunchAnimator() }
+@Module
+interface CameraProtectionModule {
+
+ @Binds fun cameraProtectionLoaderImpl(impl: CameraProtectionLoaderImpl): CameraProtectionLoader
+}
diff --git a/packages/SystemUI/src/com/android/systemui/CoreStartable.java b/packages/SystemUI/src/com/android/systemui/CoreStartable.java
index 4c9782c..39e1c41 100644
--- a/packages/SystemUI/src/com/android/systemui/CoreStartable.java
+++ b/packages/SystemUI/src/com/android/systemui/CoreStartable.java
@@ -36,7 +36,7 @@
* If your CoreStartable depends on different CoreStartables starting before it, use a
* {@link com.android.systemui.startable.Dependencies} annotation to list out those dependencies.
*
- * @see SystemUIApplication#startServicesIfNeeded()
+ * @see SystemUIApplication#startSystemUserServicesIfNeeded()
*/
public interface CoreStartable extends Dumpable {
diff --git a/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java b/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java
index d6d5c26..3e03fb8 100644
--- a/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java
+++ b/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java
@@ -146,6 +146,7 @@
private final ThreadFactory mThreadFactory;
private final DecorProviderFactory mDotFactory;
private final FaceScanningProviderFactory mFaceScanningFactory;
+ private final CameraProtectionLoader mCameraProtectionLoader;
public final int mFaceScanningViewId;
@VisibleForTesting
@@ -333,7 +334,8 @@
FaceScanningProviderFactory faceScanningFactory,
ScreenDecorationsLogger logger,
FacePropertyRepository facePropertyRepository,
- JavaAdapter javaAdapter) {
+ JavaAdapter javaAdapter,
+ CameraProtectionLoader cameraProtectionLoader) {
mContext = context;
mSecureSettings = secureSettings;
mCommandRegistry = commandRegistry;
@@ -343,6 +345,7 @@
mThreadFactory = threadFactory;
mDotFactory = dotFactory;
mFaceScanningFactory = faceScanningFactory;
+ mCameraProtectionLoader = cameraProtectionLoader;
mFaceScanningViewId = com.android.systemui.res.R.id.face_scanning_anim;
mLogger = logger;
mFacePropertyRepository = facePropertyRepository;
@@ -981,7 +984,9 @@
Resources res = mContext.getResources();
boolean enabled = res.getBoolean(R.bool.config_enableDisplayCutoutProtection);
if (enabled) {
- mCameraListener = CameraAvailabilityListener.Factory.build(mContext, mExecutor);
+ mCameraListener =
+ CameraAvailabilityListener.Factory.build(
+ mContext, mExecutor, mCameraProtectionLoader);
mCameraListener.addTransitionCallback(mCameraTransitionCallback);
mCameraListener.startListening();
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/animation/ActivityLaunchAnimatorKosmos.kt b/packages/SystemUI/src/com/android/systemui/SysUICutoutInformation.kt
similarity index 70%
copy from packages/SystemUI/tests/utils/src/com/android/systemui/animation/ActivityLaunchAnimatorKosmos.kt
copy to packages/SystemUI/src/com/android/systemui/SysUICutoutInformation.kt
index 128f58b..fc0b97e 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/animation/ActivityLaunchAnimatorKosmos.kt
+++ b/packages/SystemUI/src/com/android/systemui/SysUICutoutInformation.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2023 The Android Open Source Project
+ * Copyright (C) 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,8 +14,11 @@
* limitations under the License.
*/
-package com.android.systemui.animation
+package com.android.systemui
-import com.android.systemui.kosmos.Kosmos
+import android.view.DisplayCutout
-val Kosmos.activityLaunchAnimator by Kosmos.Fixture { ActivityLaunchAnimator() }
+data class SysUICutoutInformation(
+ val cutout: DisplayCutout,
+ val cameraProtection: CameraProtectionInfo?
+)
diff --git a/packages/SystemUI/src/com/android/systemui/SysUICutoutProvider.kt b/packages/SystemUI/src/com/android/systemui/SysUICutoutProvider.kt
new file mode 100644
index 0000000..aad9341
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/SysUICutoutProvider.kt
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2024 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.content.Context
+import android.view.DisplayCutout
+import com.android.systemui.dagger.SysUISingleton
+import javax.inject.Inject
+
+@SysUISingleton
+class SysUICutoutProvider
+@Inject
+constructor(
+ private val context: Context,
+ private val cameraProtectionLoader: CameraProtectionLoader,
+) {
+
+ private val cameraProtectionList by lazy {
+ cameraProtectionLoader.loadCameraProtectionInfoList()
+ }
+
+ fun cutoutInfoForCurrentDisplay(): SysUICutoutInformation? {
+ val display = context.display
+ val displayCutout: DisplayCutout = display.cutout ?: return null
+ val displayUniqueId: String? = display.uniqueId
+ if (displayUniqueId.isNullOrEmpty()) {
+ return SysUICutoutInformation(displayCutout, cameraProtection = null)
+ }
+ val cameraProtection: CameraProtectionInfo? =
+ cameraProtectionList.firstOrNull { it.displayUniqueId == displayUniqueId }
+ return SysUICutoutInformation(displayCutout, cameraProtection)
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java b/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java
index 8aae206..15ef61e 100644
--- a/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java
+++ b/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java
@@ -42,6 +42,7 @@
import com.android.systemui.dagger.GlobalRootComponent;
import com.android.systemui.dagger.SysUIComponent;
import com.android.systemui.dump.DumpManager;
+import com.android.systemui.process.ProcessWrapper;
import com.android.systemui.res.R;
import com.android.systemui.startable.Dependencies;
import com.android.systemui.statusbar.policy.ConfigurationController;
@@ -77,6 +78,7 @@
private SystemUIAppComponentFactoryBase.ContextAvailableCallback mContextAvailableCallback;
private SysUIComponent mSysUIComponent;
private SystemUIInitializer mInitializer;
+ private ProcessWrapper mProcessWrapper;
public SystemUIApplication() {
super();
@@ -115,6 +117,7 @@
// Enable Looper trace points.
// This allows us to see Handler callbacks on traces.
rootComponent.getMainLooper().setTraceTag(Trace.TRACE_TAG_APP);
+ mProcessWrapper = rootComponent.getProcessWrapper();
// Set the application theme that is inherited by all services. Note that setting the
// application theme in the manifest does only work for activities. Keep this in sync with
@@ -132,7 +135,7 @@
View.setTraceLayoutSteps(true);
}
- if (rootComponent.getProcessWrapper().isSystemUser()) {
+ if (mProcessWrapper.isSystemUser()) {
IntentFilter bootCompletedFilter = new
IntentFilter(Intent.ACTION_LOCKED_BOOT_COMPLETED);
bootCompletedFilter.setPriority(IntentFilter.SYSTEM_HIGH_PRIORITY);
@@ -199,7 +202,11 @@
* <p>This method must only be called from the main thread.</p>
*/
- public void startServicesIfNeeded() {
+ public void startSystemUserServicesIfNeeded() {
+ if (!mProcessWrapper.isSystemUser()) {
+ Log.wtf(TAG, "Tried starting SystemUser services on non-SystemUser");
+ return; // Per-user startables are handled in #startSystemUserServicesIfNeeded.
+ }
final String vendorComponent = mInitializer.getVendorComponent(getResources());
// Sort the startables so that we get a deterministic ordering.
@@ -219,6 +226,9 @@
* <p>This method must only be called from the main thread.</p>
*/
void startSecondaryUserServicesIfNeeded() {
+ if (mProcessWrapper.isSystemUser()) {
+ return; // Per-user startables are handled in #startSystemUserServicesIfNeeded.
+ }
// Sort the startables so that we get a deterministic ordering.
Map<Class<?>, Provider<CoreStartable>> sortedStartables = new TreeMap<>(
Comparator.comparing(Class::getName));
diff --git a/packages/SystemUI/src/com/android/systemui/SystemUIInitializer.java b/packages/SystemUI/src/com/android/systemui/SystemUIInitializer.java
index 872b005..1a9b01f 100644
--- a/packages/SystemUI/src/com/android/systemui/SystemUIInitializer.java
+++ b/packages/SystemUI/src/com/android/systemui/SystemUIInitializer.java
@@ -22,10 +22,10 @@
import android.os.HandlerThread;
import android.util.Log;
-import com.android.systemui.res.R;
import com.android.systemui.dagger.GlobalRootComponent;
import com.android.systemui.dagger.SysUIComponent;
import com.android.systemui.dagger.WMComponent;
+import com.android.systemui.res.R;
import com.android.systemui.util.InitializationChecker;
import com.android.wm.shell.dagger.WMShellConcurrencyModule;
import com.android.wm.shell.keyguard.KeyguardTransitions;
@@ -124,9 +124,6 @@
.setDesktopMode(Optional.ofNullable(null));
}
mSysUIComponent = builder.build();
- if (initializeComponents) {
- mSysUIComponent.init();
- }
// Every other part of our codebase currently relies on Dependency, so we
// really need to ensure the Dependency gets initialized early on.
diff --git a/packages/SystemUI/src/com/android/systemui/SystemUISecondaryUserService.java b/packages/SystemUI/src/com/android/systemui/SystemUISecondaryUserService.java
index f4ec6f7..407f764 100644
--- a/packages/SystemUI/src/com/android/systemui/SystemUISecondaryUserService.java
+++ b/packages/SystemUI/src/com/android/systemui/SystemUISecondaryUserService.java
@@ -19,12 +19,31 @@
import android.app.Service;
import android.content.Intent;
import android.os.IBinder;
+import android.util.Log;
+
+import com.android.systemui.process.ProcessWrapper;
+
+import javax.inject.Inject;
public class SystemUISecondaryUserService extends Service {
+ private static final String TAG = "SysUISecondaryService";
+
+ private final ProcessWrapper mProcessWrapper;
+
+ @Inject
+ SystemUISecondaryUserService(ProcessWrapper processWrapper) {
+ mProcessWrapper = processWrapper;
+ }
+
@Override
public void onCreate() {
super.onCreate();
+ if (mProcessWrapper.isSystemUser()) {
+ Log.w(TAG, "SecondaryServices started for System User. Stopping it.");
+ stopSelf();
+ return;
+ }
((SystemUIApplication) getApplication()).startSecondaryUserServicesIfNeeded();
}
diff --git a/packages/SystemUI/src/com/android/systemui/SystemUIService.java b/packages/SystemUI/src/com/android/systemui/SystemUIService.java
index 76c2282..b26be0c 100644
--- a/packages/SystemUI/src/com/android/systemui/SystemUIService.java
+++ b/packages/SystemUI/src/com/android/systemui/SystemUIService.java
@@ -77,7 +77,7 @@
super.onCreate();
// Start all of SystemUI
- ((SystemUIApplication) getApplication()).startServicesIfNeeded();
+ ((SystemUIApplication) getApplication()).startSystemUserServicesIfNeeded();
// Finish initializing dump logic
mLogBufferFreezer.attach(mBroadcastDispatcher);
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuInfoRepository.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuInfoRepository.java
index 4a06ae9..0538e7d 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuInfoRepository.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuInfoRepository.java
@@ -21,6 +21,7 @@
import static android.provider.Settings.Secure.ACCESSIBILITY_FLOATING_MENU_OPACITY;
import static android.provider.Settings.Secure.ACCESSIBILITY_FLOATING_MENU_SIZE;
import static android.provider.Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES;
+import static android.view.accessibility.AccessibilityManager.ACCESSIBILITY_BUTTON;
import static com.android.internal.accessibility.dialog.AccessibilityTargetHelper.getTargets;
import static com.android.systemui.accessibility.floatingmenu.MenuFadeEffectInfoKt.DEFAULT_FADE_EFFECT_IS_ENABLED;
@@ -46,7 +47,6 @@
import androidx.annotation.NonNull;
-import com.android.internal.accessibility.common.ShortcutConstants;
import com.android.internal.accessibility.dialog.AccessibilityTarget;
import com.android.internal.annotations.VisibleForTesting;
import com.android.systemui.Prefs;
@@ -182,7 +182,7 @@
}
void loadMenuTargetFeatures(OnInfoReady<List<AccessibilityTarget>> callback) {
- callback.onReady(getTargets(mContext, ShortcutConstants.UserShortcutType.SOFTWARE));
+ callback.onReady(getTargets(mContext, ACCESSIBILITY_BUTTON));
}
void loadMenuSizeType(OnInfoReady<Integer> callback) {
@@ -223,7 +223,7 @@
private void onTargetFeaturesChanged() {
mSettingsContentsCallback.onTargetFeaturesChanged(
- getTargets(mContext, ShortcutConstants.UserShortcutType.SOFTWARE));
+ getTargets(mContext, ACCESSIBILITY_BUTTON));
}
private Position getStartPosition() {
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuMessageView.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuMessageView.java
index bf121fb..e57323b 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuMessageView.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuMessageView.java
@@ -26,6 +26,7 @@
import android.content.res.Configuration;
import android.content.res.Resources;
import android.graphics.Rect;
+import android.text.Layout;
import android.view.Gravity;
import android.view.View;
import android.view.ViewTreeObserver;
@@ -162,6 +163,7 @@
mTextView.setPadding(/* left= */ 0, textPadding, /* right= */ 0, textPadding);
mTextView.setTextSize(COMPLEX_UNIT_PX, textSize);
mTextView.setTextColor(textColor);
+ mTextView.setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_NORMAL);
final ColorStateList colorAccent = Utils.getColorAccent(getContext());
mUndoButton.setText(res.getString(R.string.accessibility_floating_button_undo));
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayer.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayer.java
index f2e9531..a883c00 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayer.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayer.java
@@ -17,6 +17,7 @@
package com.android.systemui.accessibility.floatingmenu;
import static android.view.WindowInsets.Type.ime;
+import static android.view.accessibility.AccessibilityManager.ACCESSIBILITY_SHORTCUT_KEY;
import static androidx.core.view.WindowInsetsCompat.Type;
@@ -63,7 +64,6 @@
import androidx.recyclerview.widget.RecyclerView;
import androidx.recyclerview.widget.RecyclerViewAccessibilityDelegate;
-import com.android.internal.accessibility.common.ShortcutConstants;
import com.android.internal.accessibility.dialog.AccessibilityTarget;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.messages.nano.SystemMessageProto;
@@ -154,7 +154,7 @@
final List<ComponentName> hardwareKeyShortcutComponents =
mAccessibilityManager.getAccessibilityShortcutTargets(
- ShortcutConstants.UserShortcutType.HARDWARE)
+ ACCESSIBILITY_SHORTCUT_KEY)
.stream()
.map(ComponentName::unflattenFromString)
.toList();
diff --git a/packages/SystemUI/src/com/android/systemui/back/domain/interactor/BackActionInteractor.kt b/packages/SystemUI/src/com/android/systemui/back/domain/interactor/BackActionInteractor.kt
index 6076f32..5bd7e54 100644
--- a/packages/SystemUI/src/com/android/systemui/back/domain/interactor/BackActionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/back/domain/interactor/BackActionInteractor.kt
@@ -29,7 +29,7 @@
import com.android.systemui.scene.domain.interactor.WindowRootViewVisibilityInteractor
import com.android.systemui.shade.QuickSettingsController
import com.android.systemui.shade.ShadeController
-import com.android.systemui.shade.ShadeViewController
+import com.android.systemui.shade.domain.interactor.ShadeBackActionInteractor
import com.android.systemui.statusbar.NotificationShadeWindowController
import com.android.systemui.statusbar.StatusBarState
import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager
@@ -64,8 +64,8 @@
}
override fun onBackProgressed(backEvent: BackEvent) {
- if (shouldBackBeHandled() && shadeViewController.canBeCollapsed()) {
- shadeViewController.onBackProgressed(backEvent.progress)
+ if (shouldBackBeHandled() && shadeBackActionInteractor.canBeCollapsed()) {
+ shadeBackActionInteractor.onBackProgressed(backEvent.progress)
}
}
}
@@ -77,12 +77,12 @@
get() =
notificationShadeWindowController.windowRootView?.viewRootImpl?.onBackInvokedDispatcher
- private lateinit var shadeViewController: ShadeViewController
+ private lateinit var shadeBackActionInteractor: ShadeBackActionInteractor
private lateinit var qsController: QuickSettingsController
- fun setup(qsController: QuickSettingsController, svController: ShadeViewController) {
+ fun setup(qsController: QuickSettingsController, svController: ShadeBackActionInteractor) {
this.qsController = qsController
- this.shadeViewController = svController
+ this.shadeBackActionInteractor = svController
}
override fun start() {
@@ -114,16 +114,16 @@
return true
}
if (qsController.expanded) {
- shadeViewController.animateCollapseQs(false)
+ shadeBackActionInteractor.animateCollapseQs(false)
return true
}
- if (shadeViewController.closeUserSwitcherIfOpen()) {
+ if (shadeBackActionInteractor.closeUserSwitcherIfOpen()) {
return true
}
if (shouldBackBeHandled()) {
- if (shadeViewController.canBeCollapsed()) {
+ if (shadeBackActionInteractor.canBeCollapsed()) {
// this is the Shade dismiss animation, so make sure QQS closes when it ends.
- shadeViewController.onBackPressed()
+ shadeBackActionInteractor.onBackPressed()
shadeController.animateCollapseShade()
}
return true
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java
index 57e308f..3397906 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java
@@ -20,6 +20,7 @@
import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
import static com.android.internal.jank.InteractionJankMonitor.CUJ_BIOMETRIC_PROMPT_TRANSITION;
+import static com.android.systemui.Flags.constraintBp;
import android.animation.Animator;
import android.annotation.IntDef;
@@ -57,6 +58,7 @@
import android.window.OnBackInvokedCallback;
import android.window.OnBackInvokedDispatcher;
+import androidx.constraintlayout.widget.ConstraintLayout;
import androidx.core.view.AccessibilityDelegateCompat;
import androidx.core.view.ViewCompat;
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;
@@ -153,7 +155,7 @@
@Nullable private Spaghetti mBiometricView;
@Nullable private View mCredentialView;
private final AuthPanelController mPanelController;
- private final FrameLayout mFrameLayout;
+ private final ViewGroup mLayout;
private final ImageView mBackgroundView;
private final ScrollView mBiometricScrollView;
private final View mPanelView;
@@ -339,11 +341,16 @@
mBiometricCallback = new BiometricCallback();
final LayoutInflater layoutInflater = LayoutInflater.from(mContext);
- mFrameLayout = (FrameLayout) layoutInflater.inflate(
- R.layout.auth_container_view, this, false /* attachToRoot */);
- addView(mFrameLayout);
- mBiometricScrollView = mFrameLayout.findViewById(R.id.biometric_scrollview);
- mBackgroundView = mFrameLayout.findViewById(R.id.background);
+ if (constraintBp()) {
+ mLayout = (ConstraintLayout) layoutInflater.inflate(
+ R.layout.biometric_prompt_constraint_layout, this, false /* attachToRoot */);
+ } else {
+ mLayout = (FrameLayout) layoutInflater.inflate(
+ R.layout.auth_container_view, this, false /* attachToRoot */);
+ }
+ mBiometricScrollView = mLayout.findViewById(R.id.biometric_scrollview);
+ addView(mLayout);
+ mBackgroundView = mLayout.findViewById(R.id.background);
ViewCompat.setAccessibilityDelegate(mBackgroundView, new AccessibilityDelegateCompat() {
@Override
public void onInitializeAccessibilityNodeInfo(View host,
@@ -358,7 +365,7 @@
}
});
- mPanelView = mFrameLayout.findViewById(R.id.panel);
+ mPanelView = mLayout.findViewById(R.id.panel);
mPanelController = new AuthPanelController(mContext, mPanelView);
mBackgroundExecutor = bgExecutor;
mInteractionJankMonitor = jankMonitor;
@@ -402,20 +409,31 @@
new BiometricModalities(fpProps, faceProps),
config.mOpPackageName);
- final BiometricPromptLayout view = (BiometricPromptLayout) layoutInflater.inflate(
- R.layout.biometric_prompt_layout, null, false);
- mBiometricView = BiometricViewBinder.bind(view, viewModel, mPanelController,
- // TODO(b/201510778): This uses the wrong timeout in some cases
- getJankListener(view, TRANSIT,
- BiometricViewSizeBinder.ANIMATE_MEDIUM_TO_LARGE_DURATION_MS),
- mBackgroundView, mBiometricCallback, mApplicationCoroutineScope,
- vibratorHelper);
+ if (constraintBp()) {
+ mBiometricView = BiometricViewBinder.bind(mLayout, viewModel, null,
+ // TODO(b/201510778): This uses the wrong timeout in some cases
+ getJankListener(mLayout, TRANSIT,
+ BiometricViewSizeBinder.ANIMATE_MEDIUM_TO_LARGE_DURATION_MS),
+ mBackgroundView, mBiometricCallback, mApplicationCoroutineScope,
+ vibratorHelper);
+ } else {
+ final BiometricPromptLayout view = (BiometricPromptLayout) layoutInflater.inflate(
+ R.layout.biometric_prompt_layout, null, false);
+ mBiometricView = BiometricViewBinder.bind(view, viewModel, mPanelController,
+ // TODO(b/201510778): This uses the wrong timeout in some cases
+ getJankListener(view, TRANSIT,
+ BiometricViewSizeBinder.ANIMATE_MEDIUM_TO_LARGE_DURATION_MS),
+ mBackgroundView, mBiometricCallback, mApplicationCoroutineScope,
+ vibratorHelper);
- // TODO(b/251476085): migrate these dependencies
- if (fpProps != null && fpProps.isAnyUdfpsType()) {
- view.setUdfpsAdapter(new UdfpsDialogMeasureAdapter(view, fpProps),
- config.mScaleProvider);
+ // TODO(b/251476085): migrate these dependencies
+ if (fpProps != null && fpProps.isAnyUdfpsType()) {
+ view.setUdfpsAdapter(new UdfpsDialogMeasureAdapter(view, fpProps),
+ config.mScaleProvider);
+ }
}
+ } else if (constraintBp() && Utils.isDeviceCredentialAllowed(mConfig.mPromptInfo)) {
+ addCredentialView(true, false);
} else {
mPromptSelectorInteractorProvider.get().resetPrompt();
}
@@ -477,7 +495,7 @@
vm.setAnimateContents(animateContents);
((CredentialView) mCredentialView).init(vm, this, mPanelController, animatePanel);
- mFrameLayout.addView(mCredentialView);
+ mLayout.addView(mCredentialView);
}
@Override
@@ -488,7 +506,9 @@
@Override
public void onOrientationChanged() {
- maybeUpdatePositionForUdfps(true /* invalidate */);
+ if (!constraintBp()) {
+ maybeUpdatePositionForUdfps(true /* invalidate */);
+ }
}
@Override
@@ -502,8 +522,9 @@
mWakefulnessLifecycle.addObserver(this);
mPanelInteractionDetector.enable(
() -> animateAway(AuthDialogCallback.DISMISSED_USER_CANCELED));
-
- if (Utils.isBiometricAllowed(mConfig.mPromptInfo)) {
+ if (constraintBp()) {
+ // Do nothing on attachment with constraintLayout
+ } else if (Utils.isBiometricAllowed(mConfig.mPromptInfo)) {
mBiometricScrollView.addView(mBiometricView.asView());
} else if (Utils.isDeviceCredentialAllowed(mConfig.mPromptInfo)) {
addCredentialView(true /* animatePanel */, false /* animateContents */);
@@ -512,7 +533,9 @@
+ mConfig.mPromptInfo.getAuthenticators());
}
- maybeUpdatePositionForUdfps(false /* invalidate */);
+ if (!constraintBp()) {
+ maybeUpdatePositionForUdfps(false /* invalidate */);
+ }
if (mConfig.mSkipIntro) {
mContainerState = STATE_SHOWING;
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java
index 66fe4b3..716209d 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java
@@ -66,7 +66,7 @@
import com.android.internal.util.LatencyTracker;
import com.android.keyguard.KeyguardUpdateMonitor;
import com.android.systemui.Dumpable;
-import com.android.systemui.animation.ActivityLaunchAnimator;
+import com.android.systemui.animation.ActivityTransitionAnimator;
import com.android.systemui.biometrics.dagger.BiometricsBackground;
import com.android.systemui.biometrics.domain.interactor.UdfpsOverlayInteractor;
import com.android.systemui.biometrics.shared.model.UdfpsOverlayParams;
@@ -162,7 +162,7 @@
mUnlockedScreenOffAnimationController;
@NonNull private final LatencyTracker mLatencyTracker;
@VisibleForTesting @NonNull final BiometricDisplayListener mOrientationListener;
- @NonNull private final ActivityLaunchAnimator mActivityLaunchAnimator;
+ @NonNull private final ActivityTransitionAnimator mActivityTransitionAnimator;
@NonNull private final PrimaryBouncerInteractor mPrimaryBouncerInteractor;
@NonNull private final ShadeInteractor mShadeInteractor;
@Nullable private final TouchProcessor mTouchProcessor;
@@ -287,7 +287,7 @@
event,
fromUdfpsView
),
- mActivityLaunchAnimator,
+ mActivityTransitionAnimator,
mPrimaryBouncerInteractor,
mAlternateBouncerInteractor,
mUdfpsKeyguardAccessibilityDelegate,
@@ -663,7 +663,7 @@
@NonNull UnlockedScreenOffAnimationController unlockedScreenOffAnimationController,
@NonNull SystemUIDialogManager dialogManager,
@NonNull LatencyTracker latencyTracker,
- @NonNull ActivityLaunchAnimator activityLaunchAnimator,
+ @NonNull ActivityTransitionAnimator activityTransitionAnimator,
@NonNull @BiometricsBackground Executor biometricsExecutor,
@NonNull PrimaryBouncerInteractor primaryBouncerInteractor,
@NonNull ShadeInteractor shadeInteractor,
@@ -706,7 +706,7 @@
mSystemClock = systemClock;
mUnlockedScreenOffAnimationController = unlockedScreenOffAnimationController;
mLatencyTracker = latencyTracker;
- mActivityLaunchAnimator = activityLaunchAnimator;
+ mActivityTransitionAnimator = activityTransitionAnimator;
mSensorProps = new FingerprintSensorPropertiesInternal(
-1 /* sensorId */,
SensorProperties.STRENGTH_CONVENIENCE,
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt
index 4ea5f4c..a209eae 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt
@@ -44,7 +44,7 @@
import androidx.annotation.LayoutRes
import androidx.annotation.VisibleForTesting
import com.android.keyguard.KeyguardUpdateMonitor
-import com.android.systemui.animation.ActivityLaunchAnimator
+import com.android.systemui.animation.ActivityTransitionAnimator
import com.android.systemui.biometrics.domain.interactor.UdfpsOverlayInteractor
import com.android.systemui.biometrics.shared.model.UdfpsOverlayParams
import com.android.systemui.biometrics.ui.binder.UdfpsTouchOverlayBinder
@@ -100,7 +100,7 @@
@RequestReason val requestReason: Int,
private val controllerCallback: IUdfpsOverlayControllerCallback,
private val onTouch: (View, MotionEvent, Boolean) -> Boolean,
- private val activityLaunchAnimator: ActivityLaunchAnimator,
+ private val activityTransitionAnimator: ActivityTransitionAnimator,
private val primaryBouncerInteractor: PrimaryBouncerInteractor,
private val alternateBouncerInteractor: AlternateBouncerInteractor,
private val isDebuggable: Boolean = Build.IS_DEBUGGABLE,
@@ -304,7 +304,7 @@
unlockedScreenOffAnimationController,
dialogManager,
controller,
- activityLaunchAnimator,
+ activityTransitionAnimator,
primaryBouncerInteractor,
alternateBouncerInteractor,
udfpsKeyguardAccessibilityDelegate,
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerLegacy.kt b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerLegacy.kt
index 7020d05..018d92e 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerLegacy.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerLegacy.kt
@@ -25,7 +25,7 @@
import com.android.app.animation.Interpolators
import com.android.keyguard.BouncerPanelExpansionCalculator.aboutToShowBouncerProgress
import com.android.keyguard.KeyguardUpdateMonitor
-import com.android.systemui.animation.ActivityLaunchAnimator
+import com.android.systemui.animation.ActivityTransitionAnimator
import com.android.systemui.biometrics.UdfpsKeyguardViewLegacy.ANIMATE_APPEAR_ON_SCREEN_OFF
import com.android.systemui.biometrics.domain.interactor.UdfpsOverlayInteractor
import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor
@@ -69,7 +69,7 @@
private val unlockedScreenOffAnimationController: UnlockedScreenOffAnimationController,
systemUIDialogManager: SystemUIDialogManager,
private val udfpsController: UdfpsController,
- private val activityLaunchAnimator: ActivityLaunchAnimator,
+ private val activityTransitionAnimator: ActivityTransitionAnimator,
private val primaryBouncerInteractor: PrimaryBouncerInteractor,
private val alternateBouncerInteractor: AlternateBouncerInteractor,
private val udfpsKeyguardAccessibilityDelegate: UdfpsKeyguardAccessibilityDelegate,
@@ -137,20 +137,20 @@
}
}
- private val activityLaunchAnimatorListener: ActivityLaunchAnimator.Listener =
- object : ActivityLaunchAnimator.Listener {
- override fun onLaunchAnimationStart() {
+ private val mActivityTransitionAnimatorListener: ActivityTransitionAnimator.Listener =
+ object : ActivityTransitionAnimator.Listener {
+ override fun onTransitionAnimationStart() {
isLaunchingActivity = true
activityLaunchProgress = 0f
updateAlpha()
}
- override fun onLaunchAnimationEnd() {
+ override fun onTransitionAnimationEnd() {
isLaunchingActivity = false
updateAlpha()
}
- override fun onLaunchAnimationProgress(linearProgress: Float) {
+ override fun onTransitionAnimationProgress(linearProgress: Float) {
activityLaunchProgress = linearProgress
updateAlpha()
}
@@ -370,7 +370,7 @@
updatePauseAuth()
keyguardViewManager.setOccludingAppBiometricUI(occludingAppBiometricUI)
lockScreenShadeTransitionController.mUdfpsKeyguardViewControllerLegacy = this
- activityLaunchAnimator.addListener(activityLaunchAnimatorListener)
+ activityTransitionAnimator.addListener(mActivityTransitionAnimatorListener)
view.startIconAsyncInflate {
val animationViewInternal: View =
view.requireViewById(R.id.udfps_animation_view_internal)
@@ -389,7 +389,7 @@
if (lockScreenShadeTransitionController.mUdfpsKeyguardViewControllerLegacy === this) {
lockScreenShadeTransitionController.mUdfpsKeyguardViewControllerLegacy = null
}
- activityLaunchAnimator.removeListener(activityLaunchAnimatorListener)
+ activityTransitionAnimator.removeListener(mActivityTransitionAnimatorListener)
keyguardViewManager.removeCallback(statusBarKeyguardViewManagerCallback)
}
@@ -536,7 +536,7 @@
val udfpsActivityLaunchAlphaMultiplier =
1f -
(activityLaunchProgress *
- (ActivityLaunchAnimator.TIMINGS.totalDuration / 83))
+ (ActivityTransitionAnimator.TIMINGS.totalDuration / 83))
.coerceIn(0f, 1f)
alpha = (alpha * udfpsActivityLaunchAlphaMultiplier).toInt()
}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/BiometricStatusRepository.kt b/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/BiometricStatusRepository.kt
index d28dbc0..27bb023 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/BiometricStatusRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/BiometricStatusRepository.kt
@@ -24,17 +24,24 @@
import android.hardware.biometrics.BiometricRequestConstants.REASON_AUTH_SETTINGS
import android.hardware.biometrics.BiometricRequestConstants.REASON_ENROLL_ENROLLING
import android.hardware.biometrics.BiometricRequestConstants.REASON_ENROLL_FIND_SENSOR
+import android.hardware.biometrics.BiometricSourceType
import com.android.systemui.biometrics.shared.model.AuthenticationReason
import com.android.systemui.biometrics.shared.model.AuthenticationReason.SettingsOperations
+import com.android.systemui.biometrics.shared.model.AuthenticationState
import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging
import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.keyguard.shared.model.AcquiredFingerprintAuthenticationStatus
+import com.android.systemui.keyguard.shared.model.FingerprintAuthenticationStatus
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.filter
+import kotlinx.coroutines.flow.filterIsInstance
+import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.shareIn
/** A repository for the state of biometric authentication. */
@@ -44,6 +51,9 @@
* [NotRunning].
*/
val fingerprintAuthenticationReason: Flow<AuthenticationReason>
+
+ /** The current status of an acquired fingerprint. */
+ val fingerprintAcquiredStatus: Flow<FingerprintAuthenticationStatus>
}
@SysUISingleton
@@ -54,53 +64,53 @@
private val biometricManager: BiometricManager?
) : BiometricStatusRepository {
- override val fingerprintAuthenticationReason: Flow<AuthenticationReason> =
+ private val authenticationState: Flow<AuthenticationState> =
conflatedCallbackFlow {
- val updateFingerprintAuthenticateReason = { reason: AuthenticationReason ->
- trySendWithFailureLogging(
- reason,
- TAG,
- "Error sending fingerprintAuthenticateReason reason"
- )
+ val updateAuthenticationState = { state: AuthenticationState ->
+ trySendWithFailureLogging(state, TAG, "Error sending AuthenticationState state")
}
val authenticationStateListener =
object : AuthenticationStateListener.Stub() {
override fun onAuthenticationStarted(requestReason: Int) {
- val authenticationReason =
- when (requestReason) {
- REASON_AUTH_BP ->
- AuthenticationReason.BiometricPromptAuthentication
- REASON_AUTH_KEYGUARD ->
- AuthenticationReason.DeviceEntryAuthentication
- REASON_AUTH_OTHER -> AuthenticationReason.OtherAuthentication
- REASON_AUTH_SETTINGS ->
- AuthenticationReason.SettingsAuthentication(
- SettingsOperations.OTHER
- )
- REASON_ENROLL_ENROLLING ->
- AuthenticationReason.SettingsAuthentication(
- SettingsOperations.ENROLL_ENROLLING
- )
- REASON_ENROLL_FIND_SENSOR ->
- AuthenticationReason.SettingsAuthentication(
- SettingsOperations.ENROLL_FIND_SENSOR
- )
- else -> AuthenticationReason.Unknown
- }
- updateFingerprintAuthenticateReason(authenticationReason)
+ val authenticationReason = requestReason.toAuthenticationReason()
+ updateAuthenticationState(
+ AuthenticationState.AuthenticationStarted(authenticationReason)
+ )
}
override fun onAuthenticationStopped() {
- updateFingerprintAuthenticateReason(AuthenticationReason.NotRunning)
+ updateAuthenticationState(
+ AuthenticationState.AuthenticationStopped(
+ AuthenticationReason.NotRunning
+ )
+ )
}
override fun onAuthenticationSucceeded(requestReason: Int, userId: Int) {}
override fun onAuthenticationFailed(requestReason: Int, userId: Int) {}
+
+ override fun onAuthenticationAcquired(
+ biometricSourceType: BiometricSourceType,
+ requestReason: Int,
+ acquiredInfo: Int
+ ) {
+ val authReason = requestReason.toAuthenticationReason()
+
+ updateAuthenticationState(
+ AuthenticationState.AuthenticationAcquired(
+ biometricSourceType,
+ authReason,
+ acquiredInfo
+ )
+ )
+ }
}
- updateFingerprintAuthenticateReason(AuthenticationReason.NotRunning)
+ updateAuthenticationState(
+ AuthenticationState.AuthenticationStarted(AuthenticationReason.NotRunning)
+ )
biometricManager?.registerAuthenticationStateListener(authenticationStateListener)
awaitClose {
biometricManager?.unregisterAuthenticationStateListener(
@@ -110,7 +120,36 @@
}
.shareIn(applicationScope, started = SharingStarted.Eagerly, replay = 1)
+ override val fingerprintAuthenticationReason: Flow<AuthenticationReason> =
+ authenticationState.map { it.requestReason }
+
+ override val fingerprintAcquiredStatus: Flow<FingerprintAuthenticationStatus> =
+ authenticationState
+ .filterIsInstance<AuthenticationState.AuthenticationAcquired>()
+ .filter {
+ it.biometricSourceType == BiometricSourceType.FINGERPRINT &&
+ // TODO(b/322555228) This check will be removed after consolidating device
+ // entry auth messages (currently in DeviceEntryFingerprintAuthRepository)
+ // with BP auth messages (here)
+ it.requestReason == AuthenticationReason.BiometricPromptAuthentication
+ }
+ .map { AcquiredFingerprintAuthenticationStatus(it.requestReason, it.acquiredInfo) }
+
companion object {
private const val TAG = "BiometricStatusRepositoryImpl"
}
}
+
+private fun Int.toAuthenticationReason(): AuthenticationReason =
+ when (this) {
+ REASON_AUTH_BP -> AuthenticationReason.BiometricPromptAuthentication
+ REASON_AUTH_KEYGUARD -> AuthenticationReason.DeviceEntryAuthentication
+ REASON_AUTH_OTHER -> AuthenticationReason.OtherAuthentication
+ REASON_AUTH_SETTINGS ->
+ AuthenticationReason.SettingsAuthentication(SettingsOperations.OTHER)
+ REASON_ENROLL_ENROLLING ->
+ AuthenticationReason.SettingsAuthentication(SettingsOperations.ENROLL_ENROLLING)
+ REASON_ENROLL_FIND_SENSOR ->
+ AuthenticationReason.SettingsAuthentication(SettingsOperations.ENROLL_FIND_SENSOR)
+ else -> AuthenticationReason.Unknown
+ }
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/DisplayStateRepository.kt b/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/DisplayStateRepository.kt
index 792a7ef..c8fb044 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/DisplayStateRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/DisplayStateRepository.kt
@@ -17,31 +17,20 @@
package com.android.systemui.biometrics.data.repository
import android.content.Context
-import android.hardware.devicestate.DeviceStateManager
-import android.hardware.display.DisplayManager
-import android.hardware.display.DisplayManager.DisplayListener
-import android.hardware.display.DisplayManager.EVENT_FLAG_DISPLAY_CHANGED
-import android.os.Handler
import android.util.Size
import android.view.DisplayInfo
-import com.android.app.tracing.traceSection
-import com.android.internal.util.ArrayUtils
import com.android.systemui.biometrics.shared.model.DisplayRotation
import com.android.systemui.biometrics.shared.model.toDisplayRotation
-import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging
-import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.dagger.qualifiers.Background
-import com.android.systemui.dagger.qualifiers.Main
-import java.util.concurrent.Executor
+import com.android.systemui.display.data.repository.DeviceStateRepository
+import com.android.systemui.display.data.repository.DeviceStateRepository.DeviceState.REAR_DISPLAY
+import com.android.systemui.display.data.repository.DisplayRepository
import javax.inject.Inject
-import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
@@ -66,52 +55,23 @@
val currentDisplaySize: StateFlow<Size>
}
-// TODO(b/296211844): This class could directly use DeviceStateRepository and DisplayRepository
-// instead.
@SysUISingleton
class DisplayStateRepositoryImpl
@Inject
constructor(
- @Application applicationScope: CoroutineScope,
+ @Background backgroundScope: CoroutineScope,
@Application val context: Context,
- deviceStateManager: DeviceStateManager,
- displayManager: DisplayManager,
- @Main handler: Handler,
- @Background backgroundExecutor: Executor,
- @Background backgroundDispatcher: CoroutineDispatcher,
+ deviceStateRepository: DeviceStateRepository,
+ displayRepository: DisplayRepository,
) : DisplayStateRepository {
override val isReverseDefaultRotation =
context.resources.getBoolean(com.android.internal.R.bool.config_reverseDefaultRotation)
override val isInRearDisplayMode: StateFlow<Boolean> =
- conflatedCallbackFlow {
- val sendRearDisplayStateUpdate = { state: Boolean ->
- trySendWithFailureLogging(
- state,
- TAG,
- "Error sending rear display state update to $state"
- )
- }
-
- val callback =
- DeviceStateManager.DeviceStateCallback { state ->
- val isInRearDisplayMode =
- ArrayUtils.contains(
- context.resources.getIntArray(
- com.android.internal.R.array.config_rearDisplayDeviceStates
- ),
- state
- )
- sendRearDisplayStateUpdate(isInRearDisplayMode)
- }
-
- sendRearDisplayStateUpdate(false)
- deviceStateManager.registerCallback(backgroundExecutor, callback)
- awaitClose { deviceStateManager.unregisterCallback(callback) }
- }
- .flowOn(backgroundDispatcher)
+ deviceStateRepository.state
+ .map { it == REAR_DISPLAY }
.stateIn(
- applicationScope,
+ backgroundScope,
started = SharingStarted.Eagerly,
initialValue = false,
)
@@ -123,37 +83,10 @@
}
private val currentDisplayInfo: StateFlow<DisplayInfo> =
- conflatedCallbackFlow {
- val callback =
- object : DisplayListener {
- override fun onDisplayRemoved(displayId: Int) {}
-
- override fun onDisplayAdded(displayId: Int) {}
-
- override fun onDisplayChanged(displayId: Int) {
- traceSection(
- "DisplayStateRepository" +
- ".currentRotationDisplayListener#onDisplayChanged"
- ) {
- val displayInfo = getDisplayInfo()
- trySendWithFailureLogging(
- displayInfo,
- TAG,
- "Error sending displayInfo to $displayInfo"
- )
- }
- }
- }
- displayManager.registerDisplayListener(
- callback,
- handler,
- EVENT_FLAG_DISPLAY_CHANGED
- )
- awaitClose { displayManager.unregisterDisplayListener(callback) }
- }
- .flowOn(backgroundDispatcher)
+ displayRepository.displayChangeEvent
+ .map { getDisplayInfo() }
.stateIn(
- applicationScope,
+ backgroundScope,
started = SharingStarted.Eagerly,
initialValue = getDisplayInfo(),
)
@@ -170,7 +103,7 @@
currentDisplayInfo
.map { rotationToDisplayRotation(it.rotation) }
.stateIn(
- applicationScope,
+ backgroundScope,
started = SharingStarted.WhileSubscribed(),
initialValue = rotationToDisplayRotation(currentDisplayInfo.value.rotation)
)
@@ -179,7 +112,7 @@
currentDisplayInfo
.map { Size(it.naturalWidth, it.naturalHeight) }
.stateIn(
- applicationScope,
+ backgroundScope,
started = SharingStarted.WhileSubscribed(),
initialValue =
Size(
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/BiometricStatusInteractor.kt b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/BiometricStatusInteractor.kt
index 55a2d3d..ed1557c 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/BiometricStatusInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/BiometricStatusInteractor.kt
@@ -20,6 +20,7 @@
import com.android.systemui.biometrics.data.repository.BiometricStatusRepository
import com.android.systemui.biometrics.shared.model.AuthenticationReason
import com.android.systemui.biometrics.shared.model.AuthenticationReason.SettingsOperations
+import com.android.systemui.keyguard.shared.model.FingerprintAuthenticationStatus
import javax.inject.Inject
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
@@ -31,6 +32,9 @@
* filtered for when the overlay should be shown, otherwise [NotRunning].
*/
val sfpsAuthenticationReason: Flow<AuthenticationReason>
+
+ /** The current status of an acquired fingerprint. */
+ val fingerprintAcquiredStatus: Flow<FingerprintAuthenticationStatus>
}
class BiometricStatusInteractorImpl
@@ -50,6 +54,9 @@
}
}
+ override val fingerprintAcquiredStatus: Flow<FingerprintAuthenticationStatus> =
+ biometricStatusRepository.fingerprintAcquiredStatus
+
companion object {
private const val TAG = "BiometricStatusInteractor"
}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/shared/model/AuthenticationState.kt b/packages/SystemUI/src/com/android/systemui/biometrics/shared/model/AuthenticationState.kt
new file mode 100644
index 0000000..77cf840
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/shared/model/AuthenticationState.kt
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2024 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.biometrics.shared.model
+
+import android.hardware.biometrics.BiometricSourceType
+
+/**
+ * Describes the current state of biometric authentication, including whether authentication is
+ * started, stopped, or acquired and relevant parameters, and the [AuthenticationReason] for
+ * authentication.
+ */
+sealed interface AuthenticationState {
+ val requestReason: AuthenticationReason
+
+ /**
+ * Authentication started
+ *
+ * @param requestReason [AuthenticationReason] for starting authentication
+ */
+ data class AuthenticationStarted(override val requestReason: AuthenticationReason) :
+ AuthenticationState
+
+ /**
+ * Authentication stopped
+ *
+ * @param requestReason [AuthenticationReason.NotRunning]
+ */
+ data class AuthenticationStopped(override val requestReason: AuthenticationReason) :
+ AuthenticationState
+
+ /**
+ * Authentication acquired
+ *
+ * @param biometricSourceType indicates [BiometricSourceType] of acquired authentication
+ * @param requestReason indicates [AuthenticationReason] for requesting auth
+ * @param acquiredInfo indicates
+ */
+ data class AuthenticationAcquired(
+ val biometricSourceType: BiometricSourceType,
+ override val requestReason: AuthenticationReason,
+ val acquiredInfo: Int
+ ) : AuthenticationState
+}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewBinder.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewBinder.kt
index 285ab4a..efad21b 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewBinder.kt
@@ -41,6 +41,7 @@
import androidx.lifecycle.repeatOnLifecycle
import com.airbnb.lottie.LottieAnimationView
import com.airbnb.lottie.LottieCompositionFactory
+import com.android.systemui.Flags.constraintBp
import com.android.systemui.biometrics.AuthPanelController
import com.android.systemui.biometrics.shared.model.BiometricModalities
import com.android.systemui.biometrics.shared.model.BiometricModality
@@ -70,9 +71,9 @@
@SuppressLint("ClickableViewAccessibility")
@JvmStatic
fun bind(
- view: BiometricPromptLayout,
+ view: View,
viewModel: PromptViewModel,
- panelViewController: AuthPanelController,
+ panelViewController: AuthPanelController?,
jankListener: BiometricJankListener,
backgroundView: View,
legacyCallback: Spaghetti.Callback,
@@ -112,11 +113,18 @@
val iconOverlayView = view.requireViewById<LottieAnimationView>(R.id.biometric_icon_overlay)
val iconView = view.requireViewById<LottieAnimationView>(R.id.biometric_icon)
+ val iconSizeOverride =
+ if (constraintBp()) {
+ viewModel.fingerprintAffordanceSize
+ } else {
+ (view as BiometricPromptLayout).updatedFingerprintAffordanceSize
+ }
+
PromptIconViewBinder.bind(
iconView,
iconOverlayView,
- view.getUpdatedFingerprintAffordanceSize(),
- viewModel
+ iconSizeOverride,
+ viewModel,
)
val indicatorMessageView = view.requireViewById<TextView>(R.id.indicator)
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewSizeBinder.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewSizeBinder.kt
index d5695f3..2417fe9 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewSizeBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewSizeBinder.kt
@@ -19,29 +19,45 @@
import android.animation.Animator
import android.animation.AnimatorSet
import android.animation.ValueAnimator
+import android.graphics.Outline
+import android.graphics.Rect
+import android.transition.AutoTransition
+import android.transition.TransitionManager
import android.view.Surface
import android.view.View
import android.view.ViewGroup
+import android.view.ViewOutlineProvider
import android.view.WindowInsets
import android.view.WindowManager
import android.view.accessibility.AccessibilityManager
import android.widget.ImageView
import android.widget.TextView
+import androidx.constraintlayout.widget.ConstraintLayout
+import androidx.constraintlayout.widget.ConstraintSet
+import androidx.constraintlayout.widget.Guideline
import androidx.core.animation.addListener
+import androidx.core.view.doOnAttach
import androidx.core.view.doOnLayout
import androidx.core.view.isGone
import androidx.lifecycle.lifecycleScope
+import com.android.systemui.Flags.constraintBp
import com.android.systemui.biometrics.AuthPanelController
import com.android.systemui.biometrics.Utils
-import com.android.systemui.biometrics.ui.BiometricPromptLayout
+import com.android.systemui.biometrics.ui.viewmodel.PromptPosition
import com.android.systemui.biometrics.ui.viewmodel.PromptSize
import com.android.systemui.biometrics.ui.viewmodel.PromptViewModel
+import com.android.systemui.biometrics.ui.viewmodel.isBottom
import com.android.systemui.biometrics.ui.viewmodel.isLarge
+import com.android.systemui.biometrics.ui.viewmodel.isLeft
import com.android.systemui.biometrics.ui.viewmodel.isMedium
import com.android.systemui.biometrics.ui.viewmodel.isNullOrNotSmall
+import com.android.systemui.biometrics.ui.viewmodel.isRight
import com.android.systemui.biometrics.ui.viewmodel.isSmall
+import com.android.systemui.biometrics.ui.viewmodel.isTop
import com.android.systemui.lifecycle.repeatWhenAttached
import com.android.systemui.res.R
+import kotlin.math.abs
+import kotlin.math.min
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.launch
@@ -54,18 +70,19 @@
/** Resizes [BiometricPromptLayout] and the [panelViewController] via the [PromptViewModel]. */
fun bind(
- view: BiometricPromptLayout,
+ view: View,
viewModel: PromptViewModel,
viewsToHideWhenSmall: List<View>,
viewsToFadeInOnSizeChange: List<View>,
- panelViewController: AuthPanelController,
+ panelViewController: AuthPanelController?,
jankListener: BiometricJankListener,
) {
val windowManager = requireNotNull(view.context.getSystemService(WindowManager::class.java))
val accessibilityManager =
requireNotNull(view.context.getSystemService(AccessibilityManager::class.java))
+
fun notifyAccessibilityChanged() {
- Utils.notifyAccessibilityContentChanged(accessibilityManager, view)
+ Utils.notifyAccessibilityContentChanged(accessibilityManager, view as ViewGroup)
}
fun startMonitoredAnimation(animators: List<Animator>) {
@@ -77,149 +94,342 @@
}
}
- val iconHolderView = view.requireViewById<View>(R.id.biometric_icon_frame)
- val iconPadding = view.resources.getDimension(R.dimen.biometric_dialog_icon_padding)
- val fullSizeYOffset =
- view.resources.getDimension(R.dimen.biometric_dialog_medium_to_large_translation_offset)
+ if (constraintBp()) {
+ val leftGuideline = view.requireViewById<Guideline>(R.id.leftGuideline)
+ val rightGuideline = view.requireViewById<Guideline>(R.id.rightGuideline)
+ val bottomGuideline = view.requireViewById<Guideline>(R.id.bottomGuideline)
- // cache the original position of the icon view (as done in legacy view)
- // this must happen before any size changes can be made
- view.doOnLayout {
- // TODO(b/251476085): this old way of positioning has proven itself unreliable
- // remove this and associated thing like (UdfpsDialogMeasureAdapter) and
- // pin to the physical sensor
- val iconHolderOriginalY = iconHolderView.y
+ val iconHolderView = view.requireViewById<View>(R.id.biometric_icon)
+ val panelView = view.requireViewById<View>(R.id.panel)
+ val cornerRadius = view.resources.getDimension(R.dimen.biometric_dialog_corner_size)
- // bind to prompt
- // TODO(b/251476085): migrate the legacy panel controller and simplify this
- view.repeatWhenAttached {
- var currentSize: PromptSize? = null
- lifecycleScope.launch {
- /**
- * View is only set visible in BiometricViewSizeBinder once PromptSize is
- * determined that accounts for iconView size, to prevent prompt resizing being
- * visible to the user.
- *
- * TODO(b/288175072): May be able to remove isIconViewLoaded once constraint
- * layout is implemented
- */
- combine(viewModel.isIconViewLoaded, viewModel.size, ::Pair).collect {
- (isIconViewLoaded, size) ->
- if (!isIconViewLoaded) {
- return@collect
+ // ConstraintSets for animating between prompt sizes
+ val mediumConstraintSet = ConstraintSet()
+ mediumConstraintSet.clone(view as ConstraintLayout)
+
+ val smallConstraintSet = ConstraintSet()
+ smallConstraintSet.clone(mediumConstraintSet)
+ viewsToHideWhenSmall.forEach { smallConstraintSet.setVisibility(it.id, View.GONE) }
+
+ val largeConstraintSet = ConstraintSet()
+ largeConstraintSet.clone(mediumConstraintSet)
+ viewsToHideWhenSmall.forEach { largeConstraintSet.setVisibility(it.id, View.GONE) }
+ largeConstraintSet.setVisibility(iconHolderView.id, View.GONE)
+ largeConstraintSet.setVisibility(R.id.biometric_icon_overlay, View.GONE)
+ largeConstraintSet.setVisibility(R.id.indicator, View.GONE)
+ largeConstraintSet.setGuidelineBegin(leftGuideline.id, 0)
+ largeConstraintSet.setGuidelineEnd(rightGuideline.id, 0)
+ largeConstraintSet.setGuidelineEnd(bottomGuideline.id, 0)
+
+ // Round the panel outline
+ panelView.outlineProvider =
+ object : ViewOutlineProvider() {
+ override fun getOutline(view: View, outline: Outline) {
+ outline.setRoundRect(0, 0, view.width, view.height, cornerRadius)
+ }
+ }
+
+ view.doOnLayout {
+ val windowBounds = windowManager.maximumWindowMetrics.bounds
+ val bottomInset =
+ windowManager.maximumWindowMetrics.windowInsets
+ .getInsets(WindowInsets.Type.navigationBars())
+ .bottom
+
+ fun measureBounds(position: PromptPosition) {
+ val width = min(windowBounds.height(), windowBounds.width())
+
+ var left = -1
+ var top = -1
+ var right = -1
+ var bottom = -1
+
+ when {
+ position.isTop -> {
+ left = windowBounds.centerX() - width / 2 + viewModel.promptMargin
+ top = viewModel.promptMargin
+ right = windowBounds.centerX() - width / 2 + viewModel.promptMargin
+ bottom = iconHolderView.centerY() * 2 - iconHolderView.centerY() / 4
+ }
+ position.isBottom -> {
+ if (view.isLandscape()) {
+ left = windowBounds.centerX() - width / 2 + viewModel.promptMargin
+ top = iconHolderView.centerY()
+ right = windowBounds.centerX() - width / 2 + viewModel.promptMargin
+ bottom = bottomInset + viewModel.promptMargin
+ } else {
+ left = windowBounds.centerX() - width / 2 + viewModel.promptMargin
+ top =
+ windowBounds.height() -
+ (windowBounds.height() - iconHolderView.centerY()) * 2 +
+ viewModel.promptMargin
+ right = windowBounds.centerX() - width / 2 + viewModel.promptMargin
+ bottom = viewModel.promptMargin
+ }
}
- // prepare for animated size transitions
- for (v in viewsToHideWhenSmall) {
- v.showContentOrHide(forceHide = size.isSmall)
+ // For Udfps exclusive left and right, measure guideline to center
+ // icon in BP
+ position.isLeft -> {
+ left = viewModel.promptMargin
+ top =
+ windowBounds.height() -
+ (windowBounds.height() - iconHolderView.centerY()) * 2 +
+ viewModel.promptMargin
+ right =
+ abs(
+ windowBounds.width() - iconHolderView.centerX() * 2 +
+ viewModel.promptMargin
+ )
+ bottom = bottomInset + viewModel.promptMargin
}
- if (currentSize == null && size.isSmall) {
- iconHolderView.alpha = 0f
+ position.isRight -> {
+ left =
+ abs(
+ iconHolderView.centerX() -
+ (windowBounds.width() - iconHolderView.centerX()) -
+ viewModel.promptMargin
+ )
+ top =
+ windowBounds.height() -
+ (windowBounds.height() - iconHolderView.centerY()) * 2 +
+ viewModel.promptMargin
+ right = viewModel.promptMargin
+ bottom = bottomInset + viewModel.promptMargin
}
- if ((currentSize.isSmall && size.isMedium) || size.isSmall) {
- viewsToFadeInOnSizeChange.forEach { it.alpha = 0f }
- }
+ }
- // TODO(b/302735104): Fix wrong height due to the delay of
- // PromptContentView. addOnLayoutChangeListener() will cause crash when
- // showing credential view, since |PromptIconViewModel| won't release the
- // flow.
- // propagate size changes to legacy panel controller and animate transitions
- view.doOnLayout {
- val width = view.measuredWidth
- val height = view.measuredHeight
+ val bounds = Rect(left, top, right, bottom)
+ if (bounds.shouldAdjustLeftGuideline()) {
+ leftGuideline.setGuidelineBegin(bounds.left)
+ smallConstraintSet.setGuidelineBegin(leftGuideline.id, bounds.left)
+ mediumConstraintSet.setGuidelineBegin(leftGuideline.id, bounds.left)
+ }
+ if (bounds.shouldAdjustRightGuideline()) {
+ rightGuideline.setGuidelineEnd(bounds.right)
+ smallConstraintSet.setGuidelineEnd(rightGuideline.id, bounds.right)
+ mediumConstraintSet.setGuidelineEnd(rightGuideline.id, bounds.right)
+ }
+ if (bounds.shouldAdjustBottomGuideline()) {
+ bottomGuideline.setGuidelineEnd(bounds.bottom)
+ smallConstraintSet.setGuidelineEnd(bottomGuideline.id, bounds.bottom)
+ mediumConstraintSet.setGuidelineEnd(bottomGuideline.id, bounds.bottom)
+ }
+ }
- when {
- size.isSmall -> {
- iconHolderView.alpha = 1f
- val bottomInset =
- windowManager.maximumWindowMetrics.windowInsets
- .getInsets(WindowInsets.Type.navigationBars())
- .bottom
- iconHolderView.y =
- if (view.isLandscape()) {
- (view.height - iconHolderView.height - bottomInset) / 2f
- } else {
- view.height -
- iconHolderView.height -
- iconPadding -
- bottomInset
- }
- val newHeight =
- iconHolderView.height + (2 * iconPadding.toInt()) -
- iconHolderView.paddingTop -
- iconHolderView.paddingBottom
- panelViewController.updateForContentDimensions(
- width,
- newHeight + bottomInset,
- 0, /* animateDurationMs */
- )
- }
- size.isMedium && currentSize.isSmall -> {
- val duration = ANIMATE_SMALL_TO_MEDIUM_DURATION_MS
- panelViewController.updateForContentDimensions(
- width,
- height,
- duration,
- )
- startMonitoredAnimation(
- listOf(
- iconHolderView.asVerticalAnimator(
- duration = duration.toLong(),
- toY =
- iconHolderOriginalY -
- viewsToHideWhenSmall
- .filter { it.isGone }
- .sumOf { it.height },
- ),
- viewsToFadeInOnSizeChange.asFadeInAnimator(
- duration = duration.toLong(),
- delay = duration.toLong(),
- ),
+ view.repeatWhenAttached {
+ var currentSize: PromptSize? = null
+ lifecycleScope.launch {
+ combine(viewModel.position, viewModel.size, ::Pair).collect {
+ (position, size) ->
+ view.doOnAttach {
+ measureBounds(position)
+
+ when {
+ size.isSmall -> {
+ val ratio =
+ if (view.isLandscape()) {
+ (windowBounds.height() -
+ bottomInset -
+ viewModel.promptMargin)
+ .toFloat() / windowBounds.height()
+ } else {
+ (windowBounds.height() - viewModel.promptMargin)
+ .toFloat() / windowBounds.height()
+ }
+ smallConstraintSet.setVerticalBias(iconHolderView.id, ratio)
+
+ smallConstraintSet.applyTo(view as ConstraintLayout?)
+ }
+ size.isMedium && currentSize.isSmall -> {
+ val autoTransition = AutoTransition()
+ autoTransition.setDuration(
+ ANIMATE_SMALL_TO_MEDIUM_DURATION_MS.toLong()
)
- )
- }
- size.isMedium && currentSize.isNullOrNotSmall -> {
- panelViewController.updateForContentDimensions(
- width,
- height,
- 0, /* animateDurationMs */
- )
- }
- size.isLarge -> {
- val duration = ANIMATE_MEDIUM_TO_LARGE_DURATION_MS
- panelViewController.setUseFullScreen(true)
- panelViewController.updateForContentDimensions(
- panelViewController.containerWidth,
- panelViewController.containerHeight,
- duration,
- )
- startMonitoredAnimation(
- listOf(
- view.asVerticalAnimator(
- duration.toLong() * 2 / 3,
- toY = view.y - fullSizeYOffset
- ),
- listOf(view)
- .asFadeInAnimator(
- duration = duration.toLong() / 2,
- delay = duration.toLong(),
- ),
+ TransitionManager.beginDelayedTransition(
+ view,
+ autoTransition
)
- )
- // TODO(b/251476085): clean up (copied from legacy)
- if (view.isAttachedToWindow) {
- val parent = view.parent as? ViewGroup
- parent?.removeView(view)
+ mediumConstraintSet.applyTo(view)
+ }
+ size.isLarge -> {
+ val autoTransition = AutoTransition()
+ autoTransition.setDuration(
+ ANIMATE_MEDIUM_TO_LARGE_DURATION_MS.toLong()
+ )
+
+ TransitionManager.beginDelayedTransition(
+ view,
+ autoTransition
+ )
+ largeConstraintSet.applyTo(view)
}
}
+
+ currentSize = size
+ view.visibility = View.VISIBLE
+ viewModel.setIsIconViewLoaded(false)
+ notifyAccessibilityChanged()
+
+ view.invalidate()
+ view.requestLayout()
+ }
+ }
+ }
+ }
+ }
+ } else if (panelViewController != null) {
+ val iconHolderView = view.requireViewById<View>(R.id.biometric_icon_frame)
+ val iconPadding = view.resources.getDimension(R.dimen.biometric_dialog_icon_padding)
+ val fullSizeYOffset =
+ view.resources.getDimension(
+ R.dimen.biometric_dialog_medium_to_large_translation_offset
+ )
+
+ // cache the original position of the icon view (as done in legacy view)
+ // this must happen before any size changes can be made
+ view.doOnLayout {
+ // TODO(b/251476085): this old way of positioning has proven itself unreliable
+ // remove this and associated thing like (UdfpsDialogMeasureAdapter) and
+ // pin to the physical sensor
+ val iconHolderOriginalY = iconHolderView.y
+
+ // bind to prompt
+ // TODO(b/251476085): migrate the legacy panel controller and simplify this
+ view.repeatWhenAttached {
+ var currentSize: PromptSize? = null
+ lifecycleScope.launch {
+ /**
+ * View is only set visible in BiometricViewSizeBinder once PromptSize is
+ * determined that accounts for iconView size, to prevent prompt resizing
+ * being visible to the user.
+ *
+ * TODO(b/288175072): May be able to remove isIconViewLoaded once constraint
+ * layout is implemented
+ */
+ combine(viewModel.isIconViewLoaded, viewModel.size, ::Pair).collect {
+ (isIconViewLoaded, size) ->
+ if (!isIconViewLoaded) {
+ return@collect
}
- currentSize = size
- view.visibility = View.VISIBLE
- viewModel.setIsIconViewLoaded(false)
- notifyAccessibilityChanged()
+ // prepare for animated size transitions
+ for (v in viewsToHideWhenSmall) {
+ v.showContentOrHide(forceHide = size.isSmall)
+ }
+ if (currentSize == null && size.isSmall) {
+ iconHolderView.alpha = 0f
+ }
+ if ((currentSize.isSmall && size.isMedium) || size.isSmall) {
+ viewsToFadeInOnSizeChange.forEach { it.alpha = 0f }
+ }
+
+ // TODO(b/302735104): Fix wrong height due to the delay of
+ // PromptContentView. addOnLayoutChangeListener() will cause crash when
+ // showing credential view, since |PromptIconViewModel| won't release
+ // the
+ // flow.
+ // propagate size changes to legacy panel controller and animate
+ // transitions
+ view.doOnLayout {
+ val width = view.measuredWidth
+ val height = view.measuredHeight
+
+ when {
+ size.isSmall -> {
+ iconHolderView.alpha = 1f
+ val bottomInset =
+ windowManager.maximumWindowMetrics.windowInsets
+ .getInsets(WindowInsets.Type.navigationBars())
+ .bottom
+ iconHolderView.y =
+ if (view.isLandscape()) {
+ (view.height -
+ iconHolderView.height -
+ bottomInset) / 2f
+ } else {
+ view.height -
+ iconHolderView.height -
+ iconPadding -
+ bottomInset
+ }
+ val newHeight =
+ iconHolderView.height + (2 * iconPadding.toInt()) -
+ iconHolderView.paddingTop -
+ iconHolderView.paddingBottom
+ panelViewController.updateForContentDimensions(
+ width,
+ newHeight + bottomInset,
+ 0, /* animateDurationMs */
+ )
+ }
+ size.isMedium && currentSize.isSmall -> {
+ val duration = ANIMATE_SMALL_TO_MEDIUM_DURATION_MS
+ panelViewController.updateForContentDimensions(
+ width,
+ height,
+ duration,
+ )
+ startMonitoredAnimation(
+ listOf(
+ iconHolderView.asVerticalAnimator(
+ duration = duration.toLong(),
+ toY =
+ iconHolderOriginalY -
+ viewsToHideWhenSmall
+ .filter { it.isGone }
+ .sumOf { it.height },
+ ),
+ viewsToFadeInOnSizeChange.asFadeInAnimator(
+ duration = duration.toLong(),
+ delay = duration.toLong(),
+ ),
+ )
+ )
+ }
+ size.isMedium && currentSize.isNullOrNotSmall -> {
+ panelViewController.updateForContentDimensions(
+ width,
+ height,
+ 0, /* animateDurationMs */
+ )
+ }
+ size.isLarge -> {
+ val duration = ANIMATE_MEDIUM_TO_LARGE_DURATION_MS
+ panelViewController.setUseFullScreen(true)
+ panelViewController.updateForContentDimensions(
+ panelViewController.containerWidth,
+ panelViewController.containerHeight,
+ duration,
+ )
+
+ startMonitoredAnimation(
+ listOf(
+ view.asVerticalAnimator(
+ duration.toLong() * 2 / 3,
+ toY = view.y - fullSizeYOffset
+ ),
+ listOf(view)
+ .asFadeInAnimator(
+ duration = duration.toLong() / 2,
+ delay = duration.toLong(),
+ ),
+ )
+ )
+ // TODO(b/251476085): clean up (copied from legacy)
+ if (view.isAttachedToWindow) {
+ val parent = view.parent as? ViewGroup
+ parent?.removeView(view)
+ }
+ }
+ }
+
+ currentSize = size
+ view.visibility = View.VISIBLE
+ viewModel.setIsIconViewLoaded(false)
+ notifyAccessibilityChanged()
+ }
}
}
}
@@ -244,6 +454,20 @@
}
}
+private fun View.centerX(): Int {
+ return (x + width / 2).toInt()
+}
+
+private fun View.centerY(): Int {
+ return (y + height / 2).toInt()
+}
+
+private fun Rect.shouldAdjustLeftGuideline(): Boolean = left != -1
+
+private fun Rect.shouldAdjustRightGuideline(): Boolean = right != -1
+
+private fun Rect.shouldAdjustBottomGuideline(): Boolean = bottom != -1
+
private fun View.asVerticalAnimator(
duration: Long,
toY: Float,
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/PromptIconViewBinder.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/PromptIconViewBinder.kt
index 6e3bcf5..2e47375 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/PromptIconViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/PromptIconViewBinder.kt
@@ -17,13 +17,17 @@
package com.android.systemui.biometrics.ui.binder
+import android.graphics.Rect
import android.graphics.drawable.Animatable2
import android.graphics.drawable.AnimatedVectorDrawable
import android.graphics.drawable.Drawable
+import androidx.constraintlayout.widget.ConstraintLayout
+import androidx.constraintlayout.widget.ConstraintSet
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.repeatOnLifecycle
import com.airbnb.lottie.LottieAnimationView
import com.android.settingslib.widget.LottieColorUtils
+import com.android.systemui.Flags.constraintBp
import com.android.systemui.biometrics.ui.viewmodel.PromptIconViewModel
import com.android.systemui.biometrics.ui.viewmodel.PromptIconViewModel.AuthType
import com.android.systemui.biometrics.ui.viewmodel.PromptViewModel
@@ -119,6 +123,24 @@
}
launch {
+ viewModel.iconPosition.collect { position ->
+ if (constraintBp() && position != Rect()) {
+ val iconParams = iconView.layoutParams as ConstraintLayout.LayoutParams
+
+ if (position.left != -1) {
+ iconParams.endToEnd = ConstraintSet.UNSET
+ iconParams.leftMargin = position.left
+ }
+ if (position.top != -1) {
+ iconParams.bottomToBottom = ConstraintSet.UNSET
+ iconParams.topMargin = position.top
+ }
+ iconView.layoutParams = iconParams
+ }
+ }
+ }
+
+ launch {
viewModel.iconAsset
.sample(
combine(
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/SideFpsOverlayViewBinder.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/SideFpsOverlayViewBinder.kt
index 80d37b4..7b4be02 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/SideFpsOverlayViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/SideFpsOverlayViewBinder.kt
@@ -50,10 +50,12 @@
import dagger.Lazy
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.launch
/** Binds the side fingerprint sensor indicator view to [SideFpsOverlayViewModel]. */
+@OptIn(ExperimentalCoroutinesApi::class)
@SysUISingleton
class SideFpsOverlayViewBinder
@Inject
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptIconViewModel.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptIconViewModel.kt
index 3defec5..b7cffaf 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptIconViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptIconViewModel.kt
@@ -20,8 +20,11 @@
import android.annotation.DrawableRes
import android.annotation.RawRes
import android.content.res.Configuration
+import android.graphics.Rect
+import android.util.RotationUtils
import com.android.systemui.biometrics.domain.interactor.DisplayStateInteractor
import com.android.systemui.biometrics.domain.interactor.PromptSelectorInteractor
+import com.android.systemui.biometrics.domain.interactor.UdfpsOverlayInteractor
import com.android.systemui.biometrics.shared.model.DisplayRotation
import com.android.systemui.biometrics.shared.model.FingerprintSensorType
import com.android.systemui.res.R
@@ -42,7 +45,8 @@
constructor(
promptViewModel: PromptViewModel,
private val displayStateInteractor: DisplayStateInteractor,
- promptSelectorInteractor: PromptSelectorInteractor
+ promptSelectorInteractor: PromptSelectorInteractor,
+ udfpsOverlayInteractor: UdfpsOverlayInteractor,
) {
/** Auth types for the UI to display. */
@@ -71,7 +75,40 @@
} else if (modalities.hasFingerprintOnly) {
AuthType.Fingerprint
} else {
- throw IllegalStateException("unexpected modality: $modalities")
+ // TODO(b/288175072): Remove, currently needed for transition to credential view
+ AuthType.Fingerprint
+ }
+ }
+
+ val udfpsSensorBounds: Flow<Rect> =
+ combine(
+ udfpsOverlayInteractor.udfpsOverlayParams,
+ displayStateInteractor.currentRotation
+ ) { params, rotation ->
+ val rotatedBounds = Rect(params.sensorBounds)
+ RotationUtils.rotateBounds(
+ rotatedBounds,
+ params.naturalDisplayWidth,
+ params.naturalDisplayHeight,
+ rotation.ordinal
+ )
+ rotatedBounds
+ }
+ .distinctUntilChanged()
+
+ val iconPosition: Flow<Rect> =
+ combine(udfpsSensorBounds, promptViewModel.size, promptViewModel.modalities) {
+ sensorBounds,
+ size,
+ modalities ->
+ // If not Udfps, icon does not change from default layout position
+ if (!modalities.hasUdfps) {
+ Rect() // Empty rect, don't offset from default position
+ } else if (size.isSmall) {
+ // When small with Udfps, only set horizontal position
+ Rect(sensorBounds.left, -1, sensorBounds.right, -1)
+ } else {
+ sensorBounds
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptPosition.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptPosition.kt
new file mode 100644
index 0000000..d45dad6
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptPosition.kt
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2024 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.biometrics.ui.viewmodel
+
+/** The position of a biometric prompt */
+enum class PromptPosition {
+ Top,
+ Bottom,
+ Left,
+ Right,
+}
+
+val PromptPosition?.isBottom: Boolean
+ get() = this != null && this == PromptPosition.Bottom
+
+val PromptPosition?.isLeft: Boolean
+ get() = this != null && this == PromptPosition.Left
+
+val PromptPosition?.isRight: Boolean
+ get() = this != null && this == PromptPosition.Right
+
+val PromptPosition?.isTop: Boolean
+ get() = this != null && this == PromptPosition.Top
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModel.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModel.kt
index 0f1340a..ef5c37ea 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModel.kt
@@ -17,6 +17,7 @@
package com.android.systemui.biometrics.ui.viewmodel
import android.content.Context
+import android.content.pm.PackageManager
import android.graphics.Rect
import android.graphics.drawable.BitmapDrawable
import android.graphics.drawable.Drawable
@@ -81,11 +82,23 @@
val faceIconHeight: Int =
context.resources.getDimensionPixelSize(R.dimen.biometric_dialog_face_icon_size)
+ val fingerprintSensorDiameter: Int =
+ (udfpsOverlayInteractor.udfpsOverlayParams.value.sensorBounds.width() *
+ udfpsOverlayInteractor.udfpsOverlayParams.value.scaleFactor)
+ .toInt()
+ val fingerprintAffordanceSize: Pair<Int, Int>? =
+ if (fingerprintSensorDiameter != 0)
+ Pair(fingerprintSensorDiameter, fingerprintSensorDiameter)
+ else null
+
private val _accessibilityHint = MutableSharedFlow<String>()
/** Hint for talkback directional guidance */
val accessibilityHint: Flow<String> = _accessibilityHint.asSharedFlow()
+ val promptMargin: Int =
+ context.resources.getDimensionPixelSize(R.dimen.biometric_dialog_border_padding)
+
private val _isAuthenticating: MutableStateFlow<Boolean> = MutableStateFlow(false)
/** If the user is currently authenticating (i.e. at least one biometric is scanning). */
@@ -135,6 +148,22 @@
/** Event fired to the view indicating a [HapticFeedbackConstants] to be played */
val hapticsToPlay = _hapticsToPlay.asStateFlow()
+ /** The current position of the prompt */
+ val position: Flow<PromptPosition> =
+ combine(_forceLargeSize, modalities, displayStateInteractor.currentRotation) {
+ forceLarge,
+ modalities,
+ rotation ->
+ when {
+ forceLarge || !modalities.hasUdfps -> PromptPosition.Bottom
+ rotation == DisplayRotation.ROTATION_90 -> PromptPosition.Right
+ rotation == DisplayRotation.ROTATION_270 -> PromptPosition.Left
+ rotation == DisplayRotation.ROTATION_180 -> PromptPosition.Top
+ else -> PromptPosition.Bottom
+ }
+ }
+ .distinctUntilChanged()
+
/** The size of the prompt. */
val size: Flow<PromptSize> =
combine(
@@ -195,7 +224,12 @@
.distinctUntilChanged()
val iconViewModel: PromptIconViewModel =
- PromptIconViewModel(this, displayStateInteractor, promptSelectorInteractor)
+ PromptIconViewModel(
+ this,
+ displayStateInteractor,
+ promptSelectorInteractor,
+ udfpsOverlayInteractor
+ )
private val _isIconViewLoaded = MutableStateFlow(false)
@@ -244,7 +278,13 @@
!customBiometricPrompt() || it == null -> null
it.logoRes != -1 -> context.resources.getDrawable(it.logoRes, context.theme)
it.logoBitmap != null -> BitmapDrawable(context.resources, it.logoBitmap)
- else -> context.packageManager.getApplicationIcon(it.opPackageName)
+ else ->
+ try {
+ context.packageManager.getApplicationIcon(it.opPackageName)
+ } catch (e: PackageManager.NameNotFoundException) {
+ Log.w(TAG, "Cannot find icon for package " + it.opPackageName, e)
+ null
+ }
}
}
.distinctUntilChanged()
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/SideFpsOverlayViewModel.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/SideFpsOverlayViewModel.kt
index ce72603..cfda75c 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/SideFpsOverlayViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/SideFpsOverlayViewModel.kt
@@ -41,12 +41,14 @@
import com.android.systemui.keyguard.ui.viewmodel.SideFpsProgressBarViewModel
import com.android.systemui.res.R
import javax.inject.Inject
+import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
/** Models UI of the side fingerprint sensor indicator view. */
+@OptIn(ExperimentalCoroutinesApi::class)
class SideFpsOverlayViewModel
@Inject
constructor(
@@ -176,8 +178,8 @@
val lottieCallbacks: Flow<List<LottieCallback>> =
combine(
biometricStatusInteractor.sfpsAuthenticationReason,
- deviceEntrySideFpsOverlayInteractor.showIndicatorForDeviceEntry.distinctUntilChanged(),
- sideFpsProgressBarViewModel.isVisible,
+ deviceEntrySideFpsOverlayInteractor.showIndicatorForDeviceEntry,
+ sideFpsProgressBarViewModel.isVisible
) { reason: AuthenticationReason, showIndicatorForDeviceEntry: Boolean, progressBarIsVisible
->
val callbacks = mutableListOf<LottieCallback>()
diff --git a/packages/SystemUI/src/com/android/systemui/camera/CameraGestureHelper.kt b/packages/SystemUI/src/com/android/systemui/camera/CameraGestureHelper.kt
index 11c7a31..ecbd3f9 100644
--- a/packages/SystemUI/src/com/android/systemui/camera/CameraGestureHelper.kt
+++ b/packages/SystemUI/src/com/android/systemui/camera/CameraGestureHelper.kt
@@ -69,7 +69,7 @@
}
val resolveInfo: ResolveInfo? = packageManager.resolveActivityAsUser(
- getStartCameraIntent(),
+ getStartCameraIntent(selectedUserInteractor.getSelectedUserId()),
PackageManager.MATCH_DEFAULT_ONLY,
selectedUserInteractor.getSelectedUserId()
)
@@ -85,7 +85,7 @@
* @param source The source of the camera launch, to be passed to the camera app via [Intent]
*/
fun launchCamera(source: Int) {
- val intent: Intent = getStartCameraIntent()
+ val intent: Intent = getStartCameraIntent(selectedUserInteractor.getSelectedUserId())
intent.putExtra(CameraIntents.EXTRA_LAUNCH_SOURCE, source)
val wouldLaunchResolverActivity = activityIntentHelper.wouldLaunchResolverActivity(
intent, selectedUserInteractor.getSelectedUserId()
@@ -143,13 +143,13 @@
* Returns an [Intent] that can be used to start the camera app such that it occludes the
* lock-screen, if needed.
*/
- private fun getStartCameraIntent(): Intent {
+ private fun getStartCameraIntent(userId: Int): Intent {
val isLockScreenDismissible = keyguardStateController.canDismissLockScreen()
val isSecure = keyguardStateController.isMethodSecure
return if (isSecure && !isLockScreenDismissible) {
- cameraIntents.getSecureCameraIntent()
+ cameraIntents.getSecureCameraIntent(userId)
} else {
- cameraIntents.getInsecureCameraIntent()
+ cameraIntents.getInsecureCameraIntent(userId)
}
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/camera/CameraIntents.kt b/packages/SystemUI/src/com/android/systemui/camera/CameraIntents.kt
index 1e17059..1137586 100644
--- a/packages/SystemUI/src/com/android/systemui/camera/CameraIntents.kt
+++ b/packages/SystemUI/src/com/android/systemui/camera/CameraIntents.kt
@@ -18,9 +18,11 @@
import android.content.Context
import android.content.Intent
+import android.content.pm.PackageManager
import android.provider.MediaStore
import android.text.TextUtils
import com.android.systemui.res.R
+import android.util.Log
class CameraIntents {
companion object {
@@ -28,28 +30,33 @@
val DEFAULT_INSECURE_CAMERA_INTENT_ACTION = MediaStore.INTENT_ACTION_STILL_IMAGE_CAMERA
private val VIDEO_CAMERA_INTENT_ACTION = MediaStore.INTENT_ACTION_VIDEO_CAMERA
const val EXTRA_LAUNCH_SOURCE = "com.android.systemui.camera_launch_source"
+ const val TAG = "CameraIntents"
@JvmStatic
- fun getOverrideCameraPackage(context: Context): String? {
- context.resources.getString(R.string.config_cameraGesturePackage)?.let {
- if (!TextUtils.isEmpty(it)) {
- return it
+ fun getOverrideCameraPackage(context: Context, userId: Int): String? {
+ val packageName = context.resources.getString(R.string.config_cameraGesturePackage)!!
+ try {
+ if (!TextUtils.isEmpty(packageName)
+ && context.packageManager.getApplicationInfoAsUser(packageName, 0, userId).enabled ?: false) {
+ return packageName
}
+ } catch (e: PackageManager.NameNotFoundException) {
+ Log.w(TAG, "Missing cameraGesturePackage $packageName", e)
}
return null
}
@JvmStatic
- fun getInsecureCameraIntent(context: Context): Intent {
+ fun getInsecureCameraIntent(context: Context, userId: Int): Intent {
val intent = Intent(DEFAULT_INSECURE_CAMERA_INTENT_ACTION)
- getOverrideCameraPackage(context)?.let { intent.setPackage(it) }
+ getOverrideCameraPackage(context, userId)?.let { intent.setPackage(it) }
return intent
}
@JvmStatic
- fun getSecureCameraIntent(context: Context): Intent {
+ fun getSecureCameraIntent(context: Context, userId: Int): Intent {
val intent = Intent(DEFAULT_SECURE_CAMERA_INTENT_ACTION)
- getOverrideCameraPackage(context)?.let { intent.setPackage(it) }
+ getOverrideCameraPackage(context, userId)?.let { intent.setPackage(it) }
return intent.addFlags(Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS)
}
@@ -65,7 +72,7 @@
/** Returns an [Intent] that can be used to start the camera in video mode. */
@JvmStatic
- fun getVideoCameraIntent(): Intent {
+ fun getVideoCameraIntent(userId: Int): Intent {
return Intent(VIDEO_CAMERA_INTENT_ACTION)
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/camera/CameraIntentsWrapper.kt b/packages/SystemUI/src/com/android/systemui/camera/CameraIntentsWrapper.kt
index a434617..b65c0d4 100644
--- a/packages/SystemUI/src/com/android/systemui/camera/CameraIntentsWrapper.kt
+++ b/packages/SystemUI/src/com/android/systemui/camera/CameraIntentsWrapper.kt
@@ -29,22 +29,22 @@
/**
* Returns an [Intent] that can be used to start the camera, suitable for when the device is
- * already unlocked
+ * locked
*/
- fun getSecureCameraIntent(): Intent {
- return CameraIntents.getSecureCameraIntent(context)
+ fun getSecureCameraIntent(userId: Int): Intent {
+ return CameraIntents.getSecureCameraIntent(context, userId)
}
/**
* Returns an [Intent] that can be used to start the camera, suitable for when the device is not
* already unlocked
*/
- fun getInsecureCameraIntent(): Intent {
- return CameraIntents.getInsecureCameraIntent(context)
+ fun getInsecureCameraIntent(userId: Int): Intent {
+ return CameraIntents.getInsecureCameraIntent(context, userId)
}
/** Returns an [Intent] that can be used to start the camera in video mode. */
- fun getVideoCameraIntent(): Intent {
- return CameraIntents.getVideoCameraIntent()
+ fun getVideoCameraIntent(userId: Int): Intent {
+ return CameraIntents.getVideoCameraIntent(userId)
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/common/data/repository/PackageChangeRepository.kt b/packages/SystemUI/src/com/android/systemui/common/data/repository/PackageChangeRepository.kt
index 7c7b3db..5c64dc6 100644
--- a/packages/SystemUI/src/com/android/systemui/common/data/repository/PackageChangeRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/common/data/repository/PackageChangeRepository.kt
@@ -17,7 +17,7 @@
package com.android.systemui.common.data.repository
import android.os.UserHandle
-import com.android.systemui.common.data.shared.model.PackageChangeModel
+import com.android.systemui.common.shared.model.PackageChangeModel
import kotlinx.coroutines.flow.Flow
interface PackageChangeRepository {
diff --git a/packages/SystemUI/src/com/android/systemui/common/data/repository/PackageChangeRepositoryImpl.kt b/packages/SystemUI/src/com/android/systemui/common/data/repository/PackageChangeRepositoryImpl.kt
index b1b348c..712a352 100644
--- a/packages/SystemUI/src/com/android/systemui/common/data/repository/PackageChangeRepositoryImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/common/data/repository/PackageChangeRepositoryImpl.kt
@@ -17,7 +17,7 @@
package com.android.systemui.common.data.repository
import android.os.UserHandle
-import com.android.systemui.common.data.shared.model.PackageChangeModel
+import com.android.systemui.common.shared.model.PackageChangeModel
import com.android.systemui.dagger.SysUISingleton
import javax.inject.Inject
import kotlinx.coroutines.flow.Flow
@@ -36,7 +36,5 @@
private val monitor by lazy { monitorFactory.create(UserHandle.ALL) }
override fun packageChanged(user: UserHandle): Flow<PackageChangeModel> =
- monitor.packageChanged.filter {
- user == UserHandle.ALL || user == UserHandle.getUserHandleForUid(it.packageUid)
- }
+ monitor.packageChanged.filter { user == UserHandle.ALL || user == it.user }
}
diff --git a/packages/SystemUI/src/com/android/systemui/common/data/repository/PackageUpdateLogger.kt b/packages/SystemUI/src/com/android/systemui/common/data/repository/PackageUpdateLogger.kt
index 4184ef7..cba2b47 100644
--- a/packages/SystemUI/src/com/android/systemui/common/data/repository/PackageUpdateLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/common/data/repository/PackageUpdateLogger.kt
@@ -17,7 +17,7 @@
package com.android.systemui.common.data.repository
import android.os.UserHandle
-import com.android.systemui.common.data.shared.model.PackageChangeModel
+import com.android.systemui.common.shared.model.PackageChangeModel
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.log.LogBuffer
import com.android.systemui.log.core.LogLevel
diff --git a/packages/SystemUI/src/com/android/systemui/common/data/repository/PackageUpdateMonitor.kt b/packages/SystemUI/src/com/android/systemui/common/data/repository/PackageUpdateMonitor.kt
index f7cc344..3fed69e 100644
--- a/packages/SystemUI/src/com/android/systemui/common/data/repository/PackageUpdateMonitor.kt
+++ b/packages/SystemUI/src/com/android/systemui/common/data/repository/PackageUpdateMonitor.kt
@@ -20,9 +20,10 @@
import android.os.Handler
import android.os.UserHandle
import com.android.internal.content.PackageMonitor
-import com.android.systemui.common.data.shared.model.PackageChangeModel
+import com.android.systemui.common.shared.model.PackageChangeModel
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.util.time.SystemClock
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
@@ -51,6 +52,7 @@
@Application private val context: Context,
@Application private val scope: CoroutineScope,
private val logger: PackageUpdateLogger,
+ private val systemClock: SystemClock,
) : PackageMonitor() {
@AssistedFactory
@@ -88,12 +90,24 @@
override fun onPackageAdded(packageName: String, uid: Int) {
super.onPackageAdded(packageName, uid)
- _packageChanged.tryEmit(PackageChangeModel.Installed(packageName, uid))
+ _packageChanged.tryEmit(
+ PackageChangeModel.Installed(
+ packageName = packageName,
+ packageUid = uid,
+ timeMillis = systemClock.currentTimeMillis()
+ )
+ )
}
override fun onPackageRemoved(packageName: String, uid: Int) {
super.onPackageRemoved(packageName, uid)
- _packageChanged.tryEmit(PackageChangeModel.Uninstalled(packageName, uid))
+ _packageChanged.tryEmit(
+ PackageChangeModel.Uninstalled(
+ packageName = packageName,
+ packageUid = uid,
+ timeMillis = systemClock.currentTimeMillis()
+ )
+ )
}
override fun onPackageChanged(
@@ -102,18 +116,36 @@
components: Array<out String>
): Boolean {
super.onPackageChanged(packageName, uid, components)
- _packageChanged.tryEmit(PackageChangeModel.Changed(packageName, uid))
+ _packageChanged.tryEmit(
+ PackageChangeModel.Changed(
+ packageName = packageName,
+ packageUid = uid,
+ timeMillis = systemClock.currentTimeMillis()
+ )
+ )
return false
}
override fun onPackageUpdateStarted(packageName: String, uid: Int) {
super.onPackageUpdateStarted(packageName, uid)
- _packageChanged.tryEmit(PackageChangeModel.UpdateStarted(packageName, uid))
+ _packageChanged.tryEmit(
+ PackageChangeModel.UpdateStarted(
+ packageName = packageName,
+ packageUid = uid,
+ timeMillis = systemClock.currentTimeMillis()
+ )
+ )
}
override fun onPackageUpdateFinished(packageName: String, uid: Int) {
super.onPackageUpdateFinished(packageName, uid)
- _packageChanged.tryEmit(PackageChangeModel.UpdateFinished(packageName, uid))
+ _packageChanged.tryEmit(
+ PackageChangeModel.UpdateFinished(
+ packageName = packageName,
+ packageUid = uid,
+ timeMillis = systemClock.currentTimeMillis()
+ )
+ )
}
private companion object {
diff --git a/packages/SystemUI/src/com/android/systemui/common/data/shared/model/PackageChangeModel.kt b/packages/SystemUI/src/com/android/systemui/common/data/shared/model/PackageChangeModel.kt
deleted file mode 100644
index 3ae87c3..0000000
--- a/packages/SystemUI/src/com/android/systemui/common/data/shared/model/PackageChangeModel.kt
+++ /dev/null
@@ -1,79 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.common.data.shared.model
-
-import android.content.Intent
-
-/** Represents changes to an installed package. */
-sealed interface PackageChangeModel {
- val packageName: String
- val packageUid: Int
-
- /** Empty change, provided for convenience when a sensible default value is needed. */
- data object Empty : PackageChangeModel {
- override val packageName: String
- get() = ""
- override val packageUid: Int
- get() = 0
- }
-
- /**
- * An existing application package was uninstalled.
- *
- * Equivalent to receiving the [Intent.ACTION_PACKAGE_REMOVED] broadcast with
- * [Intent.EXTRA_REPLACING] set to false.
- */
- data class Uninstalled(override val packageName: String, override val packageUid: Int) :
- PackageChangeModel
-
- /**
- * A new version of an existing application is going to be installed.
- *
- * Equivalent to receiving the [Intent.ACTION_PACKAGE_REMOVED] broadcast with
- * [Intent.EXTRA_REPLACING] set to true.
- */
- data class UpdateStarted(override val packageName: String, override val packageUid: Int) :
- PackageChangeModel
-
- /**
- * A new version of an existing application package has been installed, replacing the old
- * version.
- *
- * Equivalent to receiving the [Intent.ACTION_PACKAGE_ADDED] broadcast with
- * [Intent.EXTRA_REPLACING] set to true.
- */
- data class UpdateFinished(override val packageName: String, override val packageUid: Int) :
- PackageChangeModel
-
- /**
- * A new application package has been installed.
- *
- * Equivalent to receiving the [Intent.ACTION_PACKAGE_ADDED] broadcast with
- * [Intent.EXTRA_REPLACING] set to false.
- */
- data class Installed(override val packageName: String, override val packageUid: Int) :
- PackageChangeModel
-
- /**
- * An existing application package has been changed (for example, a component has been enabled
- * or disabled).
- *
- * Equivalent to receiving the [Intent.ACTION_PACKAGE_CHANGED] broadcast.
- */
- data class Changed(override val packageName: String, override val packageUid: Int) :
- PackageChangeModel
-}
diff --git a/packages/SystemUI/src/com/android/systemui/common/domain/interactor/PackageChangeInteractor.kt b/packages/SystemUI/src/com/android/systemui/common/domain/interactor/PackageChangeInteractor.kt
new file mode 100644
index 0000000..59c1923
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/common/domain/interactor/PackageChangeInteractor.kt
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.common.domain.interactor
+
+import android.os.UserHandle
+import com.android.systemui.common.data.repository.PackageChangeRepository
+import com.android.systemui.common.shared.model.PackageChangeModel
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.user.domain.interactor.SelectedUserInteractor
+import javax.inject.Inject
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.filter
+import kotlinx.coroutines.flow.flatMapLatest
+
+/**
+ * Allows listening to package updates. This is recommended over registering broadcasts directly as
+ * it avoids the delay imposed by broadcasts, and provides more structured updates.
+ */
+@OptIn(ExperimentalCoroutinesApi::class)
+@SysUISingleton
+class PackageChangeInteractor
+@Inject
+constructor(
+ private val packageChangeRepository: PackageChangeRepository,
+ private val userInteractor: SelectedUserInteractor,
+) {
+ /**
+ * Emits values when packages for the specified user are changed. See supported modifications in
+ * [PackageChangeModel]
+ *
+ * @param user The user to listen to. [UserHandle.USER_ALL] may be used to listen to all users.
+ * [UserHandle.USER_CURRENT] can be used to listen to the currently active user, and
+ * automatically handles user switching.
+ * @param packageName An optional package name to filter updates by. If not specified, will
+ * receive updates for all packages.
+ */
+ fun packageChanged(user: UserHandle, packageName: String? = null): Flow<PackageChangeModel> {
+ if (user == UserHandle.CURRENT) {
+ return userInteractor.selectedUser.flatMapLatest { userId ->
+ packageChangedInternal(UserHandle.of(userId), packageName)
+ }
+ }
+ return packageChangedInternal(user, packageName)
+ }
+
+ private fun packageChangedInternal(
+ user: UserHandle,
+ packageName: String?,
+ ): Flow<PackageChangeModel> =
+ packageChangeRepository.packageChanged(user).filter { model ->
+ (model.packageName == (packageName ?: model.packageName))
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/common/shared/model/Color.kt b/packages/SystemUI/src/com/android/systemui/common/shared/model/Color.kt
new file mode 100644
index 0000000..d235c95
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/common/shared/model/Color.kt
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.common.shared.model
+
+import android.annotation.AttrRes
+import android.annotation.ColorInt
+
+/**
+ * Models a color that can be either a specific [Color.Loaded] value or a resolvable theme
+ * [Color.Attribute]
+ */
+sealed interface Color {
+
+ data class Loaded(@ColorInt val color: Int) : Color
+
+ data class Attribute(@AttrRes val attribute: Int) : Color
+}
diff --git a/packages/SystemUI/src/com/android/systemui/common/shared/model/PackageChangeModel.kt b/packages/SystemUI/src/com/android/systemui/common/shared/model/PackageChangeModel.kt
new file mode 100644
index 0000000..bcb00fd
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/common/shared/model/PackageChangeModel.kt
@@ -0,0 +1,121 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.common.shared.model
+
+import android.annotation.CurrentTimeMillisLong
+import android.content.Intent
+import android.content.pm.PackageManager
+import android.os.UserHandle
+
+/** Represents changes to an installed package. */
+sealed interface PackageChangeModel {
+ /** The package which this change corresponds to, eg "com.android.systemui". */
+ val packageName: String
+
+ /**
+ * The uid for this package, which uniquely identifies this package and user. The same package
+ * running on different users will have differing uids.
+ */
+ val packageUid: Int
+
+ /**
+ * The time in milliseconds that this update was received from [PackageManager].
+ *
+ * @see System.currentTimeMillis()
+ */
+ @get:CurrentTimeMillisLong val timeMillis: Long
+
+ /** The user from which this change originated. */
+ val user: UserHandle
+ get() = UserHandle.getUserHandleForUid(packageUid)
+
+ /** Empty change, provided for convenience when a sensible default value is needed. */
+ data object Empty : PackageChangeModel {
+ override val packageName: String
+ get() = ""
+
+ override val packageUid: Int
+ get() = 0
+
+ override val timeMillis: Long
+ get() = 0
+ }
+
+ /**
+ * An existing application package was uninstalled.
+ *
+ * Equivalent to receiving the [Intent.ACTION_PACKAGE_REMOVED] broadcast with
+ * [Intent.EXTRA_REPLACING] set to false.
+ */
+ data class Uninstalled(
+ override val packageName: String,
+ override val packageUid: Int,
+ @CurrentTimeMillisLong override val timeMillis: Long = 0,
+ ) : PackageChangeModel
+
+ /**
+ * A new version of an existing application is going to be installed.
+ *
+ * Equivalent to receiving the [Intent.ACTION_PACKAGE_REMOVED] broadcast with
+ * [Intent.EXTRA_REPLACING] set to true.
+ */
+ data class UpdateStarted(
+ override val packageName: String,
+ override val packageUid: Int,
+ @CurrentTimeMillisLong override val timeMillis: Long = 0,
+ ) : PackageChangeModel
+
+ /**
+ * A new version of an existing application package has been installed, replacing the old
+ * version.
+ *
+ * Equivalent to receiving the [Intent.ACTION_PACKAGE_ADDED] broadcast with
+ * [Intent.EXTRA_REPLACING] set to true.
+ */
+ data class UpdateFinished(
+ override val packageName: String,
+ override val packageUid: Int,
+ @CurrentTimeMillisLong override val timeMillis: Long = 0,
+ ) : PackageChangeModel
+
+ /**
+ * A new application package has been installed.
+ *
+ * Equivalent to receiving the [Intent.ACTION_PACKAGE_ADDED] broadcast with
+ * [Intent.EXTRA_REPLACING] set to false.
+ */
+ data class Installed(
+ override val packageName: String,
+ override val packageUid: Int,
+ @CurrentTimeMillisLong override val timeMillis: Long = 0,
+ ) : PackageChangeModel
+
+ /**
+ * An existing application package has been changed (for example, a component has been enabled
+ * or disabled).
+ *
+ * Equivalent to receiving the [Intent.ACTION_PACKAGE_CHANGED] broadcast.
+ */
+ data class Changed(
+ override val packageName: String,
+ override val packageUid: Int,
+ @CurrentTimeMillisLong override val timeMillis: Long = 0,
+ ) : PackageChangeModel
+
+ fun isSamePackage(other: PackageChangeModel) =
+ this.packageName == other.packageName && this.packageUid == other.packageUid
+}
diff --git a/packages/SystemUI/src/com/android/systemui/common/ui/ConfigurationState.kt b/packages/SystemUI/src/com/android/systemui/common/ui/ConfigurationState.kt
index 12be32c..964eb6f 100644
--- a/packages/SystemUI/src/com/android/systemui/common/ui/ConfigurationState.kt
+++ b/packages/SystemUI/src/com/android/systemui/common/ui/ConfigurationState.kt
@@ -24,17 +24,12 @@
import androidx.annotation.LayoutRes
import com.android.settingslib.Utils
import com.android.systemui.dagger.qualifiers.Application
-import com.android.systemui.lifecycle.repeatWhenAttached
import com.android.systemui.statusbar.policy.ConfigurationController
import com.android.systemui.statusbar.policy.onDensityOrFontScaleChanged
import com.android.systemui.statusbar.policy.onThemeChanged
import com.android.systemui.util.kotlin.emitOnStart
-import com.android.systemui.util.view.bindLatest
import javax.inject.Inject
-import kotlinx.coroutines.CoroutineDispatcher
-import kotlinx.coroutines.DisposableHandle
import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.merge
@@ -91,46 +86,3 @@
.map { layoutInflater.inflate(id, root, attachToRoot) as T }
}
}
-
-/**
- * Perform an inflation right away, then re-inflate whenever the device configuration changes, and
- * call [onInflate] on the resulting view each time. Disposes of the [DisposableHandle] returned by
- * [onInflate] when done.
- *
- * This never completes unless cancelled, it just suspends and waits for updates. It runs on a
- * background thread using [backgroundDispatcher].
- *
- * For parameters [resource], [root] and [attachToRoot], see [LayoutInflater.inflate].
- *
- * An example use-case of this is when a view needs to be re-inflated whenever a configuration
- * change occurs, which would require the ViewBinder to then re-bind the new view. For example, the
- * code in the parent view's binder would look like:
- * ```
- * parentView.repeatWhenAttached {
- * configurationState
- * .reinflateAndBindLatest(
- * R.layout.my_layout,
- * parentView,
- * attachToRoot = false,
- * coroutineScope = lifecycleScope,
- * configurationController.onThemeChanged,
- * ) { view: ChildView ->
- * ChildViewBinder.bind(view, childViewModel)
- * }
- * }
- * ```
- *
- * In turn, the bind method (passed through [onInflate]) uses [repeatWhenAttached], which returns a
- * [DisposableHandle].
- */
-suspend fun <T : View> ConfigurationState.reinflateAndBindLatest(
- @LayoutRes resource: Int,
- root: ViewGroup?,
- attachToRoot: Boolean,
- backgroundDispatcher: CoroutineDispatcher,
- onInflate: (T) -> DisposableHandle?,
-) {
- inflateLayout<T>(resource, root, attachToRoot)
- .flowOn(backgroundDispatcher)
- .bindLatest(onInflate)
-}
diff --git a/packages/SystemUI/src/com/android/systemui/communal/dagger/CommunalModule.kt b/packages/SystemUI/src/com/android/systemui/communal/dagger/CommunalModule.kt
index dc07c1b..0bad33b 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/dagger/CommunalModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/dagger/CommunalModule.kt
@@ -20,6 +20,7 @@
import com.android.systemui.communal.data.repository.CommunalMediaRepositoryModule
import com.android.systemui.communal.data.repository.CommunalPrefsRepositoryModule
import com.android.systemui.communal.data.repository.CommunalRepositoryModule
+import com.android.systemui.communal.data.repository.CommunalSettingsRepositoryModule
import com.android.systemui.communal.data.repository.CommunalTutorialRepositoryModule
import com.android.systemui.communal.data.repository.CommunalWidgetRepositoryModule
import com.android.systemui.communal.widgets.EditWidgetsActivityStarter
@@ -36,6 +37,7 @@
CommunalWidgetRepositoryModule::class,
CommunalDatabaseModule::class,
CommunalPrefsRepositoryModule::class,
+ CommunalSettingsRepositoryModule::class,
]
)
interface CommunalModule {
diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/model/CommunalEnabledState.kt b/packages/SystemUI/src/com/android/systemui/communal/data/model/CommunalEnabledState.kt
new file mode 100644
index 0000000..83a5bdb
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/communal/data/model/CommunalEnabledState.kt
@@ -0,0 +1,69 @@
+/*
+ * Copyright (C) 2024 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.communal.data.model
+
+import com.android.systemui.log.table.Diffable
+import com.android.systemui.log.table.TableRowLogger
+import java.util.EnumSet
+
+/** Reasons that communal is disabled, primarily for logging. */
+enum class DisabledReason(val loggingString: String) {
+ /** Communal should be disabled due to invalid current user */
+ DISABLED_REASON_INVALID_USER("invalidUser"),
+ /** Communal should be disabled due to the flag being off */
+ DISABLED_REASON_FLAG("flag"),
+ /** Communal should be disabled because the user has turned off the setting */
+ DISABLED_REASON_USER_SETTING("userSetting"),
+ /** Communal is disabled by the device policy app */
+ DISABLED_REASON_DEVICE_POLICY("devicePolicy"),
+}
+
+/**
+ * Model representing the reasons communal hub should be disabled. Allows logging reasons separately
+ * for debugging.
+ */
+@JvmInline
+value class CommunalEnabledState(
+ private val disabledReasons: EnumSet<DisabledReason> =
+ EnumSet.noneOf(DisabledReason::class.java)
+) : Diffable<CommunalEnabledState>, Set<DisabledReason> by disabledReasons {
+
+ /** Creates [CommunalEnabledState] with a single reason for being disabled */
+ constructor(reason: DisabledReason) : this(EnumSet.of(reason))
+
+ /** Checks if there are any reasons communal should be disabled. If none, returns true. */
+ val enabled: Boolean
+ get() = isEmpty()
+
+ override fun logDiffs(prevVal: CommunalEnabledState, row: TableRowLogger) {
+ for (reason in DisabledReason.entries) {
+ val newVal = contains(reason)
+ if (newVal != prevVal.contains(reason)) {
+ row.logChange(
+ columnName = reason.loggingString,
+ value = newVal,
+ )
+ }
+ }
+ }
+
+ override fun logFull(row: TableRowLogger) {
+ for (reason in DisabledReason.entries) {
+ row.logChange(columnName = reason.loggingString, value = contains(reason))
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/model/CommunalMediaModel.kt b/packages/SystemUI/src/com/android/systemui/communal/data/model/CommunalMediaModel.kt
index c46f0d1..33edb80 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/data/model/CommunalMediaModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/data/model/CommunalMediaModel.kt
@@ -21,21 +21,21 @@
/** Data model of media on the communal hub. */
data class CommunalMediaModel(
- val hasAnyMediaOrRecommendation: Boolean,
+ val hasActiveMediaOrRecommendation: Boolean,
val createdTimestampMillis: Long = 0L,
) : Diffable<CommunalMediaModel> {
companion object {
val INACTIVE =
CommunalMediaModel(
- hasAnyMediaOrRecommendation = false,
+ hasActiveMediaOrRecommendation = false,
)
}
override fun logDiffs(prevVal: CommunalMediaModel, row: TableRowLogger) {
- if (hasAnyMediaOrRecommendation != prevVal.hasAnyMediaOrRecommendation) {
+ if (hasActiveMediaOrRecommendation != prevVal.hasActiveMediaOrRecommendation) {
row.logChange(
columnName = "isMediaActive",
- value = hasAnyMediaOrRecommendation,
+ value = hasActiveMediaOrRecommendation,
)
}
diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalMediaRepository.kt b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalMediaRepository.kt
index 2b66491..201be51 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalMediaRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalMediaRepository.kt
@@ -73,10 +73,10 @@
)
private fun updateMediaModel(data: MediaData? = null) {
- if (mediaDataManager.hasAnyMediaOrRecommendation()) {
+ if (mediaDataManager.hasActiveMediaOrRecommendation()) {
_mediaModel.value =
CommunalMediaModel(
- hasAnyMediaOrRecommendation = true,
+ hasActiveMediaOrRecommendation = true,
createdTimestampMillis = data?.createdTimestampMillis ?: 0L,
)
} else {
diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalPrefsRepository.kt b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalPrefsRepository.kt
index 85aeb4d..0e9b32f 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalPrefsRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalPrefsRepository.kt
@@ -28,8 +28,8 @@
import com.android.systemui.log.table.TableLogBuffer
import com.android.systemui.log.table.logDiffsForTable
import com.android.systemui.settings.UserFileManager
-import com.android.systemui.settings.UserFileManagerExt.observeSharedPreferences
import com.android.systemui.user.data.repository.UserRepository
+import com.android.systemui.util.kotlin.SharedPreferencesExt.observe
import javax.inject.Inject
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
@@ -97,8 +97,8 @@
}
private fun observeCtaDismissState(user: UserInfo): Flow<Boolean> =
- userFileManager
- .observeSharedPreferences(FILE_NAME, Context.MODE_PRIVATE, user.id)
+ getSharedPrefsForUser(user)
+ .observe(CTA_DISMISSED_STATE)
// Emit at the start of collection to ensure we get an initial value
.onStart { emit(Unit) }
.map { getCtaDismissedState() }
diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalRepository.kt b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalRepository.kt
index addd880..4a06585f5 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalRepository.kt
@@ -16,22 +16,14 @@
package com.android.systemui.communal.data.repository
-import com.android.systemui.Flags.communalHub
import com.android.systemui.communal.shared.model.CommunalSceneKey
import com.android.systemui.communal.shared.model.ObservableCommunalTransitionState
import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.dagger.qualifiers.Background
-import com.android.systemui.flags.FeatureFlagsClassic
-import com.android.systemui.flags.Flags
import com.android.systemui.scene.data.repository.SceneContainerRepository
import com.android.systemui.scene.shared.flag.SceneContainerFlags
import com.android.systemui.scene.shared.model.SceneKey
-import com.android.systemui.user.data.repository.UserRepository
-import com.android.systemui.util.settings.SecureSettings
-import com.android.systemui.util.settings.SettingsProxyExt.observerFlow
import javax.inject.Inject
-import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
@@ -39,26 +31,13 @@
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
-import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.flow.onStart
-import kotlinx.coroutines.flow.shareIn
import kotlinx.coroutines.flow.stateIn
-import kotlinx.coroutines.withContext
/** Encapsulates the state of communal mode. */
interface CommunalRepository {
- /** Whether communal features are enabled. */
- val isCommunalEnabled: Boolean
-
- /**
- * A {@link StateFlow} that tracks whether communal hub is enabled (it can be disabled in
- * settings).
- */
- val communalEnabledState: StateFlow<Boolean>
-
/** Whether the communal hub is showing. */
val isCommunalHubShowing: Flow<Boolean>
@@ -87,37 +66,11 @@
class CommunalRepositoryImpl
@Inject
constructor(
- @Application private val applicationScope: CoroutineScope,
@Background backgroundScope: CoroutineScope,
- @Background private val backgroundDispatcher: CoroutineDispatcher,
- private val featureFlagsClassic: FeatureFlagsClassic,
sceneContainerFlags: SceneContainerFlags,
sceneContainerRepository: SceneContainerRepository,
- userRepository: UserRepository,
- private val secureSettings: SecureSettings
) : CommunalRepository {
- private val communalEnabledSettingState: Flow<Boolean> =
- userRepository.selectedUserInfo
- .flatMapLatest { userInfo -> observeSettings(userInfo.id) }
- .shareIn(scope = applicationScope, started = SharingStarted.WhileSubscribed())
-
- override val communalEnabledState: StateFlow<Boolean> =
- if (featureFlagsClassic.isEnabled(Flags.COMMUNAL_SERVICE_ENABLED) && communalHub()) {
- communalEnabledSettingState
- .filterNotNull()
- .stateIn(
- scope = applicationScope,
- started = SharingStarted.Eagerly,
- initialValue = true
- )
- } else {
- MutableStateFlow(false)
- }
-
- override val isCommunalEnabled: Boolean
- get() = communalEnabledState.value
-
private val _desiredScene: MutableStateFlow<CommunalSceneKey> =
MutableStateFlow(CommunalSceneKey.DEFAULT)
override val desiredScene: StateFlow<CommunalSceneKey> = _desiredScene.asStateFlow()
@@ -153,26 +106,4 @@
} else {
desiredScene.map { sceneKey -> sceneKey == CommunalSceneKey.Communal }
}
-
- private fun observeSettings(userId: Int): Flow<Boolean> =
- secureSettings
- .observerFlow(
- userId = userId,
- names =
- arrayOf(
- GLANCEABLE_HUB_ENABLED,
- )
- )
- // Force an update
- .onStart { emit(Unit) }
- .map { readFromSettings(userId) }
-
- private suspend fun readFromSettings(userId: Int): Boolean =
- withContext(backgroundDispatcher) {
- secureSettings.getIntForUser(GLANCEABLE_HUB_ENABLED, 1, userId) == 1
- }
-
- companion object {
- private const val GLANCEABLE_HUB_ENABLED = "glanceable_hub_enabled"
- }
}
diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalSettingsRepository.kt b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalSettingsRepository.kt
new file mode 100644
index 0000000..201b049
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalSettingsRepository.kt
@@ -0,0 +1,126 @@
+/*
+ * Copyright (C) 2024 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.communal.data.repository
+
+import android.app.admin.DevicePolicyManager
+import android.app.admin.DevicePolicyManager.KEYGUARD_DISABLE_WIDGETS_ALL
+import android.content.IntentFilter
+import android.content.pm.UserInfo
+import com.android.systemui.Flags.communalHub
+import com.android.systemui.broadcast.BroadcastDispatcher
+import com.android.systemui.communal.data.model.CommunalEnabledState
+import com.android.systemui.communal.data.model.DisabledReason
+import com.android.systemui.communal.data.model.DisabledReason.DISABLED_REASON_DEVICE_POLICY
+import com.android.systemui.communal.data.model.DisabledReason.DISABLED_REASON_FLAG
+import com.android.systemui.communal.data.model.DisabledReason.DISABLED_REASON_INVALID_USER
+import com.android.systemui.communal.data.model.DisabledReason.DISABLED_REASON_USER_SETTING
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.flags.FeatureFlagsClassic
+import com.android.systemui.flags.Flags
+import com.android.systemui.util.kotlin.emitOnStart
+import com.android.systemui.util.settings.SecureSettings
+import com.android.systemui.util.settings.SettingsProxyExt.observerFlow
+import java.util.EnumSet
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.onStart
+
+interface CommunalSettingsRepository {
+ /** A [CommunalEnabledState] for the specified user. */
+ fun getEnabledState(user: UserInfo): Flow<CommunalEnabledState>
+}
+
+@SysUISingleton
+class CommunalSettingsRepositoryImpl
+@Inject
+constructor(
+ @Background private val bgDispatcher: CoroutineDispatcher,
+ private val featureFlagsClassic: FeatureFlagsClassic,
+ private val secureSettings: SecureSettings,
+ private val broadcastDispatcher: BroadcastDispatcher,
+ private val devicePolicyManager: DevicePolicyManager,
+) : CommunalSettingsRepository {
+
+ private val flagEnabled: Boolean by lazy {
+ featureFlagsClassic.isEnabled(Flags.COMMUNAL_SERVICE_ENABLED) && communalHub()
+ }
+
+ override fun getEnabledState(user: UserInfo): Flow<CommunalEnabledState> {
+ if (!user.isMain) {
+ return flowOf(CommunalEnabledState(DISABLED_REASON_INVALID_USER))
+ }
+ if (!flagEnabled) {
+ return flowOf(CommunalEnabledState(DISABLED_REASON_FLAG))
+ }
+ return combine(
+ getEnabledByUser(user).mapToReason(DISABLED_REASON_USER_SETTING),
+ getAllowedByDevicePolicy(user).mapToReason(DISABLED_REASON_DEVICE_POLICY),
+ ) { reasons ->
+ reasons.filterNotNull()
+ }
+ .map { reasons ->
+ if (reasons.isEmpty()) {
+ EnumSet.noneOf(DisabledReason::class.java)
+ } else {
+ EnumSet.copyOf(reasons)
+ }
+ }
+ .map { reasons -> CommunalEnabledState(reasons) }
+ .flowOn(bgDispatcher)
+ }
+
+ private fun getEnabledByUser(user: UserInfo): Flow<Boolean> =
+ secureSettings
+ .observerFlow(userId = user.id, names = arrayOf(GLANCEABLE_HUB_ENABLED))
+ // Force an update
+ .onStart { emit(Unit) }
+ .map {
+ secureSettings.getIntForUser(
+ GLANCEABLE_HUB_ENABLED,
+ ENABLED_SETTING_DEFAULT,
+ user.id,
+ ) == 1
+ }
+
+ private fun getAllowedByDevicePolicy(user: UserInfo): Flow<Boolean> =
+ broadcastDispatcher
+ .broadcastFlow(
+ filter =
+ IntentFilter(DevicePolicyManager.ACTION_DEVICE_POLICY_MANAGER_STATE_CHANGED),
+ user = user.userHandle
+ )
+ .emitOnStart()
+ .map { devicePolicyManager.areKeyguardWidgetsAllowed(user.id) }
+
+ companion object {
+ const val GLANCEABLE_HUB_ENABLED = "glanceable_hub_enabled"
+ private const val ENABLED_SETTING_DEFAULT = 1
+ }
+}
+
+private fun DevicePolicyManager.areKeyguardWidgetsAllowed(userId: Int): Boolean =
+ (getKeyguardDisabledFeatures(null, userId) and KEYGUARD_DISABLE_WIDGETS_ALL) == 0
+
+private fun Flow<Boolean>.mapToReason(reason: DisabledReason) = map { enabled ->
+ if (enabled) null else reason
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/animation/ActivityLaunchAnimatorKosmos.kt b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalSettingsRepositoryModule.kt
similarity index 63%
copy from packages/SystemUI/tests/utils/src/com/android/systemui/animation/ActivityLaunchAnimatorKosmos.kt
copy to packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalSettingsRepositoryModule.kt
index 128f58b..a931d3f 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/animation/ActivityLaunchAnimatorKosmos.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalSettingsRepositoryModule.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2023 The Android Open Source Project
+ * Copyright (C) 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,8 +14,13 @@
* limitations under the License.
*/
-package com.android.systemui.animation
+package com.android.systemui.communal.data.repository
-import com.android.systemui.kosmos.Kosmos
+import dagger.Binds
+import dagger.Module
-val Kosmos.activityLaunchAnimator by Kosmos.Fixture { ActivityLaunchAnimator() }
+@Module
+interface CommunalSettingsRepositoryModule {
+ @Binds
+ fun communalSettingsRepository(impl: CommunalSettingsRepositoryImpl): CommunalSettingsRepository
+}
diff --git a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt
index 75a27a2..23f590e 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt
@@ -42,7 +42,6 @@
import com.android.systemui.log.table.TableLogBuffer
import com.android.systemui.log.table.logDiffsForTable
import com.android.systemui.smartspace.data.repository.SmartspaceRepository
-import com.android.systemui.user.data.repository.UserRepository
import com.android.systemui.util.kotlin.BooleanFlowOperators.and
import com.android.systemui.util.kotlin.BooleanFlowOperators.not
import com.android.systemui.util.kotlin.BooleanFlowOperators.or
@@ -74,8 +73,8 @@
private val communalPrefsRepository: CommunalPrefsRepository,
mediaRepository: CommunalMediaRepository,
smartspaceRepository: SmartspaceRepository,
- userRepository: UserRepository,
keyguardInteractor: KeyguardInteractor,
+ private val communalSettingsInteractor: CommunalSettingsInteractor,
private val appWidgetHost: CommunalAppWidgetHost,
private val editWidgetsActivityStarter: EditWidgetsActivityStarter,
@CommunalLog logBuffer: LogBuffer,
@@ -90,13 +89,12 @@
/** Whether communal features are enabled. */
val isCommunalEnabled: Boolean
- get() = communalRepository.isCommunalEnabled
+ get() = communalSettingsInteractor.isCommunalEnabled.value
/** Whether communal features are enabled and available. */
val isCommunalAvailable: Flow<Boolean> =
and(
- communalRepository.communalEnabledState,
- userRepository.selectedUserInfo.map { it.isMain },
+ communalSettingsInteractor.isCommunalEnabled,
not(keyguardInteractor.isEncryptedOrLockdown),
or(keyguardInteractor.isKeyguardVisible, keyguardInteractor.isDreaming)
)
@@ -318,7 +316,7 @@
)
// Add UMO
- if (media.hasAnyMediaOrRecommendation) {
+ if (media.hasActiveMediaOrRecommendation) {
ongoingContent.add(
CommunalContentModel.Umo(
createdTimestampMillis = media.createdTimestampMillis,
diff --git a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalSettingsInteractor.kt b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalSettingsInteractor.kt
new file mode 100644
index 0000000..0b096ce
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalSettingsInteractor.kt
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2024 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.communal.domain.interactor
+
+import com.android.systemui.communal.data.model.CommunalEnabledState
+import com.android.systemui.communal.data.repository.CommunalSettingsRepository
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.log.dagger.CommunalTableLog
+import com.android.systemui.log.table.TableLogBuffer
+import com.android.systemui.log.table.logDiffsForTable
+import com.android.systemui.user.domain.interactor.SelectedUserInteractor
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.stateIn
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SysUISingleton
+class CommunalSettingsInteractor
+@Inject
+constructor(
+ @Background private val bgScope: CoroutineScope,
+ private val repository: CommunalSettingsRepository,
+ userInteractor: SelectedUserInteractor,
+ @CommunalTableLog tableLogBuffer: TableLogBuffer,
+) {
+ /** Whether or not communal is enabled for the currently selected user. */
+ val isCommunalEnabled: StateFlow<Boolean> =
+ userInteractor.selectedUserInfo
+ .flatMapLatest { user -> repository.getEnabledState(user) }
+ .logDiffsForTable(
+ tableLogBuffer = tableLogBuffer,
+ columnPrefix = "disabledReason",
+ initialValue = CommunalEnabledState()
+ )
+ .map { model -> model.enabled }
+ // Start this eagerly since the value is accessed synchronously in many places.
+ .stateIn(scope = bgScope, started = SharingStarted.Eagerly, initialValue = false)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalTutorialInteractor.kt b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalTutorialInteractor.kt
index 1404ee2..25dfc02 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalTutorialInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalTutorialInteractor.kt
@@ -28,17 +28,18 @@
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.flow.transformWhile
import kotlinx.coroutines.launch
/** Encapsulates business-logic related to communal tutorial state. */
@@ -51,6 +52,7 @@
private val communalTutorialRepository: CommunalTutorialRepository,
keyguardInteractor: KeyguardInteractor,
private val communalRepository: CommunalRepository,
+ private val communalSettingsInteractor: CommunalSettingsInteractor,
communalInteractor: CommunalInteractor,
@CommunalTableLog tableLogBuffer: TableLogBuffer,
) {
@@ -110,20 +112,24 @@
return null
}
- private var job: Job? = null
private fun listenForTransitionToUpdateTutorialState() {
- if (!communalRepository.isCommunalEnabled) {
- return
- }
- job =
- scope.launch {
- tutorialStateToUpdate.collect {
- communalTutorialRepository.setTutorialState(it)
- if (it == Settings.Secure.HUB_MODE_TUTORIAL_COMPLETED) {
- job?.cancel()
+ scope.launch {
+ communalSettingsInteractor.isCommunalEnabled
+ .flatMapLatest { enabled ->
+ if (!enabled) {
+ emptyFlow()
+ } else {
+ tutorialStateToUpdate
}
}
- }
+ .transformWhile { tutorialState ->
+ emit(tutorialState)
+ tutorialState != Settings.Secure.HUB_MODE_TUTORIAL_COMPLETED
+ }
+ .collect { tutorialState ->
+ communalTutorialRepository.setTutorialState(tutorialState)
+ }
+ }
}
init {
diff --git a/packages/SystemUI/src/com/android/systemui/communal/smartspace/CommunalSmartspaceController.kt b/packages/SystemUI/src/com/android/systemui/communal/smartspace/CommunalSmartspaceController.kt
index c5dac77..80db535 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/smartspace/CommunalSmartspaceController.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/smartspace/CommunalSmartspaceController.kt
@@ -19,14 +19,12 @@
import android.app.smartspace.SmartspaceConfig
import android.app.smartspace.SmartspaceManager
import android.app.smartspace.SmartspaceSession
-import android.app.smartspace.SmartspaceTarget
import android.content.Context
import android.util.Log
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.plugins.BcSmartspaceDataPlugin
import com.android.systemui.plugins.BcSmartspaceDataPlugin.SmartspaceTargetListener
-import com.android.systemui.plugins.BcSmartspaceDataPlugin.SmartspaceView
import com.android.systemui.plugins.BcSmartspaceDataPlugin.UI_SURFACE_GLANCEABLE_HUB
import com.android.systemui.smartspace.SmartspacePrecondition
import com.android.systemui.smartspace.SmartspaceTargetFilter
@@ -64,11 +62,6 @@
// A shadow copy of listeners is maintained to track whether the session should remain open.
private var listeners = mutableSetOf<SmartspaceTargetListener>()
- private var unfilteredListeners = mutableSetOf<SmartspaceTargetListener>()
-
- // Smartspace can be used on multiple displays, such as when the user casts their screen
- private var smartspaceViews = mutableSetOf<SmartspaceView>()
-
var preconditionListener =
object : SmartspacePrecondition.Listener {
override fun onCriteriaChanged() {
@@ -101,9 +94,7 @@
}
private fun hasActiveSessionListeners(): Boolean {
- return smartspaceViews.isNotEmpty() ||
- listeners.isNotEmpty() ||
- unfilteredListeners.isNotEmpty()
+ return listeners.isNotEmpty()
}
private fun connectSession() {
@@ -188,8 +179,4 @@
private fun reloadSmartspace() {
session?.requestSmartspaceUpdate()
}
-
- private fun onTargetsAvailableUnfiltered(targets: List<SmartspaceTarget>) {
- unfilteredListeners.forEach { it.onSmartspaceTargetsUpdated(targets) }
- }
}
diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt
index 0c12841..40d2d16 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt
@@ -87,7 +87,7 @@
with(mediaHost) {
expansion = MediaHostState.EXPANDED
expandedMatchesParentHeight = true
- showsOnlyActiveMedia = false
+ showsOnlyActiveMedia = true
falsingProtectionNeeded = false
init(MediaHierarchyManager.LOCATION_COMMUNAL_HUB)
}
diff --git a/packages/SystemUI/src/com/android/systemui/communal/widgets/WidgetInteractionHandler.kt b/packages/SystemUI/src/com/android/systemui/communal/widgets/WidgetInteractionHandler.kt
index afa7fa9..4c1e77b 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/widgets/WidgetInteractionHandler.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/widgets/WidgetInteractionHandler.kt
@@ -19,7 +19,7 @@
import android.app.PendingIntent
import android.view.View
import android.widget.RemoteViews
-import com.android.systemui.animation.ActivityLaunchAnimator
+import com.android.systemui.animation.ActivityTransitionAnimator
import com.android.systemui.common.ui.view.getNearestParent
import com.android.systemui.plugins.ActivityStarter
import javax.inject.Inject
@@ -42,7 +42,7 @@
private fun startActivity(view: View, pendingIntent: PendingIntent): Boolean {
val hostView = view.getNearestParent<CommunalAppWidgetHostView>()
- val animationController = hostView?.let(ActivityLaunchAnimator.Controller::fromView)
+ val animationController = hostView?.let(ActivityTransitionAnimator.Controller::fromView)
activityStarter.startPendingIntentMaybeDismissingKeyguard(
pendingIntent,
diff --git a/packages/SystemUI/src/com/android/systemui/complication/DreamHomeControlsComplication.java b/packages/SystemUI/src/com/android/systemui/complication/DreamHomeControlsComplication.java
index 9d4ed20..afa2375 100644
--- a/packages/SystemUI/src/com/android/systemui/complication/DreamHomeControlsComplication.java
+++ b/packages/SystemUI/src/com/android/systemui/complication/DreamHomeControlsComplication.java
@@ -35,7 +35,7 @@
import com.android.internal.logging.UiEventLogger;
import com.android.settingslib.Utils;
import com.android.systemui.CoreStartable;
-import com.android.systemui.animation.ActivityLaunchAnimator;
+import com.android.systemui.animation.ActivityTransitionAnimator;
import com.android.systemui.complication.dagger.DreamHomeControlsComplicationComponent;
import com.android.systemui.controls.ControlsServiceInfo;
import com.android.systemui.controls.dagger.ControlsComponent;
@@ -275,8 +275,9 @@
.putExtra(ControlsUiController.EXTRA_ANIMATE, true)
.putExtra(ControlsUiController.EXIT_TO_DREAM, true);
- final ActivityLaunchAnimator.Controller controller =
- v != null ? ActivityLaunchAnimator.Controller.fromView(v, null /* cujType */)
+ final ActivityTransitionAnimator.Controller controller =
+ v != null
+ ? ActivityTransitionAnimator.Controller.fromView(v, null /* cujType */)
: null;
if (mControlsComponent.getVisibility() == AVAILABLE) {
// Controls can be made visible.
diff --git a/packages/SystemUI/src/com/android/systemui/compose/BaseComposeFacade.kt b/packages/SystemUI/src/com/android/systemui/compose/BaseComposeFacade.kt
index 947cb02..9a4dfdd 100644
--- a/packages/SystemUI/src/com/android/systemui/compose/BaseComposeFacade.kt
+++ b/packages/SystemUI/src/com/android/systemui/compose/BaseComposeFacade.kt
@@ -17,7 +17,6 @@
package com.android.systemui.compose
-import android.app.Dialog
import android.content.Context
import android.view.View
import android.view.WindowInsets
@@ -28,12 +27,13 @@
import com.android.systemui.communal.ui.viewmodel.BaseCommunalViewModel
import com.android.systemui.communal.widgets.WidgetConfigurator
import com.android.systemui.keyboard.stickykeys.ui.viewmodel.StickyKeysIndicatorViewModel
+import com.android.systemui.keyguard.shared.model.LockscreenSceneBlueprint
+import com.android.systemui.keyguard.ui.viewmodel.LockscreenContentViewModel
import com.android.systemui.people.ui.viewmodel.PeopleViewModel
import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel
import com.android.systemui.scene.shared.model.Scene
import com.android.systemui.scene.shared.model.SceneKey
import com.android.systemui.scene.ui.viewmodel.SceneContainerViewModel
-import com.android.systemui.statusbar.phone.SystemUIDialogFactory
import com.android.systemui.volume.panel.ui.viewmodel.VolumePanelViewModel
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.StateFlow
@@ -96,11 +96,11 @@
sceneByKey: Map<SceneKey, Scene>,
): View
- /** Creates sticky key dialog presenting provided [viewModel] */
- fun createStickyKeysDialog(
- dialogFactory: SystemUIDialogFactory,
+ /** Creates sticky key indicator content presenting provided [viewModel] */
+ fun createStickyKeysIndicatorContent(
+ context: Context,
viewModel: StickyKeysIndicatorViewModel
- ): Dialog
+ ): View
/** Create a [View] to represent [viewModel] on screen. */
fun createCommunalView(
@@ -117,4 +117,11 @@
/** Creates a container that hosts the communal UI and handles gesture transitions. */
fun createCommunalContainer(context: Context, viewModel: BaseCommunalViewModel): View
+
+ /** Creates a [View] that represents the Lockscreen. */
+ fun createLockscreen(
+ context: Context,
+ viewModel: LockscreenContentViewModel,
+ blueprints: Set<@JvmSuppressWildcards LockscreenSceneBlueprint>,
+ ): View
}
diff --git a/packages/SystemUI/src/com/android/systemui/controls/panels/AuthorizedPanelsRepository.kt b/packages/SystemUI/src/com/android/systemui/controls/panels/AuthorizedPanelsRepository.kt
index ae9c37a..b35bec4 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/panels/AuthorizedPanelsRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/panels/AuthorizedPanelsRepository.kt
@@ -17,11 +17,16 @@
package com.android.systemui.controls.panels
+import android.os.UserHandle
+import kotlinx.coroutines.flow.Flow
+
/**
* Repository for keeping track of which packages the panel has authorized to show control panels
* (embedded activity).
*/
interface AuthorizedPanelsRepository {
+ /** Exposes the authorized panels as a [Flow] for subscribing to updates */
+ fun observeAuthorizedPanels(user: UserHandle): Flow<Set<String>>
/** A set of package names that the user has previously authorized to show panels. */
fun getAuthorizedPanels(): Set<String>
diff --git a/packages/SystemUI/src/com/android/systemui/controls/panels/AuthorizedPanelsRepositoryImpl.kt b/packages/SystemUI/src/com/android/systemui/controls/panels/AuthorizedPanelsRepositoryImpl.kt
index 4e935df..7c2dae3 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/panels/AuthorizedPanelsRepositoryImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/panels/AuthorizedPanelsRepositoryImpl.kt
@@ -19,11 +19,16 @@
import android.content.Context
import android.content.SharedPreferences
+import android.os.UserHandle
import com.android.systemui.res.R
import com.android.systemui.settings.UserFileManager
import com.android.systemui.settings.UserTracker
import com.android.systemui.statusbar.policy.DeviceControlsControllerImpl
+import com.android.systemui.util.kotlin.SharedPreferencesExt.observe
import javax.inject.Inject
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.onStart
class AuthorizedPanelsRepositoryImpl
@Inject
@@ -33,19 +38,24 @@
private val userTracker: UserTracker,
) : AuthorizedPanelsRepository {
+ override fun observeAuthorizedPanels(user: UserHandle): Flow<Set<String>> {
+ val prefs = instantiateSharedPrefs(user)
+ return prefs.observe(KEY).onStart { emit(Unit) }.map { getAuthorizedPanelsInternal(prefs) }
+ }
+
override fun getAuthorizedPanels(): Set<String> {
- return getAuthorizedPanelsInternal(instantiateSharedPrefs())
+ return getAuthorizedPanelsInternal(instantiateSharedPrefs(userTracker.userHandle))
}
override fun getPreferredPackages(): Set<String> =
context.resources.getStringArray(R.array.config_controlsPreferredPackages).toSet()
override fun addAuthorizedPanels(packageNames: Set<String>) {
- addAuthorizedPanelsInternal(instantiateSharedPrefs(), packageNames)
+ addAuthorizedPanelsInternal(instantiateSharedPrefs(userTracker.userHandle), packageNames)
}
override fun removeAuthorizedPanels(packageNames: Set<String>) {
- with(instantiateSharedPrefs()) {
+ with(instantiateSharedPrefs(userTracker.userHandle)) {
val currentSet = getAuthorizedPanelsInternal(this)
edit().putStringSet(KEY, currentSet - packageNames).apply()
}
@@ -63,12 +73,12 @@
sharedPreferences.edit().putStringSet(KEY, currentSet + packageNames).apply()
}
- private fun instantiateSharedPrefs(): SharedPreferences {
+ private fun instantiateSharedPrefs(user: UserHandle): SharedPreferences {
val sharedPref =
userFileManager.getSharedPreferences(
DeviceControlsControllerImpl.PREFS_CONTROLS_FILE,
Context.MODE_PRIVATE,
- userTracker.userId,
+ user.identifier,
)
// We should add default packages when we've never run this
diff --git a/packages/SystemUI/src/com/android/systemui/controls/panels/SelectedComponentRepositoryImpl.kt b/packages/SystemUI/src/com/android/systemui/controls/panels/SelectedComponentRepositoryImpl.kt
index 0baa81a..9be04940 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/panels/SelectedComponentRepositoryImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/panels/SelectedComponentRepositoryImpl.kt
@@ -20,21 +20,18 @@
import android.content.Context
import android.content.SharedPreferences
import android.os.UserHandle
-import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.dagger.qualifiers.Background
-import com.android.systemui.flags.FeatureFlags
import com.android.systemui.settings.UserFileManager
import com.android.systemui.settings.UserTracker
import com.android.systemui.statusbar.policy.DeviceControlsControllerImpl
+import com.android.systemui.util.kotlin.SharedPreferencesExt.observe
import javax.inject.Inject
import kotlinx.coroutines.CoroutineDispatcher
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOn
-import kotlinx.coroutines.launch
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.onStart
@OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
@SysUISingleton
@@ -43,9 +40,7 @@
constructor(
private val userFileManager: UserFileManager,
private val userTracker: UserTracker,
- private val featureFlags: FeatureFlags,
- @Background private val bgDispatcher: CoroutineDispatcher,
- @Application private val applicationScope: CoroutineScope
+ @Background private val bgDispatcher: CoroutineDispatcher
) : SelectedComponentRepository {
private companion object {
@@ -66,22 +61,11 @@
override fun selectedComponentFlow(
userHandle: UserHandle
): Flow<SelectedComponentRepository.SelectedComponent?> {
- return conflatedCallbackFlow {
- val sharedPreferencesByUserId = getSharedPreferencesForUser(userHandle.identifier)
- val listener =
- SharedPreferences.OnSharedPreferenceChangeListener { _, key ->
- applicationScope.launch(bgDispatcher) {
- if (key == PREF_COMPONENT) {
- trySend(getSelectedComponent(userHandle))
- }
- }
- }
- sharedPreferencesByUserId.registerOnSharedPreferenceChangeListener(listener)
- send(getSelectedComponent(userHandle))
- awaitClose {
- sharedPreferencesByUserId.unregisterOnSharedPreferenceChangeListener(listener)
- }
- }
+ val prefs = getSharedPreferencesForUser(userHandle.identifier)
+ return prefs
+ .observe(PREF_COMPONENT)
+ .onStart { emit(Unit) }
+ .map { getSelectedComponent(userHandle) }
.flowOn(bgDispatcher)
}
diff --git a/packages/SystemUI/src/com/android/systemui/controls/start/ControlsStartable.kt b/packages/SystemUI/src/com/android/systemui/controls/start/ControlsStartable.kt
index 20bfbc9..3458d3e 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/start/ControlsStartable.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/start/ControlsStartable.kt
@@ -26,8 +26,8 @@
import androidx.annotation.WorkerThread
import com.android.systemui.CoreStartable
import com.android.systemui.broadcast.BroadcastDispatcher
-import com.android.systemui.common.data.shared.model.PackageChangeModel
-import com.android.systemui.common.data.repository.PackageChangeRepository
+import com.android.systemui.common.shared.model.PackageChangeModel
+import com.android.systemui.common.domain.interactor.PackageChangeInteractor
import com.android.systemui.controls.controller.ControlsController
import com.android.systemui.controls.dagger.ControlsComponent
import com.android.systemui.controls.management.ControlsListingController
@@ -42,6 +42,7 @@
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.filter
+import kotlinx.coroutines.flow.filterIsInstance
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
@@ -70,7 +71,7 @@
private val userTracker: UserTracker,
private val authorizedPanelsRepository: AuthorizedPanelsRepository,
private val selectedComponentRepository: SelectedComponentRepository,
- private val packageChangeRepository: PackageChangeRepository,
+ private val packageChangeInteractor: PackageChangeInteractor,
private val userManager: UserManager,
private val broadcastDispatcher: BroadcastDispatcher,
) : CoreStartable {
@@ -114,12 +115,13 @@
private fun monitorPackageUninstall() {
packageJob?.cancel()
- packageJob = packageChangeRepository.packageChanged(userTracker.userHandle)
+ packageJob = packageChangeInteractor.packageChanged(userTracker.userHandle)
+ .filterIsInstance<PackageChangeModel.Uninstalled>()
.filter {
val selectedPackage =
selectedComponentRepository.getSelectedComponent()?.componentName?.packageName
// Selected package was uninstalled
- (it is PackageChangeModel.Uninstalled) && (it.packageName == selectedPackage)
+ it.packageName == selectedPackage
}
.onEach { selectedComponentRepository.removeSelectedComponent() }
.flowOn(bgDispatcher)
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/DefaultServiceBinder.java b/packages/SystemUI/src/com/android/systemui/dagger/DefaultServiceBinder.java
index 92eeace..904d5898 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/DefaultServiceBinder.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/DefaultServiceBinder.java
@@ -23,6 +23,7 @@
import com.android.systemui.dreams.DreamOverlayService;
import com.android.systemui.dump.SystemUIAuxiliaryDumpService;
import com.android.systemui.keyguard.KeyguardService;
+import com.android.systemui.recordissue.IssueRecordingService;
import com.android.systemui.screenrecord.RecordingService;
import com.android.systemui.statusbar.phone.NotificationListenerWithPlugins;
import com.android.systemui.wallpapers.ImageWallpaper;
@@ -85,4 +86,11 @@
@IntoMap
@ClassKey(RecordingService.class)
public abstract Service bindRecordingService(RecordingService service);
+
+ /** Inject into IssueRecordingService */
+ @Binds
+ @IntoMap
+ @ClassKey(IssueRecordingService.class)
+ public abstract Service bindIssueRecordingService(IssueRecordingService service);
+
}
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java
index e9d1e94..f7bc5cdc 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java
@@ -26,10 +26,13 @@
import com.android.systemui.ScreenDecorationsModule;
import com.android.systemui.accessibility.SystemActionsModule;
import com.android.systemui.battery.BatterySaverModule;
+import com.android.systemui.display.ui.viewmodel.ConnectingDisplayViewModel;
import com.android.systemui.dock.DockManager;
import com.android.systemui.dock.DockManagerImpl;
import com.android.systemui.doze.DozeHost;
import com.android.systemui.media.dagger.MediaModule;
+import com.android.systemui.media.muteawait.MediaMuteAwaitConnectionCli;
+import com.android.systemui.media.nearby.NearbyMediaDevicesManager;
import com.android.systemui.navigationbar.NavigationBarControllerModule;
import com.android.systemui.navigationbar.gestural.GestureModule;
import com.android.systemui.plugins.qs.QSFactory;
@@ -42,7 +45,7 @@
import com.android.systemui.rotationlock.RotationLockModule;
import com.android.systemui.scene.SceneContainerFrameworkModule;
import com.android.systemui.screenshot.ReferenceScreenshotModule;
-import com.android.systemui.settings.dagger.MultiUserUtilsModule;
+import com.android.systemui.settings.MultiUserUtilsModule;
import com.android.systemui.shade.NotificationShadeWindowControllerImpl;
import com.android.systemui.shade.ShadeModule;
import com.android.systemui.statusbar.CommandQueue;
@@ -63,6 +66,7 @@
import com.android.systemui.statusbar.policy.SensorPrivacyController;
import com.android.systemui.statusbar.policy.SensorPrivacyControllerImpl;
import com.android.systemui.toast.ToastModule;
+import com.android.systemui.unfold.SysUIUnfoldStartableModule;
import com.android.systemui.unfold.UnfoldTransitionModule;
import com.android.systemui.volume.dagger.VolumeModule;
import com.android.systemui.wallpapers.dagger.WallpaperModule;
@@ -92,12 +96,15 @@
AospPolicyModule.class,
BatterySaverModule.class,
CollapsedStatusBarFragmentStartableModule.class,
+ ConnectingDisplayViewModel.StartableModule.class,
GestureModule.class,
HeadsUpModule.class,
KeyboardShortcutsModule.class,
MediaModule.class,
+ MediaMuteAwaitConnectionCli.StartableModule.class,
MultiUserUtilsModule.class,
NavigationBarControllerModule.class,
+ NearbyMediaDevicesManager.StartableModule.class,
PowerModule.class,
QSModule.class,
RearDisplayModule.class,
@@ -108,6 +115,7 @@
ShadeModule.class,
StartCentralSurfacesModule.class,
SceneContainerFrameworkModule.class,
+ SysUIUnfoldStartableModule.class,
UnfoldTransitionModule.Startables.class,
ToastModule.class,
VolumeModule.class,
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java b/packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java
index e7b8773..3b0c281 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java
@@ -19,25 +19,15 @@
import com.android.systemui.BootCompleteCacheImpl;
import com.android.systemui.CoreStartable;
import com.android.systemui.Dependency;
-import com.android.systemui.Flags;
import com.android.systemui.InitController;
import com.android.systemui.SystemUIAppComponentFactoryBase;
import com.android.systemui.dagger.qualifiers.PerUser;
-import com.android.systemui.display.ui.viewmodel.ConnectingDisplayViewModel;
import com.android.systemui.dump.DumpManager;
import com.android.systemui.keyguard.KeyguardSliceProvider;
-import com.android.systemui.media.muteawait.MediaMuteAwaitConnectionCli;
-import com.android.systemui.media.nearby.NearbyMediaDevicesManager;
import com.android.systemui.people.PeopleProvider;
import com.android.systemui.statusbar.NotificationInsetsModule;
import com.android.systemui.statusbar.QsFrameTranslateModule;
import com.android.systemui.statusbar.policy.ConfigurationController;
-import com.android.systemui.unfold.FoldStateLogger;
-import com.android.systemui.unfold.FoldStateLoggingProvider;
-import com.android.systemui.unfold.SysUIUnfoldComponent;
-import com.android.systemui.unfold.UnfoldTransitionProgressProvider;
-import com.android.systemui.unfold.dagger.UnfoldBg;
-import com.android.systemui.unfold.progress.UnfoldTransitionProgressForwarder;
import com.android.wm.shell.back.BackAnimation;
import com.android.wm.shell.bubbles.Bubbles;
import com.android.wm.shell.desktopmode.DesktopMode;
@@ -126,42 +116,6 @@
}
/**
- * Initializes all the SysUI components.
- */
- default void init() {
- // Initialize components that have no direct tie to the dagger dependency graph,
- // but are critical to this component's operation
- getSysUIUnfoldComponent()
- .ifPresent(
- c -> {
- c.getUnfoldLightRevealOverlayAnimation().init();
- c.getUnfoldTransitionWallpaperController().init();
- c.getUnfoldHapticsPlayer();
- c.getNaturalRotationUnfoldProgressProvider().init();
- c.getUnfoldLatencyTracker().init();
- });
- // No init method needed, just needs to be gotten so that it's created.
- getMediaMuteAwaitConnectionCli();
- getNearbyMediaDevicesManager();
- getConnectingDisplayViewModel().init();
- getFoldStateLoggingProvider().ifPresent(FoldStateLoggingProvider::init);
- getFoldStateLogger().ifPresent(FoldStateLogger::init);
-
- Optional<UnfoldTransitionProgressProvider> unfoldTransitionProgressProvider;
-
- if (Flags.unfoldAnimationBackgroundProgress()) {
- unfoldTransitionProgressProvider = getBgUnfoldTransitionProgressProvider();
- } else {
- unfoldTransitionProgressProvider = getUnfoldTransitionProgressProvider();
- }
- unfoldTransitionProgressProvider
- .ifPresent(
- (progressProvider) ->
- getUnfoldTransitionProgressForwarder()
- .ifPresent(progressProvider::addCallback));
- }
-
- /**
* Provides a BootCompleteCache.
*/
@SysUISingleton
@@ -180,37 +134,6 @@
ContextComponentHelper getContextComponentHelper();
/**
- * Creates a UnfoldTransitionProgressProvider that calculates progress in the background.
- */
- @SysUISingleton
- @UnfoldBg
- Optional<UnfoldTransitionProgressProvider> getBgUnfoldTransitionProgressProvider();
-
- /**
- * Creates a UnfoldTransitionProgressProvider that calculates progress in the main thread.
- */
- @SysUISingleton
- Optional<UnfoldTransitionProgressProvider> getUnfoldTransitionProgressProvider();
-
- /**
- * Creates a UnfoldTransitionProgressForwarder.
- */
- @SysUISingleton
- Optional<UnfoldTransitionProgressForwarder> getUnfoldTransitionProgressForwarder();
-
- /**
- * Creates a FoldStateLoggingProvider.
- */
- @SysUISingleton
- Optional<FoldStateLoggingProvider> getFoldStateLoggingProvider();
-
- /**
- * Creates a FoldStateLogger.
- */
- @SysUISingleton
- Optional<FoldStateLogger> getFoldStateLogger();
-
- /**
* Main dependency providing module.
*/
@SysUISingleton
@@ -227,22 +150,6 @@
InitController getInitController();
/**
- * For devices with a hinge: access objects within this component
- */
- Optional<SysUIUnfoldComponent> getSysUIUnfoldComponent();
-
- /** */
- MediaMuteAwaitConnectionCli getMediaMuteAwaitConnectionCli();
-
- /** */
- NearbyMediaDevicesManager getNearbyMediaDevicesManager();
-
- /**
- * Creates a ConnectingDisplayViewModel
- */
- ConnectingDisplayViewModel getConnectingDisplayViewModel();
-
- /**
* Returns {@link CoreStartable}s that should be started with the application.
*/
Map<Class<?>, Provider<CoreStartable>> getStartables();
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt b/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt
index 5ee2045..a3d6ad4 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt
@@ -47,7 +47,7 @@
import com.android.systemui.media.taptotransfer.receiver.MediaTttChipControllerReceiver
import com.android.systemui.media.taptotransfer.sender.MediaTttSenderCoordinator
import com.android.systemui.mediaprojection.taskswitcher.MediaProjectionTaskSwitcherCoreStartable
-import com.android.systemui.settings.dagger.MultiUserUtilsModule
+import com.android.systemui.settings.MultiUserUtilsModule
import com.android.systemui.shortcut.ShortcutKeyDispatcher
import com.android.systemui.statusbar.ImmersiveModeConfirmation
import com.android.systemui.statusbar.gesture.GesturePointerEventListener
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
index efcbd47..1720de8 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
@@ -17,6 +17,7 @@
package com.android.systemui.dagger;
import android.app.INotificationManager;
+import android.app.Service;
import android.content.Context;
import android.service.dreams.IDreamManager;
@@ -27,6 +28,8 @@
import com.android.keyguard.dagger.KeyguardBouncerComponent;
import com.android.systemui.BootCompleteCache;
import com.android.systemui.BootCompleteCacheImpl;
+import com.android.systemui.CameraProtectionModule;
+import com.android.systemui.SystemUISecondaryUserService;
import com.android.systemui.accessibility.AccessibilityModule;
import com.android.systemui.accessibility.data.repository.AccessibilityRepositoryModule;
import com.android.systemui.appops.dagger.AppOpsModule;
@@ -149,6 +152,8 @@
import dagger.BindsOptionalOf;
import dagger.Module;
import dagger.Provides;
+import dagger.multibindings.ClassKey;
+import dagger.multibindings.IntoMap;
import java.util.Collections;
import java.util.Optional;
@@ -177,6 +182,7 @@
BouncerInteractorModule.class,
BouncerRepositoryModule.class,
BouncerViewModule.class,
+ CameraProtectionModule.class,
ClipboardOverlayModule.class,
ClockRegistryModule.class,
CommunalModule.class,
@@ -382,4 +388,9 @@
@Binds
abstract LargeScreenShadeInterpolator largeScreensShadeInterpolator(
LargeScreenShadeInterpolatorImpl impl);
+
+ @Binds
+ @IntoMap
+ @ClassKey(SystemUISecondaryUserService.class)
+ abstract Service bindsSystemUISecondaryUserService(SystemUISecondaryUserService service);
}
diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFingerprintAuthInteractor.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFingerprintAuthInteractor.kt
index 684627b..2461c26 100644
--- a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFingerprintAuthInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFingerprintAuthInteractor.kt
@@ -39,4 +39,6 @@
/** Provide the current status of fingerprint authentication. */
val authenticationStatus: Flow<FingerprintAuthenticationStatus> =
repository.authenticationStatus
+
+ val isLockedOut: Flow<Boolean> = repository.isLockedOut
}
diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/SystemUIDeviceEntryFaceAuthInteractor.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/SystemUIDeviceEntryFaceAuthInteractor.kt
index 98130eb..cf91e14 100644
--- a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/SystemUIDeviceEntryFaceAuthInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/SystemUIDeviceEntryFaceAuthInteractor.kt
@@ -36,7 +36,6 @@
import com.android.systemui.deviceentry.shared.model.ErrorFaceAuthenticationStatus
import com.android.systemui.deviceentry.shared.model.FaceAuthenticationStatus
import com.android.systemui.keyguard.data.repository.BiometricSettingsRepository
-import com.android.systemui.keyguard.data.repository.DeviceEntryFingerprintAuthRepository
import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
import com.android.systemui.keyguard.shared.model.TransitionState
import com.android.systemui.log.FaceAuthenticationLogger
@@ -78,7 +77,7 @@
private val keyguardTransitionInteractor: KeyguardTransitionInteractor,
private val faceAuthenticationLogger: FaceAuthenticationLogger,
private val keyguardUpdateMonitor: KeyguardUpdateMonitor,
- private val deviceEntryFingerprintAuthRepository: DeviceEntryFingerprintAuthRepository,
+ private val deviceEntryFingerprintAuthInteractor: DeviceEntryFingerprintAuthInteractor,
private val userRepository: UserRepository,
private val facePropertyRepository: FacePropertyRepository,
private val faceWakeUpTriggersConfig: FaceWakeUpTriggersConfig,
@@ -149,14 +148,24 @@
}
.launchIn(applicationScope)
- deviceEntryFingerprintAuthRepository.isLockedOut
- .onEach {
- if (it) {
+ deviceEntryFingerprintAuthInteractor.isLockedOut
+ .sample(biometricSettingsRepository.isFaceAuthEnrolledAndEnabled, ::Pair)
+ .filter { (_, faceEnabledAndEnrolled) ->
+ // We don't care about this if face auth is not enabled.
+ faceEnabledAndEnrolled
+ }
+ .map { (fpLockedOut, _) -> fpLockedOut }
+ .sample(userRepository.selectedUser, ::Pair)
+ .onEach { (fpLockedOut, currentUser) ->
+ if (fpLockedOut) {
faceAuthenticationLogger.faceLockedOut("Fingerprint locked out")
- // We don't care about this if face auth is not enabled.
if (isFaceAuthEnabledAndEnrolled()) {
repository.setLockedOut(true)
}
+ } else {
+ // Fingerprint is not locked out anymore, revert face lockout state back to
+ // previous value.
+ resetLockedOutState(currentUser.userInfo.id)
}
}
.launchIn(applicationScope)
@@ -169,10 +178,7 @@
val wasSwitching = previous.selectionStatus == SelectionStatus.SELECTION_IN_PROGRESS
val isSwitching = curr.selectionStatus == SelectionStatus.SELECTION_IN_PROGRESS
if (wasSwitching && !isSwitching) {
- val lockoutMode = facePropertyRepository.getLockoutMode(curr.userInfo.id)
- repository.setLockedOut(
- lockoutMode == LockoutMode.PERMANENT || lockoutMode == LockoutMode.TIMED
- )
+ resetLockedOutState(curr.userInfo.id)
yield()
runFaceAuth(
FaceAuthUiEvent.FACE_AUTH_UPDATED_USER_SWITCHING,
@@ -185,6 +191,13 @@
.launchIn(applicationScope)
}
+ private suspend fun resetLockedOutState(currentUserId: Int) {
+ val lockoutMode = facePropertyRepository.getLockoutMode(currentUserId)
+ repository.setLockedOut(
+ lockoutMode == LockoutMode.PERMANENT || lockoutMode == LockoutMode.TIMED
+ )
+ }
+
override fun onSwipeUpOnBouncer() {
runFaceAuth(FaceAuthUiEvent.FACE_AUTH_TRIGGERED_SWIPE_UP_ON_BOUNCER, false)
}
diff --git a/packages/SystemUI/src/com/android/systemui/display/data/repository/DisplayRepository.kt b/packages/SystemUI/src/com/android/systemui/display/data/repository/DisplayRepository.kt
index c93b8e1..1230156 100644
--- a/packages/SystemUI/src/com/android/systemui/display/data/repository/DisplayRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/display/data/repository/DisplayRepository.kt
@@ -292,7 +292,7 @@
private fun <T> Flow<T>.debugLog(flowName: String): Flow<T> {
return if (DEBUG) {
- traceEach(flowName, logcat = true)
+ traceEach(flowName, logcat = true, traceEmissionCount = true)
} else {
this
}
diff --git a/packages/SystemUI/src/com/android/systemui/display/ui/viewmodel/ConnectingDisplayViewModel.kt b/packages/SystemUI/src/com/android/systemui/display/ui/viewmodel/ConnectingDisplayViewModel.kt
index 10aa703..190062c 100644
--- a/packages/SystemUI/src/com/android/systemui/display/ui/viewmodel/ConnectingDisplayViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/display/ui/viewmodel/ConnectingDisplayViewModel.kt
@@ -18,6 +18,7 @@
import android.app.Dialog
import android.content.Context
import com.android.server.policy.feature.flags.Flags
+import com.android.systemui.CoreStartable
import com.android.systemui.biometrics.Utils
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
@@ -26,6 +27,10 @@
import com.android.systemui.display.domain.interactor.ConnectedDisplayInteractor.PendingDisplay
import com.android.systemui.display.ui.view.MirroringConfirmationDialog
import com.android.systemui.statusbar.policy.ConfigurationController
+import dagger.Binds
+import dagger.Module
+import dagger.multibindings.ClassKey
+import dagger.multibindings.IntoMap
import javax.inject.Inject
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
@@ -47,12 +52,12 @@
@Application private val scope: CoroutineScope,
@Background private val bgDispatcher: CoroutineDispatcher,
private val configurationController: ConfigurationController,
-) {
+) : CoreStartable {
private var dialog: Dialog? = null
/** Starts listening for pending displays. */
- fun init() {
+ override fun start() {
val pendingDisplayFlow = connectedDisplayInteractor.pendingDisplay
val concurrentDisplaysInProgessFlow =
if (Flags.enableDualDisplayBlocking()) {
@@ -96,4 +101,12 @@
dialog?.hide()
dialog = null
}
+
+ @Module
+ interface StartableModule {
+ @Binds
+ @IntoMap
+ @ClassKey(ConnectingDisplayViewModel::class)
+ fun bindsConnectingDisplayViewModel(impl: ConnectingDisplayViewModel): CoreStartable
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeDockHandler.java b/packages/SystemUI/src/com/android/systemui/doze/DozeDockHandler.java
index c331164..537cacd 100644
--- a/packages/SystemUI/src/com/android/systemui/doze/DozeDockHandler.java
+++ b/packages/SystemUI/src/com/android/systemui/doze/DozeDockHandler.java
@@ -93,7 +93,11 @@
}
mDockState = dockState;
- if (isPulsing()) {
+ if (mMachine.isExecutingTransition() || isPulsing()) {
+ // If the device is in the middle of executing a transition or is pulsing,
+ // exit early instead of requesting a new state. DozeMachine
+ // will check the docked state and resolveIntermediateState in the next
+ // transition after pulse done.
return;
}
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/HomeControlsDreamService.kt b/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/HomeControlsDreamService.kt
index e04a505..e74814a 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/HomeControlsDreamService.kt
+++ b/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/HomeControlsDreamService.kt
@@ -21,11 +21,20 @@
import android.service.dreams.DreamService
import android.window.TaskFragmentInfo
import com.android.systemui.controls.settings.ControlsSettingsRepository
+import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.dreams.DreamLogger
import com.android.systemui.dreams.homecontrols.domain.interactor.HomeControlsComponentInteractor
+import com.android.systemui.dreams.homecontrols.domain.interactor.HomeControlsComponentInteractor.Companion.MAX_UPDATE_CORRELATION_DELAY
import com.android.systemui.log.LogBuffer
import com.android.systemui.log.dagger.DreamLog
import javax.inject.Inject
+import kotlin.time.Duration.Companion.seconds
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.SupervisorJob
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
class HomeControlsDreamService
@Inject
@@ -34,11 +43,13 @@
private val taskFragmentFactory: TaskFragmentComponent.Factory,
private val homeControlsComponentInteractor: HomeControlsComponentInteractor,
private val dreamActivityProvider: DreamActivityProvider,
+ @Background private val bgDispatcher: CoroutineDispatcher,
@DreamLog logBuffer: LogBuffer
) : DreamService() {
- private lateinit var taskFragmentComponent: TaskFragmentComponent
-
+ private val serviceJob = SupervisorJob()
+ private val serviceScope = CoroutineScope(bgDispatcher + serviceJob)
private val logger = DreamLogger(logBuffer, "HomeControlsDreamService")
+ private lateinit var taskFragmentComponent: TaskFragmentComponent
override fun onAttachedToWindow() {
super.onAttachedToWindow()
@@ -47,6 +58,11 @@
finish()
return
}
+
+ // Start monitoring package updates to possibly restart the dream if the home controls
+ // package is updated while we are dreaming.
+ serviceScope.launch { homeControlsComponentInteractor.monitorUpdatesAndRestart() }
+
taskFragmentComponent =
taskFragmentFactory
.create(
@@ -62,6 +78,7 @@
if (taskFragmentInfo.isEmpty) {
logger.d("Finishing dream due to TaskFragment being empty")
finish()
+ homeControlsComponentInteractor.onTaskFragmentEmpty()
}
}
@@ -84,5 +101,19 @@
override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
taskFragmentComponent.destroy()
+ serviceScope.launch {
+ delay(CANCELLATION_DELAY_AFTER_DETACHED)
+ serviceJob.cancel("Dream detached from window")
+ }
+ }
+
+ private companion object {
+ /**
+ * Defines how long after the dream ends that we should keep monitoring for package updates
+ * to attempt a restart of the dream. This should be larger than
+ * [MAX_UPDATE_CORRELATION_DELAY] as it also includes the time the package update takes to
+ * complete.
+ */
+ val CANCELLATION_DELAY_AFTER_DETACHED = 5.seconds
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/domain/interactor/HomeControlsComponentInteractor.kt b/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/domain/interactor/HomeControlsComponentInteractor.kt
index 91e0547..f0067dc 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/domain/interactor/HomeControlsComponentInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/domain/interactor/HomeControlsComponentInteractor.kt
@@ -16,8 +16,13 @@
package com.android.systemui.dreams.homecontrols.domain.interactor
+import android.annotation.SuppressLint
+import android.app.DreamManager
import android.content.ComponentName
+import android.os.UserHandle
import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
+import com.android.systemui.common.domain.interactor.PackageChangeInteractor
+import com.android.systemui.common.shared.model.PackageChangeModel
import com.android.systemui.controls.ControlsServiceInfo
import com.android.systemui.controls.dagger.ControlsComponent
import com.android.systemui.controls.management.ControlsListingController
@@ -27,15 +32,24 @@
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.user.data.repository.UserRepository
import com.android.systemui.util.kotlin.getOrNull
+import com.android.systemui.util.kotlin.pairwiseBy
+import com.android.systemui.util.kotlin.sample
+import com.android.systemui.util.time.SystemClock
import javax.inject.Inject
+import kotlin.math.abs
+import kotlin.time.Duration.Companion.milliseconds
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.emptyFlow
+import kotlinx.coroutines.flow.filter
+import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onStart
@@ -47,24 +61,33 @@
@Inject
constructor(
private val selectedComponentRepository: SelectedComponentRepository,
- private val controlsComponent: ControlsComponent,
- private val authorizedPanelsRepository: AuthorizedPanelsRepository,
+ controlsComponent: ControlsComponent,
+ authorizedPanelsRepository: AuthorizedPanelsRepository,
userRepository: UserRepository,
+ private val packageChangeInteractor: PackageChangeInteractor,
+ private val systemClock: SystemClock,
+ private val dreamManager: DreamManager,
@Background private val bgScope: CoroutineScope
) {
- private val controlsListingController =
+ private val controlsListingController: ControlsListingController? =
controlsComponent.getControlsListingController().getOrNull()
/** Gets the current user's selected panel, or null if there isn't one */
- private val selectedItem: Flow<SelectedComponentRepository.SelectedComponent?> =
+ private val selectedPanel: Flow<SelectedComponentRepository.SelectedComponent?> =
userRepository.selectedUserInfo
.flatMapLatest { user ->
selectedComponentRepository.selectedComponentFlow(user.userHandle)
}
.map { if (it?.isPanel == true) it else null }
- /** Gets all the available panels which are authorized by the user */
- private fun allPanelItem(): Flow<List<PanelComponent>> {
+ /** Gets the current user's authorized panels */
+ private val allAuthorizedPanels: Flow<Set<String>> =
+ userRepository.selectedUserInfo.flatMapLatest { user ->
+ authorizedPanelsRepository.observeAuthorizedPanels(user.userHandle)
+ }
+
+ /** Gets all the available services from [ControlsListingController] */
+ private fun allAvailableServices(): Flow<List<ControlsServiceInfo>> {
if (controlsListingController == null) {
return emptyFlow()
}
@@ -79,26 +102,92 @@
awaitClose { controlsListingController.removeCallback(listener) }
}
.onStart { emit(controlsListingController.getCurrentServices()) }
- .map { serviceInfos ->
- val authorizedPanels = authorizedPanelsRepository.getAuthorizedPanels()
- serviceInfos.mapNotNull {
- val panelActivity = it.panelActivity
- if (it.componentName.packageName in authorizedPanels && panelActivity != null) {
- PanelComponent(it.componentName, panelActivity)
- } else {
- null
- }
+ }
+
+ /** Gets all panels which are available and authorized by the user */
+ private val allAvailableAndAuthorizedPanels: Flow<List<PanelComponent>> =
+ combine(
+ allAvailableServices(),
+ allAuthorizedPanels,
+ ) { serviceInfos, authorizedPanels ->
+ serviceInfos.mapNotNull {
+ val panelActivity = it.panelActivity
+ if (it.componentName.packageName in authorizedPanels && panelActivity != null) {
+ PanelComponent(it.componentName, panelActivity)
+ } else {
+ null
}
}
- }
+ }
+
val panelComponent: StateFlow<ComponentName?> =
- combine(allPanelItem(), selectedItem) { items, selected ->
+ combine(
+ allAvailableAndAuthorizedPanels,
+ selectedPanel,
+ ) { panels, selected ->
val item =
- items.firstOrNull { it.componentName == selected?.componentName }
- ?: items.firstOrNull()
+ panels.firstOrNull { it.componentName == selected?.componentName }
+ ?: panels.firstOrNull()
item?.panelActivity
}
.stateIn(bgScope, SharingStarted.WhileSubscribed(), null)
- data class PanelComponent(val componentName: ComponentName, val panelActivity: ComponentName)
+ private val taskFragmentFinished =
+ MutableSharedFlow<Long>(replay = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST)
+
+ fun onTaskFragmentEmpty() {
+ taskFragmentFinished.tryEmit(systemClock.currentTimeMillis())
+ }
+
+ /**
+ * Monitors if the current home panel package is updated and causes the dream to finish, and
+ * attempts to restart the dream in this case.
+ */
+ @SuppressLint("MissingPermission")
+ suspend fun monitorUpdatesAndRestart() {
+ taskFragmentFinished.resetReplayCache()
+ panelComponent
+ .flatMapLatest { component ->
+ if (component == null) return@flatMapLatest emptyFlow()
+ packageChangeInteractor.packageChanged(UserHandle.CURRENT, component.packageName)
+ }
+ .filter { it.isUpdate() }
+ // Wait for an UpdatedStarted - UpdateFinished pair to ensure the update has finished.
+ .pairwiseBy(::validateUpdatePair)
+ .filterNotNull()
+ .sample(taskFragmentFinished, ::Pair)
+ .filter { (updateStarted, lastFinishedTimestamp) ->
+ abs(updateStarted.timeMillis - lastFinishedTimestamp) <=
+ MAX_UPDATE_CORRELATION_DELAY.inWholeMilliseconds
+ }
+ .collect { dreamManager.startDream() }
+ }
+
+ private data class PanelComponent(
+ val componentName: ComponentName,
+ val panelActivity: ComponentName,
+ )
+
+ companion object {
+ /**
+ * The maximum delay between a package update **starting** and the task fragment finishing
+ * which causes us to correlate the package update as the cause of the task fragment
+ * finishing.
+ */
+ val MAX_UPDATE_CORRELATION_DELAY = 500.milliseconds
+ }
}
+
+private fun PackageChangeModel.isUpdate() =
+ this is PackageChangeModel.UpdateStarted || this is PackageChangeModel.UpdateFinished
+
+private fun validateUpdatePair(
+ updateStarted: PackageChangeModel,
+ updateFinished: PackageChangeModel
+): PackageChangeModel.UpdateStarted? =
+ when {
+ !updateStarted.isSamePackage(updateFinished) -> null
+ updateStarted !is PackageChangeModel.UpdateStarted -> null
+ updateFinished !is PackageChangeModel.UpdateFinished -> null
+ else -> updateStarted
+ }
diff --git a/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt b/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt
index df0566e..41ce3fd 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt
+++ b/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt
@@ -23,9 +23,12 @@
import com.android.server.notification.Flags.politeNotifications
import com.android.server.notification.Flags.vibrateWhileUnlocked
import com.android.systemui.Flags.FLAG_KEYGUARD_BOTTOM_AREA_REFACTOR
+import com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT
import com.android.systemui.Flags.keyguardBottomAreaRefactor
+import com.android.systemui.Flags.migrateClocksToBlueprint
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.flags.Flags.MIGRATE_KEYGUARD_STATUS_BAR_VIEW
+import com.android.systemui.keyguard.shared.ComposeLockscreen
import com.android.systemui.keyguard.shared.KeyguardShadeMigrationNssl
import com.android.systemui.scene.shared.flag.SceneContainerFlag
import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor
@@ -55,6 +58,11 @@
// SceneContainer dependencies
SceneContainerFlag.getFlagDependencies().forEach { (alpha, beta) -> alpha dependsOn beta }
SceneContainerFlag.getMainStaticFlag() dependsOn MIGRATE_KEYGUARD_STATUS_BAR_VIEW
+
+ // ComposeLockscreen dependencies
+ ComposeLockscreen.token dependsOn KeyguardShadeMigrationNssl.token
+ ComposeLockscreen.token dependsOn keyguardBottomAreaRefactor
+ ComposeLockscreen.token dependsOn migrateClocksToBlueprint
}
private inline val politeNotifications
@@ -65,4 +73,6 @@
get() = FlagToken(FLAG_VIBRATE_WHILE_UNLOCKED, vibrateWhileUnlocked())
private inline val keyguardBottomAreaRefactor
get() = FlagToken(FLAG_KEYGUARD_BOTTOM_AREA_REFACTOR, keyguardBottomAreaRefactor())
+ private inline val migrateClocksToBlueprint
+ get() = FlagToken(FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT, migrateClocksToBlueprint())
}
diff --git a/packages/SystemUI/src/com/android/systemui/haptics/slider/HapticSliderViewBinder.kt b/packages/SystemUI/src/com/android/systemui/haptics/slider/HapticSliderViewBinder.kt
new file mode 100644
index 0000000..304fdd6
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/haptics/slider/HapticSliderViewBinder.kt
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2024 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.haptics.slider
+
+import android.view.View
+import androidx.lifecycle.lifecycleScope
+import com.android.systemui.lifecycle.repeatWhenAttached
+import kotlinx.coroutines.awaitCancellation
+
+object HapticSliderViewBinder {
+ /**
+ * Binds a [SeekableSliderHapticPlugin] to a [View]. The binded view should be a
+ * [android.widget.SeekBar] or a container of a [android.widget.SeekBar]
+ */
+ @JvmStatic
+ fun bind(view: View?, plugin: SeekableSliderHapticPlugin) {
+ view?.repeatWhenAttached {
+ plugin.startInScope(lifecycleScope)
+ try {
+ awaitCancellation()
+ } finally {
+ plugin.stop()
+ }
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/haptics/slider/SeekableSliderHapticPlugin.kt b/packages/SystemUI/src/com/android/systemui/haptics/slider/SeekableSliderHapticPlugin.kt
index 58fb6a9..931a869 100644
--- a/packages/SystemUI/src/com/android/systemui/haptics/slider/SeekableSliderHapticPlugin.kt
+++ b/packages/SystemUI/src/com/android/systemui/haptics/slider/SeekableSliderHapticPlugin.kt
@@ -20,11 +20,8 @@
import android.view.VelocityTracker
import android.widget.SeekBar
import androidx.annotation.VisibleForTesting
-import com.android.systemui.dagger.qualifiers.Application
-import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.statusbar.VibratorHelper
import com.android.systemui.util.time.SystemClock
-import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
@@ -43,10 +40,8 @@
constructor(
vibratorHelper: VibratorHelper,
systemClock: SystemClock,
- @Main private val mainDispatcher: CoroutineDispatcher,
- @Application private val applicationScope: CoroutineScope,
sliderHapticFeedbackConfig: SliderHapticFeedbackConfig = SliderHapticFeedbackConfig(),
- sliderTrackerConfig: SeekableSliderTrackerConfig = SeekableSliderTrackerConfig(),
+ private val sliderTrackerConfig: SeekableSliderTrackerConfig = SeekableSliderTrackerConfig(),
) {
private val velocityTracker = VelocityTracker.obtain()
@@ -61,19 +56,15 @@
systemClock,
)
- private val sliderTracker =
- SeekableSliderTracker(
- sliderHapticFeedbackProvider,
- sliderEventProducer,
- mainDispatcher,
- sliderTrackerConfig,
- )
+ private var sliderTracker: SeekableSliderTracker? = null
+
+ private var pluginScope: CoroutineScope? = null
val isTracking: Boolean
- get() = sliderTracker.isTracking
+ get() = sliderTracker?.isTracking == true
- val trackerState: SliderState
- get() = sliderTracker.currentState
+ val trackerState: SliderState?
+ get() = sliderTracker?.currentState
/**
* A waiting [Job] for a timer that estimates the key-up event when a key-down event is
@@ -89,14 +80,20 @@
get() = keyUpJob != null && keyUpJob?.isActive == true
/**
- * Start the plugin.
- *
- * This starts the tracking of slider states, events and triggering of haptic feedback.
+ * Specify the scope for the plugin's operations and start the slider tracker in this scope.
+ * This also involves the key-up timer job.
*/
- fun start() {
- if (!isTracking) {
- sliderTracker.startTracking()
- }
+ fun startInScope(scope: CoroutineScope) {
+ if (sliderTracker != null) stop()
+ sliderTracker =
+ SeekableSliderTracker(
+ sliderHapticFeedbackProvider,
+ sliderEventProducer,
+ scope,
+ sliderTrackerConfig,
+ )
+ pluginScope = scope
+ sliderTracker?.startTracking()
}
/**
@@ -104,7 +101,7 @@
*
* This stops the tracking of slider states, events and triggers of haptic feedback.
*/
- fun stop() = sliderTracker.stopTracking()
+ fun stop() = sliderTracker?.stopTracking()
/** React to a touch event */
fun onTouchEvent(event: MotionEvent?) {
@@ -147,9 +144,9 @@
/**
* An external key was pressed (e.g., a volume key).
*
- * This event is used to estimate the key-up event based on by running a timer as a waiting
- * coroutine in the [keyUpTimerScope]. A key-up event in a slider corresponds to an onArrowUp
- * event. Therefore, [onArrowUp] must be called after the timeout.
+ * This event is used to estimate the key-up event based on a running a timer as a waiting
+ * coroutine in the [pluginScope]. A key-up event in a slider corresponds to an onArrowUp event.
+ * Therefore, [onArrowUp] must be called after the timeout.
*/
fun onKeyDown() {
if (!isTracking) return
@@ -159,7 +156,7 @@
keyUpJob?.cancel()
}
keyUpJob =
- applicationScope.launch {
+ pluginScope?.launch {
delay(KEY_UP_TIMEOUT)
onArrowUp()
}
diff --git a/packages/SystemUI/src/com/android/systemui/haptics/slider/SeekableSliderTracker.kt b/packages/SystemUI/src/com/android/systemui/haptics/slider/SeekableSliderTracker.kt
index 10098fa..0af3038 100644
--- a/packages/SystemUI/src/com/android/systemui/haptics/slider/SeekableSliderTracker.kt
+++ b/packages/SystemUI/src/com/android/systemui/haptics/slider/SeekableSliderTracker.kt
@@ -17,9 +17,7 @@
package com.android.systemui.haptics.slider
import androidx.annotation.VisibleForTesting
-import com.android.systemui.dagger.qualifiers.Main
import kotlin.math.abs
-import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
@@ -31,21 +29,20 @@
*
* The tracker runs a state machine to execute actions on touch-based events typical of a seekable
* slider such as [android.widget.SeekBar]. Coroutines responsible for running the state machine,
- * collecting slider events and maintaining waiting states are run on the main thread via the
- * [com.android.systemui.dagger.qualifiers.Main] coroutine dispatcher.
+ * collecting slider events and maintaining waiting states are run on the provided [CoroutineScope].
*
* @param[sliderStateListener] Listener of the slider state.
* @param[sliderEventProducer] Producer of slider events arising from the slider.
- * @param[mainDispatcher] [CoroutineDispatcher] used to launch coroutines for the collection of
- * slider events and the launch of timer jobs.
+ * @param[trackerScope] [CoroutineScope] used to launch coroutines for the collection of slider
+ * events and the launch of timer jobs.
* @property[config] Configuration parameters of the slider tracker.
*/
class SeekableSliderTracker(
sliderStateListener: SliderStateListener,
sliderEventProducer: SliderEventProducer,
- @Main mainDispatcher: CoroutineDispatcher,
+ trackerScope: CoroutineScope,
private val config: SeekableSliderTrackerConfig = SeekableSliderTrackerConfig(),
-) : SliderTracker(CoroutineScope(mainDispatcher), sliderStateListener, sliderEventProducer) {
+) : SliderTracker(trackerScope, sliderStateListener, sliderEventProducer) {
// History of the latest progress collected from slider events
private var latestProgress = 0f
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/stickykeys/data/repository/StickyKeysRepository.kt b/packages/SystemUI/src/com/android/systemui/keyboard/stickykeys/data/repository/StickyKeysRepository.kt
index ec29bd6..89cdd25 100644
--- a/packages/SystemUI/src/com/android/systemui/keyboard/stickykeys/data/repository/StickyKeysRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/stickykeys/data/repository/StickyKeysRepository.kt
@@ -32,19 +32,13 @@
import com.android.systemui.keyboard.stickykeys.shared.model.ModifierKey.CTRL
import com.android.systemui.keyboard.stickykeys.shared.model.ModifierKey.META
import com.android.systemui.keyboard.stickykeys.shared.model.ModifierKey.SHIFT
-import com.android.systemui.user.data.repository.UserRepository
-import com.android.systemui.util.settings.SecureSettings
-import com.android.systemui.util.settings.SettingsProxyExt.observerFlow
+import com.android.systemui.util.settings.repository.UserAwareSecureSettingsRepository
import kotlinx.coroutines.CoroutineDispatcher
-import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.distinctUntilChanged
-import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
-import kotlinx.coroutines.flow.onStart
import javax.inject.Inject
interface StickyKeysRepository {
@@ -53,14 +47,12 @@
}
@SysUISingleton
-@OptIn(ExperimentalCoroutinesApi::class)
class StickyKeysRepositoryImpl
@Inject
constructor(
private val inputManager: InputManager,
@Background private val backgroundDispatcher: CoroutineDispatcher,
- private val secureSettings: SecureSettings,
- userRepository: UserRepository,
+ secureSettingsRepository: UserAwareSecureSettingsRepository,
private val stickyKeysLogger: StickyKeysLogger,
) : StickyKeysRepository {
@@ -78,25 +70,10 @@
.flowOn(backgroundDispatcher)
override val settingEnabled: Flow<Boolean> =
- userRepository.selectedUserInfo
- .flatMapLatest { stickyKeySettingObserver(it.id) }
- .flowOn(backgroundDispatcher)
-
- private fun stickyKeySettingObserver(userId: Int): Flow<Boolean> {
- return secureSettings
- .observerFlow(userId, SETTING_KEY)
- .onStart { emit(Unit) }
- .map { isSettingEnabledForCurrentUser(userId) }
- .distinctUntilChanged()
+ secureSettingsRepository
+ .boolSettingForActiveUser(SETTING_KEY, defaultValue = false)
.onEach { stickyKeysLogger.logNewSettingValue(it) }
- }
-
- private fun isSettingEnabledForCurrentUser(userId: Int) =
- secureSettings.getIntForUser(
- /* name= */ SETTING_KEY,
- /* default= */ 0,
- /* userHandle= */ userId
- ) != 0
+ .flowOn(backgroundDispatcher)
private fun toStickyKeysMap(state: StickyModifierState): LinkedHashMap<ModifierKey, Locked> {
val keys = linkedMapOf<ModifierKey, Locked>()
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/stickykeys/shared/model/StickyKey.kt b/packages/SystemUI/src/com/android/systemui/keyboard/stickykeys/shared/model/StickyKey.kt
index d5f082a..72a81cb 100644
--- a/packages/SystemUI/src/com/android/systemui/keyboard/stickykeys/shared/model/StickyKey.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/stickykeys/shared/model/StickyKey.kt
@@ -19,10 +19,10 @@
@JvmInline
value class Locked(val locked: Boolean)
-enum class ModifierKey(val text: String) {
+enum class ModifierKey(val displayedText: String) {
ALT("ALT LEFT"),
ALT_GR("ALT RIGHT"),
CTRL("CTRL"),
- META("META"),
+ META("ACTION"),
SHIFT("SHIFT"),
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/stickykeys/ui/StickyKeyDialogFactory.kt b/packages/SystemUI/src/com/android/systemui/keyboard/stickykeys/ui/StickyKeyDialogFactory.kt
new file mode 100644
index 0000000..3ed58a7
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/stickykeys/ui/StickyKeyDialogFactory.kt
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2024 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.keyboard.stickykeys.ui
+
+import android.app.Dialog
+import android.content.Context
+import android.view.Gravity
+import android.view.Window
+import android.view.WindowManager
+import android.view.WindowManager.LayoutParams.FLAG_DIM_BEHIND
+import android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
+import android.view.WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE
+import android.view.WindowManager.LayoutParams.TYPE_STATUS_BAR_SUB_PANEL
+import androidx.activity.ComponentDialog
+import com.android.systemui.compose.ComposeFacade
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.keyboard.stickykeys.ui.viewmodel.StickyKeysIndicatorViewModel
+import com.android.systemui.res.R
+import javax.inject.Inject
+
+@SysUISingleton
+class StickyKeyDialogFactory
+@Inject
+constructor(
+ @Application val context: Context,
+) {
+
+ fun create(viewModel: StickyKeysIndicatorViewModel): Dialog {
+ return createStickyKeyIndicator(viewModel)
+ }
+
+ private fun createStickyKeyIndicator(viewModel: StickyKeysIndicatorViewModel): Dialog {
+ return ComponentDialog(context, R.style.Theme_SystemUI_Dialog).apply {
+ // because we're requesting window feature it must be called before setting content
+ window?.setStickyKeyWindowAttributes()
+ setContentView(ComposeFacade.createStickyKeysIndicatorContent(context, viewModel))
+ }
+ }
+
+ private fun Window.setStickyKeyWindowAttributes() {
+ requestFeature(Window.FEATURE_NO_TITLE)
+ setType(TYPE_STATUS_BAR_SUB_PANEL)
+ addFlags(FLAG_NOT_FOCUSABLE or FLAG_NOT_TOUCHABLE)
+ clearFlags(FLAG_DIM_BEHIND)
+ setGravity(Gravity.TOP or Gravity.END)
+ attributes =
+ WindowManager.LayoutParams().apply {
+ copyFrom(attributes)
+ title = "StickyKeysIndicator"
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/stickykeys/ui/StickyKeysIndicatorCoordinator.kt b/packages/SystemUI/src/com/android/systemui/keyboard/stickykeys/ui/StickyKeysIndicatorCoordinator.kt
index c3a618d..842fd04 100644
--- a/packages/SystemUI/src/com/android/systemui/keyboard/stickykeys/ui/StickyKeysIndicatorCoordinator.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/stickykeys/ui/StickyKeysIndicatorCoordinator.kt
@@ -18,16 +18,11 @@
import android.app.Dialog
import android.util.Log
-import android.view.Gravity
-import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
-import android.view.Window
-import android.view.WindowManager
import com.android.systemui.compose.ComposeFacade
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.keyboard.stickykeys.StickyKeysLogger
import com.android.systemui.keyboard.stickykeys.ui.viewmodel.StickyKeysIndicatorViewModel
-import com.android.systemui.statusbar.phone.SystemUIDialogFactory
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import javax.inject.Inject
@@ -37,7 +32,7 @@
@Inject
constructor(
@Application private val applicationScope: CoroutineScope,
- private val dialogFactory: SystemUIDialogFactory,
+ private val stickyKeyDialogFactory: StickyKeyDialogFactory,
private val viewModel: StickyKeysIndicatorViewModel,
private val stickyKeysLogger: StickyKeysLogger,
) {
@@ -57,25 +52,10 @@
dialog?.dismiss()
dialog = null
} else if (dialog == null) {
- dialog = ComposeFacade.createStickyKeysDialog(dialogFactory, viewModel).apply {
- window?.setAttributes()
- show()
- }
+ dialog = stickyKeyDialogFactory.create(viewModel)
+ dialog?.show()
}
}
}
}
-
- private fun Window.setAttributes() {
- setType(WindowManager.LayoutParams.TYPE_STATUS_BAR_SUB_PANEL)
- addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED)
- addFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE)
- clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND)
- setGravity(Gravity.TOP or Gravity.END)
- attributes = WindowManager.LayoutParams().apply {
- copyFrom(attributes)
- width = WRAP_CONTENT
- title = "StickyKeysIndicator"
- }
- }
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardIndication.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardIndication.java
index 9b83b75..ee3706a 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardIndication.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardIndication.java
@@ -80,7 +80,7 @@
}
/**
- * Click listener for messsage.
+ * Click listener for message.
*/
public @Nullable View.OnClickListener getClickListener() {
return mOnClickListener;
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java
index b5f9c69..2e233d8 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java
@@ -17,7 +17,6 @@
package com.android.systemui.keyguard;
import static android.content.pm.PackageManager.PERMISSION_GRANTED;
-import static android.view.RemoteAnimationTarget.MODE_CLOSING;
import static android.view.RemoteAnimationTarget.MODE_OPENING;
import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_APPEARING;
import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_GOING_AWAY;
@@ -61,7 +60,6 @@
import android.view.RemoteAnimationDefinition;
import android.view.RemoteAnimationTarget;
import android.view.SurfaceControl;
-import android.view.WindowManager;
import android.view.WindowManagerPolicyConstants;
import android.window.IRemoteTransition;
import android.window.IRemoteTransitionFinishedCallback;
@@ -108,20 +106,7 @@
private final ScreenOnCoordinator mScreenOnCoordinator;
private final ShellTransitions mShellTransitions;
private final DisplayTracker mDisplayTracker;
- private PowerInteractor mPowerInteractor;
-
- private static int newModeToLegacyMode(int newMode) {
- switch (newMode) {
- case WindowManager.TRANSIT_OPEN:
- case WindowManager.TRANSIT_TO_FRONT:
- return MODE_OPENING;
- case WindowManager.TRANSIT_CLOSE:
- case WindowManager.TRANSIT_TO_BACK:
- return MODE_CLOSING;
- default:
- return 2; // MODE_CHANGING
- }
- }
+ private final PowerInteractor mPowerInteractor;
private static RemoteAnimationTarget[] wrap(TransitionInfo info, boolean wallpapers,
SurfaceControl.Transaction t, ArrayMap<SurfaceControl, SurfaceControl> leashMap,
@@ -253,8 +238,7 @@
public void mergeAnimation(IBinder candidateTransition, TransitionInfo candidateInfo,
SurfaceControl.Transaction candidateT, IBinder currentTransition,
- IRemoteTransitionFinishedCallback candidateFinishCallback)
- throws RemoteException {
+ IRemoteTransitionFinishedCallback candidateFinishCallback) {
if ((candidateInfo.getFlags() & TRANSIT_FLAG_KEYGUARD_APPEARING) != 0) {
keyguardViewMediator.setPendingLock(true);
keyguardViewMediator.cancelKeyguardExitAnimation();
@@ -265,13 +249,13 @@
runner.onAnimationCancelled();
finish(currentTransition);
} catch (RemoteException e) {
- // nothing, we'll just let it finish on its own I guess.
+ // Ignore.
}
}
@Override
- public void onTransitionConsumed(IBinder transition, boolean aborted)
- throws RemoteException {
+ public void onTransitionConsumed(IBinder transition, boolean aborted) {
+ // No-op.
}
private static void initAlphaForAnimationTargets(@NonNull SurfaceControl.Transaction t,
@@ -283,7 +267,7 @@
}
private void finish(IBinder transition) throws RemoteException {
- IRemoteTransitionFinishedCallback finishCallback = null;
+ final IRemoteTransitionFinishedCallback finishCallback;
SurfaceControl.Transaction finishTransaction = null;
synchronized (mLeashMap) {
@@ -343,7 +327,7 @@
@Override
public void onCreate() {
- ((SystemUIApplication) getApplication()).startServicesIfNeeded();
+ ((SystemUIApplication) getApplication()).startSystemUserServicesIfNeeded();
if (mShellTransitions == null || !Transitions.ENABLE_SHELL_TRANSITIONS) {
RemoteAnimationDefinition definition = new RemoteAnimationDefinition();
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardUnlockAnimationController.kt b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardUnlockAnimationController.kt
index 53c81e5..e18e463 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardUnlockAnimationController.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardUnlockAnimationController.kt
@@ -109,7 +109,7 @@
* This animation will take place entirely within the Launcher window. We can safely unlock the
* device, end remote animations, etc. even if this is still running.
*/
-const val LAUNCHER_ICONS_ANIMATION_DURATION_MS = 633L
+const val LAUNCHER_ICONS_ANIMATION_DURATION_MS = 1633L
/**
* How long to wait for the shade to get out of the way before starting the canned unlock animation.
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt
index 7f43fac..abe49ee 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt
@@ -20,6 +20,12 @@
import android.content.Context
import android.view.LayoutInflater
import android.view.View
+import androidx.constraintlayout.widget.ConstraintSet
+import androidx.constraintlayout.widget.ConstraintSet.BOTTOM
+import androidx.constraintlayout.widget.ConstraintSet.END
+import androidx.constraintlayout.widget.ConstraintSet.PARENT_ID
+import androidx.constraintlayout.widget.ConstraintSet.START
+import androidx.constraintlayout.widget.ConstraintSet.TOP
import com.android.internal.jank.InteractionJankMonitor
import com.android.keyguard.KeyguardStatusView
import com.android.keyguard.KeyguardStatusViewController
@@ -29,10 +35,13 @@
import com.android.systemui.CoreStartable
import com.android.systemui.Flags.keyguardBottomAreaRefactor
import com.android.systemui.common.ui.ConfigurationState
+import com.android.systemui.compose.ComposeFacade
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.deviceentry.domain.interactor.DeviceEntryHapticsInteractor
import com.android.systemui.deviceentry.shared.DeviceEntryUdfpsRefactor
import com.android.systemui.flags.FeatureFlagsClassic
+import com.android.systemui.keyguard.shared.ComposeLockscreen
+import com.android.systemui.keyguard.shared.model.LockscreenSceneBlueprint
import com.android.systemui.keyguard.ui.binder.KeyguardBlueprintViewBinder
import com.android.systemui.keyguard.ui.binder.KeyguardIndicationAreaBinder
import com.android.systemui.keyguard.ui.binder.KeyguardRootViewBinder
@@ -44,6 +53,7 @@
import com.android.systemui.keyguard.ui.viewmodel.KeyguardClockViewModel
import com.android.systemui.keyguard.ui.viewmodel.KeyguardIndicationAreaViewModel
import com.android.systemui.keyguard.ui.viewmodel.KeyguardRootViewModel
+import com.android.systemui.keyguard.ui.viewmodel.LockscreenContentViewModel
import com.android.systemui.keyguard.ui.viewmodel.OccludingAppDeviceEntryMessageViewModel
import com.android.systemui.plugins.FalsingManager
import com.android.systemui.res.R
@@ -88,6 +98,8 @@
private val falsingManager: FalsingManager,
private val aodAlphaViewModel: AodAlphaViewModel,
private val keyguardClockViewModel: KeyguardClockViewModel,
+ private val lockscreenContentViewModel: LockscreenContentViewModel,
+ private val lockscreenSceneBlueprintsLazy: Lazy<Set<LockscreenSceneBlueprint>>,
) : CoreStartable {
private var rootViewHandle: DisposableHandle? = null
@@ -115,11 +127,28 @@
initializeViews()
if (!SceneContainerFlag.isEnabled) {
- KeyguardBlueprintViewBinder.bind(
- keyguardRootView,
- keyguardBlueprintViewModel,
- keyguardClockViewModel
- )
+ if (ComposeLockscreen.isEnabled) {
+ val composeView =
+ ComposeFacade.createLockscreen(
+ context = context,
+ viewModel = lockscreenContentViewModel,
+ blueprints = lockscreenSceneBlueprintsLazy.get(),
+ )
+ composeView.id = View.generateViewId()
+ val cs = ConstraintSet()
+ cs.clone(keyguardRootView)
+ cs.connect(composeView.id, START, PARENT_ID, START)
+ cs.connect(composeView.id, END, PARENT_ID, END)
+ cs.connect(composeView.id, TOP, PARENT_ID, TOP)
+ cs.connect(composeView.id, BOTTOM, PARENT_ID, BOTTOM)
+ keyguardRootView.addView(composeView)
+ } else {
+ KeyguardBlueprintViewBinder.bind(
+ keyguardRootView,
+ keyguardBlueprintViewModel,
+ keyguardClockViewModel,
+ )
+ }
}
keyguardBlueprintCommandListener.start()
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
index f085e88..0ee924d 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
@@ -130,8 +130,8 @@
import com.android.systemui.DejankUtils;
import com.android.systemui.Dumpable;
import com.android.systemui.EventLogTags;
-import com.android.systemui.animation.ActivityLaunchAnimator;
-import com.android.systemui.animation.LaunchAnimator;
+import com.android.systemui.animation.ActivityTransitionAnimator;
+import com.android.systemui.animation.TransitionAnimator;
import com.android.systemui.broadcast.BroadcastDispatcher;
import com.android.systemui.classifier.FalsingCollector;
import com.android.systemui.dagger.qualifiers.Main;
@@ -444,7 +444,7 @@
/**
* Whether a hide is pending and we are just waiting for #startKeyguardExitAnimation to be
* called.
- * */
+ */
private boolean mHiding;
/**
@@ -689,17 +689,18 @@
} else {
resetStateLocked();
}
- }
- if (simState == TelephonyManager.SIM_STATE_ABSENT) {
- // MVNO SIMs can become transiently NOT_READY when switching networks,
- // so we should only lock when they are ABSENT.
- if (lastSimStateWasLocked) {
- if (DEBUG_SIM_STATES) Log.d(TAG, "SIM moved to ABSENT when the "
- + "previous state was locked. Reset the state.");
+ } else {
+ if (lastSimStateWasLocked && mShowing) {
+ if (DEBUG_SIM_STATES) {
+ Log.d(TAG, "SIM moved to "
+ + "NOT_READY/ABSENT/UNKNOWN when the previous state "
+ + "was locked. Reset the state.");
+ }
resetStateLocked();
}
- mSimWasLocked.append(slotId, false);
}
+
+ mSimWasLocked.append(slotId, false);
}
break;
case TelephonyManager.SIM_STATE_PIN_REQUIRED:
@@ -956,16 +957,17 @@
* Animation launch controller for activities that occlude the keyguard.
*/
@VisibleForTesting
- final ActivityLaunchAnimator.Controller mOccludeAnimationController =
- new ActivityLaunchAnimator.Controller() {
+ final ActivityTransitionAnimator.Controller mOccludeAnimationController =
+ new ActivityTransitionAnimator.Controller() {
@Override
- public void onLaunchAnimationStart(boolean isExpandingFullyAbove) {
+ public void onTransitionAnimationStart(boolean isExpandingFullyAbove) {
mOccludeAnimationPlaying = true;
mScrimControllerLazy.get().setOccludeAnimationPlaying(true);
}
@Override
- public void onLaunchAnimationCancelled(@Nullable Boolean newKeyguardOccludedState) {
+ public void onTransitionAnimationCancelled(
+ @Nullable Boolean newKeyguardOccludedState) {
Log.d(TAG, "Occlude launch animation cancelled. Occluded state is now: "
+ mOccluded);
mOccludeAnimationPlaying = false;
@@ -976,7 +978,7 @@
}
@Override
- public void onLaunchAnimationEnd(boolean launchIsFullScreen) {
+ public void onTransitionAnimationEnd(boolean launchIsFullScreen) {
if (launchIsFullScreen) {
mShadeController.get().instantCollapseShade();
}
@@ -993,23 +995,23 @@
@NonNull
@Override
- public ViewGroup getLaunchContainer() {
+ public ViewGroup getTransitionContainer() {
return ((ViewGroup) mKeyguardViewControllerLazy.get()
.getViewRootImpl().getView());
}
@Override
- public void setLaunchContainer(@NonNull ViewGroup launchContainer) {
+ public void setTransitionContainer(@NonNull ViewGroup transitionContainer) {
// No-op, launch container is always the shade.
Log.wtf(TAG, "Someone tried to change the launch container for the "
- + "ActivityLaunchAnimator, which should never happen.");
+ + "ActivityTransitionAnimator, which should never happen.");
}
@NonNull
@Override
- public LaunchAnimator.State createAnimatorState() {
- final int fullWidth = getLaunchContainer().getWidth();
- final int fullHeight = getLaunchContainer().getHeight();
+ public TransitionAnimator.State createAnimatorState() {
+ final int fullWidth = getTransitionContainer().getWidth();
+ final int fullHeight = getTransitionContainer().getHeight();
if (mUpdateMonitor.isSecureCameraLaunchedOverKeyguard()) {
final float initialHeight = fullHeight / 3f;
@@ -1017,7 +1019,7 @@
// Start the animation near the power button, at one-third size, since the
// camera was launched from the power button.
- return new LaunchAnimator.State(
+ return new TransitionAnimator.State(
(int) (mPowerButtonY - initialHeight / 2f) /* top */,
(int) (mPowerButtonY + initialHeight / 2f) /* bottom */,
(int) (fullWidth - initialWidth) /* left */,
@@ -1029,7 +1031,7 @@
// Start the animation in the center of the screen, scaled down to half
// size.
- return new LaunchAnimator.State(
+ return new TransitionAnimator.State(
(int) (fullHeight - initialHeight) / 2,
(int) (initialHeight + (fullHeight - initialHeight) / 2),
(int) (fullWidth - initialWidth) / 2,
@@ -1166,8 +1168,8 @@
/**
* Animation controller for activities that unocclude the keyguard. This does not use the
- * ActivityLaunchAnimator since we're just translating down, rather than emerging from a view
- * or the power button.
+ * ActivityTransitionAnimator since we're just translating down, rather than emerging from a
+ * view or the power button.
*/
private final IRemoteAnimationRunner mUnoccludeAnimationRunner =
new IRemoteAnimationRunner.Stub() {
@@ -1346,7 +1348,7 @@
private boolean mWallpaperSupportsAmbientMode;
private final KeyguardTransitions mKeyguardTransitions;
- private final Lazy<ActivityLaunchAnimator> mActivityLaunchAnimator;
+ private final Lazy<ActivityTransitionAnimator> mActivityTransitionAnimator;
private final Lazy<ScrimController> mScrimControllerLazy;
private final IActivityTaskManager mActivityTaskManagerService;
@@ -1393,7 +1395,7 @@
WallpaperRepository wallpaperRepository,
Lazy<ShadeController> shadeControllerLazy,
Lazy<NotificationShadeWindowController> notificationShadeWindowControllerLazy,
- Lazy<ActivityLaunchAnimator> activityLaunchAnimator,
+ Lazy<ActivityTransitionAnimator> activityTransitionAnimator,
Lazy<ScrimController> scrimControllerLazy,
IActivityTaskManager activityTaskManagerService,
FeatureFlags featureFlags,
@@ -1458,7 +1460,7 @@
mJavaAdapter = javaAdapter;
mWallpaperRepository = wallpaperRepository;
- mActivityLaunchAnimator = activityLaunchAnimator;
+ mActivityTransitionAnimator = activityTransitionAnimator;
mScrimControllerLazy = scrimControllerLazy;
mActivityTaskManagerService = activityTaskManagerService;
@@ -3811,15 +3813,15 @@
/**
* Implementation of RemoteAnimationRunner that creates a new
- * {@link ActivityLaunchAnimator.Runner} whenever onAnimationStart is called, delegating the
+ * {@link ActivityTransitionAnimator.Runner} whenever onAnimationStart is called, delegating the
* remote animation methods to that runner.
*/
private class ActivityLaunchRemoteAnimationRunner extends IRemoteAnimationRunner.Stub {
- private final ActivityLaunchAnimator.Controller mActivityLaunchController;
- @Nullable private ActivityLaunchAnimator.Runner mRunner;
+ private final ActivityTransitionAnimator.Controller mActivityLaunchController;
+ @Nullable private ActivityTransitionAnimator.Runner mRunner;
- ActivityLaunchRemoteAnimationRunner(ActivityLaunchAnimator.Controller controller) {
+ ActivityLaunchRemoteAnimationRunner(ActivityTransitionAnimator.Controller controller) {
mActivityLaunchController = controller;
}
@@ -3836,7 +3838,7 @@
RemoteAnimationTarget[] nonApps,
IRemoteAnimationFinishedCallback finishedCallback)
throws RemoteException {
- mRunner = mActivityLaunchAnimator.get().createRunner(mActivityLaunchController);
+ mRunner = mActivityTransitionAnimator.get().createRunner(mActivityLaunchController);
mRunner.onAnimationStart(transit, apps, wallpapers, nonApps, finishedCallback);
}
}
@@ -3849,7 +3851,7 @@
extends ActivityLaunchRemoteAnimationRunner {
OccludeActivityLaunchRemoteAnimationRunner(
- ActivityLaunchAnimator.Controller controller) {
+ ActivityTransitionAnimator.Controller controller) {
super(controller);
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java b/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java
index 70da3e7..0b227fa 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java
@@ -35,7 +35,7 @@
import com.android.keyguard.dagger.KeyguardUserSwitcherComponent;
import com.android.keyguard.mediator.ScreenOnCoordinator;
import com.android.systemui.CoreStartable;
-import com.android.systemui.animation.ActivityLaunchAnimator;
+import com.android.systemui.animation.ActivityTransitionAnimator;
import com.android.systemui.broadcast.BroadcastDispatcher;
import com.android.systemui.classifier.FalsingCollector;
import com.android.systemui.classifier.FalsingModule;
@@ -150,7 +150,7 @@
WallpaperRepository wallpaperRepository,
Lazy<ShadeController> shadeController,
Lazy<NotificationShadeWindowController> notificationShadeWindowController,
- Lazy<ActivityLaunchAnimator> activityLaunchAnimator,
+ Lazy<ActivityTransitionAnimator> activityTransitionAnimator,
Lazy<ScrimController> scrimControllerLazy,
IActivityTaskManager activityTaskManagerService,
FeatureFlags featureFlags,
@@ -196,7 +196,7 @@
wallpaperRepository,
shadeController,
notificationShadeWindowController,
- activityLaunchAnimator,
+ activityTransitionAnimator,
scrimControllerLazy,
activityTaskManagerService,
featureFlags,
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/VideoCameraQuickAffordanceConfig.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/VideoCameraQuickAffordanceConfig.kt
index bbdd903..3e6e3b7 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/VideoCameraQuickAffordanceConfig.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/VideoCameraQuickAffordanceConfig.kt
@@ -50,14 +50,13 @@
@Background private val backgroundDispatcher: CoroutineDispatcher,
) : KeyguardQuickAffordanceConfig {
- private val intent: Intent by lazy {
- cameraIntents.getVideoCameraIntent().apply {
+ private val intent: Intent
+ get() = cameraIntents.getVideoCameraIntent(userTracker.userId).apply {
putExtra(
CameraIntents.EXTRA_LAUNCH_SOURCE,
StatusBarManager.CAMERA_LAUNCH_SOURCE_QUICK_AFFORDANCE,
)
}
- }
override val key: String
get() = BuiltInKeyguardQuickAffordanceKeys.VIDEO_CAMERA
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/DeviceEntryFingerprintAuthRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/DeviceEntryFingerprintAuthRepository.kt
index 9a13558d..b152eea 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/DeviceEntryFingerprintAuthRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/DeviceEntryFingerprintAuthRepository.kt
@@ -22,6 +22,7 @@
import com.android.keyguard.KeyguardUpdateMonitor
import com.android.keyguard.KeyguardUpdateMonitorCallback
import com.android.systemui.biometrics.AuthController
+import com.android.systemui.biometrics.shared.model.AuthenticationReason
import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging
import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
import com.android.systemui.dagger.SysUISingleton
@@ -174,6 +175,8 @@
mainDispatcher
) // keyguardUpdateMonitor requires registration on main thread.
+ // TODO(b/322555228) Remove after consolidating device entry auth messages with BP auth messages
+ // in BiometricStatusRepository
override val authenticationStatus: Flow<FingerprintAuthenticationStatus>
get() = conflatedCallbackFlow {
val callback =
@@ -236,7 +239,8 @@
sendUpdateIfFingerprint(
biometricSourceType,
AcquiredFingerprintAuthenticationStatus(
- acquireInfo,
+ AuthenticationReason.DeviceEntryAuthentication,
+ acquireInfo
),
)
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardClockRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardClockRepository.kt
index 5e3779a..59288a1 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardClockRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardClockRepository.kt
@@ -60,6 +60,8 @@
val currentClock: StateFlow<ClockController?>
+ val previewClock: StateFlow<ClockController>
+
val clockEventController: ClockEventController
fun setClockSize(@ClockSize size: Int)
}
@@ -120,6 +122,15 @@
initialValue = clockRegistry.createCurrentClock()
)
+ override val previewClock: StateFlow<ClockController> =
+ currentClockId
+ .map { clockRegistry.createCurrentClock() }
+ .stateIn(
+ scope = applicationScope,
+ started = SharingStarted.WhileSubscribed(),
+ initialValue = clockRegistry.createCurrentClock()
+ )
+
@VisibleForTesting
suspend fun getClockSize(): SettingsClockSize {
return withContext(backgroundDispatcher) {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/BurnInInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/BurnInInteractor.kt
index cc1cf91..7ae70a9 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/BurnInInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/BurnInInteractor.kt
@@ -74,6 +74,7 @@
BurnInModel(translationX, translationY, burnInHelperWrapper.burnInScale())
}
.distinctUntilChanged()
+ .stateIn(scope, SharingStarted.Lazily, BurnInModel())
/**
* Use for max burn-in offsets that are NOT specified in pixels. This flow will recalculate the
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/DeviceEntrySideFpsOverlayInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/DeviceEntrySideFpsOverlayInteractor.kt
index b1a2297..e017129 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/DeviceEntrySideFpsOverlayInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/DeviceEntrySideFpsOverlayInteractor.kt
@@ -94,6 +94,7 @@
context.resources.getBoolean(R.bool.config_show_sidefps_hint_on_bouncer)
val sfpsDetectionRunning = keyguardUpdateMonitor.isFingerprintDetectionRunning
val isUnlockingWithFpAllowed = keyguardUpdateMonitor.isUnlockingWithFingerprintAllowed
+
return primaryBouncerInteractor.isBouncerShowing() &&
sfpsEnabled &&
sfpsDetectionRunning &&
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAlternateBouncerTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAlternateBouncerTransitionInteractor.kt
index a97c152..0cf74a1 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAlternateBouncerTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAlternateBouncerTransitionInteractor.kt
@@ -24,15 +24,15 @@
import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository
import com.android.systemui.keyguard.shared.model.KeyguardState
import com.android.systemui.power.domain.interactor.PowerInteractor
-import com.android.systemui.util.kotlin.Utils.Companion.sample
-import com.android.systemui.util.kotlin.sample
+import com.android.systemui.util.kotlin.Utils.Companion.sample as sampleCombine
+import com.android.systemui.util.kotlin.sample as sampleUtil
import com.android.wm.shell.animation.Interpolators
import javax.inject.Inject
import kotlin.time.Duration.Companion.milliseconds
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.delay
-import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.FlowPreview
+import kotlinx.coroutines.flow.sample
import kotlinx.coroutines.launch
@SysUISingleton
@@ -62,14 +62,17 @@
listenForTransitionToCamera(scope, keyguardInteractor)
}
+ @FlowPreview
private fun listenForAlternateBouncerToLockscreenHubAodOrDozing() {
scope.launch {
keyguardInteractor.alternateBouncerShowing
// Add a slight delay, as alternateBouncer and primaryBouncer showing event changes
// will arrive with a small gap in time. This prevents a transition to LOCKSCREEN
// happening prematurely.
- .onEach { delay(50) }
- .sample(
+ // This should eventually be removed in favor of
+ // [KeyguardTransitionInteractor#startDismissKeyguardTransition]
+ .sample(150L)
+ .sampleCombine(
keyguardInteractor.primaryBouncerShowing,
startedKeyguardTransitionStep,
powerInteractor.isAwake,
@@ -111,19 +114,20 @@
private fun listenForAlternateBouncerToGone() {
scope.launch {
- keyguardInteractor.isKeyguardGoingAway.sample(finishedKeyguardState, ::Pair).collect {
- (isKeyguardGoingAway, keyguardState) ->
- if (isKeyguardGoingAway && keyguardState == KeyguardState.ALTERNATE_BOUNCER) {
- startTransitionTo(KeyguardState.GONE)
+ keyguardInteractor.isKeyguardGoingAway
+ .sampleUtil(finishedKeyguardState, ::Pair)
+ .collect { (isKeyguardGoingAway, keyguardState) ->
+ if (isKeyguardGoingAway && keyguardState == KeyguardState.ALTERNATE_BOUNCER) {
+ startTransitionTo(KeyguardState.GONE)
+ }
}
- }
}
}
private fun listenForAlternateBouncerToPrimaryBouncer() {
scope.launch {
keyguardInteractor.primaryBouncerShowing
- .sample(startedKeyguardTransitionStep, ::Pair)
+ .sampleUtil(startedKeyguardTransitionStep, ::Pair)
.collect { (isPrimaryBouncerShowing, startedKeyguardState) ->
if (
isPrimaryBouncerShowing &&
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractor.kt
index 8fa33ee7..e0b5c0e 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractor.kt
@@ -21,18 +21,18 @@
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.keyguard.KeyguardWmStateRefactor
import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository
import com.android.systemui.keyguard.shared.model.BiometricUnlockModel.Companion.isWakeAndUnlock
import com.android.systemui.keyguard.shared.model.DozeStateModel
import com.android.systemui.keyguard.shared.model.KeyguardState
import com.android.systemui.keyguard.shared.model.TransitionModeOnCanceled
-import com.android.systemui.util.kotlin.Utils.Companion.toTriple
+import com.android.systemui.util.kotlin.Utils.Companion.sample
import com.android.systemui.util.kotlin.sample
import javax.inject.Inject
import kotlin.time.Duration.Companion.milliseconds
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.launch
@SysUISingleton
@@ -85,18 +85,21 @@
keyguardInteractor
.dozeTransitionTo(DozeStateModel.FINISH)
.sample(
- combine(
- startedKeyguardTransitionStep,
- keyguardInteractor.isKeyguardOccluded,
- ::Pair
- ),
- ::toTriple
+ startedKeyguardTransitionStep,
+ keyguardInteractor.isKeyguardOccluded,
+ keyguardInteractor.biometricUnlockState,
)
- .collect { (_, lastStartedStep, occluded) ->
- if (lastStartedStep.to == KeyguardState.AOD && !occluded) {
+ .collect { (_, lastStartedStep, occluded, biometricUnlockState) ->
+ if (
+ lastStartedStep.to == KeyguardState.AOD &&
+ !occluded &&
+ !isWakeAndUnlock(biometricUnlockState)
+ ) {
val modeOnCanceled =
if (lastStartedStep.from == KeyguardState.LOCKSCREEN) {
TransitionModeOnCanceled.REVERSE
+ } else if (lastStartedStep.from == KeyguardState.GONE) {
+ TransitionModeOnCanceled.RESET
} else {
TransitionModeOnCanceled.LAST_VALUE
}
@@ -126,15 +129,29 @@
}
private fun listenForAodToGone() {
+ if (KeyguardWmStateRefactor.isEnabled) {
+ return
+ }
+
scope.launch {
keyguardInteractor.biometricUnlockState.sample(finishedKeyguardState, ::Pair).collect {
(biometricUnlockState, keyguardState) ->
+ KeyguardWmStateRefactor.assertInLegacyMode()
if (keyguardState == KeyguardState.AOD && isWakeAndUnlock(biometricUnlockState)) {
startTransitionTo(KeyguardState.GONE)
}
}
}
}
+
+ /**
+ * Dismisses AOD and transitions to GONE. This is called whenever authentication occurs while on
+ * AOD.
+ */
+ fun dismissAod() {
+ scope.launch { startTransitionTo(KeyguardState.GONE) }
+ }
+
override fun getDefaultAnimatorForTransitionsToState(toState: KeyguardState): ValueAnimator {
return ValueAnimator().apply {
interpolator = Interpolators.LINEAR
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractor.kt
index 7477624..6b85a63 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractor.kt
@@ -68,11 +68,11 @@
scope.launch {
keyguardInteractor.isKeyguardShowing
.sample(
- startedKeyguardTransitionStep,
+ currentKeyguardState,
communalInteractor.isIdleOnCommunal,
)
- .collect { (isKeyguardShowing, lastStartedStep, isIdleOnCommunal) ->
- if (isKeyguardShowing && lastStartedStep.to == KeyguardState.GONE) {
+ .collect { (isKeyguardShowing, currentState, isIdleOnCommunal) ->
+ if (isKeyguardShowing && currentState == KeyguardState.GONE) {
val to =
if (isIdleOnCommunal) {
KeyguardState.GLANCEABLE_HUB
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractor.kt
index deb70b7..1da0a0e 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractor.kt
@@ -26,7 +26,6 @@
import com.android.systemui.keyguard.KeyguardWmStateRefactor
import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository
import com.android.systemui.keyguard.shared.model.KeyguardState
-import com.android.systemui.keyguard.shared.model.KeyguardSurfaceBehindModel
import com.android.systemui.keyguard.shared.model.StatusBarState.KEYGUARD
import com.android.systemui.keyguard.shared.model.TransitionInfo
import com.android.systemui.keyguard.shared.model.TransitionModeOnCanceled
@@ -36,7 +35,6 @@
import com.android.systemui.util.kotlin.Utils.Companion.toQuad
import com.android.systemui.util.kotlin.Utils.Companion.toTriple
import com.android.systemui.util.kotlin.sample
-import dagger.Lazy
import java.util.UUID
import javax.inject.Inject
import kotlin.time.Duration.Companion.milliseconds
@@ -64,7 +62,7 @@
private val shadeRepository: ShadeRepository,
private val powerInteractor: PowerInteractor,
private val glanceableHubTransitions: GlanceableHubTransitions,
- inWindowLauncherUnlockAnimationInteractor: Lazy<InWindowLauncherUnlockAnimationInteractor>,
+ private val swipeToDismissInteractor: SwipeToDismissInteractor,
) :
TransitionInteractor(
fromState = KeyguardState.LOCKSCREEN,
@@ -102,49 +100,7 @@
return@map null
}
- true // TODO(b/278086361): Implement continuous swipe to unlock.
- }
- .onStart {
- // Default to null ("don't care, use a reasonable default").
- emit(null)
- }
- .distinctUntilChanged()
-
- /**
- * The surface behind view params to use for the transition from LOCKSCREEN, or null if we don't
- * care and should use a reasonable default.
- */
- val surfaceBehindModel: Flow<KeyguardSurfaceBehindModel?> =
- combine(
- transitionInteractor.startedKeyguardTransitionStep,
- transitionInteractor.transitionStepsFromState(KeyguardState.LOCKSCREEN),
- inWindowLauncherUnlockAnimationInteractor
- .get()
- .transitioningToGoneWithInWindowAnimation,
- ) { startedStep, fromLockscreenStep, transitioningToGoneWithInWindowAnimation ->
- if (startedStep.to != KeyguardState.GONE) {
- // Only LOCKSCREEN -> GONE has specific surface params (for the unlock
- // animation).
- return@combine null
- } else if (transitioningToGoneWithInWindowAnimation) {
- // If we're prepared for the in-window unlock, we're going to play an animation
- // in the window. Make it fully visible.
- KeyguardSurfaceBehindModel(
- alpha = 1f,
- )
- } else if (fromLockscreenStep.value > 0.5f) {
- // Start the animation once we're 50% transitioned to GONE.
- KeyguardSurfaceBehindModel(
- animateFromAlpha = 0f,
- alpha = 1f,
- animateFromTranslationY = 500f,
- translationY = 0f
- )
- } else {
- KeyguardSurfaceBehindModel(
- alpha = 0f,
- )
- }
+ true // Make the surface visible during LS -> GONE transitions.
}
.onStart {
// Default to null ("don't care, use a reasonable default").
@@ -325,6 +281,13 @@
private fun listenForLockscreenToGoneDragging() {
if (KeyguardWmStateRefactor.isEnabled) {
+ // When the refactor is enabled, we no longer use isKeyguardGoingAway.
+ scope.launch {
+ swipeToDismissInteractor.dismissFling.collect { _ ->
+ startTransitionTo(KeyguardState.GONE)
+ }
+ }
+
return
}
@@ -332,6 +295,7 @@
keyguardInteractor.isKeyguardGoingAway
.sample(startedKeyguardTransitionStep, ::Pair)
.collect { pair ->
+ KeyguardWmStateRefactor.assertInLegacyMode()
val (isKeyguardGoingAway, lastStartedStep) = pair
if (isKeyguardGoingAway && lastStartedStep.to == KeyguardState.LOCKSCREEN) {
startTransitionTo(KeyguardState.GONE)
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/InWindowLauncherUnlockAnimationInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/InWindowLauncherUnlockAnimationInteractor.kt
index e7d74a5..8ec831c 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/InWindowLauncherUnlockAnimationInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/InWindowLauncherUnlockAnimationInteractor.kt
@@ -23,7 +23,6 @@
import com.android.systemui.keyguard.shared.model.KeyguardState
import com.android.systemui.shared.system.ActivityManagerWrapper
import com.android.systemui.shared.system.smartspace.SmartspaceState
-import com.android.systemui.util.kotlin.sample
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.SharingStarted
@@ -52,10 +51,7 @@
val transitioningToGoneWithInWindowAnimation: StateFlow<Boolean> =
transitionInteractor
.isInTransitionToState(KeyguardState.GONE)
- .sample(repository.launcherActivityClass, ::Pair)
- .map { (isTransitioningToGone, launcherActivityClass) ->
- isTransitioningToGone && isActivityClassUnderneath(launcherActivityClass)
- }
+ .map { transitioningToGone -> transitioningToGone && isLauncherUnderneath() }
.stateIn(scope, SharingStarted.Eagerly, false)
/**
@@ -91,11 +87,11 @@
}
/**
- * Whether an activity with the given [activityClass] name is currently underneath the
- * lockscreen (it's at the top of the activity task stack).
+ * Whether the Launcher is currently underneath the lockscreen (it's at the top of the activity
+ * task stack).
*/
- private fun isActivityClassUnderneath(activityClass: String?): Boolean {
- return activityClass?.let {
+ fun isLauncherUnderneath(): Boolean {
+ return repository.launcherActivityClass.value?.let {
activityManager.runningTask?.topActivity?.className?.equals(it)
}
?: false
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardClockInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardClockInteractor.kt
index 356c408..196770a 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardClockInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardClockInteractor.kt
@@ -44,6 +44,8 @@
val currentClock: StateFlow<ClockController?> = keyguardClockRepository.currentClock
+ val previewClock: StateFlow<ClockController> = keyguardClockRepository.previewClock
+
var clock: ClockController? by keyguardClockRepository.clockEventController::clock
val clockSize: StateFlow<Int> = keyguardClockRepository.clockSize
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardSurfaceBehindInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardSurfaceBehindInteractor.kt
index 922baa3..c496a6e 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardSurfaceBehindInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardSurfaceBehindInteractor.kt
@@ -16,75 +16,87 @@
package com.android.systemui.keyguard.domain.interactor
+import android.content.Context
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.keyguard.data.repository.KeyguardSurfaceBehindRepository
+import com.android.systemui.keyguard.domain.interactor.WindowManagerLockscreenVisibilityInteractor.Companion.isSurfaceVisible
import com.android.systemui.keyguard.shared.model.KeyguardState
import com.android.systemui.keyguard.shared.model.KeyguardSurfaceBehindModel
+import com.android.systemui.statusbar.notification.domain.interactor.NotificationLaunchAnimationInteractor
+import com.android.systemui.util.kotlin.toPx
+import dagger.Lazy
import javax.inject.Inject
-import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
-import kotlinx.coroutines.flow.flatMapLatest
-import kotlinx.coroutines.flow.flowOf
-import kotlinx.coroutines.flow.map
+
+/**
+ * Distance over which the surface behind the keyguard is animated in during a Y-translation
+ * animation.
+ */
+const val SURFACE_TRANSLATION_Y_DISTANCE_DP = 250
@SysUISingleton
class KeyguardSurfaceBehindInteractor
@Inject
constructor(
private val repository: KeyguardSurfaceBehindRepository,
- private val fromLockscreenInteractor: FromLockscreenTransitionInteractor,
- private val fromPrimaryBouncerInteractor: FromPrimaryBouncerTransitionInteractor,
+ context: Context,
transitionInteractor: KeyguardTransitionInteractor,
+ inWindowLauncherUnlockAnimationInteractor: Lazy<InWindowLauncherUnlockAnimationInteractor>,
+ swipeToDismissInteractor: SwipeToDismissInteractor,
+ notificationLaunchInteractor: NotificationLaunchAnimationInteractor,
) {
-
- @OptIn(ExperimentalCoroutinesApi::class)
+ /**
+ * The view params to use for the surface. These params describe the alpha/translation values to
+ * apply, as well as animation parameters if necessary.
+ */
val viewParams: Flow<KeyguardSurfaceBehindModel> =
- transitionInteractor.isInTransitionToAnyState
- .flatMapLatest { isInTransition ->
- if (!isInTransition) {
- defaultParams
- } else {
- combine(
- transitionSpecificViewParams,
- defaultParams,
- ) { transitionParams, defaultParams ->
- transitionParams ?: defaultParams
+ combine(
+ transitionInteractor.startedKeyguardTransitionStep,
+ transitionInteractor.currentKeyguardState,
+ notificationLaunchInteractor.isLaunchAnimationRunning,
+ ) { startedStep, currentState, notifAnimationRunning ->
+ // If we're in transition to GONE, special unlock animation params apply.
+ if (startedStep.to == KeyguardState.GONE && currentState != KeyguardState.GONE) {
+ if (notifAnimationRunning) {
+ // If the notification launch animation is running, leave the alpha at 0f.
+ // The ActivityLaunchAnimator will morph it from the notification at the
+ // appropriate time.
+ return@combine KeyguardSurfaceBehindModel(
+ alpha = 0f,
+ )
+ } else if (
+ inWindowLauncherUnlockAnimationInteractor.get().isLauncherUnderneath()
+ ) {
+ // The Launcher icons have their own translation/alpha animations during the
+ // in-window animation. We'll just make the surface visible and let Launcher
+ // do its thing.
+ return@combine KeyguardSurfaceBehindModel(
+ alpha = 1f,
+ )
+ } else {
+ // Otherwise, animate a surface in via alpha/translation, and apply the
+ // swipe velocity (if available) to the translation spring.
+ return@combine KeyguardSurfaceBehindModel(
+ animateFromAlpha = 0f,
+ alpha = 1f,
+ animateFromTranslationY =
+ SURFACE_TRANSLATION_Y_DISTANCE_DP.toPx(context).toFloat(),
+ translationY = 0f,
+ startVelocity = swipeToDismissInteractor.dismissFling.value?.velocity
+ ?: 0f,
+ )
}
}
+
+ // Default to the visibility of the current state, with no animations.
+ KeyguardSurfaceBehindModel(alpha = if (isSurfaceVisible(currentState)) 1f else 0f)
}
.distinctUntilChanged()
val isAnimatingSurface = repository.isAnimatingSurface
- private val defaultParams =
- transitionInteractor.finishedKeyguardState.map { state ->
- KeyguardSurfaceBehindModel(
- alpha =
- if (WindowManagerLockscreenVisibilityInteractor.isSurfaceVisible(state)) 1f
- else 0f
- )
- }
-
- /**
- * View params provided by the transition interactor for the most recently STARTED transition.
- * This is used to run transition-specific animations on the surface.
- *
- * If null, there are no transition-specific view params needed for this transition and we will
- * use a reasonable default.
- */
- @OptIn(ExperimentalCoroutinesApi::class)
- private val transitionSpecificViewParams: Flow<KeyguardSurfaceBehindModel?> =
- transitionInteractor.startedKeyguardTransitionStep.flatMapLatest { startedStep ->
- when (startedStep.from) {
- KeyguardState.LOCKSCREEN -> fromLockscreenInteractor.surfaceBehindModel
- KeyguardState.PRIMARY_BOUNCER -> fromPrimaryBouncerInteractor.surfaceBehindModel
- // Return null for other states, where no transition specific params are needed.
- else -> flowOf(null)
- }
- }
-
fun setAnimatingSurface(animating: Boolean) {
repository.setAnimatingSurface(animating)
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt
index b43ab5e..310f13d 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt
@@ -60,6 +60,7 @@
private val fromLockscreenTransitionInteractor: dagger.Lazy<FromLockscreenTransitionInteractor>,
private val fromPrimaryBouncerTransitionInteractor:
dagger.Lazy<FromPrimaryBouncerTransitionInteractor>,
+ private val fromAodTransitionInteractor: dagger.Lazy<FromAodTransitionInteractor>,
) {
private val TAG = this::class.simpleName
@@ -346,6 +347,7 @@
when (val startedState = startedKeyguardState.replayCache.last()) {
LOCKSCREEN -> fromLockscreenTransitionInteractor.get().dismissKeyguard()
PRIMARY_BOUNCER -> fromPrimaryBouncerTransitionInteractor.get().dismissPrimaryBouncer()
+ AOD -> fromAodTransitionInteractor.get().dismissAod()
else ->
Log.e(
"KeyguardTransitionInteractor",
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LightRevealScrimInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LightRevealScrimInteractor.kt
index 19d00cf..c7f262a 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LightRevealScrimInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LightRevealScrimInteractor.kt
@@ -42,7 +42,7 @@
private val lightRevealScrimRepository: LightRevealScrimRepository,
@Application private val scope: CoroutineScope,
private val scrimLogger: ScrimLogger,
- powerInteractor: PowerInteractor,
+ private val powerInteractor: PowerInteractor,
) {
init {
@@ -83,11 +83,13 @@
// (invisible) jank. However, we need to still pass through 1f and 0f to ensure that the
// correct end states are respected even if the screen turned off (or was still off)
// when the animation finished
- powerInteractor.screenPowerState.value != ScreenPowerState.SCREEN_OFF ||
- it == 1f ||
- it == 0f
+ screenIsShowingContent() || it == 1f || it == 0f
}
+ private fun screenIsShowingContent() =
+ powerInteractor.screenPowerState.value != ScreenPowerState.SCREEN_OFF &&
+ powerInteractor.screenPowerState.value != ScreenPowerState.SCREEN_TURNING_ON
+
companion object {
/**
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/SwipeToDismissInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/SwipeToDismissInteractor.kt
new file mode 100644
index 0000000..86e4115
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/SwipeToDismissInteractor.kt
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyguard.domain.interactor
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.shade.data.repository.ShadeRepository
+import com.android.systemui.util.kotlin.Utils.Companion.sample
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.filter
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.stateIn
+
+/**
+ * Handles logic around the swipe to dismiss gesture, where the user swipes up on the dismissable
+ * lockscreen to unlock the device.
+ */
+@SysUISingleton
+class SwipeToDismissInteractor
+@Inject
+constructor(
+ @Background backgroundScope: CoroutineScope,
+ shadeRepository: ShadeRepository,
+ transitionInteractor: KeyguardTransitionInteractor,
+ keyguardInteractor: KeyguardInteractor,
+) {
+ /**
+ * Emits a [FlingInfo] whenever a swipe to dismiss gesture has started a fling animation on the
+ * lockscreen while it's dismissable.
+ *
+ * This value is collected by [FromLockscreenTransitionInteractor] to start a transition from
+ * LOCKSCREEN -> GONE, and by [KeyguardSurfaceBehindInteractor] to match the surface remote
+ * animation's velocity to the fling velocity, if applicable.
+ */
+ val dismissFling =
+ shadeRepository.currentFling
+ .sample(
+ transitionInteractor.startedKeyguardState,
+ keyguardInteractor.isKeyguardDismissible
+ )
+ .filter { (flingInfo, startedState, keyguardDismissable) ->
+ flingInfo != null &&
+ !flingInfo.expand &&
+ startedState == KeyguardState.LOCKSCREEN &&
+ keyguardDismissable
+ }
+ .map { (flingInfo, _) -> flingInfo }
+ .stateIn(backgroundScope, SharingStarted.Eagerly, null)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/TransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/TransitionInteractor.kt
index 5c2df45..3ccbdba 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/TransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/TransitionInteractor.kt
@@ -60,6 +60,7 @@
// The following are MutableSharedFlows, and do not require flowOn
val startedKeyguardState = transitionInteractor.startedKeyguardState
val finishedKeyguardState = transitionInteractor.finishedKeyguardState
+ val currentKeyguardState = transitionInteractor.currentKeyguardState
suspend fun startTransitionTo(
toState: KeyguardState,
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/WindowManagerLockscreenVisibilityInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/WindowManagerLockscreenVisibilityInteractor.kt
index 49af664..b81793e 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/WindowManagerLockscreenVisibilityInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/WindowManagerLockscreenVisibilityInteractor.kt
@@ -19,6 +19,8 @@
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.keyguard.shared.model.BiometricUnlockModel
import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.statusbar.notification.domain.interactor.NotificationLaunchAnimationInteractor
+import javax.inject.Inject
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
@@ -26,7 +28,6 @@
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
-import javax.inject.Inject
@SysUISingleton
class WindowManagerLockscreenVisibilityInteractor
@@ -37,6 +38,7 @@
surfaceBehindInteractor: KeyguardSurfaceBehindInteractor,
fromLockscreenInteractor: FromLockscreenTransitionInteractor,
fromBouncerInteractor: FromPrimaryBouncerTransitionInteractor,
+ notificationLaunchAnimationInteractor: NotificationLaunchAnimationInteractor,
) {
private val defaultSurfaceBehindVisibility =
transitionInteractor.finishedKeyguardState.map(::isSurfaceVisible)
@@ -72,8 +74,7 @@
*/
@OptIn(ExperimentalCoroutinesApi::class)
val surfaceBehindVisibility: Flow<Boolean> =
- transitionInteractor
- .isInTransitionToAnyState
+ transitionInteractor.isInTransitionToAnyState
.flatMapLatest { isInTransition ->
if (!isInTransition) {
defaultSurfaceBehindVisibility
@@ -99,12 +100,16 @@
combine(
transitionInteractor.isInTransitionToState(KeyguardState.GONE),
transitionInteractor.finishedKeyguardState,
- surfaceBehindInteractor.isAnimatingSurface
- ) { isInTransitionToGone, finishedState, isAnimatingSurface ->
+ surfaceBehindInteractor.isAnimatingSurface,
+ notificationLaunchAnimationInteractor.isLaunchAnimationRunning,
+ ) { isInTransitionToGone, finishedState, isAnimatingSurface, notifLaunchRunning ->
+ // Using the animation if we're animating it directly, or if the
+ // ActivityLaunchAnimator is in the process of animating it.
+ val animationsRunning = isAnimatingSurface || notifLaunchRunning
// We may still be animating the surface after the keyguard is fully GONE, since
// some animations (like the translation spring) are not tied directly to the
// transition step amount.
- isInTransitionToGone || (finishedState == KeyguardState.GONE && isAnimatingSurface)
+ isInTransitionToGone || (finishedState == KeyguardState.GONE && animationsRunning)
}
.distinctUntilChanged()
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/ComposeLockscreen.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/ComposeLockscreen.kt
new file mode 100644
index 0000000..7f0b483
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/ComposeLockscreen.kt
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyguard.shared
+
+import com.android.systemui.Flags
+import com.android.systemui.compose.ComposeFacade
+import com.android.systemui.flags.FlagToken
+import com.android.systemui.flags.RefactorFlagUtils
+
+/** Helper for reading or using the compose lockscreen flag state. */
+@Suppress("NOTHING_TO_INLINE")
+object ComposeLockscreen {
+ /** The aconfig flag name */
+ const val FLAG_NAME = Flags.FLAG_COMPOSE_LOCKSCREEN
+
+ /** A token used for dependency declaration */
+ val token: FlagToken
+ get() = FlagToken(FLAG_NAME, isEnabled)
+
+ /** Is the refactor enabled */
+ @JvmStatic
+ inline val isEnabled
+ get() = Flags.composeLockscreen() && ComposeFacade.isComposeAvailable()
+
+ /**
+ * Called to ensure code is only run when the flag is enabled. This protects users from the
+ * unintended behaviors caused by accidentally running new logic, while also crashing on an eng
+ * build to ensure that the refactor author catches issues in testing.
+ */
+ @JvmStatic
+ inline fun isUnexpectedlyInLegacyMode() =
+ RefactorFlagUtils.isUnexpectedlyInLegacyMode(isEnabled, FLAG_NAME)
+
+ /**
+ * Called to ensure code is only run when the flag is disabled. This will throw an exception if
+ * the flag is enabled to ensure that the refactor author catches issues in testing.
+ */
+ @JvmStatic
+ inline fun assertInLegacyMode() = RefactorFlagUtils.assertInLegacyMode(isEnabled, FLAG_NAME)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/FingerprintAuthenticationModels.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/FingerprintAuthenticationModels.kt
index cc385a8..474de77 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/FingerprintAuthenticationModels.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/FingerprintAuthenticationModels.kt
@@ -20,6 +20,7 @@
import android.hardware.biometrics.BiometricFingerprintConstants.FINGERPRINT_ACQUIRED_START
import android.hardware.fingerprint.FingerprintManager
import android.os.SystemClock.elapsedRealtime
+import com.android.systemui.biometrics.shared.model.AuthenticationReason
/**
* Fingerprint authentication status provided by
@@ -40,8 +41,10 @@
) : FingerprintAuthenticationStatus()
/** Fingerprint acquired message. */
-data class AcquiredFingerprintAuthenticationStatus(val acquiredInfo: Int) :
- FingerprintAuthenticationStatus() {
+data class AcquiredFingerprintAuthenticationStatus(
+ val authenticationReason: AuthenticationReason,
+ val acquiredInfo: Int
+) : FingerprintAuthenticationStatus() {
val fingerprintCaptureStarted: Boolean = acquiredInfo == FINGERPRINT_ACQUIRED_START
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardSurfaceBehindModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardSurfaceBehindModel.kt
index 7fb5cfd..aad8a4b 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardSurfaceBehindModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardSurfaceBehindModel.kt
@@ -44,6 +44,9 @@
* running, in which case we'll animate from the current value to [translationY].
*/
val animateFromTranslationY: Float = translationY,
+
+ /** Velocity with which to start the Y-translation spring animation. */
+ val startVelocity: Float = 0f,
) {
fun willAnimateAlpha(): Boolean {
return animateFromAlpha != alpha
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/LockscreenSceneBlueprint.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/LockscreenSceneBlueprint.kt
similarity index 60%
copy from packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/LockscreenSceneBlueprint.kt
copy to packages/SystemUI/src/com/android/systemui/keyguard/shared/model/LockscreenSceneBlueprint.kt
index 6d9cba4..2eafb83 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/LockscreenSceneBlueprint.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/LockscreenSceneBlueprint.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2023 The Android Open Source Project
+ * Copyright (C) 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,18 +14,14 @@
* limitations under the License.
*/
-package com.android.systemui.keyguard.ui.composable.blueprint
+package com.android.systemui.keyguard.shared.model
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Modifier
-import com.android.compose.animation.scene.SceneScope
-
-/** Defines interface for classes that can render the content for a specific blueprint/layout. */
+/**
+ * Defines interface for classes that can render the content for a specific blueprint/layout.
+ *
+ * The actual rendering is done by a compose-aware sub-interface.
+ */
interface LockscreenSceneBlueprint {
-
/** The ID that uniquely identifies this blueprint across all other blueprints. */
val id: String
-
- /** Renders the content of this blueprint. */
- @Composable fun SceneScope.Content(modifier: Modifier)
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlow.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlow.kt
index 00b7989..8b278cd 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlow.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlow.kt
@@ -112,6 +112,38 @@
interpolator: Interpolator = LINEAR,
name: String? = null
): Flow<Float> {
+ return sharedFlowWithState(
+ duration = duration,
+ onStep = onStep,
+ startTime = startTime,
+ onStart = onStart,
+ onCancel = onCancel,
+ onFinish = onFinish,
+ interpolator = interpolator,
+ name = name,
+ )
+ .mapNotNull { stateToValue -> stateToValue.value }
+ }
+
+ /**
+ * Transitions will occur over a [transitionDuration] with [TransitionStep]s being emitted
+ * in the range of [0, 1]. View animations should begin and end within a subset of this
+ * range. This function maps the [startTime] and [duration] into [0, 1], when this subset is
+ * valid.
+ *
+ * Will return a [StateToValue], which encompasses the calculated value as well as the
+ * transitionState that is associated with it.
+ */
+ fun sharedFlowWithState(
+ duration: Duration,
+ onStep: (Float) -> Float,
+ startTime: Duration = 0.milliseconds,
+ onStart: (() -> Unit)? = null,
+ onCancel: (() -> Float)? = null,
+ onFinish: (() -> Float)? = null,
+ interpolator: Interpolator = LINEAR,
+ name: String? = null
+ ): Flow<StateToValue> {
if (!duration.isPositive()) {
throw IllegalArgumentException("duration must be a positive number: $duration")
}
@@ -164,7 +196,6 @@
.also { logger.logTransitionStep(name, step, it.value) }
}
.distinctUntilChanged()
- .mapNotNull { stateToValue -> stateToValue.value }
}
/**
@@ -174,9 +205,9 @@
return sharedFlow(duration = 1.milliseconds, onStep = { value }, onFinish = { value })
}
}
-
- data class StateToValue(
- val transitionState: TransitionState,
- val value: Float?,
- )
}
+
+data class StateToValue(
+ val transitionState: TransitionState = TransitionState.FINISHED,
+ val value: Float? = 0f,
+)
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBottomAreaViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBottomAreaViewBinder.kt
index 96e83b0..3630b40 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBottomAreaViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBottomAreaViewBinder.kt
@@ -32,7 +32,7 @@
import androidx.lifecycle.repeatOnLifecycle
import com.android.app.animation.Interpolators
import com.android.settingslib.Utils
-import com.android.systemui.animation.ActivityLaunchAnimator
+import com.android.systemui.animation.ActivityTransitionAnimator
import com.android.systemui.animation.Expandable
import com.android.systemui.animation.view.LaunchableLinearLayout
import com.android.systemui.common.shared.model.Icon
@@ -534,7 +534,7 @@
activityStarter.postStartActivityDismissingKeyguard(
WallpaperPickerIntentUtils.getIntent(view.context, LAUNCH_SOURCE_KEYGUARD),
/* delay= */ 0,
- /* animationController= */ ActivityLaunchAnimator.Controller.fromView(view),
+ /* animationController= */ ActivityTransitionAnimator.Controller.fromView(view),
/* customMessage= */ view.context.getString(R.string.keyguard_unlock_to_customize_ls)
)
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardClockViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardClockViewBinder.kt
index f0e89f9..62a6e8b 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardClockViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardClockViewBinder.kt
@@ -18,7 +18,6 @@
import android.transition.TransitionManager
import android.transition.TransitionSet
-import android.util.Log
import android.view.View.INVISIBLE
import androidx.annotation.VisibleForTesting
import androidx.constraintlayout.helper.widget.Layer
@@ -36,7 +35,6 @@
import com.android.systemui.keyguard.ui.viewmodel.KeyguardClockViewModel
import com.android.systemui.lifecycle.repeatWhenAttached
import com.android.systemui.plugins.clocks.ClockController
-import com.android.systemui.res.R
import com.android.systemui.shared.clocks.DEFAULT_CLOCK_ID
import kotlinx.coroutines.launch
@@ -78,10 +76,6 @@
launch {
if (!migrateClocksToBlueprint()) return@launch
viewModel.clockShouldBeCentered.collect { clockShouldBeCentered ->
- Log.d(
- "ClockViewBinder",
- "Sherry clockShouldBeCentered $clockShouldBeCentered"
- )
viewModel.clock?.let {
// Weather clock also has hasCustomPositionUpdatedAnimation as true
// TODO(b/323020908): remove ID check
@@ -169,16 +163,12 @@
rootView: ConstraintLayout,
) {
clockController?.let { clock ->
- clock.smallClock.layout.views[0].id = R.id.lockscreen_clock_view
- if (clock.largeClock.layout.views.size == 1) {
- clock.largeClock.layout.views[0].id = R.id.lockscreen_clock_view_large
- }
- // small clock should either be a single view or container with id
- // `lockscreen_clock_view`
clock.smallClock.layout.views.forEach {
rootView.addView(it).apply { it.visibility = INVISIBLE }
}
- clock.largeClock.layout.views.forEach { rootView.addView(it) }
+ clock.largeClock.layout.views.forEach {
+ rootView.addView(it).apply { it.visibility = INVISIBLE }
+ }
}
}
fun applyConstraints(
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardPreviewClockViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardPreviewClockViewBinder.kt
index 1b5b329..b56c998 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardPreviewClockViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardPreviewClockViewBinder.kt
@@ -17,12 +17,35 @@
package com.android.systemui.keyguard.ui.binder
+import android.content.Context
import android.view.View
+import android.view.View.INVISIBLE
+import android.view.View.VISIBLE
+import android.view.ViewGroup
+import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
+import androidx.constraintlayout.widget.ConstraintLayout
+import androidx.constraintlayout.widget.ConstraintSet
+import androidx.constraintlayout.widget.ConstraintSet.BOTTOM
+import androidx.constraintlayout.widget.ConstraintSet.PARENT_ID
+import androidx.constraintlayout.widget.ConstraintSet.START
+import androidx.constraintlayout.widget.ConstraintSet.TOP
import androidx.core.view.isVisible
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.repeatOnLifecycle
+import com.android.keyguard.ClockEventController
+import com.android.systemui.customization.R as customizationR
+import com.android.systemui.keyguard.shared.model.SettingsClockSize
+import com.android.systemui.keyguard.ui.preview.KeyguardPreviewRenderer
+import com.android.systemui.keyguard.ui.view.layout.sections.ClockSection.Companion.getDimen
+import com.android.systemui.keyguard.ui.view.layout.sections.setVisibility
import com.android.systemui.keyguard.ui.viewmodel.KeyguardPreviewClockViewModel
import com.android.systemui.lifecycle.repeatWhenAttached
+import com.android.systemui.plugins.clocks.ClockController
+import com.android.systemui.res.R
+import com.android.systemui.util.Utils
+import kotlin.reflect.KSuspendFunction1
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.launch
/** Binder for the small clock view, large clock view. */
object KeyguardPreviewClockViewBinder {
@@ -45,4 +68,129 @@
}
}
}
+
+ @JvmStatic
+ fun bind(
+ context: Context,
+ rootView: ConstraintLayout,
+ viewModel: KeyguardPreviewClockViewModel,
+ clockEventController: ClockEventController,
+ updateClockAppearance: KSuspendFunction1<ClockController, Unit>
+ ) {
+ rootView.repeatWhenAttached {
+ repeatOnLifecycle(Lifecycle.State.STARTED) {
+ launch {
+ combine(viewModel.selectedClockSize, viewModel.previewClock) { _, clock ->
+ clock
+ }
+ .collect { previewClock ->
+ viewModel.lastClock?.let { lastClock ->
+ (lastClock.largeClock.layout.views +
+ lastClock.smallClock.layout.views)
+ .forEach { rootView.removeView(it) }
+ }
+ viewModel.lastClock = previewClock
+ clockEventController.clock = previewClock
+ updateClockAppearance(previewClock)
+
+ if (viewModel.shouldHighlightSelectedAffordance) {
+ (previewClock.largeClock.layout.views +
+ previewClock.smallClock.layout.views)
+ .forEach { it.alpha = KeyguardPreviewRenderer.DIM_ALPHA }
+ }
+ previewClock.largeClock.layout.views.forEach {
+ (it.parent as? ViewGroup)?.removeView(it)
+ rootView.addView(it)
+ }
+
+ previewClock.smallClock.layout.views.forEach {
+ (it.parent as? ViewGroup)?.removeView(it)
+ rootView.addView(it)
+ }
+ applyPreviewConstraints(context, rootView, viewModel)
+ }
+ }
+ }
+ }
+ }
+
+ private fun applyClockDefaultConstraints(context: Context, constraints: ConstraintSet) {
+ constraints.apply {
+ constrainWidth(R.id.lockscreen_clock_view_large, ConstraintSet.WRAP_CONTENT)
+ constrainHeight(R.id.lockscreen_clock_view_large, ConstraintSet.WRAP_CONTENT)
+ val largeClockTopMargin =
+ context.resources.getDimensionPixelSize(R.dimen.status_bar_height) +
+ context.resources.getDimensionPixelSize(
+ customizationR.dimen.small_clock_padding_top
+ ) +
+ context.resources.getDimensionPixelSize(
+ R.dimen.keyguard_smartspace_top_offset
+ ) +
+ getDimen(context, DATE_WEATHER_VIEW_HEIGHT) +
+ getDimen(context, ENHANCED_SMARTSPACE_HEIGHT)
+ connect(R.id.lockscreen_clock_view_large, TOP, PARENT_ID, TOP, largeClockTopMargin)
+ connect(R.id.lockscreen_clock_view_large, START, PARENT_ID, START)
+ connect(
+ R.id.lockscreen_clock_view_large,
+ ConstraintSet.END,
+ PARENT_ID,
+ ConstraintSet.END
+ )
+
+ connect(R.id.lockscreen_clock_view_large, BOTTOM, R.id.lock_icon_view, TOP)
+ constrainWidth(R.id.lockscreen_clock_view, WRAP_CONTENT)
+ constrainHeight(
+ R.id.lockscreen_clock_view,
+ context.resources.getDimensionPixelSize(customizationR.dimen.small_clock_height)
+ )
+ connect(
+ R.id.lockscreen_clock_view,
+ START,
+ PARENT_ID,
+ START,
+ context.resources.getDimensionPixelSize(customizationR.dimen.clock_padding_start) +
+ context.resources.getDimensionPixelSize(R.dimen.status_view_margin_horizontal)
+ )
+ val smallClockTopMargin =
+ context.resources.getDimensionPixelSize(R.dimen.keyguard_clock_top_margin) +
+ Utils.getStatusBarHeaderHeightKeyguard(context)
+ connect(R.id.lockscreen_clock_view, TOP, PARENT_ID, TOP, smallClockTopMargin)
+ }
+ }
+
+ private fun applyPreviewConstraints(
+ context: Context,
+ rootView: ConstraintLayout,
+ viewModel: KeyguardPreviewClockViewModel
+ ) {
+ val cs = ConstraintSet().apply { clone(rootView) }
+ val clock = viewModel.previewClock.value
+ applyClockDefaultConstraints(context, cs)
+ clock.largeClock.layout.applyPreviewConstraints(cs)
+ clock.smallClock.layout.applyPreviewConstraints(cs)
+
+ // When selectedClockSize is the initial value, make both clocks invisible to avoid
+ // flickering
+ val largeClockVisibility =
+ when (viewModel.selectedClockSize.value) {
+ SettingsClockSize.DYNAMIC -> VISIBLE
+ SettingsClockSize.SMALL -> INVISIBLE
+ null -> INVISIBLE
+ }
+ val smallClockVisibility =
+ when (viewModel.selectedClockSize.value) {
+ SettingsClockSize.DYNAMIC -> INVISIBLE
+ SettingsClockSize.SMALL -> VISIBLE
+ null -> INVISIBLE
+ }
+
+ cs.apply {
+ setVisibility(clock.largeClock.layout.views, largeClockVisibility)
+ setVisibility(clock.smallClock.layout.views, smallClockVisibility)
+ }
+ cs.applyTo(rootView)
+ }
+
+ private const val DATE_WEATHER_VIEW_HEIGHT = "date_weather_view_height"
+ private const val ENHANCED_SMARTSPACE_HEIGHT = "enhanced_smartspace_height"
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt
index 48092c6..9e7c70d 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt
@@ -145,9 +145,7 @@
launch {
viewModel.burnInLayerVisibility.collect { visibility ->
childViews[burnInLayerId]?.visibility = visibility
- // Reset alpha only for the icons, as they currently have their
- // own animator
- childViews[aodNotificationIconContainerId]?.alpha = 0f
+ childViews[aodNotificationIconContainerId]?.visibility = visibility
}
}
@@ -191,6 +189,7 @@
.collect { y ->
childViews[burnInLayerId]?.translationY = y
childViews[largeClockId]?.translationY = y
+ childViews[aodNotificationIconContainerId]?.translationY = y
}
}
@@ -200,6 +199,7 @@
.collect { x ->
childViews[burnInLayerId]?.translationX = x
childViews[largeClockId]?.translationX = x
+ childViews[aodNotificationIconContainerId]?.translationX = x
}
}
@@ -219,6 +219,10 @@
// transition with other parts in burnInLayer
childViews[burnInLayerId]?.scaleX = scaleViewModel.scale
childViews[burnInLayerId]?.scaleY = scaleViewModel.scale
+ childViews[aodNotificationIconContainerId]?.scaleX =
+ scaleViewModel.scale
+ childViews[aodNotificationIconContainerId]?.scaleY =
+ scaleViewModel.scale
}
}
}
@@ -307,6 +311,12 @@
}
}
+ if (KeyguardShadeMigrationNssl.isEnabled) {
+ burnInParams.update { current ->
+ current.copy(translationY = { childViews[burnInLayerId]?.translationY })
+ }
+ }
+
onLayoutChangeListener = OnLayoutChange(viewModel, burnInParams)
view.addOnLayoutChangeListener(onLayoutChangeListener)
@@ -429,11 +439,17 @@
}
when {
!isVisible.isAnimating -> {
- alpha = 1f
if (!KeyguardShadeMigrationNssl.isEnabled) {
translationY = 0f
}
- visibility = if (isVisible.value) View.VISIBLE else View.INVISIBLE
+ visibility =
+ if (isVisible.value) {
+ alpha = 1f
+ View.VISIBLE
+ } else {
+ alpha = 0f
+ View.INVISIBLE
+ }
}
newAodTransition() -> {
animateInIconTranslation()
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSettingsViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSettingsViewBinder.kt
index f67cb68..b1adef4 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSettingsViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSettingsViewBinder.kt
@@ -22,7 +22,7 @@
import androidx.core.view.isVisible
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.repeatOnLifecycle
-import com.android.systemui.animation.ActivityLaunchAnimator
+import com.android.systemui.animation.ActivityTransitionAnimator
import com.android.systemui.common.ui.binder.IconViewBinder
import com.android.systemui.common.ui.binder.TextViewBinder
import com.android.systemui.keyguard.ui.viewmodel.KeyguardLongPressViewModel
@@ -115,7 +115,7 @@
activityStarter.postStartActivityDismissingKeyguard(
WallpaperPickerIntentUtils.getIntent(view.context, LAUNCH_SOURCE_KEYGUARD),
/* delay= */ 0,
- /* animationController= */ ActivityLaunchAnimator.Controller.fromView(view),
+ /* animationController= */ ActivityTransitionAnimator.Controller.fromView(view),
/* customMessage= */ view.context.getString(R.string.keyguard_unlock_to_customize_ls)
)
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSurfaceBehindParamsApplier.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSurfaceBehindParamsApplier.kt
index 8587022..fb6efd3 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSurfaceBehindParamsApplier.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSurfaceBehindParamsApplier.kt
@@ -67,8 +67,8 @@
SpringAnimation(animatedTranslationY).apply {
spring =
SpringForce().apply {
- stiffness = 200f
- dampingRatio = 1f
+ stiffness = 275f
+ dampingRatio = 0.98f
}
addUpdateListener { _, _, _ -> applyToSurfaceBehind() }
addEndListener { _, _, _, _ ->
@@ -84,7 +84,7 @@
private var animatedAlpha = 0f
private var alphaAnimator =
ValueAnimator.ofFloat(0f, 1f).apply {
- duration = 500
+ duration = 150
interpolator = Interpolators.ALPHA_IN
addUpdateListener {
animatedAlpha = it.animatedValue as Float
@@ -162,6 +162,7 @@
// If the spring isn't running yet, set the start value. Otherwise, respect the
// current position.
animatedTranslationY.value = viewParams.animateFromTranslationY
+ translateYSpring.setStartVelocity(viewParams.startVelocity)
}
translateYSpring.animateToFinalPosition(viewParams.translationY)
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt
index a0c0095..c14917b 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt
@@ -46,6 +46,7 @@
import com.android.keyguard.ClockEventController
import com.android.keyguard.KeyguardClockSwitch
import com.android.systemui.Flags.keyguardBottomAreaRefactor
+import com.android.systemui.Flags.migrateClocksToBlueprint
import com.android.systemui.animation.view.LaunchableImageView
import com.android.systemui.biometrics.domain.interactor.UdfpsOverlayInteractor
import com.android.systemui.broadcast.BroadcastDispatcher
@@ -197,6 +198,9 @@
shouldHighlightSelectedAffordance = shouldHighlightSelectedAffordance,
)
}
+ if (migrateClocksToBlueprint()) {
+ clockViewModel.shouldHighlightSelectedAffordance = shouldHighlightSelectedAffordance
+ }
runBlocking(mainDispatcher) {
host =
SurfaceControlViewHost(
@@ -396,11 +400,21 @@
if (!shouldHideClock) {
setUpClock(previewContext, rootView)
- KeyguardPreviewClockViewBinder.bind(
- largeClockHostView,
- smallClockHostView,
- clockViewModel,
- )
+ if (migrateClocksToBlueprint()) {
+ KeyguardPreviewClockViewBinder.bind(
+ context,
+ keyguardRootView,
+ clockViewModel,
+ clockController,
+ ::updateClockAppearance
+ )
+ } else {
+ KeyguardPreviewClockViewBinder.bind(
+ largeClockHostView,
+ smallClockHostView,
+ clockViewModel,
+ )
+ }
}
setUpSmartspace(previewContext, rootView)
@@ -474,55 +488,61 @@
private fun setUpClock(previewContext: Context, parentView: ViewGroup) {
val resources = parentView.resources
- largeClockHostView = FrameLayout(previewContext)
- largeClockHostView.layoutParams =
- FrameLayout.LayoutParams(
- FrameLayout.LayoutParams.MATCH_PARENT,
- FrameLayout.LayoutParams.MATCH_PARENT,
- )
- parentView.addView(largeClockHostView)
- largeClockHostView.isInvisible = true
+ if (!migrateClocksToBlueprint()) {
+ largeClockHostView = FrameLayout(previewContext)
+ largeClockHostView.layoutParams =
+ FrameLayout.LayoutParams(
+ FrameLayout.LayoutParams.MATCH_PARENT,
+ FrameLayout.LayoutParams.MATCH_PARENT,
+ )
+ largeClockHostView.isInvisible = true
+ parentView.addView(largeClockHostView)
- smallClockHostView = FrameLayout(previewContext)
- val layoutParams =
- FrameLayout.LayoutParams(
- FrameLayout.LayoutParams.WRAP_CONTENT,
- resources.getDimensionPixelSize(
- com.android.systemui.customization.R.dimen.small_clock_height
+ smallClockHostView = FrameLayout(previewContext)
+ val layoutParams =
+ FrameLayout.LayoutParams(
+ FrameLayout.LayoutParams.WRAP_CONTENT,
+ resources.getDimensionPixelSize(
+ com.android.systemui.customization.R.dimen.small_clock_height
+ )
)
+ layoutParams.topMargin =
+ KeyguardPreviewSmartspaceViewModel.getStatusBarHeight(resources) +
+ resources.getDimensionPixelSize(
+ com.android.systemui.customization.R.dimen.small_clock_padding_top
+ )
+ smallClockHostView.layoutParams = layoutParams
+ smallClockHostView.setPaddingRelative(
+ /* start = */ resources.getDimensionPixelSize(
+ com.android.systemui.customization.R.dimen.clock_padding_start
+ ),
+ /* top = */ 0,
+ /* end = */ 0,
+ /* bottom = */ 0
)
- layoutParams.topMargin =
- KeyguardPreviewSmartspaceViewModel.getStatusBarHeight(resources) +
- resources.getDimensionPixelSize(
- com.android.systemui.customization.R.dimen.small_clock_padding_top
- )
- smallClockHostView.layoutParams = layoutParams
- smallClockHostView.setPaddingRelative(
- resources.getDimensionPixelSize(
- com.android.systemui.customization.R.dimen.clock_padding_start
- ),
- 0,
- 0,
- 0
- )
- smallClockHostView.clipChildren = false
- parentView.addView(smallClockHostView)
- smallClockHostView.isInvisible = true
+ smallClockHostView.clipChildren = false
+ parentView.addView(smallClockHostView)
+ smallClockHostView.isInvisible = true
+ }
// TODO (b/283465254): Move the listeners to KeyguardClockRepository
- val clockChangeListener =
- object : ClockRegistry.ClockChangeListener {
- override fun onCurrentClockChanged() {
- onClockChanged()
+ if (!migrateClocksToBlueprint()) {
+ val clockChangeListener =
+ object : ClockRegistry.ClockChangeListener {
+ override fun onCurrentClockChanged() {
+ onClockChanged()
+ }
}
- }
- clockRegistry.registerClockChangeListener(clockChangeListener)
- disposables.add(
- DisposableHandle { clockRegistry.unregisterClockChangeListener(clockChangeListener) }
- )
+ clockRegistry.registerClockChangeListener(clockChangeListener)
+ disposables.add(
+ DisposableHandle {
+ clockRegistry.unregisterClockChangeListener(clockChangeListener)
+ }
+ )
- clockController.registerListeners(parentView)
- disposables.add(DisposableHandle { clockController.unregisterListeners() })
+ clockController.registerListeners(parentView)
+ disposables.add(DisposableHandle { clockController.unregisterListeners() })
+ }
val receiver =
object : BroadcastReceiver() {
@@ -542,50 +562,61 @@
)
disposables.add(DisposableHandle { broadcastDispatcher.unregisterReceiver(receiver) })
- val layoutChangeListener =
- View.OnLayoutChangeListener { _, _, _, _, _, _, _, _, _ ->
- if (clockController.clock !is DefaultClockController) {
- clockController.clock
- ?.largeClock
- ?.events
- ?.onTargetRegionChanged(KeyguardClockSwitch.getLargeClockRegion(parentView))
- clockController.clock
- ?.smallClock
- ?.events
- ?.onTargetRegionChanged(KeyguardClockSwitch.getSmallClockRegion(parentView))
+ if (!migrateClocksToBlueprint()) {
+ val layoutChangeListener =
+ View.OnLayoutChangeListener { _, _, _, _, _, _, _, _, _ ->
+ if (clockController.clock !is DefaultClockController) {
+ clockController.clock
+ ?.largeClock
+ ?.events
+ ?.onTargetRegionChanged(
+ KeyguardClockSwitch.getLargeClockRegion(parentView)
+ )
+ clockController.clock
+ ?.smallClock
+ ?.events
+ ?.onTargetRegionChanged(
+ KeyguardClockSwitch.getSmallClockRegion(parentView)
+ )
+ }
}
- }
- parentView.addOnLayoutChangeListener(layoutChangeListener)
- disposables.add(
- DisposableHandle { parentView.removeOnLayoutChangeListener(layoutChangeListener) }
- )
+ parentView.addOnLayoutChangeListener(layoutChangeListener)
+ disposables.add(
+ DisposableHandle { parentView.removeOnLayoutChangeListener(layoutChangeListener) }
+ )
+ }
onClockChanged()
}
+ private suspend fun updateClockAppearance(clock: ClockController) {
+ clockController.clock = clock
+ val colors = wallpaperColors
+ if (clockRegistry.seedColor == null && colors != null) {
+ // Seed color null means users do not override any color on the clock. The default
+ // color will need to use wallpaper's extracted color and consider if the
+ // wallpaper's color is dark or light.
+ val style = themeStyle ?: fetchThemeStyleFromSetting().also { themeStyle = it }
+ val wallpaperColorScheme = ColorScheme(colors, darkTheme = false, style)
+ val lightClockColor = wallpaperColorScheme.accent1.s100
+ val darkClockColor = wallpaperColorScheme.accent2.s600
+
+ // Note that when [wallpaperColors] is null, isWallpaperDark is true.
+ val isWallpaperDark: Boolean =
+ (colors.colorHints.and(WallpaperColors.HINT_SUPPORTS_DARK_TEXT)) == 0
+ clock.events.onSeedColorChanged(
+ if (isWallpaperDark) lightClockColor else darkClockColor
+ )
+ }
+ }
private fun onClockChanged() {
+ if (migrateClocksToBlueprint()) {
+ return
+ }
coroutineScope.launch {
val clock = clockRegistry.createCurrentClock()
clockController.clock = clock
-
- val colors = wallpaperColors
- if (clockRegistry.seedColor == null && colors != null) {
- // Seed color null means users do not override any color on the clock. The default
- // color will need to use wallpaper's extracted color and consider if the
- // wallpaper's color is dark or light.
- val style = themeStyle ?: fetchThemeStyleFromSetting().also { themeStyle = it }
- val wallpaperColorScheme = ColorScheme(colors, darkTheme = false, style)
- val lightClockColor = wallpaperColorScheme.accent1.s100
- val darkClockColor = wallpaperColorScheme.accent2.s600
-
- // Note that when [wallpaperColors] is null, isWallpaperDark is true.
- val isWallpaperDark: Boolean =
- (colors.colorHints.and(WallpaperColors.HINT_SUPPORTS_DARK_TEXT)) == 0
- clock.events.onSeedColorChanged(
- if (isWallpaperDark) lightClockColor else darkClockColor
- )
- }
-
+ updateClockAppearance(clock)
updateLargeClock(clock)
updateSmallClock(clock)
}
@@ -626,6 +657,9 @@
}
private fun updateLargeClock(clock: ClockController) {
+ if (migrateClocksToBlueprint()) {
+ return
+ }
clock.largeClock.events.onTargetRegionChanged(
KeyguardClockSwitch.getLargeClockRegion(largeClockHostView)
)
@@ -637,6 +671,9 @@
}
private fun updateSmallClock(clock: ClockController) {
+ if (migrateClocksToBlueprint()) {
+ return
+ }
clock.smallClock.events.onTargetRegionChanged(
KeyguardClockSwitch.getSmallClockRegion(smallClockHostView)
)
@@ -656,6 +693,6 @@
private const val KEY_DISPLAY_ID = "display_id"
private const val KEY_COLORS = "wallpaper_colors"
- private const val DIM_ALPHA = 0.3f
+ const val DIM_ALPHA = 0.3f
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodBurnInSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodBurnInSection.kt
index 3d36eb0..9a1fcc1 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodBurnInSection.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodBurnInSection.kt
@@ -41,11 +41,13 @@
return
}
- val nic = constraintLayout.requireViewById<View>(R.id.aod_notification_icon_container)
+ // The burn-in layer requires at least 1 view at all times
+ val emptyView = View(context, null).apply { id = View.generateViewId() }
+ constraintLayout.addView(emptyView)
burnInLayer =
AodBurnInLayer(context).apply {
id = R.id.burn_in_layer
- addView(nic)
+ addView(emptyView)
if (!migrateClocksToBlueprint()) {
val statusView =
constraintLayout.requireViewById<View>(R.id.keyguard_status_view)
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSection.kt
index b1178f5..631b342 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSection.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSection.kt
@@ -201,13 +201,16 @@
}
private fun getDimen(name: String): Int {
- val res = context.packageManager.getResourcesForApplication(context.packageName)
- val id = res.getIdentifier(name, "dimen", context.packageName)
- return res.getDimensionPixelSize(id)
+ return getDimen(context, name)
}
companion object {
private const val DATE_WEATHER_VIEW_HEIGHT = "date_weather_view_height"
private const val ENHANCED_SMARTSPACE_HEIGHT = "enhanced_smartspace_height"
+ fun getDimen(context: Context, name: String): Int {
+ val res = context.packageManager.getResourcesForApplication(context.packageName)
+ val id = res.getIdentifier(name, "dimen", context.packageName)
+ return res.getDimensionPixelSize(id)
+ }
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/NotificationStackScrollLayoutSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/NotificationStackScrollLayoutSection.kt
index a651c10..52d94a0 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/NotificationStackScrollLayoutSection.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/NotificationStackScrollLayoutSection.kt
@@ -57,7 +57,7 @@
private val mainDispatcher: CoroutineDispatcher,
) : KeyguardSection() {
private val placeHolderId = R.id.nssl_placeholder
- private var disposableHandle: DisposableHandle? = null
+ private val disposableHandles: MutableList<DisposableHandle> = mutableListOf()
/**
* Align the notification placeholder bottom to the top of either the lock icon or the ambient
@@ -102,8 +102,9 @@
if (!KeyguardShadeMigrationNssl.isEnabled) {
return
}
- disposableHandle?.dispose()
- disposableHandle =
+
+ disposeHandles()
+ disposableHandles.add(
SharedNotificationContainerBinder.bind(
sharedNotificationContainer,
sharedNotificationContainerViewModel,
@@ -112,19 +113,28 @@
notificationStackSizeCalculator,
mainDispatcher,
)
+ )
+
if (sceneContainerFlags.flexiNotifsEnabled()) {
- NotificationStackAppearanceViewBinder.bind(
- context,
- sharedNotificationContainer,
- notificationStackAppearanceViewModel,
- ambientState,
- controller,
+ disposableHandles.add(
+ NotificationStackAppearanceViewBinder.bind(
+ context,
+ sharedNotificationContainer,
+ notificationStackAppearanceViewModel,
+ ambientState,
+ controller,
+ )
)
}
}
override fun removeViews(constraintLayout: ConstraintLayout) {
- disposableHandle?.dispose()
+ disposeHandles()
constraintLayout.removeView(placeHolderId)
}
+
+ private fun disposeHandles() {
+ disposableHandles.forEach { it.dispose() }
+ disposableHandles.clear()
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToGoneTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToGoneTransitionViewModel.kt
index 3737e6f..d26356e 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToGoneTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToGoneTransitionViewModel.kt
@@ -24,6 +24,7 @@
import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow
import com.android.systemui.keyguard.ui.transitions.DeviceEntryIconTransition
import javax.inject.Inject
+import kotlin.time.Duration.Companion.milliseconds
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
@@ -46,6 +47,14 @@
to = KeyguardState.GONE,
)
+ val lockscreenAlpha: Flow<Float> =
+ transitionAnimation.sharedFlow(
+ duration = 200.milliseconds,
+ onStep = { 1 - it },
+ onFinish = { 0f },
+ onCancel = { 1f },
+ )
+
/** Scrim alpha values */
val scrimAlpha: Flow<ScrimAlpha> =
bouncerToGoneFlows.scrimAlpha(TO_GONE_DURATION, ALTERNATE_BOUNCER)
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodBurnInViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodBurnInViewModel.kt
index 780e323..8110de2 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodBurnInViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodBurnInViewModel.kt
@@ -28,6 +28,13 @@
import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
import com.android.systemui.keyguard.shared.model.BurnInModel
+import com.android.systemui.keyguard.shared.model.KeyguardState.ALTERNATE_BOUNCER
+import com.android.systemui.keyguard.shared.model.KeyguardState.AOD
+import com.android.systemui.keyguard.shared.model.KeyguardState.GONE
+import com.android.systemui.keyguard.shared.model.TransitionState
+import com.android.systemui.keyguard.shared.model.TransitionState.RUNNING
+import com.android.systemui.keyguard.shared.model.TransitionState.STARTED
+import com.android.systemui.keyguard.ui.StateToValue
import com.android.systemui.plugins.clocks.ClockController
import com.android.systemui.res.R
import javax.inject.Inject
@@ -55,6 +62,7 @@
private val keyguardInteractor: KeyguardInteractor,
private val keyguardTransitionInteractor: KeyguardTransitionInteractor,
private val goneToAodTransitionViewModel: GoneToAodTransitionViewModel,
+ private val aodToLockscreenTransitionViewModel: AodToLockscreenTransitionViewModel,
private val occludedToLockscreenTransitionViewModel: OccludedToLockscreenTransitionViewModel,
private val keyguardClockViewModel: KeyguardClockViewModel,
) {
@@ -80,21 +88,22 @@
burnIn(params).map { it.translationY.toFloat() }.onStart { emit(0f) },
goneToAodTransitionViewModel
.enterFromTopTranslationY(enterFromTopAmount)
- .onStart { emit(0f) },
+ .onStart { emit(StateToValue()) },
occludedToLockscreenTransitionViewModel.lockscreenTranslationY.onStart {
emit(0f)
},
- ) {
- keyguardTransitionY,
- burnInTranslationY,
- goneToAodTransitionTranslationY,
- occludedToLockscreenTransitionTranslationY ->
-
- // All values need to be combined for a smooth translation
- keyguardTransitionY +
- burnInTranslationY +
- goneToAodTransitionTranslationY +
- occludedToLockscreenTransitionTranslationY
+ aodToLockscreenTransitionViewModel.translationY(params.translationY).onStart {
+ emit(StateToValue())
+ },
+ ) { keyguardTranslationY, burnInY, goneToAod, occludedToLockscreen, aodToLockscreen
+ ->
+ if (isInTransition(aodToLockscreen.transitionState)) {
+ aodToLockscreen.value ?: 0f
+ } else if (isInTransition(goneToAod.transitionState)) {
+ (goneToAod.value ?: 0f) + burnInY
+ } else {
+ burnInY + occludedToLockscreen + keyguardTranslationY
+ }
}
}
.distinctUntilChanged()
@@ -112,12 +121,19 @@
}
}
+ private fun isInTransition(state: TransitionState): Boolean {
+ return state == STARTED || state == RUNNING
+ }
+
private fun burnIn(
params: BurnInParameters,
): Flow<BurnInModel> {
return combine(
merge(
- keyguardTransitionInteractor.goneToAodTransition.map { it.value },
+ keyguardTransitionInteractor.transition(GONE, AOD).map { it.value },
+ keyguardTransitionInteractor.transition(ALTERNATE_BOUNCER, AOD).map {
+ it.value
+ },
keyguardTransitionInteractor.dozeAmountTransition.map { it.value },
)
.map { dozeAmount -> Interpolators.FAST_OUT_SLOW_IN.getInterpolation(dozeAmount) },
@@ -179,6 +195,8 @@
val topInset: Int = 0,
/** Status view top, without translation added in */
val statusViewTop: Int = 0,
+ /** The current y translation of the view */
+ val translationY: () -> Float? = { null }
)
/**
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodToLockscreenTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodToLockscreenTransitionViewModel.kt
index 266fd02..6d1d3cb 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodToLockscreenTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodToLockscreenTransitionViewModel.kt
@@ -16,11 +16,14 @@
package com.android.systemui.keyguard.ui.viewmodel
+import android.util.MathUtils
+import com.android.app.animation.Interpolators.FAST_OUT_SLOW_IN
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.deviceentry.domain.interactor.DeviceEntryUdfpsInteractor
import com.android.systemui.keyguard.domain.interactor.FromAodTransitionInteractor.Companion.TO_LOCKSCREEN_DURATION
import com.android.systemui.keyguard.shared.model.KeyguardState
import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow
+import com.android.systemui.keyguard.ui.StateToValue
import com.android.systemui.keyguard.ui.transitions.DeviceEntryIconTransition
import javax.inject.Inject
import kotlin.time.Duration.Companion.milliseconds
@@ -48,6 +51,22 @@
to = KeyguardState.LOCKSCREEN,
)
+ /**
+ * Begin the transition from wherever the y-translation value is currently. This helps ensure a
+ * smooth transition if a transition in canceled.
+ */
+ fun translationY(currentTranslationY: () -> Float?): Flow<StateToValue> {
+ var startValue = 0f
+ return transitionAnimation.sharedFlowWithState(
+ duration = 500.milliseconds,
+ onStart = {
+ startValue = currentTranslationY() ?: 0f
+ startValue
+ },
+ onStep = { MathUtils.lerp(startValue, 0f, FAST_OUT_SLOW_IN.getInterpolation(it)) },
+ )
+ }
+
/** Ensure alpha is set to be visible */
val lockscreenAlpha: Flow<Float> =
transitionAnimation.sharedFlow(
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GoneToAodTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GoneToAodTransitionViewModel.kt
index ba04fd3..85885b0 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GoneToAodTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GoneToAodTransitionViewModel.kt
@@ -22,6 +22,7 @@
import com.android.systemui.keyguard.domain.interactor.FromGoneTransitionInteractor.Companion.TO_AOD_DURATION
import com.android.systemui.keyguard.shared.model.KeyguardState
import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow
+import com.android.systemui.keyguard.ui.StateToValue
import com.android.systemui.keyguard.ui.transitions.DeviceEntryIconTransition
import javax.inject.Inject
import kotlin.time.Duration.Companion.milliseconds
@@ -48,8 +49,8 @@
)
/** y-translation from the top of the screen for AOD */
- fun enterFromTopTranslationY(translatePx: Int): Flow<Float> {
- return transitionAnimation.sharedFlow(
+ fun enterFromTopTranslationY(translatePx: Int): Flow<StateToValue> {
+ return transitionAnimation.sharedFlowWithState(
startTime = 600.milliseconds,
duration = 500.milliseconds,
onStart = { translatePx },
@@ -63,10 +64,12 @@
/** alpha animation upon entering AOD */
val enterFromTopAnimationAlpha: Flow<Float> =
transitionAnimation.sharedFlow(
- startTime = 600.milliseconds,
- duration = 500.milliseconds,
+ startTime = 700.milliseconds,
+ duration = 400.milliseconds,
onStart = { 0f },
onStep = { it },
+ onFinish = { 1f },
+ onCancel = { 1f },
)
val deviceEntryBackgroundViewAlpha: Flow<Float> =
transitionAnimation.immediatelyTransitionTo(0f)
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardPreviewClockViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardPreviewClockViewModel.kt
index 5301302..f95a8a7 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardPreviewClockViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardPreviewClockViewModel.kt
@@ -20,9 +20,14 @@
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor
import com.android.systemui.keyguard.shared.model.SettingsClockSize
+import com.android.systemui.plugins.clocks.ClockController
import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.stateIn
/** View model for the small clock view, large clock view. */
class KeyguardPreviewClockViewModel
@@ -30,11 +35,24 @@
constructor(
@Application private val context: Context,
interactor: KeyguardClockInteractor,
+ @Application private val applicationScope: CoroutineScope,
) {
+ var shouldHighlightSelectedAffordance: Boolean = false
val isLargeClockVisible: Flow<Boolean> =
interactor.selectedClockSize.map { it == SettingsClockSize.DYNAMIC }
val isSmallClockVisible: Flow<Boolean> =
interactor.selectedClockSize.map { it == SettingsClockSize.SMALL }
+
+ var lastClock: ClockController? = null
+
+ val previewClock: StateFlow<ClockController> = interactor.previewClock
+
+ val selectedClockSize: StateFlow<SettingsClockSize?> =
+ interactor.selectedClockSize.stateIn(
+ scope = applicationScope,
+ started = SharingStarted.WhileSubscribed(),
+ initialValue = null
+ )
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt
index 709e184..ec13228 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt
@@ -30,6 +30,8 @@
import com.android.systemui.keyguard.shared.model.KeyguardState.AOD
import com.android.systemui.keyguard.shared.model.KeyguardState.GONE
import com.android.systemui.keyguard.shared.model.KeyguardState.LOCKSCREEN
+import com.android.systemui.keyguard.shared.model.TransitionState.RUNNING
+import com.android.systemui.keyguard.shared.model.TransitionState.STARTED
import com.android.systemui.statusbar.notification.domain.interactor.NotificationsKeyguardInteractor
import com.android.systemui.statusbar.phone.DozeParameters
import com.android.systemui.statusbar.phone.ScreenOffAnimationController
@@ -48,6 +50,7 @@
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.merge
+import kotlinx.coroutines.flow.onStart
@OptIn(ExperimentalCoroutinesApi::class)
@SysUISingleton
@@ -61,6 +64,9 @@
keyguardTransitionInteractor: KeyguardTransitionInteractor,
private val notificationsKeyguardInteractor: NotificationsKeyguardInteractor,
aodToLockscreenTransitionViewModel: AodToLockscreenTransitionViewModel,
+ lockscreenToGoneTransitionViewModel: LockscreenToGoneTransitionViewModel,
+ alternateBouncerToGoneTransitionViewModel: AlternateBouncerToGoneTransitionViewModel,
+ primaryBouncerToGoneTransitionViewModel: PrimaryBouncerToGoneTransitionViewModel,
lockscreenToGlanceableHubTransitionViewModel: LockscreenToGlanceableHubTransitionViewModel,
glanceableHubToLockscreenTransitionViewModel: GlanceableHubToLockscreenTransitionViewModel,
screenOffAnimationController: ScreenOffAnimationController,
@@ -75,6 +81,12 @@
val goneToAodTransition = keyguardTransitionInteractor.transition(from = GONE, to = AOD)
+ private val goneToAodTransitionRunning: Flow<Boolean> =
+ goneToAodTransition
+ .map { it.transitionState == STARTED || it.transitionState == RUNNING }
+ .onStart { emit(false) }
+ .distinctUntilChanged()
+
/** Last point that the root view was tapped */
val lastRootViewTapPosition: Flow<Point?> = keyguardInteractor.lastRootViewTapPosition
@@ -92,10 +104,15 @@
val alpha: Flow<Float> =
combine(
communalInteractor.isIdleOnCommunal,
+ // The transitions are mutually exclusive, so they are safe to merge to get the last
+ // value emitted by any of them. Do not add flows that cannot make this guarantee.
merge(
aodAlphaViewModel.alpha,
lockscreenToGlanceableHubTransitionViewModel.keyguardAlpha,
glanceableHubToLockscreenTransitionViewModel.keyguardAlpha,
+ lockscreenToGoneTransitionViewModel.lockscreenAlpha,
+ primaryBouncerToGoneTransitionViewModel.lockscreenAlpha,
+ alternateBouncerToGoneTransitionViewModel.lockscreenAlpha,
)
) { isIdleOnCommunal, alpha ->
if (isIdleOnCommunal) {
@@ -130,6 +147,7 @@
/** Is the notification icon container visible? */
val isNotifIconContainerVisible: Flow<AnimatedValue<Boolean>> =
combine(
+ goneToAodTransitionRunning,
keyguardTransitionInteractor.finishedKeyguardState.map {
KeyguardState.lockscreenVisibleInState(it)
},
@@ -137,6 +155,7 @@
areNotifsFullyHiddenAnimated(),
isPulseExpandingAnimated(),
) {
+ goneToAodTransitionRunning: Boolean,
onKeyguard: Boolean,
isBypassEnabled: Boolean,
notifsFullyHidden: AnimatedValue<Boolean>,
@@ -146,7 +165,9 @@
// Hide the AOD icons if we're not in the KEYGUARD state unless the screen off
// animation is playing, in which case we want them to be visible if we're
// animating in the AOD UI and will be switching to KEYGUARD shortly.
- !onKeyguard && !screenOffAnimationController.shouldShowAodIconsWhenShade() ->
+ goneToAodTransitionRunning ||
+ (!onKeyguard &&
+ !screenOffAnimationController.shouldShowAodIconsWhenShade()) ->
AnimatedValue.NotAnimating(false)
else ->
zip(notifsFullyHidden, pulseExpanding) {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneViewModel.kt
index 2b28a71..fa18557 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneViewModel.kt
@@ -16,7 +16,7 @@
package com.android.systemui.keyguard.ui.viewmodel
-import com.android.systemui.communal.domain.interactor.CommunalInteractor
+import com.android.systemui.communal.domain.interactor.CommunalSettingsInteractor
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor
@@ -36,7 +36,7 @@
constructor(
@Application applicationScope: CoroutineScope,
deviceEntryInteractor: DeviceEntryInteractor,
- communalInteractor: CommunalInteractor,
+ communalSettingsInteractor: CommunalSettingsInteractor,
val longPress: KeyguardLongPressViewModel,
val notifications: NotificationsPlaceholderViewModel,
) {
@@ -55,10 +55,12 @@
}
/** The key of the scene we should switch to when swiping left. */
- val leftDestinationSceneKey: SceneKey? =
- if (communalInteractor.isCommunalEnabled) {
- SceneKey.Communal
- } else {
- null
- }
+ val leftDestinationSceneKey: StateFlow<SceneKey?> =
+ communalSettingsInteractor.isCommunalEnabled
+ .map { available -> if (available) SceneKey.Communal else null }
+ .stateIn(
+ scope = applicationScope,
+ started = SharingStarted.WhileSubscribed(),
+ initialValue = null,
+ )
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToGoneTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToGoneTransitionViewModel.kt
index a26ef07..d981650 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToGoneTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToGoneTransitionViewModel.kt
@@ -46,12 +46,14 @@
val shortcutsAlpha: Flow<Float> =
transitionAnimation.sharedFlow(
- duration = 250.milliseconds,
+ duration = 200.milliseconds,
onStep = { 1 - it },
onFinish = { 0f },
onCancel = { 1f },
)
+ val lockscreenAlpha: Flow<Float> = shortcutsAlpha
+
override val deviceEntryParentViewAlpha: Flow<Float> =
transitionAnimation.immediatelyTransitionTo(0f)
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/SideFpsProgressBarViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/SideFpsProgressBarViewModel.kt
index ca9c857..67c42f0 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/SideFpsProgressBarViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/SideFpsProgressBarViewModel.kt
@@ -22,8 +22,10 @@
import androidx.annotation.VisibleForTesting
import androidx.core.animation.addListener
import com.android.systemui.Flags
+import com.android.systemui.biometrics.domain.interactor.BiometricStatusInteractor
import com.android.systemui.biometrics.domain.interactor.DisplayStateInteractor
import com.android.systemui.biometrics.domain.interactor.SideFpsSensorInteractor
+import com.android.systemui.biometrics.shared.model.AuthenticationReason
import com.android.systemui.biometrics.shared.model.DisplayRotation
import com.android.systemui.biometrics.shared.model.isDefaultOrientation
import com.android.systemui.dagger.SysUISingleton
@@ -34,6 +36,7 @@
import com.android.systemui.keyguard.shared.model.AcquiredFingerprintAuthenticationStatus
import com.android.systemui.keyguard.shared.model.ErrorFingerprintAuthenticationStatus
import com.android.systemui.keyguard.shared.model.FailFingerprintAuthenticationStatus
+import com.android.systemui.keyguard.shared.model.FingerprintAuthenticationStatus
import com.android.systemui.keyguard.shared.model.SuccessFingerprintAuthenticationStatus
import com.android.systemui.power.domain.interactor.PowerInteractor
import com.android.systemui.res.R
@@ -49,10 +52,12 @@
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.flow.onCompletion
import kotlinx.coroutines.launch
@@ -62,7 +67,8 @@
@Inject
constructor(
private val context: Context,
- private val fpAuthRepository: DeviceEntryFingerprintAuthInteractor,
+ private val biometricStatusInteractor: BiometricStatusInteractor,
+ private val deviceEntryFingerprintAuthInteractor: DeviceEntryFingerprintAuthInteractor,
private val sfpsSensorInteractor: SideFpsSensorInteractor,
// todo (b/317432075) Injecting DozeServiceHost directly instead of using it through
// DozeInteractor as DozeServiceHost already depends on DozeInteractor.
@@ -86,6 +92,23 @@
private val additionalSensorLengthPadding =
context.resources.getDimension(R.dimen.sfps_progress_bar_length_extra_padding).toInt()
+ // Merged [FingerprintAuthenticationStatus] from BiometricPrompt acquired messages and
+ // device entry authentication messages
+ private val mergedFingerprintAuthenticationStatus =
+ merge(
+ biometricStatusInteractor.fingerprintAcquiredStatus,
+ deviceEntryFingerprintAuthInteractor.authenticationStatus
+ )
+ .filter {
+ if (it is AcquiredFingerprintAuthenticationStatus) {
+ it.authenticationReason == AuthenticationReason.DeviceEntryAuthentication ||
+ it.authenticationReason ==
+ AuthenticationReason.BiometricPromptAuthentication
+ } else {
+ true
+ }
+ }
+
val isVisible: Flow<Boolean> = _visible.asStateFlow()
val progress: Flow<Float> = _progress.asStateFlow()
@@ -147,7 +170,14 @@
viewLeftTop
}
- val isFingerprintAuthRunning: Flow<Boolean> = fpAuthRepository.isRunning
+ val isFingerprintAuthRunning: Flow<Boolean> =
+ combine(
+ deviceEntryFingerprintAuthInteractor.isRunning,
+ biometricStatusInteractor.sfpsAuthenticationReason
+ ) { deviceEntryAuthIsRunning, sfpsAuthReason ->
+ deviceEntryAuthIsRunning ||
+ sfpsAuthReason == AuthenticationReason.BiometricPromptAuthentication
+ }
val rotation: Flow<Float> =
combine(displayStateInteractor.currentRotation, sfpsSensorInteractor.sensorLocation, ::Pair)
@@ -185,7 +215,8 @@
sfpsSensorInteractor.authenticationDuration
.flatMapLatest { authDuration ->
_animator?.cancel()
- fpAuthRepository.authenticationStatus.map { authStatus ->
+ mergedFingerprintAuthenticationStatus.map {
+ authStatus: FingerprintAuthenticationStatus ->
when (authStatus) {
is AcquiredFingerprintAuthenticationStatus -> {
if (authStatus.fingerprintCaptureStarted) {
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java b/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java
index 23029e6..ac579d6 100644
--- a/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java
@@ -628,4 +628,13 @@
public static LogBuffer providePackageChangeRepoLogBuffer(LogBufferFactory factory) {
return factory.create("PackageChangeRepo", 50);
}
+
+ /** Provides a {@link LogBuffer} for NavBarButtonClicks. */
+ @Provides
+ @SysUISingleton
+ @NavBarButtonClickLog
+ public static LogBuffer provideNavBarButtonClickLogBuffer(LogBufferFactory factory) {
+ return factory.create("NavBarButtonClick", 50);
+ }
+
}
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/NavBarButtonClickLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/NavBarButtonClickLog.java
new file mode 100644
index 0000000..939dab2
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/NavBarButtonClickLog.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+package com.android.systemui.log.dagger;
+
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import com.android.systemui.log.LogBuffer;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.Retention;
+
+import javax.inject.Qualifier;
+
+/** A {@link LogBuffer} for {@link com.android.systemui.navigationbar.NavigationBar}. */
+@Qualifier
+@Documented
+@Retention(RUNTIME)
+public @interface NavBarButtonClickLog {
+}
diff --git a/packages/SystemUI/src/com/android/systemui/media/OWNERS b/packages/SystemUI/src/com/android/systemui/media/OWNERS
index b2d00df..69ea57b 100644
--- a/packages/SystemUI/src/com/android/systemui/media/OWNERS
+++ b/packages/SystemUI/src/com/android/systemui/media/OWNERS
@@ -1,5 +1 @@
per-file MediaProjectionPermissionActivity.java = michaelwr@google.com
-
-# Haptics team also works on Ringtone
-per-file NotificationPlayer.java = file:/services/core/java/com/android/server/vibrator/OWNERS
-per-file RingtonePlayer.java = file:/services/core/java/com/android/server/vibrator/OWNERS
diff --git a/packages/SystemUI/src/com/android/systemui/media/RingtonePlayer.java b/packages/SystemUI/src/com/android/systemui/media/RingtonePlayer.java
index 7a48836..3ab0420 100644
--- a/packages/SystemUI/src/com/android/systemui/media/RingtonePlayer.java
+++ b/packages/SystemUI/src/com/android/systemui/media/RingtonePlayer.java
@@ -16,9 +16,6 @@
package com.android.systemui.media;
-import static java.util.Objects.requireNonNull;
-
-import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.ContentResolver;
import android.content.Context;
@@ -37,8 +34,6 @@
import android.os.RemoteException;
import android.os.ServiceManager;
import android.os.UserHandle;
-import android.os.VibrationEffect;
-import android.os.vibrator.Flags;
import android.provider.MediaStore;
import android.util.Log;
@@ -58,7 +53,7 @@
@SysUISingleton
public class RingtonePlayer implements CoreStartable {
private static final String TAG = "RingtonePlayer";
- private static final boolean LOGD = true;
+ private static final boolean LOGD = false;
private final Context mContext;
// TODO: support Uri switching under same IBinder
@@ -91,11 +86,20 @@
*/
private class Client implements IBinder.DeathRecipient {
private final IBinder mToken;
- private Ringtone mRingtone;
+ private final Ringtone mRingtone;
- Client(@NonNull IBinder token, @NonNull Ringtone ringtone) {
- mToken = requireNonNull(token);
- mRingtone = requireNonNull(ringtone);
+ public Client(IBinder token, Uri uri, UserHandle user, AudioAttributes aa) {
+ this(token, uri, user, aa, null);
+ }
+
+ Client(IBinder token, Uri uri, UserHandle user, AudioAttributes aa,
+ @Nullable VolumeShaper.Configuration volumeShaperConfig) {
+ mToken = token;
+
+ mRingtone = new Ringtone(getContextForUser(user), false);
+ mRingtone.setAudioAttributesField(aa);
+ mRingtone.setUri(uri, volumeShaperConfig);
+ mRingtone.createLocalMediaPlayer();
}
@Override
@@ -112,48 +116,24 @@
@Override
public void play(IBinder token, Uri uri, AudioAttributes aa, float volume, boolean looping)
throws RemoteException {
- if (Flags.hapticsCustomizationRingtoneV2Enabled()) {
- playRemoteRingtone(token, uri, aa, true, Ringtone.MEDIA_SOUND,
- null, volume, looping, /* hapticGenerator= */ false,
- null);
- } else {
- playWithVolumeShaping(token, uri, aa, volume, looping, null);
- }
+ playWithVolumeShaping(token, uri, aa, volume, looping, null);
}
-
@Override
- public void playWithVolumeShaping(
- IBinder token, Uri uri, AudioAttributes aa, float volume,
+ public void playWithVolumeShaping(IBinder token, Uri uri, AudioAttributes aa, float volume,
boolean looping, @Nullable VolumeShaper.Configuration volumeShaperConfig)
throws RemoteException {
if (LOGD) {
- Log.d(TAG, "playWithVolumeShaping(token=" + token + ", uri=" + uri + ", uid="
+ Log.d(TAG, "play(token=" + token + ", uri=" + uri + ", uid="
+ Binder.getCallingUid() + ")");
}
Client client;
synchronized (mClients) {
client = mClients.get(token);
- }
- // Don't hold the lock while constructing the ringtone, since it can be slow. The caller
- // shouldn't call play on the same ringtone from 2 threads, so this shouldn't race and
- // waste the build.
- if (client == null) {
- final UserHandle user = Binder.getCallingUserHandle();
- Ringtone ringtone = Ringtone.createV1WithCustomAudioAttributes(
- getContextForUser(user), aa, uri, volumeShaperConfig,
- /* allowRemote= */ false);
- synchronized (mClients) {
- client = mClients.get(token);
- if (client == null) {
- client = new Client(token, ringtone);
- token.linkToDeath(client, 0);
- mClients.put(token, client);
- ringtone = null; // "owned" by the client now.
- }
- }
- // Clean up ringtone if it was abandoned (a client already existed).
- if (ringtone != null) {
- ringtone.stop();
+ if (client == null) {
+ final UserHandle user = Binder.getCallingUserHandle();
+ client = new Client(token, uri, user, aa, volumeShaperConfig);
+ token.linkToDeath(client, 0);
+ mClients.put(token, client);
}
}
client.mRingtone.setLooping(looping);
@@ -162,54 +142,6 @@
}
@Override
- public void playRemoteRingtone(IBinder token, Uri uri, AudioAttributes aa,
- boolean useExactAudioAttributes,
- @Ringtone.RingtoneMedia int enabledMedia, @Nullable VibrationEffect vibrationEffect,
- float volume,
- boolean looping, boolean isHapticGeneratorEnabled,
- @Nullable VolumeShaper.Configuration volumeShaperConfig)
- throws RemoteException {
- if (LOGD) {
- Log.d(TAG, "playRemoteRingtone(token=" + token + ", uri=" + uri + ", uid="
- + Binder.getCallingUid() + ")");
- }
-
- // Don't hold the lock while constructing the ringtone, since it can be slow. The caller
- // shouldn't call play on the same ringtone from 2 threads, so this shouldn't race and
- // waste the build.
- Client client;
- synchronized (mClients) {
- client = mClients.get(token);
- }
- if (client == null) {
- final UserHandle user = Binder.getCallingUserHandle();
- Ringtone ringtone = new Ringtone.Builder(getContextForUser(user), enabledMedia, aa)
- .setLocalOnly()
- .setUri(uri)
- .setLooping(looping)
- .setInitialSoundVolume(volume)
- .setUseExactAudioAttributes(useExactAudioAttributes)
- .setEnableHapticGenerator(isHapticGeneratorEnabled)
- .setVibrationEffect(vibrationEffect)
- .setVolumeShaperConfig(volumeShaperConfig)
- .build();
- if (ringtone == null) {
- return;
- }
- synchronized (mClients) {
- client = mClients.get(token);
- if (client == null) {
- client = new Client(token, ringtone);
- token.linkToDeath(client, 0);
- mClients.put(token, client);
- }
- }
- }
- // Ensure the client is initialized outside the all-clients lock, as it can be slow.
- client.mRingtone.play();
- }
-
- @Override
public void stop(IBinder token) {
if (LOGD) Log.d(TAG, "stop(token=" + token + ")");
Client client;
@@ -235,10 +167,10 @@
return false;
}
}
+
@Override
public void setPlaybackProperties(IBinder token, float volume, boolean looping,
- boolean hapticGeneratorEnabled) {
- // RingtoneV1-exclusive path.
+ boolean hapticGeneratorEnabled) {
Client client;
synchronized (mClients) {
client = mClients.get(token);
@@ -252,39 +184,6 @@
}
@Override
- public void setHapticGeneratorEnabled(IBinder token, boolean hapticGeneratorEnabled) {
- Client client;
- synchronized (mClients) {
- client = mClients.get(token);
- }
- if (client != null) {
- client.mRingtone.setHapticGeneratorEnabled(hapticGeneratorEnabled);
- }
- }
-
- @Override
- public void setLooping(IBinder token, boolean looping) {
- Client client;
- synchronized (mClients) {
- client = mClients.get(token);
- }
- if (client != null) {
- client.mRingtone.setLooping(looping);
- }
- }
-
- @Override
- public void setVolume(IBinder token, float volume) {
- Client client;
- synchronized (mClients) {
- client = mClients.get(token);
- }
- if (client != null) {
- client.mRingtone.setVolume(volume);
- }
- }
-
- @Override
public void playAsync(Uri uri, UserHandle user, boolean looping, AudioAttributes aa,
float volume) {
if (LOGD) Log.d(TAG, "playAsync(uri=" + uri + ", user=" + user + ")");
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/models/player/SeekBarObserver.kt b/packages/SystemUI/src/com/android/systemui/media/controls/models/player/SeekBarObserver.kt
index 3f4f347..8d918e7 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/models/player/SeekBarObserver.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/models/player/SeekBarObserver.kt
@@ -22,9 +22,12 @@
import androidx.annotation.UiThread
import androidx.lifecycle.Observer
import com.android.app.animation.Interpolators
+import com.android.app.tracing.TraceStateLogger
import com.android.internal.annotations.VisibleForTesting
-import com.android.systemui.res.R
import com.android.systemui.media.controls.ui.SquigglyProgress
+import com.android.systemui.res.R
+
+private const val TAG = "SeekBarObserver"
/**
* Observer for changes from SeekBarViewModel.
@@ -39,6 +42,10 @@
@JvmStatic val RESET_ANIMATION_THRESHOLD_MS: Int = 250
}
+ // Trace state loggers for playing and listening states of progress bar.
+ private val playingStateLogger = TraceStateLogger("$TAG#playing")
+ private val listeningStateLogger = TraceStateLogger("$TAG#listening")
+
val seekBarEnabledMaxHeight =
holder.seekBar.context.resources.getDimensionPixelSize(
R.dimen.qs_media_enabled_seekbar_height
@@ -103,9 +110,13 @@
return
}
+ playingStateLogger.log("${data.playing}")
+ listeningStateLogger.log("${data.listening}")
+
holder.seekBar.thumb.alpha = if (data.seekAvailable) 255 else 0
holder.seekBar.isEnabled = data.seekAvailable
- progressDrawable?.animate = data.playing && !data.scrubbing && animationEnabled
+ progressDrawable?.animate =
+ data.playing && !data.scrubbing && animationEnabled && data.listening
progressDrawable?.transitionEnabled = !data.seekAvailable
if (holder.seekBar.maxHeight != seekBarEnabledMaxHeight) {
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/models/player/SeekBarViewModel.kt b/packages/SystemUI/src/com/android/systemui/media/controls/models/player/SeekBarViewModel.kt
index a91917a..40a9b9c 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/models/player/SeekBarViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/models/player/SeekBarViewModel.kt
@@ -84,7 +84,16 @@
@Background private val bgExecutor: RepeatableExecutor,
private val falsingManager: FalsingManager,
) {
- private var _data = Progress(false, false, false, false, null, 0)
+ private var _data =
+ Progress(
+ enabled = false,
+ seekAvailable = false,
+ playing = false,
+ scrubbing = false,
+ elapsedTime = null,
+ duration = 0,
+ listening = false
+ )
set(value) {
val enabledChanged = value.enabled != field.enabled
field = value
@@ -239,7 +248,7 @@
)
false
else true
- _data = Progress(enabled, seekAvailable, playing, scrubbing, position, duration)
+ _data = Progress(enabled, seekAvailable, playing, scrubbing, position, duration, listening)
checkIfPollingNeeded()
}
@@ -258,6 +267,7 @@
scrubbing = false,
elapsedTime = position,
duration = 100,
+ listening = false,
)
}
@@ -548,9 +558,12 @@
data class Progress(
val enabled: Boolean,
val seekAvailable: Boolean,
+ /** whether playback state is not paused or connecting */
val playing: Boolean,
val scrubbing: Boolean,
val elapsedTime: Int?,
- val duration: Int
+ val duration: Int,
+ /** whether seekBar is listening to progress updates */
+ val listening: Boolean,
)
}
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/LocalMediaManagerFactory.kt b/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/LocalMediaManagerFactory.kt
index 785a1e8..1d3cfd2 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/LocalMediaManagerFactory.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/LocalMediaManagerFactory.kt
@@ -30,7 +30,7 @@
private val localBluetoothManager: LocalBluetoothManager?
) {
/** Creates a [LocalMediaManager] for the given package. */
- fun create(packageName: String): LocalMediaManager {
+ fun create(packageName: String?): LocalMediaManager {
return InfoMediaManager.createInstance(context, packageName, null, localBluetoothManager)
.run { LocalMediaManager(context, localBluetoothManager, this, packageName) }
}
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaCarouselScrollHandler.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaCarouselScrollHandler.kt
index 02f0d12..038582c 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaCarouselScrollHandler.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaCarouselScrollHandler.kt
@@ -26,14 +26,15 @@
import androidx.core.view.GestureDetectorCompat
import androidx.dynamicanimation.animation.FloatPropertyCompat
import androidx.dynamicanimation.animation.SpringForce
+import com.android.app.tracing.TraceStateLogger
import com.android.internal.annotations.VisibleForTesting
import com.android.settingslib.Utils
import com.android.systemui.Gefingerpoken
-import com.android.systemui.res.R
import com.android.systemui.classifier.Classifier.NOTIFICATION_DISMISS
import com.android.systemui.media.controls.util.MediaUiEventLogger
import com.android.systemui.plugins.FalsingManager
import com.android.systemui.qs.PageIndicator
+import com.android.systemui.res.R
import com.android.systemui.util.concurrency.DelayableExecutor
import com.android.wm.shell.animation.PhysicsAnimator
@@ -42,6 +43,7 @@
private const val SCROLL_DELAY = 100L
private const val RUBBERBAND_FACTOR = 0.2f
private const val SETTINGS_BUTTON_TRANSLATION_FRACTION = 0.3f
+private const val TAG = "MediaCarouselScrollHandler"
/**
* Default spring configuration to use for animations where stiffness and/or damping ratio were not
@@ -63,6 +65,9 @@
private val logSmartspaceImpression: (Boolean) -> Unit,
private val logger: MediaUiEventLogger
) {
+ /** Trace state logger for media carousel visibility */
+ private val visibleStateLogger = TraceStateLogger("$TAG#visibleToUser")
+
/** Is the view in RTL */
val isRtl: Boolean
get() = scrollView.isLayoutRtl
@@ -182,6 +187,7 @@
if (field != value) {
field = value
seekBarUpdateListener.invoke(field)
+ visibleStateLogger.log("$visibleToUser")
}
}
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 5720cc7..eae7789 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
@@ -81,7 +81,7 @@
import com.android.internal.widget.CachingIconView;
import com.android.settingslib.widget.AdaptiveIcon;
import com.android.systemui.ActivityIntentHelper;
-import com.android.systemui.animation.ActivityLaunchAnimator;
+import com.android.systemui.animation.ActivityTransitionAnimator;
import com.android.systemui.animation.GhostedViewLaunchAnimatorController;
import com.android.systemui.bluetooth.BroadcastDialogController;
import com.android.systemui.broadcast.BroadcastSender;
@@ -1309,7 +1309,7 @@
}
@Nullable
- private ActivityLaunchAnimator.Controller buildLaunchAnimatorController(
+ private ActivityTransitionAnimator.Controller buildLaunchAnimatorController(
TransitionLayout player) {
if (!(player.getParent() instanceof ViewGroup)) {
// TODO(b/192194319): Throw instead of just logging.
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaHierarchyManager.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaHierarchyManager.kt
index c33ab12..35e0271 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaHierarchyManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaHierarchyManager.kt
@@ -586,7 +586,7 @@
coroutineScope.launch {
communalInteractor.isCommunalShowing.collect { value ->
isCommunalShowing = value
- updateDesiredLocation()
+ updateDesiredLocation(forceNoAnimation = true)
}
}
}
@@ -1149,16 +1149,12 @@
when {
mediaFlags.isSceneContainerEnabled() -> desiredLocation
dreamOverlayActive && dreamMediaComplicationActive -> LOCATION_DREAM_OVERLAY
-
- // UMO should show in communal unless the shade is expanding or visible.
- isCommunalShowing && qsExpansion == 0.0f -> LOCATION_COMMUNAL_HUB
(qsExpansion > 0.0f || inSplitShade) && !onLockscreen -> LOCATION_QS
qsExpansion > 0.4f && onLockscreen -> LOCATION_QS
onLockscreen && isSplitShadeExpanding() -> LOCATION_QS
onLockscreen && isTransformingToFullShadeAndInQQS() -> LOCATION_QQS
-
- // Communal does not have its own StatusBarState so it should always have higher
- // priority for the UMO over the lockscreen.
+ // TODO(b/311234666): revisit logic once interactions between the hub and
+ // shade/keyguard state are finalized
isCommunalShowing -> LOCATION_COMMUNAL_HUB
onLockscreen && allowMediaPlayerOnLockScreen -> LOCATION_LOCKSCREEN
else -> LOCATION_QQS
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/SquigglyProgress.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/SquigglyProgress.kt
index 16dfc21..47df021 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/SquigglyProgress.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/SquigglyProgress.kt
@@ -32,6 +32,7 @@
import android.util.MathUtils.lerpInvSat
import androidx.annotation.VisibleForTesting
import com.android.app.animation.Interpolators
+import com.android.app.tracing.traceSection
import com.android.internal.graphics.ColorUtils
import kotlin.math.abs
import kotlin.math.cos
@@ -127,6 +128,10 @@
}
override fun draw(canvas: Canvas) {
+ traceSection("SquigglyProgress#draw") { drawTraced(canvas) }
+ }
+
+ private fun drawTraced(canvas: Canvas) {
if (animate) {
invalidateSelf()
val now = SystemClock.uptimeMillis()
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java
index 375a0ce..687f268 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java
@@ -78,7 +78,7 @@
import com.android.settingslib.media.LocalMediaManager;
import com.android.settingslib.media.MediaDevice;
import com.android.settingslib.utils.ThreadUtils;
-import com.android.systemui.animation.ActivityLaunchAnimator;
+import com.android.systemui.animation.ActivityTransitionAnimator;
import com.android.systemui.animation.DialogLaunchAnimator;
import com.android.systemui.broadcast.BroadcastSender;
import com.android.systemui.flags.FeatureFlags;
@@ -400,7 +400,7 @@
void tryToLaunchInAppRoutingIntent(String routeId, View view) {
ComponentName componentName = mLocalMediaManager.getLinkedItemComponentName();
if (componentName != null) {
- ActivityLaunchAnimator.Controller controller =
+ ActivityTransitionAnimator.Controller controller =
mDialogLaunchAnimator.createActivityLaunchController(view);
Intent launchIntent = new Intent(ACTION_TRANSFER_MEDIA);
launchIntent.setComponent(componentName);
@@ -412,7 +412,7 @@
}
void tryToLaunchMediaApplication(View view) {
- ActivityLaunchAnimator.Controller controller =
+ ActivityTransitionAnimator.Controller controller =
mDialogLaunchAnimator.createActivityLaunchController(view);
Intent launchIntent = getAppLaunchIntent();
if (launchIntent != null) {
@@ -881,7 +881,7 @@
}
void launchBluetoothPairing(View view) {
- ActivityLaunchAnimator.Controller controller =
+ ActivityTransitionAnimator.Controller controller =
mDialogLaunchAnimator.createActivityLaunchController(view);
if (controller == null || (mKeyGuardManager != null
diff --git a/packages/SystemUI/src/com/android/systemui/media/muteawait/MediaMuteAwaitConnectionCli.kt b/packages/SystemUI/src/com/android/systemui/media/muteawait/MediaMuteAwaitConnectionCli.kt
index 2ae3a63..e475647 100644
--- a/packages/SystemUI/src/com/android/systemui/media/muteawait/MediaMuteAwaitConnectionCli.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/muteawait/MediaMuteAwaitConnectionCli.kt
@@ -16,13 +16,18 @@
package com.android.systemui.media.muteawait
-import android.content.Context
+import android.annotation.SuppressLint
import android.media.AudioAttributes.USAGE_MEDIA
import android.media.AudioDeviceAttributes
import android.media.AudioManager
+import com.android.systemui.CoreStartable
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.statusbar.commandline.Command
import com.android.systemui.statusbar.commandline.CommandRegistry
+import dagger.Binds
+import dagger.Module
+import dagger.multibindings.ClassKey
+import dagger.multibindings.IntoMap
import java.io.PrintWriter
import java.util.concurrent.TimeUnit
import javax.inject.Inject
@@ -30,14 +35,15 @@
/** A command line interface to manually test [MediaMuteAwaitConnectionManager]. */
@SysUISingleton
class MediaMuteAwaitConnectionCli @Inject constructor(
- commandRegistry: CommandRegistry,
- private val context: Context
-) {
- init {
+ private val commandRegistry: CommandRegistry,
+ private val audioManager: AudioManager,
+) : CoreStartable {
+ override fun start() {
commandRegistry.registerCommand(MEDIA_MUTE_AWAIT_COMMAND) { MuteAwaitCommand() }
}
inner class MuteAwaitCommand : Command {
+ @SuppressLint("MissingPermission")
override fun execute(pw: PrintWriter, args: List<String>) {
val device = AudioDeviceAttributes(
AudioDeviceAttributes.ROLE_OUTPUT,
@@ -49,8 +55,6 @@
)
val startOrCancel = args[2]
- val audioManager: AudioManager =
- context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
when (startOrCancel) {
START ->
audioManager.muteAwaitConnection(
@@ -65,6 +69,14 @@
"[type] [name] [$START|$CANCEL]")
}
}
+
+ @Module
+ interface StartableModule {
+ @Binds
+ @IntoMap
+ @ClassKey(MediaMuteAwaitConnectionCli::class)
+ fun bindsMediaMuteAwaitConnectionCli(impl: MediaMuteAwaitConnectionCli): CoreStartable
+ }
}
private const val MEDIA_MUTE_AWAIT_COMMAND = "media-mute-await"
diff --git a/packages/SystemUI/src/com/android/systemui/media/nearby/NearbyMediaDevicesManager.kt b/packages/SystemUI/src/com/android/systemui/media/nearby/NearbyMediaDevicesManager.kt
index 64b772b..0dc10f6 100644
--- a/packages/SystemUI/src/com/android/systemui/media/nearby/NearbyMediaDevicesManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/nearby/NearbyMediaDevicesManager.kt
@@ -18,9 +18,14 @@
import android.media.INearbyMediaDevicesProvider
import android.media.INearbyMediaDevicesUpdateCallback
-import com.android.systemui.dagger.SysUISingleton
import android.os.IBinder
+import com.android.systemui.CoreStartable
+import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.statusbar.CommandQueue
+import dagger.Binds
+import dagger.Module
+import dagger.multibindings.ClassKey
+import dagger.multibindings.IntoMap
import javax.inject.Inject
/**
@@ -30,9 +35,9 @@
*/
@SysUISingleton
class NearbyMediaDevicesManager @Inject constructor(
- commandQueue: CommandQueue,
+ private val commandQueue: CommandQueue,
private val logger: NearbyMediaDevicesLogger
-) {
+) : CoreStartable {
private var providers: MutableList<INearbyMediaDevicesProvider> = mutableListOf()
private var activeCallbacks: MutableList<INearbyMediaDevicesUpdateCallback> = mutableListOf()
@@ -69,7 +74,7 @@
}
}
- init {
+ override fun start() {
commandQueue.addCallback(commandQueueCallbacks)
}
@@ -108,4 +113,12 @@
}
}
}
+
+ @Module
+ interface StartableModule {
+ @Binds
+ @IntoMap
+ @ClassKey(NearbyMediaDevicesManager::class)
+ fun bindsNearbyMediaDevicesManager(impl: NearbyMediaDevicesManager): CoreStartable
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/MediaProjectionServiceHelper.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/MediaProjectionServiceHelper.kt
index f1cade7..afe6285 100644
--- a/packages/SystemUI/src/com/android/systemui/mediaprojection/MediaProjectionServiceHelper.kt
+++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/MediaProjectionServiceHelper.kt
@@ -16,6 +16,9 @@
package com.android.systemui.mediaprojection
+import android.compat.annotation.ChangeId
+import android.compat.annotation.Disabled
+import android.compat.annotation.Overridable
import android.content.Context
import android.media.projection.IMediaProjection
import android.media.projection.IMediaProjectionManager
@@ -31,6 +34,18 @@
*/
class MediaProjectionServiceHelper {
companion object {
+ /**
+ * This change id ensures that users are presented with a choice of capturing a single app
+ * or the entire screen when initiating a MediaProjection session, overriding the usage of
+ * MediaProjectionConfig#createConfigForDefaultDisplay.
+ *
+ * @hide
+ */
+ @ChangeId
+ @Overridable
+ @Disabled
+ const val OVERRIDE_DISABLE_MEDIA_PROJECTION_SINGLE_APP_OPTION = 316897322L // buganizer id
+
private const val TAG = "MediaProjectionServiceHelper"
private val service =
IMediaProjectionManager.Stub.asInterface(
diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/MediaProjectionPermissionActivity.java b/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/MediaProjectionPermissionActivity.java
index 8b034b2..0769731 100644
--- a/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/MediaProjectionPermissionActivity.java
+++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/MediaProjectionPermissionActivity.java
@@ -16,21 +16,26 @@
package com.android.systemui.mediaprojection.permission;
+import static android.Manifest.permission.LOG_COMPAT_CHANGE;
+import static android.Manifest.permission.READ_COMPAT_CHANGE_CONFIG;
import static android.media.projection.IMediaProjectionManager.EXTRA_PACKAGE_REUSING_GRANTED_CONSENT;
import static android.media.projection.IMediaProjectionManager.EXTRA_USER_REVIEW_GRANTED_CONSENT;
import static android.media.projection.ReviewGrantedConsentResult.RECORD_CANCEL;
import static android.media.projection.ReviewGrantedConsentResult.RECORD_CONTENT_DISPLAY;
import static android.view.WindowManager.LayoutParams.SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS;
+import static com.android.systemui.mediaprojection.MediaProjectionServiceHelper.OVERRIDE_DISABLE_MEDIA_PROJECTION_SINGLE_APP_OPTION;
import static com.android.systemui.mediaprojection.permission.ScreenShareOptionKt.ENTIRE_SCREEN;
import static com.android.systemui.mediaprojection.permission.ScreenShareOptionKt.SINGLE_APP;
import android.annotation.Nullable;
+import android.annotation.RequiresPermission;
import android.app.Activity;
import android.app.ActivityManager;
import android.app.ActivityOptions.LaunchCookie;
import android.app.AlertDialog;
import android.app.StatusBarManager;
+import android.app.compat.CompatChanges;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
@@ -104,6 +109,7 @@
}
@Override
+ @RequiresPermission(allOf = {READ_COMPAT_CHANGE_CONFIG, LOG_COMPAT_CHANGE})
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
@@ -231,6 +237,9 @@
// the correct screen width when in split screen.
Context dialogContext = getApplicationContext();
if (isPartialScreenSharingEnabled()) {
+ final boolean overrideDisableSingleAppOption = CompatChanges.isChangeEnabled(
+ OVERRIDE_DISABLE_MEDIA_PROJECTION_SINGLE_APP_OPTION,
+ mPackageName, getHostUserHandle());
MediaProjectionPermissionDialogDelegate delegate =
new MediaProjectionPermissionDialogDelegate(
dialogContext,
@@ -242,6 +251,7 @@
},
() -> finish(RECORD_CANCEL, /* projection= */ null),
appName,
+ overrideDisableSingleAppOption,
mUid,
mMediaProjectionMetricsLogger);
mDialog =
diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/MediaProjectionPermissionDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/MediaProjectionPermissionDialogDelegate.kt
index 0f54e93..9ce8070 100644
--- a/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/MediaProjectionPermissionDialogDelegate.kt
+++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/MediaProjectionPermissionDialogDelegate.kt
@@ -30,11 +30,12 @@
private val onStartRecordingClicked: Consumer<MediaProjectionPermissionDialogDelegate>,
private val onCancelClicked: Runnable,
private val appName: String?,
+ forceShowPartialScreenshare: Boolean,
hostUid: Int,
mediaProjectionMetricsLogger: MediaProjectionMetricsLogger,
) :
BaseMediaProjectionPermissionDialogDelegate<AlertDialog>(
- createOptionList(context, appName, mediaProjectionConfig),
+ createOptionList(context, appName, mediaProjectionConfig, forceShowPartialScreenshare),
appName,
hostUid,
mediaProjectionMetricsLogger
@@ -65,7 +66,8 @@
private fun createOptionList(
context: Context,
appName: String?,
- mediaProjectionConfig: MediaProjectionConfig?
+ mediaProjectionConfig: MediaProjectionConfig?,
+ overrideDisableSingleAppOption: Boolean = false,
): List<ScreenShareOption> {
val singleAppWarningText =
if (appName == null) {
@@ -80,8 +82,13 @@
R.string.media_projection_entry_app_permission_dialog_warning_entire_screen
}
+ // The single app option should only be disabled if there is an app name provided,
+ // the client has setup a MediaProjection with
+ // MediaProjectionConfig#createConfigForDefaultDisplay, AND it hasn't been overridden by
+ // the OVERRIDE_DISABLE_SINGLE_APP_OPTION per-app override.
val singleAppOptionDisabled =
appName != null &&
+ !overrideDisableSingleAppOption &&
mediaProjectionConfig?.regionToCapture ==
MediaProjectionConfig.CAPTURE_REGION_FIXED_DISPLAY
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/NavBarButtonClickLogger.kt b/packages/SystemUI/src/com/android/systemui/navigationbar/NavBarButtonClickLogger.kt
new file mode 100644
index 0000000..408acf3
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/NavBarButtonClickLogger.kt
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2024 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.navigationbar
+
+import com.android.systemui.log.LogBuffer
+import com.android.systemui.log.core.LogLevel
+import com.android.systemui.log.dagger.NavBarButtonClickLog
+import javax.inject.Inject
+
+class NavBarButtonClickLogger
+@Inject
+constructor(@NavBarButtonClickLog private val buffer: LogBuffer) {
+ fun logHomeButtonClick() {
+ buffer.log(TAG, LogLevel.DEBUG, {}, { "Home Button Triggered" })
+ }
+
+ fun logBackButtonClick() {
+ buffer.log(TAG, LogLevel.DEBUG, {}, { "Back Button Triggered" })
+ }
+
+ fun logRecentsButtonClick() {
+ buffer.log(TAG, LogLevel.DEBUG, {}, { "Recents Button Triggered" })
+ }
+
+ fun logImeSwitcherClick() {
+ buffer.log(TAG, LogLevel.DEBUG, {}, { "Ime Switcher Triggered" })
+ }
+
+ fun logAccessibilityButtonClick() {
+ buffer.log(TAG, LogLevel.DEBUG, {}, { "Accessibility Button Triggered" })
+ }
+}
+
+private const val TAG = "NavBarButtonClick"
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/NavBarHelper.java b/packages/SystemUI/src/com/android/systemui/navigationbar/NavBarHelper.java
index 7d13397..dfe41eb 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/NavBarHelper.java
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/NavBarHelper.java
@@ -56,7 +56,6 @@
import androidx.annotation.NonNull;
-import com.android.internal.accessibility.common.ShortcutConstants;
import com.android.systemui.Dumpable;
import com.android.systemui.accessibility.AccessibilityButtonModeObserver;
import com.android.systemui.accessibility.AccessibilityButtonTargetsObserver;
@@ -381,7 +380,7 @@
// permission
final List<String> a11yButtonTargets =
mAccessibilityManager.getAccessibilityShortcutTargets(
- ShortcutConstants.UserShortcutType.SOFTWARE);
+ AccessibilityManager.ACCESSIBILITY_BUTTON);
final int requestingServices = a11yButtonTargets.size();
clickable = requestingServices >= 1;
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java
index 068e5fd..95b75ac 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java
@@ -84,11 +84,7 @@
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.Surface;
-import android.view.SurfaceControl;
-import android.view.SurfaceControl.Transaction;
import android.view.View;
-import android.view.ViewRootImpl;
-import android.view.ViewRootImpl.SurfaceChangedCallback;
import android.view.ViewTreeObserver;
import android.view.ViewTreeObserver.InternalInsetsInfo;
import android.view.ViewTreeObserver.OnComputeInternalInsetsListener;
@@ -285,6 +281,7 @@
private boolean mImeVisible;
private final Rect mSamplingBounds = new Rect();
private final Binder mInsetsSourceOwner = new Binder();
+ private final NavBarButtonClickLogger mNavBarButtonClickLogger;
/**
* When quickswitching between apps of different orientations, we draw a secondary home handle
@@ -559,7 +556,8 @@
UserContextProvider userContextProvider,
WakefulnessLifecycle wakefulnessLifecycle,
TaskStackChangeListeners taskStackChangeListeners,
- DisplayTracker displayTracker) {
+ DisplayTracker displayTracker,
+ NavBarButtonClickLogger navBarButtonClickLogger) {
super(navigationBarView);
mFrame = navigationBarFrame;
mContext = context;
@@ -601,6 +599,7 @@
mTaskStackChangeListeners = taskStackChangeListeners;
mDisplayTracker = displayTracker;
mEdgeBackGestureHandler = navBarHelper.getEdgeBackGestureHandler();
+ mNavBarButtonClickLogger = navBarButtonClickLogger;
mNavColorSampleMargin = getResources()
.getDimensionPixelSize(R.dimen.navigation_handle_sample_horizontal_margin);
@@ -1276,6 +1275,10 @@
ButtonDispatcher homeButton = mView.getHomeButton();
homeButton.setOnTouchListener(this::onHomeTouch);
+ homeButton.setNavBarButtonClickLogger(mNavBarButtonClickLogger);
+
+ ButtonDispatcher backButton = mView.getBackButton();
+ backButton.setNavBarButtonClickLogger(mNavBarButtonClickLogger);
reconfigureHomeLongClick();
@@ -1388,6 +1391,8 @@
}
private void onRecentsClick(View v) {
+ mNavBarButtonClickLogger.logRecentsButtonClick();
+
if (LatencyTracker.isEnabled(mContext)) {
LatencyTracker.getInstance(mContext).onActionStart(
LatencyTracker.ACTION_TOGGLE_RECENTS);
@@ -1397,6 +1402,7 @@
}
private void onImeSwitcherClick(View v) {
+ mNavBarButtonClickLogger.logImeSwitcherClick();
mInputMethodManager.showInputMethodPickerFromSystem(
true /* showAuxiliarySubtypes */, mDisplayId);
mUiEventLogger.log(KeyButtonView.NavBarButtonEvent.NAVBAR_IME_SWITCHER_BUTTON_TAP);
@@ -1486,6 +1492,7 @@
}
private void onAccessibilityClick(View v) {
+ mNavBarButtonClickLogger.logAccessibilityButtonClick();
final Display display = v.getDisplay();
mAccessibilityManager.notifyAccessibilityButtonClicked(
display != null ? display.getDisplayId() : mDisplayTracker.getDefaultDisplayId());
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/TaskbarDelegate.java b/packages/SystemUI/src/com/android/systemui/navigationbar/TaskbarDelegate.java
index 0167287..aa03e6e 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/TaskbarDelegate.java
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/TaskbarDelegate.java
@@ -395,7 +395,7 @@
}
if (displayId == mDisplayId) {
mLightBarController.onNavigationBarAppearanceChanged(appearance, nbModeChanged,
- BarTransitions.MODE_TRANSPARENT, navbarColorManagedByIme);
+ mTransitionMode, navbarColorManagedByIme);
}
if (mBehavior != behavior) {
mBehavior = behavior;
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/buttons/ButtonDispatcher.java b/packages/SystemUI/src/com/android/systemui/navigationbar/buttons/ButtonDispatcher.java
index 5739abc..fc37b9f 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/buttons/ButtonDispatcher.java
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/buttons/ButtonDispatcher.java
@@ -23,6 +23,9 @@
import android.animation.ValueAnimator;
import android.view.View;
import android.view.View.AccessibilityDelegate;
+import android.view.ViewGroup;
+
+import com.android.systemui.navigationbar.NavBarButtonClickLogger;
import java.util.ArrayList;
@@ -52,6 +55,7 @@
private boolean mVertical;
private ValueAnimator mFadeAnimator;
private AccessibilityDelegate mAccessibilityDelegate;
+ private NavBarButtonClickLogger mNavBarButtonClickLogger;
private final ValueAnimator.AnimatorUpdateListener mAlphaListener = animation ->
setAlpha(
@@ -341,4 +345,36 @@
*/
public void onDestroy() {
}
+
+ /**
+ * Sets the NavBarButtonClickLogger for all the KeyButtonViews respectively.
+ */
+ public void setNavBarButtonClickLogger(NavBarButtonClickLogger navBarButtonClickLogger) {
+ if (navBarButtonClickLogger != null) {
+ mNavBarButtonClickLogger = navBarButtonClickLogger;
+ final int size = mViews.size();
+ for (int i = 0; i < size; i++) {
+ final View v = mViews.get(i);
+ setNavBarButtonClickLoggerForViewChildren(v);
+ }
+ }
+ }
+
+ /**
+ * Recursively explores view hierarchy until the children of provided view are of type
+ * KeyButtonView, so the NavBarButtonClickLogger can be set on them.
+ */
+ private void setNavBarButtonClickLoggerForViewChildren(View v) {
+ if (v instanceof KeyButtonView) {
+ ((KeyButtonView) v).setNavBarButtonClickLogger(mNavBarButtonClickLogger);
+ return;
+ }
+
+ if (v instanceof ViewGroup viewGroup) {
+ final int childrenCount = viewGroup.getChildCount();
+ for (int i = 0; i < childrenCount; i++) {
+ setNavBarButtonClickLoggerForViewChildren(viewGroup.getChildAt(i));
+ }
+ }
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/buttons/KeyButtonView.java b/packages/SystemUI/src/com/android/systemui/navigationbar/buttons/KeyButtonView.java
index df6843d..dbe87ea 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/buttons/KeyButtonView.java
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/buttons/KeyButtonView.java
@@ -59,6 +59,7 @@
import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
import com.android.systemui.Dependency;
import com.android.systemui.assist.AssistManager;
+import com.android.systemui.navigationbar.NavBarButtonClickLogger;
import com.android.systemui.recents.OverviewProxyService;
import com.android.systemui.res.R;
import com.android.systemui.shared.navigationbar.KeyButtonRipple;
@@ -86,6 +87,7 @@
private final Paint mOvalBgPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG);
private float mDarkIntensity;
private boolean mHasOvalBg = false;
+ private NavBarButtonClickLogger mNavBarButtonClickLogger;
@VisibleForTesting
public enum NavBarButtonEvent implements UiEventLogger.UiEventEnum {
@@ -197,6 +199,10 @@
mOnClickListener = onClickListener;
}
+ public void setNavBarButtonClickLogger(NavBarButtonClickLogger navBarButtonClickLogger) {
+ mNavBarButtonClickLogger = navBarButtonClickLogger;
+ }
+
public void loadAsync(Icon icon) {
new AsyncTask<Icon, Void, Drawable>() {
@Override
@@ -389,11 +395,19 @@
uiEvent = longPressSet
? NavBarButtonEvent.NAVBAR_BACK_BUTTON_LONGPRESS
: NavBarButtonEvent.NAVBAR_BACK_BUTTON_TAP;
+
+ if (mNavBarButtonClickLogger != null) {
+ mNavBarButtonClickLogger.logBackButtonClick();
+ }
break;
case KeyEvent.KEYCODE_HOME:
uiEvent = longPressSet
? NavBarButtonEvent.NAVBAR_HOME_BUTTON_LONGPRESS
: NavBarButtonEvent.NAVBAR_HOME_BUTTON_TAP;
+
+ if (mNavBarButtonClickLogger != null) {
+ mNavBarButtonClickLogger.logHomeButtonClick();
+ }
break;
case KeyEvent.KEYCODE_APP_SWITCH:
uiEvent = longPressSet
diff --git a/packages/SystemUI/src/com/android/systemui/process/ProcessWrapper.java b/packages/SystemUI/src/com/android/systemui/process/ProcessWrapper.java
index 2751072..3671dd4 100644
--- a/packages/SystemUI/src/com/android/systemui/process/ProcessWrapper.java
+++ b/packages/SystemUI/src/com/android/systemui/process/ProcessWrapper.java
@@ -16,6 +16,9 @@
package com.android.systemui.process;
+import android.os.Process;
+import android.os.UserHandle;
+
import javax.inject.Inject;
/**
@@ -30,6 +33,15 @@
* Returns {@code true} if System User is running the current process.
*/
public boolean isSystemUser() {
- return android.os.Process.myUserHandle().isSystem();
+ return myUserHandle().isSystem();
+ }
+
+ /**
+ * Returns {@link UserHandle} as returned statically by {@link Process#myUserHandle()}.
+ *
+ * Please strongly consider using {@link com.android.systemui.settings.UserTracker} instead.
+ */
+ public UserHandle myUserHandle() {
+ return Process.myUserHandle();
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/HeaderPrivacyIconsController.kt b/packages/SystemUI/src/com/android/systemui/qs/HeaderPrivacyIconsController.kt
index 0941a20..3fd9803 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/HeaderPrivacyIconsController.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/HeaderPrivacyIconsController.kt
@@ -11,7 +11,7 @@
import androidx.annotation.WorkerThread
import com.android.internal.R
import com.android.internal.logging.UiEventLogger
-import com.android.systemui.animation.ActivityLaunchAnimator
+import com.android.systemui.animation.ActivityTransitionAnimator
import com.android.systemui.appops.AppOpsController
import com.android.systemui.broadcast.BroadcastDispatcher
import com.android.systemui.flags.FeatureFlags
@@ -175,7 +175,7 @@
startSafetyCenter.flags = Intent.FLAG_ACTIVITY_NEW_TASK
uiExecutor.execute {
activityStarter.startActivity(startSafetyCenter, true,
- ActivityLaunchAnimator.Controller.fromView(privacyChip))
+ ActivityTransitionAnimator.Controller.fromView(privacyChip))
}
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSContainerImpl.java b/packages/SystemUI/src/com/android/systemui/qs/QSContainerImpl.java
index a2dfc01..c0d9644 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSContainerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSContainerImpl.java
@@ -157,6 +157,12 @@
super.measureChildWithMargins(child, parentWidthMeasureSpec, widthUsed,
parentHeightMeasureSpec, heightUsed);
}
+ } else {
+ // Don't measure the customizer with all the children, it will be measured separately
+ if (child != mQSCustomizer) {
+ super.measureChildWithMargins(child, parentWidthMeasureSpec, widthUsed,
+ parentHeightMeasureSpec, heightUsed);
+ }
}
}
@@ -248,6 +254,25 @@
+ mHeader.getHeight();
}
+ // These next two methods are used with Scene container to determine the size of QQS and QS .
+
+ /**
+ * Returns the size of the QQS container, regardless of the measured size of this view.
+ * @return size in pixels of QQS
+ */
+ public int getQqsHeight() {
+ return mHeader.getHeight();
+ }
+
+ /**
+ * Returns the size of QS (or the QSCustomizer), regardless of the measured size of this view
+ * @return size in pixels of QS (or QSCustomizer)
+ */
+ public int getQsHeight() {
+ return mQSCustomizer.isCustomizing() ? mQSCustomizer.getMeasuredHeight()
+ : mQSPanel.getMeasuredHeight();
+ }
+
public void setExpansion(float expansion) {
mQsExpansion = expansion;
if (mQSPanelContainer != null) {
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSImpl.java b/packages/SystemUI/src/com/android/systemui/qs/QSImpl.java
index 290821e..4d55714 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSImpl.java
@@ -984,6 +984,14 @@
return mListeningAndVisibilityLifecycleOwner;
}
+ public int getQQSHeight() {
+ return mContainer.getQqsHeight();
+ }
+
+ public int getQSHeight() {
+ return mContainer.getQsHeight();
+ }
+
@NeverCompile
@Override
public void dump(PrintWriter pw, String[] args) {
diff --git a/packages/SystemUI/src/com/android/systemui/qs/external/CustomTile.java b/packages/SystemUI/src/com/android/systemui/qs/external/CustomTile.java
index bd13d06..b53c245 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/external/CustomTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/external/CustomTile.java
@@ -51,7 +51,7 @@
import com.android.internal.jank.InteractionJankMonitor;
import com.android.internal.logging.MetricsLogger;
import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
-import com.android.systemui.animation.ActivityLaunchAnimator;
+import com.android.systemui.animation.ActivityTransitionAnimator;
import com.android.systemui.dagger.qualifiers.Background;
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.plugins.ActivityStarter;
@@ -65,14 +65,14 @@
import com.android.systemui.qs.tileimpl.QSTileImpl;
import com.android.systemui.settings.DisplayTracker;
-import java.util.Objects;
-import java.util.concurrent.atomic.AtomicBoolean;
-
import dagger.Lazy;
import dagger.assisted.Assisted;
import dagger.assisted.AssistedFactory;
import dagger.assisted.AssistedInject;
+import java.util.Objects;
+import java.util.concurrent.atomic.AtomicBoolean;
+
public class CustomTile extends QSTileImpl<State> implements TileChangeListener,
CustomTileInterface {
public static final String PREFIX = "custom(";
@@ -540,9 +540,9 @@
} else {
Log.i(TAG, "The activity is starting");
- ActivityLaunchAnimator.Controller controller =
+ ActivityTransitionAnimator.Controller controller =
mViewClicked == null ? null :
- ActivityLaunchAnimator.Controller.fromView(
+ ActivityTransitionAnimator.Controller.fromView(
mViewClicked,
InteractionJankMonitor.CUJ_SHADE_APP_LAUNCH_FROM_QS_TILE
);
diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/InstalledTilesComponentRepository.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/InstalledTilesComponentRepository.kt
index dc39c97..93021ba 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/InstalledTilesComponentRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/InstalledTilesComponentRepository.kt
@@ -27,7 +27,7 @@
import android.service.quicksettings.TileService
import androidx.annotation.GuardedBy
import com.android.systemui.common.data.repository.PackageChangeRepository
-import com.android.systemui.common.data.shared.model.PackageChangeModel
+import com.android.systemui.common.shared.model.PackageChangeModel
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.dagger.qualifiers.Background
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSIconViewImpl.java b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSIconViewImpl.java
index 53f287b..720120b 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSIconViewImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSIconViewImpl.java
@@ -47,6 +47,8 @@
public static final long QS_ANIM_LENGTH = 350;
+ private static final long ICON_APPLIED_TRANSACTION_ID = -1;
+
protected final View mIcon;
protected int mIconSizePx;
private boolean mAnimationEnabled = true;
@@ -57,7 +59,8 @@
@VisibleForTesting
QSTile.Icon mLastIcon;
- private boolean mIconChangeScheduled;
+ private long mScheduledIconChangeTransactionId = ICON_APPLIED_TRANSACTION_ID;
+ private long mHighestScheduledIconChangeTransactionId = ICON_APPLIED_TRANSACTION_ID;
private ValueAnimator mColorAnimator = new ValueAnimator();
@@ -117,7 +120,7 @@
}
protected void updateIcon(ImageView iv, State state, boolean allowAnimations) {
- mIconChangeScheduled = false;
+ mScheduledIconChangeTransactionId = ICON_APPLIED_TRANSACTION_ID;
final QSTile.Icon icon = state.iconSupplier != null ? state.iconSupplier.get() : state.icon;
if (!Objects.equals(icon, iv.getTag(R.id.qs_icon_tag))) {
boolean shouldAnimate = allowAnimations && shouldAnimate(iv);
@@ -173,9 +176,10 @@
mState = state.state;
mDisabledByPolicy = state.disabledByPolicy;
if (mTint != 0 && allowAnimations && shouldAnimate(iv)) {
- mIconChangeScheduled = true;
+ final long iconTransactionId = getNextIconTransactionId();
+ mScheduledIconChangeTransactionId = iconTransactionId;
animateGrayScale(mTint, color, iv, () -> {
- if (mIconChangeScheduled) {
+ if (mScheduledIconChangeTransactionId == iconTransactionId) {
updateIcon(iv, state, allowAnimations);
}
});
@@ -237,6 +241,11 @@
child.layout(left, top, left + child.getMeasuredWidth(), top + child.getMeasuredHeight());
}
+ private long getNextIconTransactionId() {
+ mHighestScheduledIconChangeTransactionId++;
+ return mHighestScheduledIconChangeTransactionId;
+ }
+
/**
* Color to tint the tile icon based on state
*/
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileImpl.java b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileImpl.java
index 529d684..35cac4b 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileImpl.java
@@ -57,7 +57,7 @@
import com.android.settingslib.RestrictedLockUtils;
import com.android.settingslib.RestrictedLockUtilsInternal;
import com.android.systemui.Dumpable;
-import com.android.systemui.animation.ActivityLaunchAnimator;
+import com.android.systemui.animation.ActivityTransitionAnimator;
import com.android.systemui.plugins.ActivityStarter;
import com.android.systemui.plugins.FalsingManager;
import com.android.systemui.plugins.qs.QSTile;
@@ -70,7 +70,6 @@
import com.android.systemui.qs.logging.QSLogger;
import java.io.PrintWriter;
-import java.util.ArrayList;
/**
* Base quick-settings tile, extend this to create a new tile.
@@ -412,8 +411,8 @@
* @param view The view from which the opening window will be animated.
*/
protected void handleLongClick(@Nullable View view) {
- ActivityLaunchAnimator.Controller animationController =
- view != null ? ActivityLaunchAnimator.Controller.fromView(view,
+ ActivityTransitionAnimator.Controller animationController =
+ view != null ? ActivityTransitionAnimator.Controller.fromView(view,
InteractionJankMonitor.CUJ_SHADE_APP_LAUNCH_FROM_QS_TILE) : null;
mActivityStarter.postStartActivityDismissingKeyguard(getLongClickIntent(), 0,
animationController);
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/AlarmTile.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/AlarmTile.kt
index d98141f..688f3ca 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/AlarmTile.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/AlarmTile.kt
@@ -14,7 +14,7 @@
import com.android.internal.jank.InteractionJankMonitor
import com.android.internal.logging.MetricsLogger
import com.android.systemui.res.R
-import com.android.systemui.animation.ActivityLaunchAnimator
+import com.android.systemui.animation.ActivityTransitionAnimator
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.plugins.ActivityStarter
@@ -75,7 +75,7 @@
override fun handleClick(view: View?) {
val animationController = view?.let {
- ActivityLaunchAnimator.Controller.fromView(
+ ActivityTransitionAnimator.Controller.fromView(
it, InteractionJankMonitor.CUJ_SHADE_APP_LAUNCH_FROM_QS_TILE)
}
val pendingIntent = lastAlarmInfo?.showIntent
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/CastTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/CastTile.java
index a698208..690b711 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/CastTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/CastTile.java
@@ -38,7 +38,7 @@
import com.android.internal.jank.InteractionJankMonitor;
import com.android.internal.logging.MetricsLogger;
import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
-import com.android.systemui.animation.ActivityLaunchAnimator;
+import com.android.systemui.animation.ActivityTransitionAnimator;
import com.android.systemui.animation.DialogCuj;
import com.android.systemui.animation.DialogLaunchAnimator;
import com.android.systemui.dagger.qualifiers.Background;
@@ -222,7 +222,7 @@
mContext,
ROUTE_TYPE_REMOTE_DISPLAY,
v -> {
- ActivityLaunchAnimator.Controller controller =
+ ActivityTransitionAnimator.Controller controller =
mDialogLaunchAnimator.createActivityLaunchController(v);
if (controller == null) {
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/DeviceControlsTile.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/DeviceControlsTile.kt
index 91b2d97..bb175e2 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/DeviceControlsTile.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/DeviceControlsTile.kt
@@ -26,7 +26,7 @@
import com.android.internal.jank.InteractionJankMonitor
import com.android.internal.logging.MetricsLogger
import com.android.systemui.res.R
-import com.android.systemui.animation.ActivityLaunchAnimator
+import com.android.systemui.animation.ActivityTransitionAnimator
import com.android.systemui.controls.ControlsServiceInfo
import com.android.systemui.controls.dagger.ControlsComponent
import com.android.systemui.controls.dagger.ControlsComponent.Visibility.AVAILABLE
@@ -112,7 +112,7 @@
putExtra(ControlsUiController.EXTRA_ANIMATE, true)
}
val animationController = view?.let {
- ActivityLaunchAnimator.Controller.fromView(
+ ActivityTransitionAnimator.Controller.fromView(
it, InteractionJankMonitor.CUJ_SHADE_APP_LAUNCH_FROM_QS_TILE)
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/QRCodeScannerTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/QRCodeScannerTile.java
index f70e27d..de9a08e 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/QRCodeScannerTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/QRCodeScannerTile.java
@@ -27,8 +27,7 @@
import com.android.internal.jank.InteractionJankMonitor;
import com.android.internal.logging.MetricsLogger;
-import com.android.systemui.res.R;
-import com.android.systemui.animation.ActivityLaunchAnimator;
+import com.android.systemui.animation.ActivityTransitionAnimator;
import com.android.systemui.dagger.qualifiers.Background;
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.plugins.ActivityStarter;
@@ -40,6 +39,7 @@
import com.android.systemui.qs.QsEventLogger;
import com.android.systemui.qs.logging.QSLogger;
import com.android.systemui.qs.tileimpl.QSTileImpl;
+import com.android.systemui.res.R;
import javax.inject.Inject;
@@ -108,8 +108,8 @@
return;
}
- ActivityLaunchAnimator.Controller animationController =
- view == null ? null : ActivityLaunchAnimator.Controller.fromView(view,
+ ActivityTransitionAnimator.Controller animationController =
+ view == null ? null : ActivityTransitionAnimator.Controller.fromView(view,
InteractionJankMonitor.CUJ_SHADE_APP_LAUNCH_FROM_QS_TILE);
mActivityStarter.startActivity(intent, true /* dismissShade */,
animationController, true /* showOverLockscreenWhenLocked */);
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/QuickAccessWalletTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/QuickAccessWalletTile.java
index 3b8fb26..1b73225 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/QuickAccessWalletTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/QuickAccessWalletTile.java
@@ -43,7 +43,7 @@
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.jank.InteractionJankMonitor;
import com.android.internal.logging.MetricsLogger;
-import com.android.systemui.animation.ActivityLaunchAnimator;
+import com.android.systemui.animation.ActivityTransitionAnimator;
import com.android.systemui.dagger.qualifiers.Background;
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.plugins.ActivityStarter;
@@ -132,8 +132,8 @@
@Override
protected void handleClick(@Nullable View view) {
- ActivityLaunchAnimator.Controller animationController =
- view == null ? null : ActivityLaunchAnimator.Controller.fromView(view,
+ ActivityTransitionAnimator.Controller animationController =
+ view == null ? null : ActivityTransitionAnimator.Controller.fromView(view,
InteractionJankMonitor.CUJ_SHADE_APP_LAUNCH_FROM_QS_TILE);
mUiHandler.post(
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/RecordIssueTile.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/RecordIssueTile.kt
index 88863cb..a474868 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/RecordIssueTile.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/RecordIssueTile.kt
@@ -42,6 +42,7 @@
import com.android.systemui.qs.QsEventLogger
import com.android.systemui.qs.logging.QSLogger
import com.android.systemui.qs.tileimpl.QSTileImpl
+import com.android.systemui.recordissue.IssueRecordingService
import com.android.systemui.recordissue.RecordIssueDialogDelegate
import com.android.systemui.res.R
import com.android.systemui.screenrecord.RecordingService
@@ -107,7 +108,7 @@
PendingIntent.getService(
userContextProvider.userContext,
RecordingService.REQUEST_CODE,
- RecordingService.getStopIntent(userContextProvider.userContext),
+ IssueRecordingService.getStopIntent(userContextProvider.userContext),
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
.send(BroadcastOptions.makeBasic().apply { isInteractive = true }.toBundle())
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/base/actions/QSTileIntentUserInputHandler.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/base/actions/QSTileIntentUserInputHandler.kt
index fe10eaa..7192f58 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/base/actions/QSTileIntentUserInputHandler.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/base/actions/QSTileIntentUserInputHandler.kt
@@ -22,7 +22,7 @@
import android.os.UserHandle
import android.view.View
import com.android.internal.jank.InteractionJankMonitor
-import com.android.systemui.animation.ActivityLaunchAnimator
+import com.android.systemui.animation.ActivityTransitionAnimator
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.plugins.ActivityStarter
import javax.inject.Inject
@@ -53,9 +53,9 @@
) : QSTileIntentUserInputHandler {
override fun handle(view: View?, intent: Intent) {
- val animationController: ActivityLaunchAnimator.Controller? =
+ val animationController: ActivityTransitionAnimator.Controller? =
view?.let {
- ActivityLaunchAnimator.Controller.fromView(
+ ActivityTransitionAnimator.Controller.fromView(
it,
InteractionJankMonitor.CUJ_SHADE_APP_LAUNCH_FROM_QS_TILE,
)
@@ -70,9 +70,9 @@
requestLaunchingDefaultActivity: Boolean
) {
if (pendingIntent.isActivity) {
- val animationController: ActivityLaunchAnimator.Controller? =
+ val animationController: ActivityTransitionAnimator.Controller? =
view?.let {
- ActivityLaunchAnimator.Controller.fromView(
+ ActivityTransitionAnimator.Controller.fromView(
it,
InteractionJankMonitor.CUJ_SHADE_APP_LAUNCH_FROM_QS_TILE,
)
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogController.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogController.java
index 211b4594..41de65c1 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogController.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogController.java
@@ -75,7 +75,7 @@
import com.android.settingslib.net.SignalStrengthUtil;
import com.android.settingslib.wifi.WifiUtils;
import com.android.settingslib.wifi.dpp.WifiDppIntentHelper;
-import com.android.systemui.animation.ActivityLaunchAnimator;
+import com.android.systemui.animation.ActivityTransitionAnimator;
import com.android.systemui.animation.DialogLaunchAnimator;
import com.android.systemui.broadcast.BroadcastDispatcher;
import com.android.systemui.dagger.qualifiers.Background;
@@ -748,7 +748,7 @@
}
private void startActivity(Intent intent, View view) {
- ActivityLaunchAnimator.Controller controller =
+ ActivityTransitionAnimator.Controller controller =
mDialogLaunchAnimator.createActivityLaunchController(view);
if (controller == null && mCallback != null) {
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/flashlight/domain/FlashlightMapper.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/flashlight/domain/FlashlightMapper.kt
index 1b3b584..58e7613 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/flashlight/domain/FlashlightMapper.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/flashlight/domain/FlashlightMapper.kt
@@ -40,7 +40,7 @@
val icon =
Icon.Loaded(
resources.getDrawable(
- if (data.isEnabled) {
+ if (data is FlashlightTileModel.FlashlightAvailable && data.isEnabled) {
R.drawable.qs_flashlight_icon_on
} else {
R.drawable.qs_flashlight_icon_off
@@ -51,17 +51,22 @@
)
this.icon = { icon }
- if (data.isEnabled) {
+ contentDescription = label
+
+ if (data is FlashlightTileModel.FlashlightTemporarilyUnavailable) {
+ activationState = QSTileState.ActivationState.UNAVAILABLE
+ secondaryLabel =
+ resources.getString(R.string.quick_settings_flashlight_camera_in_use)
+ stateDescription = secondaryLabel
+ supportedActions = setOf()
+ return@build
+ } else if (data is FlashlightTileModel.FlashlightAvailable && data.isEnabled) {
activationState = QSTileState.ActivationState.ACTIVE
secondaryLabel = resources.getStringArray(R.array.tile_states_flashlight)[2]
} else {
activationState = QSTileState.ActivationState.INACTIVE
secondaryLabel = resources.getStringArray(R.array.tile_states_flashlight)[1]
}
- contentDescription = label
- supportedActions =
- setOf(
- QSTileState.UserAction.CLICK,
- )
+ supportedActions = setOf(QSTileState.UserAction.CLICK)
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/flashlight/domain/interactor/FlashlightTileDataInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/flashlight/domain/interactor/FlashlightTileDataInteractor.kt
index 53d4cf9..1544804 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/flashlight/domain/interactor/FlashlightTileDataInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/flashlight/domain/interactor/FlashlightTileDataInteractor.kt
@@ -38,25 +38,30 @@
user: UserHandle,
triggers: Flow<DataUpdateTrigger>
): Flow<FlashlightTileModel> = conflatedCallbackFlow {
- val initialValue = flashlightController.isEnabled
- trySend(FlashlightTileModel(initialValue))
-
val callback =
object : FlashlightController.FlashlightListener {
override fun onFlashlightChanged(enabled: Boolean) {
- trySend(FlashlightTileModel(enabled))
+ trySend(FlashlightTileModel.FlashlightAvailable(enabled))
}
override fun onFlashlightError() {
- trySend(FlashlightTileModel(false))
+ trySend(FlashlightTileModel.FlashlightAvailable(false))
}
override fun onFlashlightAvailabilityChanged(available: Boolean) {
- trySend(FlashlightTileModel(flashlightController.isEnabled))
+ trySend(
+ if (available)
+ FlashlightTileModel.FlashlightAvailable(flashlightController.isEnabled)
+ else FlashlightTileModel.FlashlightTemporarilyUnavailable
+ )
}
}
flashlightController.addCallback(callback)
awaitClose { flashlightController.removeCallback(callback) }
}
+ /**
+ * Used to determine if the tile should be displayed. Not to be confused with the availability
+ * in the data model above. This flow signals whether the tile should be shown or hidden.
+ */
override fun availability(user: UserHandle): Flow<Boolean> =
flowOf(flashlightController.hasFlashlight())
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/flashlight/domain/interactor/FlashlightTileUserActionInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/flashlight/domain/interactor/FlashlightTileUserActionInteractor.kt
index 9180e30..bedd65e 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/flashlight/domain/interactor/FlashlightTileUserActionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/flashlight/domain/interactor/FlashlightTileUserActionInteractor.kt
@@ -35,7 +35,10 @@
with(input) {
when (action) {
is QSTileUserAction.Click -> {
- if (!ActivityManager.isUserAMonkey()) {
+ if (
+ !ActivityManager.isUserAMonkey() &&
+ input.data is FlashlightTileModel.FlashlightAvailable
+ ) {
flashlightController.setFlashlight(!input.data.isEnabled)
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/flashlight/domain/model/FlashlightTileModel.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/flashlight/domain/model/FlashlightTileModel.kt
index ef6b2be..f54b371 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/flashlight/domain/model/FlashlightTileModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/flashlight/domain/model/FlashlightTileModel.kt
@@ -16,9 +16,14 @@
package com.android.systemui.qs.tiles.impl.flashlight.domain.model
-/**
- * Flashlight tile model.
- *
- * @param isEnabled is true when the falshlight is enabled;
- */
-@JvmInline value class FlashlightTileModel(val isEnabled: Boolean)
+sealed interface FlashlightTileModel {
+ /**
+ * In this state, the tile can be turned on or off.
+ *
+ * @param isEnabled is true when the flashlight is on and false when off.
+ */
+ @JvmInline value class FlashlightAvailable(val isEnabled: Boolean) : FlashlightTileModel
+
+ /** In this state the tile is grayed out and flashlight cannot be turned on. */
+ data object FlashlightTemporarilyUnavailable : FlashlightTileModel
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/ui/adapter/QSSceneAdapter.kt b/packages/SystemUI/src/com/android/systemui/qs/ui/adapter/QSSceneAdapter.kt
index 0d43396..3d12eed 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/ui/adapter/QSSceneAdapter.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/ui/adapter/QSSceneAdapter.kt
@@ -28,6 +28,7 @@
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.plugins.qs.QSContainerController
+import com.android.systemui.qs.QSContainerImpl
import com.android.systemui.qs.QSImpl
import com.android.systemui.qs.dagger.QSSceneComponent
import com.android.systemui.res.R
@@ -68,6 +69,15 @@
/** Set the current state for QS. [state]. */
fun setState(state: State)
+ /** The current height of QQS in the current [qsView], or 0 if there's no view. */
+ val qqsHeight: Int
+
+ /**
+ * The current height of QS in the current [qsView], or 0 if there's no view. If customizing, it
+ * will return the height allocated to the customizer.
+ */
+ val qsHeight: Int
+
sealed class State(
val isVisible: Boolean,
val expansion: Float,
@@ -121,6 +131,11 @@
val qsImpl = _qsImpl.asStateFlow()
override val qsView: Flow<View> = _qsImpl.map { it?.view }.filterNotNull()
+ override val qqsHeight: Int
+ get() = qsImpl.value?.qqsHeight ?: 0
+ override val qsHeight: Int
+ get() = qsImpl.value?.qsHeight ?: 0
+
// Same config changes as in FragmentHostManager
private val interestingChanges =
InterestingConfigChanges(
diff --git a/packages/SystemUI/src/com/android/systemui/recordissue/IssueRecordingService.kt b/packages/SystemUI/src/com/android/systemui/recordissue/IssueRecordingService.kt
new file mode 100644
index 0000000..f487258
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/recordissue/IssueRecordingService.kt
@@ -0,0 +1,107 @@
+/*
+ * Copyright (C) 2024 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.recordissue
+
+import android.app.NotificationManager
+import android.content.Context
+import android.content.Intent
+import android.content.res.Resources
+import android.os.Handler
+import com.android.internal.logging.UiEventLogger
+import com.android.systemui.dagger.qualifiers.LongRunning
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.res.R
+import com.android.systemui.screenrecord.RecordingController
+import com.android.systemui.screenrecord.RecordingService
+import com.android.systemui.screenrecord.RecordingServiceStrings
+import com.android.systemui.settings.UserContextProvider
+import com.android.systemui.statusbar.phone.KeyguardDismissUtil
+import java.util.concurrent.Executor
+import javax.inject.Inject
+
+class IssueRecordingService
+@Inject
+constructor(
+ controller: RecordingController,
+ @LongRunning executor: Executor,
+ @Main handler: Handler,
+ uiEventLogger: UiEventLogger,
+ notificationManager: NotificationManager,
+ userContextProvider: UserContextProvider,
+ keyguardDismissUtil: KeyguardDismissUtil
+) :
+ RecordingService(
+ controller,
+ executor,
+ handler,
+ uiEventLogger,
+ notificationManager,
+ userContextProvider,
+ keyguardDismissUtil
+ ) {
+
+ override fun getTag(): String = TAG
+
+ override fun getChannelId(): String = CHANNEL_ID
+
+ override fun provideRecordingServiceStrings(): RecordingServiceStrings = IrsStrings(resources)
+
+ companion object {
+ private const val TAG = "IssueRecordingService"
+ private const val CHANNEL_ID = "issue_record"
+
+ /**
+ * Get an intent to stop the issue recording service.
+ *
+ * @param context Context from the requesting activity
+ * @return
+ */
+ fun getStopIntent(context: Context): Intent =
+ Intent(context, RecordingService::class.java)
+ .setAction(ACTION_STOP)
+ .putExtra(Intent.EXTRA_USER_HANDLE, context.userId)
+
+ /**
+ * Get an intent to start the issue recording service.
+ *
+ * @param context Context from the requesting activity
+ */
+ fun getStartIntent(context: Context): Intent =
+ Intent(context, RecordingService::class.java).setAction(ACTION_START)
+ }
+}
+
+private class IrsStrings(private val res: Resources) : RecordingServiceStrings(res) {
+ override val title
+ get() = res.getString(R.string.issuerecord_title)
+ override val notificationChannelDescription
+ get() = res.getString(R.string.issuerecord_channel_description)
+ override val startErrorResId
+ get() = R.string.issuerecord_start_error
+ override val startError
+ get() = res.getString(R.string.issuerecord_start_error)
+ override val saveErrorResId
+ get() = R.string.issuerecord_save_error
+ override val saveError
+ get() = res.getString(R.string.issuerecord_save_error)
+ override val ongoingRecording
+ get() = res.getString(R.string.issuerecord_ongoing_screen_only)
+ override val backgroundProcessingLabel
+ get() = res.getString(R.string.issuerecord_background_processing_label)
+ override val saveTitle
+ get() = res.getString(R.string.issuerecord_save_title)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/recordissue/RecordIssueDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/recordissue/RecordIssueDialogDelegate.kt
index e051df4..80f11f1 100644
--- a/packages/SystemUI/src/com/android/systemui/recordissue/RecordIssueDialogDelegate.kt
+++ b/packages/SystemUI/src/com/android/systemui/recordissue/RecordIssueDialogDelegate.kt
@@ -17,7 +17,6 @@
package com.android.systemui.recordissue
import android.annotation.SuppressLint
-import android.app.Activity
import android.app.BroadcastOptions
import android.app.PendingIntent
import android.content.Context
@@ -45,7 +44,6 @@
import com.android.systemui.qs.tiles.RecordIssueTile
import com.android.systemui.res.R
import com.android.systemui.screenrecord.RecordingService
-import com.android.systemui.screenrecord.ScreenRecordingAudioSource
import com.android.systemui.settings.UserContextProvider
import com.android.systemui.settings.UserFileManager
import com.android.systemui.settings.UserTracker
@@ -183,13 +181,7 @@
PendingIntent.getForegroundService(
userContextProvider.userContext,
RecordingService.REQUEST_CODE,
- RecordingService.getStartIntent(
- userContextProvider.userContext,
- Activity.RESULT_OK,
- ScreenRecordingAudioSource.NONE.ordinal,
- false,
- null
- ),
+ IssueRecordingService.getStartIntent(userContextProvider.userContext),
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
.send(BroadcastOptions.makeBasic().apply { isInteractive = true }.toBundle())
diff --git a/packages/SystemUI/src/com/android/systemui/scene/KeyguardlessSceneContainerFrameworkModule.kt b/packages/SystemUI/src/com/android/systemui/scene/KeyguardlessSceneContainerFrameworkModule.kt
index fcbe9a6..356eb85 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/KeyguardlessSceneContainerFrameworkModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/KeyguardlessSceneContainerFrameworkModule.kt
@@ -45,8 +45,8 @@
sceneKeys =
listOf(
SceneKey.Gone,
- SceneKey.Shade,
SceneKey.QuickSettings,
+ SceneKey.Shade,
),
initialSceneKey = SceneKey.Gone,
)
diff --git a/packages/SystemUI/src/com/android/systemui/scene/SceneContainerFrameworkModule.kt b/packages/SystemUI/src/com/android/systemui/scene/SceneContainerFrameworkModule.kt
index 0f3acaf..c7d3a4a 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/SceneContainerFrameworkModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/SceneContainerFrameworkModule.kt
@@ -69,8 +69,8 @@
SceneKey.Communal,
SceneKey.Lockscreen,
SceneKey.Bouncer,
- SceneKey.Shade,
SceneKey.QuickSettings,
+ SceneKey.Shade,
),
initialSceneKey = SceneKey.Lockscreen,
)
diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt
index 37abc40..dcd87c0 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt
@@ -18,6 +18,7 @@
package com.android.systemui.scene.domain.startable
+import android.app.StatusBarManager
import com.android.systemui.CoreStartable
import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor
import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
@@ -42,6 +43,7 @@
import com.android.systemui.scene.shared.model.SceneModel
import com.android.systemui.statusbar.NotificationShadeWindowController
import com.android.systemui.statusbar.notification.stack.shared.flexiNotifsEnabled
+import com.android.systemui.statusbar.phone.CentralSurfaces
import com.android.systemui.statusbar.policy.domain.interactor.DeviceProvisioningInteractor
import com.android.systemui.util.asIndenting
import com.android.systemui.util.printSection
@@ -52,6 +54,7 @@
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.collectLatest
+import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.distinctUntilChangedBy
import kotlinx.coroutines.flow.emptyFlow
@@ -84,6 +87,7 @@
private val authenticationInteractor: Lazy<AuthenticationInteractor>,
private val windowController: NotificationShadeWindowController,
private val deviceProvisioningInteractor: DeviceProvisioningInteractor,
+ private val centralSurfaces: CentralSurfaces,
) : CoreStartable {
override fun start() {
@@ -94,6 +98,7 @@
hydrateSystemUiState()
collectFalsingSignals()
hydrateWindowFocus()
+ hydrateInteractionState()
} else {
sceneLogger.logFrameworkEnabled(
isEnabled = false,
@@ -115,11 +120,15 @@
private fun hydrateVisibility() {
applicationScope.launch {
// TODO(b/296114544): Combine with some global hun state to make it visible!
- deviceProvisioningInteractor.isFactoryResetProtectionActive
- .flatMapLatest { isFrpActive ->
- if (isFrpActive) {
- flowOf(false to "Factory Reset Protection is active")
- } else {
+ combine(
+ deviceProvisioningInteractor.isDeviceProvisioned,
+ deviceProvisioningInteractor.isFactoryResetProtectionActive,
+ ) { isDeviceProvisioned, isFrpActive ->
+ isDeviceProvisioned && !isFrpActive
+ }
+ .distinctUntilChanged()
+ .flatMapLatest { isAllowedToBeVisible ->
+ if (isAllowedToBeVisible) {
sceneInteractor.transitionState
.mapNotNull { state ->
when (state) {
@@ -140,6 +149,8 @@
}
}
.distinctUntilChanged()
+ } else {
+ flowOf(false to "Device not provisioned or Factory Reset Protection active")
}
}
.collect { (isVisible, loggingReason) ->
@@ -369,6 +380,46 @@
}
}
+ /** Keeps the interaction state of [CentralSurfaces] up-to-date. */
+ private fun hydrateInteractionState() {
+ applicationScope.launch {
+ deviceEntryInteractor.isUnlocked
+ .map { !it }
+ .flatMapLatest { isDeviceLocked ->
+ if (isDeviceLocked) {
+ sceneInteractor.transitionState
+ .mapNotNull { it as? ObservableTransitionState.Idle }
+ .map { it.scene }
+ .distinctUntilChanged()
+ .map { sceneKey ->
+ when (sceneKey) {
+ // When locked, showing the lockscreen scene should be reported
+ // as "interacting" while showing other scenes should report as
+ // "not interacting".
+ //
+ // This is done here in order to match the legacy
+ // implementation. The real reason why is lost to lore and myth.
+ SceneKey.Lockscreen -> true
+ SceneKey.Bouncer -> false
+ SceneKey.Shade -> false
+ else -> null
+ }
+ }
+ } else {
+ flowOf(null)
+ }
+ }
+ .collect { isInteractingOrNull ->
+ isInteractingOrNull?.let { isInteracting ->
+ centralSurfaces.setInteracting(
+ StatusBarManager.WINDOW_STATUS_BAR,
+ isInteracting,
+ )
+ }
+ }
+ }
+ }
+
private fun switchToScene(targetSceneKey: SceneKey, loggingReason: String) {
sceneInteractor.changeScene(
scene = SceneModel(targetSceneKey),
diff --git a/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingService.java b/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingService.java
index b5a1313..b355d2d 100644
--- a/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingService.java
+++ b/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingService.java
@@ -24,7 +24,6 @@
import android.app.Service;
import android.content.Context;
import android.content.Intent;
-import android.content.res.Resources;
import android.graphics.drawable.Icon;
import android.media.MediaRecorder;
import android.net.Uri;
@@ -71,8 +70,8 @@
private static final String EXTRA_SHOW_TAPS = "extra_showTaps";
private static final String EXTRA_CAPTURE_TARGET = "extra_captureTarget";
- private static final String ACTION_START = "com.android.systemui.screenrecord.START";
- private static final String ACTION_STOP = "com.android.systemui.screenrecord.STOP";
+ protected static final String ACTION_START = "com.android.systemui.screenrecord.START";
+ protected static final String ACTION_STOP = "com.android.systemui.screenrecord.STOP";
private static final String ACTION_STOP_NOTIF =
"com.android.systemui.screenrecord.STOP_FROM_NOTIF";
private static final String ACTION_SHARE = "com.android.systemui.screenrecord.SHARE";
@@ -90,6 +89,7 @@
private final NotificationManager mNotificationManager;
private final UserContextProvider mUserContextTracker;
private int mNotificationId = NOTIF_BASE_ID;
+ private RecordingServiceStrings mStrings;
@Inject
public RecordingService(RecordingController controller, @LongRunning Executor executor,
@@ -134,9 +134,9 @@
return Service.START_NOT_STICKY;
}
String action = intent.getAction();
- Log.d(TAG, "onStartCommand " + action);
+ Log.d(getTag(), "onStartCommand " + action);
NotificationChannel channel = new NotificationChannel(
- CHANNEL_ID,
+ getChannelId(),
getString(R.string.screenrecord_title),
NotificationManager.IMPORTANCE_DEFAULT);
channel.setDescription(getString(R.string.screenrecord_channel_description));
@@ -152,7 +152,7 @@
mNotificationId = NOTIF_BASE_ID + (int) SystemClock.uptimeMillis();
mAudioSource = ScreenRecordingAudioSource
.values()[intent.getIntExtra(EXTRA_AUDIO_SOURCE, 0)];
- Log.d(TAG, "recording with audio source " + mAudioSource);
+ Log.d(getTag(), "recording with audio source " + mAudioSource);
mShowTaps = intent.getBooleanExtra(EXTRA_SHOW_TAPS, false);
MediaProjectionCaptureTarget captureTarget =
intent.getParcelableExtra(EXTRA_CAPTURE_TARGET,
@@ -207,7 +207,7 @@
.setType("video/mp4")
.putExtra(Intent.EXTRA_STREAM, shareUri);
mKeyguardDismissUtil.executeWhenUnlocked(() -> {
- String shareLabel = getResources().getString(R.string.screenrecord_share_label);
+ String shareLabel = strings().getShareLabel();
startActivity(Intent.createChooser(shareIntent, shareLabel)
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
// Remove notification
@@ -270,13 +270,11 @@
*/
@VisibleForTesting
protected void createErrorNotification() {
- Resources res = getResources();
Bundle extras = new Bundle();
- extras.putString(Notification.EXTRA_SUBSTITUTE_APP_NAME,
- res.getString(R.string.screenrecord_title));
- String notificationTitle = res.getString(R.string.screenrecord_start_error);
+ extras.putString(Notification.EXTRA_SUBSTITUTE_APP_NAME, strings().getTitle());
+ String notificationTitle = strings().getStartError();
- Notification.Builder builder = new Notification.Builder(this, CHANNEL_ID)
+ Notification.Builder builder = new Notification.Builder(this, getChannelId())
.setSmallIcon(R.drawable.ic_screenrecord)
.setContentTitle(notificationTitle)
.addExtras(extras);
@@ -290,14 +288,12 @@
@VisibleForTesting
protected void createRecordingNotification() {
- Resources res = getResources();
Bundle extras = new Bundle();
- extras.putString(Notification.EXTRA_SUBSTITUTE_APP_NAME,
- res.getString(R.string.screenrecord_title));
+ extras.putString(Notification.EXTRA_SUBSTITUTE_APP_NAME, strings().getTitle());
String notificationTitle = mAudioSource == ScreenRecordingAudioSource.NONE
- ? res.getString(R.string.screenrecord_ongoing_screen_only)
- : res.getString(R.string.screenrecord_ongoing_screen_and_audio);
+ ? strings().getOngoingRecording()
+ : strings().getOngoingRecordingWithAudio();
PendingIntent pendingIntent = PendingIntent.getService(
this,
@@ -306,9 +302,9 @@
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
Notification.Action stopAction = new Notification.Action.Builder(
Icon.createWithResource(this, R.drawable.ic_android),
- getResources().getString(R.string.screenrecord_stop_label),
+ strings().getStopLabel(),
pendingIntent).build();
- Notification.Builder builder = new Notification.Builder(this, CHANNEL_ID)
+ Notification.Builder builder = new Notification.Builder(this, getChannelId())
.setSmallIcon(R.drawable.ic_screenrecord)
.setContentTitle(notificationTitle)
.setUsesChronometer(true)
@@ -323,19 +319,17 @@
@VisibleForTesting
protected Notification createProcessingNotification() {
- Resources res = getApplicationContext().getResources();
String notificationTitle = mAudioSource == ScreenRecordingAudioSource.NONE
- ? res.getString(R.string.screenrecord_ongoing_screen_only)
- : res.getString(R.string.screenrecord_ongoing_screen_and_audio);
+ ? strings().getOngoingRecording()
+ : strings().getOngoingRecordingWithAudio();
Bundle extras = new Bundle();
- extras.putString(Notification.EXTRA_SUBSTITUTE_APP_NAME,
- res.getString(R.string.screenrecord_title));
+ extras.putString(Notification.EXTRA_SUBSTITUTE_APP_NAME, strings().getTitle());
- Notification.Builder builder = new Notification.Builder(this, CHANNEL_ID)
+ Notification.Builder builder = new Notification.Builder(this, getChannelId())
.setContentTitle(notificationTitle)
.setContentText(
- getResources().getString(R.string.screenrecord_background_processing_label))
+ strings().getBackgroundProcessingLabel())
.setSmallIcon(R.drawable.ic_screenrecord)
.setGroup(GROUP_KEY)
.addExtras(extras);
@@ -351,7 +345,7 @@
Notification.Action shareAction = new Notification.Action.Builder(
Icon.createWithResource(this, R.drawable.ic_screenrecord),
- getResources().getString(R.string.screenrecord_share_label),
+ strings().getShareLabel(),
PendingIntent.getService(
this,
REQUEST_CODE,
@@ -360,13 +354,12 @@
.build();
Bundle extras = new Bundle();
- extras.putString(Notification.EXTRA_SUBSTITUTE_APP_NAME,
- getResources().getString(R.string.screenrecord_title));
+ extras.putString(Notification.EXTRA_SUBSTITUTE_APP_NAME, strings().getTitle());
- Notification.Builder builder = new Notification.Builder(this, CHANNEL_ID)
+ Notification.Builder builder = new Notification.Builder(this, getChannelId())
.setSmallIcon(R.drawable.ic_screenrecord)
- .setContentTitle(getResources().getString(R.string.screenrecord_save_title))
- .setContentText(getResources().getString(R.string.screenrecord_save_text))
+ .setContentTitle(strings().getSaveTitle())
+ .setContentText(strings().getSaveText())
.setContentIntent(PendingIntent.getActivity(
this,
REQUEST_CODE,
@@ -394,15 +387,15 @@
private void postGroupNotification(UserHandle currentUser) {
Bundle extras = new Bundle();
extras.putString(Notification.EXTRA_SUBSTITUTE_APP_NAME,
- getResources().getString(R.string.screenrecord_title));
- Notification groupNotif = new Notification.Builder(this, CHANNEL_ID)
+ strings().getTitle());
+ Notification groupNotif = new Notification.Builder(this, getChannelId())
.setSmallIcon(R.drawable.ic_screenrecord)
- .setContentTitle(getResources().getString(R.string.screenrecord_save_title))
+ .setContentTitle(strings().getSaveTitle())
.setGroup(GROUP_KEY)
.setGroupSummary(true)
.setExtras(extras)
.build();
- mNotificationManager.notifyAsUser(TAG, NOTIF_BASE_ID, groupNotif, currentUser);
+ mNotificationManager.notifyAsUser(getTag(), NOTIF_BASE_ID, groupNotif, currentUser);
}
private void stopService() {
@@ -413,7 +406,7 @@
if (userId == USER_ID_NOT_SPECIFIED) {
userId = mUserContextTracker.getUserContext().getUserId();
}
- Log.d(TAG, "notifying for user " + userId);
+ Log.d(getTag(), "notifying for user " + userId);
setTapsVisible(mOriginalShowTaps);
if (getRecorder() != null) {
try {
@@ -424,7 +417,7 @@
// let's release the recorder and delete all temporary files in this case
getRecorder().release();
showErrorToast(R.string.screenrecord_start_error);
- Log.e(TAG, "stopRecording called, but there was an error when ending"
+ Log.e(getTag(), "stopRecording called, but there was an error when ending"
+ "recording");
exception.printStackTrace();
createErrorNotification();
@@ -435,7 +428,7 @@
throw new RuntimeException(throwable);
}
} else {
- Log.e(TAG, "stopRecording called, but recorder was null");
+ Log.e(getTag(), "stopRecording called, but recorder was null");
}
updateState(false);
stopForeground(STOP_FOREGROUND_DETACH);
@@ -449,13 +442,13 @@
mLongExecutor.execute(() -> {
try {
- Log.d(TAG, "saving recording");
+ Log.d(getTag(), "saving recording");
Notification notification = createSaveNotification(getRecorder().save());
postGroupNotification(currentUser);
mNotificationManager.notifyAsUser(null, mNotificationId, notification,
currentUser);
} catch (IOException | IllegalStateException e) {
- Log.e(TAG, "Error saving screen recording: " + e.getMessage());
+ Log.e(getTag(), "Error saving screen recording: " + e.getMessage());
e.printStackTrace();
showErrorToast(R.string.screenrecord_save_error);
mNotificationManager.cancelAsUser(null, mNotificationId, currentUser);
@@ -468,6 +461,26 @@
Settings.System.putInt(getContentResolver(), Settings.System.SHOW_TOUCHES, value);
}
+ protected String getTag() {
+ return TAG;
+ }
+
+ protected String getChannelId() {
+ return CHANNEL_ID;
+ }
+
+ private RecordingServiceStrings strings() {
+ if (mStrings == null) {
+ mStrings = provideRecordingServiceStrings();
+ }
+ return mStrings;
+ }
+
+ protected RecordingServiceStrings provideRecordingServiceStrings() {
+ return new RecordingServiceStrings(getResources());
+ }
+
+
/**
* Get an intent to stop the recording service.
* @param context Context from the requesting activity
@@ -484,25 +497,25 @@
* @param context
* @return
*/
- protected static Intent getNotificationIntent(Context context) {
- return new Intent(context, RecordingService.class).setAction(ACTION_STOP_NOTIF);
+ protected Intent getNotificationIntent(Context context) {
+ return new Intent(context, this.getClass()).setAction(ACTION_STOP_NOTIF);
}
- private static Intent getShareIntent(Context context, String path) {
- return new Intent(context, RecordingService.class).setAction(ACTION_SHARE)
+ private Intent getShareIntent(Context context, String path) {
+ return new Intent(context, this.getClass()).setAction(ACTION_SHARE)
.putExtra(EXTRA_PATH, path);
}
@Override
public void onInfo(MediaRecorder mr, int what, int extra) {
- Log.d(TAG, "Media recorder info: " + what);
+ Log.d(getTag(), "Media recorder info: " + what);
onStartCommand(getStopIntent(this), 0, 0);
}
@Override
public void onStopped() {
if (mController.isRecording()) {
- Log.d(TAG, "Stopping recording because the system requested the stop");
+ Log.d(getTag(), "Stopping recording because the system requested the stop");
stopService();
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingServiceStrings.kt b/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingServiceStrings.kt
new file mode 100644
index 0000000..fdb1eb6a
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingServiceStrings.kt
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.screenrecord
+
+import android.content.res.Resources
+import com.android.systemui.res.R
+
+open class RecordingServiceStrings(private val res: Resources) {
+ open val title
+ get() = res.getString(R.string.screenrecord_title)
+ open val notificationChannelDescription
+ get() = res.getString(R.string.screenrecord_channel_description)
+ open val startErrorResId
+ get() = R.string.screenrecord_start_error
+ open val startError
+ get() = res.getString(R.string.screenrecord_start_error)
+ open val saveErrorResId
+ get() = R.string.screenrecord_save_error
+ open val saveError
+ get() = res.getString(R.string.screenrecord_save_error)
+ open val ongoingRecording
+ get() = res.getString(R.string.screenrecord_ongoing_screen_only)
+ open val backgroundProcessingLabel
+ get() = res.getString(R.string.screenrecord_background_processing_label)
+ open val saveTitle
+ get() = res.getString(R.string.screenrecord_save_title)
+
+ val saveText
+ get() = res.getString(R.string.screenrecord_save_text)
+ val ongoingRecordingWithAudio
+ get() = res.getString(R.string.screenrecord_ongoing_screen_and_audio)
+ val stopLabel
+ get() = res.getString(R.string.screenrecord_stop_label)
+ val shareLabel
+ get() = res.getString(R.string.screenrecord_share_label)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/settings/dagger/MultiUserUtilsModule.java b/packages/SystemUI/src/com/android/systemui/settings/MultiUserUtilsModule.java
similarity index 85%
rename from packages/SystemUI/src/com/android/systemui/settings/dagger/MultiUserUtilsModule.java
rename to packages/SystemUI/src/com/android/systemui/settings/MultiUserUtilsModule.java
index a0dd924..fd807db 100644
--- a/packages/SystemUI/src/com/android/systemui/settings/dagger/MultiUserUtilsModule.java
+++ b/packages/SystemUI/src/com/android/systemui/settings/MultiUserUtilsModule.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.systemui.settings.dagger;
+package com.android.systemui.settings;
import android.app.ActivityManager;
import android.app.IActivityManager;
@@ -29,14 +29,6 @@
import com.android.systemui.dagger.qualifiers.Background;
import com.android.systemui.dump.DumpManager;
import com.android.systemui.flags.FeatureFlagsClassic;
-import com.android.systemui.settings.DisplayTracker;
-import com.android.systemui.settings.DisplayTrackerImpl;
-import com.android.systemui.settings.UserContentResolverProvider;
-import com.android.systemui.settings.UserContextProvider;
-import com.android.systemui.settings.UserFileManager;
-import com.android.systemui.settings.UserFileManagerImpl;
-import com.android.systemui.settings.UserTracker;
-import com.android.systemui.settings.UserTrackerImpl;
import dagger.Binds;
import dagger.Module;
diff --git a/packages/SystemUI/src/com/android/systemui/settings/SecureSettingsRepositoryModule.kt b/packages/SystemUI/src/com/android/systemui/settings/SecureSettingsRepositoryModule.kt
new file mode 100644
index 0000000..6199a83
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/settings/SecureSettingsRepositoryModule.kt
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.settings
+
+import android.content.ContentResolver
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.shared.settings.data.repository.SecureSettingsRepository
+import com.android.systemui.shared.settings.data.repository.SecureSettingsRepositoryImpl
+import dagger.Module
+import dagger.Provides
+import kotlinx.coroutines.CoroutineDispatcher
+
+@Module
+object SecureSettingsRepositoryModule {
+ @JvmStatic
+ @Provides
+ @SysUISingleton
+ fun provideSecureSettingsRepository(
+ contentResolver: ContentResolver,
+ @Background backgroundDispatcher: CoroutineDispatcher,
+ ): SecureSettingsRepository =
+ SecureSettingsRepositoryImpl(contentResolver, backgroundDispatcher)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/settings/UserFileManagerExt.kt b/packages/SystemUI/src/com/android/systemui/settings/UserFileManagerExt.kt
deleted file mode 100644
index b09bfe2..0000000
--- a/packages/SystemUI/src/com/android/systemui/settings/UserFileManagerExt.kt
+++ /dev/null
@@ -1,43 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- *
- */
-
-package com.android.systemui.settings
-
-import android.annotation.UserIdInt
-import android.content.Context
-import android.content.SharedPreferences
-import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
-import kotlinx.coroutines.channels.awaitClose
-import kotlinx.coroutines.flow.Flow
-
-/** Extension functions for [UserFileManager]. */
-object UserFileManagerExt {
-
- /** Returns a flow of [Unit] that is invoked each time the shared preference is updated. */
- fun UserFileManager.observeSharedPreferences(
- fileName: String,
- @Context.PreferencesMode mode: Int,
- @UserIdInt userId: Int
- ): Flow<Unit> = conflatedCallbackFlow {
- val sharedPrefs = getSharedPreferences(fileName, mode, userId)
-
- val listener = SharedPreferences.OnSharedPreferenceChangeListener { _, _ -> trySend(Unit) }
-
- sharedPrefs.registerOnSharedPreferenceChangeListener(listener)
- awaitClose { sharedPrefs.unregisterOnSharedPreferenceChangeListener(listener) }
- }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessSliderController.java b/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessSliderController.java
index be1fa2b..861a2ed 100644
--- a/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessSliderController.java
+++ b/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessSliderController.java
@@ -30,11 +30,11 @@
import com.android.internal.logging.UiEventLogger;
import com.android.settingslib.RestrictedLockUtils;
import com.android.systemui.Gefingerpoken;
-import com.android.systemui.res.R;
import com.android.systemui.classifier.Classifier;
-import com.android.systemui.dagger.qualifiers.Main;
-import com.android.systemui.haptics.slider.SeekableSliderEventProducer;
+import com.android.systemui.haptics.slider.HapticSliderViewBinder;
+import com.android.systemui.haptics.slider.SeekableSliderHapticPlugin;
import com.android.systemui.plugins.FalsingManager;
+import com.android.systemui.res.R;
import com.android.systemui.statusbar.VibratorHelper;
import com.android.systemui.statusbar.policy.BrightnessMirrorController;
import com.android.systemui.util.ViewController;
@@ -42,8 +42,6 @@
import javax.inject.Inject;
-import kotlinx.coroutines.CoroutineDispatcher;
-
/**
* {@code ViewController} for a {@code BrightnessSliderView}
*
@@ -63,23 +61,16 @@
private final FalsingManager mFalsingManager;
private final UiEventLogger mUiEventLogger;
- private final BrightnessSliderHapticPlugin mBrightnessSliderHapticPlugin;
+ private final SeekableSliderHapticPlugin mBrightnessSliderHapticPlugin;
private final Gefingerpoken mOnInterceptListener = new Gefingerpoken() {
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
+ mBrightnessSliderHapticPlugin.onTouchEvent(ev);
int action = ev.getActionMasked();
if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
mFalsingManager.isFalseTouch(Classifier.BRIGHTNESS_SLIDER);
- if (mBrightnessSliderHapticPlugin.getVelocityTracker() != null) {
- mBrightnessSliderHapticPlugin.getVelocityTracker().clear();
- }
- } else if (action == MotionEvent.ACTION_DOWN || action == MotionEvent.ACTION_MOVE) {
- if (mBrightnessSliderHapticPlugin.getVelocityTracker() != null) {
- mBrightnessSliderHapticPlugin.getVelocityTracker().addMovement(ev);
- }
}
-
return false;
}
@@ -93,7 +84,7 @@
BrightnessSliderView brightnessSliderView,
FalsingManager falsingManager,
UiEventLogger uiEventLogger,
- BrightnessSliderHapticPlugin brightnessSliderHapticPlugin) {
+ SeekableSliderHapticPlugin brightnessSliderHapticPlugin) {
super(brightnessSliderView);
mFalsingManager = falsingManager;
mUiEventLogger = uiEventLogger;
@@ -112,7 +103,6 @@
protected void onViewAttached() {
mView.setOnSeekBarChangeListener(mSeekListener);
mView.setOnInterceptListener(mOnInterceptListener);
- mBrightnessSliderHapticPlugin.start();
}
@Override
@@ -120,7 +110,6 @@
mView.setOnSeekBarChangeListener(null);
mView.setOnDispatchTouchEventListener(null);
mView.setOnInterceptListener(null);
- mBrightnessSliderHapticPlugin.stop();
}
@Override
@@ -225,10 +214,8 @@
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
if (mListener != null) {
mListener.onChanged(mTracking, progress, false);
- SeekableSliderEventProducer eventProducer =
- mBrightnessSliderHapticPlugin.getSeekableSliderEventProducer();
- if (eventProducer != null && fromUser) {
- eventProducer.onProgressChanged(seekBar, progress, fromUser);
+ if (fromUser) {
+ mBrightnessSliderHapticPlugin.onProgressChanged(seekBar, progress, fromUser);
}
}
}
@@ -236,14 +223,10 @@
@Override
public void onStartTrackingTouch(SeekBar seekBar) {
mTracking = true;
- mUiEventLogger.log(BrightnessSliderEvent.SLIDER_STARTED_TRACKING_TOUCH);
+ mUiEventLogger.log(BrightnessSliderEvent.BRIGHTNESS_SLIDER_STARTED_TRACKING_TOUCH);
if (mListener != null) {
mListener.onChanged(mTracking, getValue(), false);
- SeekableSliderEventProducer eventProducer =
- mBrightnessSliderHapticPlugin.getSeekableSliderEventProducer();
- if (eventProducer != null) {
- eventProducer.onStartTrackingTouch(seekBar);
- }
+ mBrightnessSliderHapticPlugin.onStartTrackingTouch(seekBar);
}
if (mMirrorController != null) {
@@ -255,14 +238,10 @@
@Override
public void onStopTrackingTouch(SeekBar seekBar) {
mTracking = false;
- mUiEventLogger.log(BrightnessSliderEvent.SLIDER_STOPPED_TRACKING_TOUCH);
+ mUiEventLogger.log(BrightnessSliderEvent.BRIGHTNESS_SLIDER_STOPPED_TRACKING_TOUCH);
if (mListener != null) {
mListener.onChanged(mTracking, getValue(), true);
- SeekableSliderEventProducer eventProducer =
- mBrightnessSliderHapticPlugin.getSeekableSliderEventProducer();
- if (eventProducer != null) {
- eventProducer.onStopTrackingTouch(seekBar);
- }
+ mBrightnessSliderHapticPlugin.onStopTrackingTouch(seekBar);
}
if (mMirrorController != null) {
@@ -280,21 +259,18 @@
private final UiEventLogger mUiEventLogger;
private final VibratorHelper mVibratorHelper;
private final SystemClock mSystemClock;
- private final CoroutineDispatcher mMainDispatcher;
@Inject
public Factory(
FalsingManager falsingManager,
UiEventLogger uiEventLogger,
VibratorHelper vibratorHelper,
- SystemClock clock,
- @Main CoroutineDispatcher mainDispatcher
+ SystemClock clock
) {
mFalsingManager = falsingManager;
mUiEventLogger = uiEventLogger;
mVibratorHelper = vibratorHelper;
mSystemClock = clock;
- mMainDispatcher = mainDispatcher;
}
/**
@@ -310,15 +286,11 @@
int layout = getLayout();
BrightnessSliderView root = (BrightnessSliderView) LayoutInflater.from(context)
.inflate(layout, viewRoot, false);
- BrightnessSliderHapticPlugin plugin;
- if (hapticBrightnessSlider()) {
- plugin = new BrightnessSliderHapticPluginImpl(
+ SeekableSliderHapticPlugin plugin = new SeekableSliderHapticPlugin(
mVibratorHelper,
- mSystemClock,
- mMainDispatcher
- );
- } else {
- plugin = new BrightnessSliderHapticPlugin() {};
+ mSystemClock);
+ if (hapticBrightnessSlider()) {
+ HapticSliderViewBinder.bind(viewRoot, plugin);
}
return new BrightnessSliderController(root, mFalsingManager, mUiEventLogger, plugin);
}
diff --git a/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessSliderEvent.java b/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessSliderEvent.java
index 3a30880..a8641bf 100644
--- a/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessSliderEvent.java
+++ b/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessSliderEvent.java
@@ -21,10 +21,10 @@
public enum BrightnessSliderEvent implements UiEventLogger.UiEventEnum {
- @UiEvent(doc = "slider started to track touch")
- SLIDER_STARTED_TRACKING_TOUCH(1472),
- @UiEvent(doc = "slider stopped tracking touch")
- SLIDER_STOPPED_TRACKING_TOUCH(1473);
+ @UiEvent(doc = "brightness slider started to track touch")
+ BRIGHTNESS_SLIDER_STARTED_TRACKING_TOUCH(1472),
+ @UiEvent(doc = "brightness slider stopped tracking touch")
+ BRIGHTNESS_SLIDER_STOPPED_TRACKING_TOUCH(1473);
private final int mId;
diff --git a/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessSliderHapticPlugin.kt b/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessSliderHapticPlugin.kt
deleted file mode 100644
index f775114..0000000
--- a/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessSliderHapticPlugin.kt
+++ /dev/null
@@ -1,46 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.settings.brightness
-
-import android.view.VelocityTracker
-import com.android.systemui.haptics.slider.SeekableSliderEventProducer
-
-/** Plugin component for the System UI brightness slider to incorporate dynamic haptics */
-interface BrightnessSliderHapticPlugin {
-
- /** Finger velocity tracker */
- val velocityTracker: VelocityTracker?
- get() = null
-
- /** Producer of slider events from the underlying [android.widget.SeekBar] */
- val seekableSliderEventProducer: SeekableSliderEventProducer?
- get() = null
-
- /**
- * Start the plugin.
- *
- * This starts the tracking of slider states, events and triggering of haptic feedback.
- */
- fun start() {}
-
- /**
- * Stop the plugin
- *
- * This stops the tracking of slider states, events and triggers of haptic feedback.
- */
- fun stop() {}
-}
diff --git a/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessSliderHapticPluginImpl.kt b/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessSliderHapticPluginImpl.kt
deleted file mode 100644
index 32561f0..0000000
--- a/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessSliderHapticPluginImpl.kt
+++ /dev/null
@@ -1,69 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.settings.brightness
-
-import android.view.VelocityTracker
-import com.android.systemui.dagger.qualifiers.Main
-import com.android.systemui.haptics.slider.SeekableSliderEventProducer
-import com.android.systemui.haptics.slider.SeekableSliderTracker
-import com.android.systemui.haptics.slider.SliderHapticFeedbackProvider
-import com.android.systemui.haptics.slider.SliderTracker
-import com.android.systemui.statusbar.VibratorHelper
-import com.android.systemui.util.time.SystemClock
-import kotlinx.coroutines.CoroutineDispatcher
-
-/**
- * Implementation of the [BrightnessSliderHapticPlugin].
- *
- * For the specifics of the brightness slider in System UI, a [SeekableSliderEventProducer] is used
- * as the producer of slider events, a [SliderHapticFeedbackProvider] is used as the listener of
- * slider states to play haptic feedback depending on the state, and a [SeekableSliderTracker] is
- * used as the state machine handler that tracks and manipulates the slider state.
- */
-class BrightnessSliderHapticPluginImpl
-@JvmOverloads
-constructor(
- vibratorHelper: VibratorHelper,
- systemClock: SystemClock,
- @Main mainDispatcher: CoroutineDispatcher,
- override val velocityTracker: VelocityTracker = VelocityTracker.obtain(),
- override val seekableSliderEventProducer: SeekableSliderEventProducer =
- SeekableSliderEventProducer(),
-) : BrightnessSliderHapticPlugin {
-
- private val sliderHapticFeedbackProvider: SliderHapticFeedbackProvider =
- SliderHapticFeedbackProvider(vibratorHelper, velocityTracker, clock = systemClock)
- private val sliderTracker: SliderTracker =
- SeekableSliderTracker(
- sliderHapticFeedbackProvider,
- seekableSliderEventProducer,
- mainDispatcher,
- )
-
- val isTracking: Boolean
- get() = sliderTracker.isTracking
-
- override fun start() {
- if (!sliderTracker.isTracking) {
- sliderTracker.startTracking()
- }
- }
-
- override fun stop() {
- sliderTracker.stopTracking()
- }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt b/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt
index 1c7cc00..df845f5 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt
@@ -35,6 +35,7 @@
import com.android.systemui.util.kotlin.collectFlow
import javax.inject.Inject
import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.flowOf
/**
* Controller that's responsible for the glanceable hub container view and its touch handling.
@@ -105,13 +106,9 @@
*/
private var shadeShowing = false
- /** Returns true if the glanceable hub is enabled and the container view can be created. */
- fun isEnabled(): Boolean {
- return communalInteractor.isCommunalEnabled && isComposeAvailable()
- }
-
- /** Returns a {@link StateFlow} that tracks whether communal hub is available. */
- fun communalAvailable(): Flow<Boolean> = communalInteractor.isCommunalAvailable
+ /** Returns a flow that tracks whether communal hub is available. */
+ fun communalAvailable(): Flow<Boolean> =
+ if (isComposeAvailable()) communalInteractor.isCommunalAvailable else flowOf(false)
/**
* Creates the container view containing the glanceable hub UI.
@@ -127,9 +124,6 @@
/** Override for testing. */
@VisibleForTesting
internal fun initView(containerView: View): View {
- if (!isEnabled()) {
- throw RuntimeException("Glanceable hub is not enabled")
- }
if (communalContainerView != null) {
throw RuntimeException("Communal view has already been initialized")
}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
index 09e4e75..353e143 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
@@ -112,8 +112,8 @@
import com.android.systemui.DejankUtils;
import com.android.systemui.Dumpable;
import com.android.systemui.Gefingerpoken;
-import com.android.systemui.animation.ActivityLaunchAnimator;
-import com.android.systemui.animation.LaunchAnimator;
+import com.android.systemui.animation.ActivityTransitionAnimator;
+import com.android.systemui.animation.TransitionAnimator;
import com.android.systemui.biometrics.AuthController;
import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor;
import com.android.systemui.bouncer.shared.constants.KeyguardBouncerConstants;
@@ -165,6 +165,7 @@
import com.android.systemui.power.domain.interactor.PowerInteractor;
import com.android.systemui.power.shared.model.WakefulnessModel;
import com.android.systemui.res.R;
+import com.android.systemui.shade.data.repository.FlingInfo;
import com.android.systemui.shade.data.repository.ShadeRepository;
import com.android.systemui.shade.domain.interactor.ShadeAnimationInteractor;
import com.android.systemui.shade.transition.ShadeTransitionController;
@@ -258,7 +259,7 @@
/** The parallax amount of the quick settings translation when dragging down the panel. */
public static final float QS_PARALLAX_AMOUNT = 0.175f;
private static final long ANIMATION_DELAY_ICON_FADE_IN =
- ActivityLaunchAnimator.TIMINGS.getTotalDuration()
+ ActivityTransitionAnimator.TIMINGS.getTotalDuration()
- CollapsedStatusBarFragment.FADE_IN_DURATION
- CollapsedStatusBarFragment.FADE_IN_DELAY - 48;
private static final int NO_FIXED_DURATION = -1;
@@ -1020,22 +1021,24 @@
instantCollapse();
} else {
mView.animate().cancel();
- mView.animate()
- .alpha(0f)
- .setStartDelay(0)
- // Translate up by 4%.
- .translationY(mView.getHeight() * -0.04f)
- // This start delay is to give us time to animate out before
- // the launcher icons animation starts, so use that as our
- // duration.
- .setDuration(unlockAnimationStartDelay)
- .setInterpolator(EMPHASIZED_ACCELERATE)
- .withEndAction(() -> {
- instantCollapse();
- mView.setAlpha(1f);
- mView.setTranslationY(0f);
- })
- .start();
+ if (!KeyguardShadeMigrationNssl.isEnabled()) {
+ mView.animate()
+ .alpha(0f)
+ .setStartDelay(0)
+ // Translate up by 4%.
+ .translationY(mView.getHeight() * -0.04f)
+ // This start delay is to give us time to animate out before
+ // the launcher icons animation starts, so use that as our
+ // duration.
+ .setDuration(unlockAnimationStartDelay)
+ .setInterpolator(EMPHASIZED_ACCELERATE)
+ .withEndAction(() -> {
+ instantCollapse();
+ mView.setAlpha(1f);
+ mView.setTranslationY(0f);
+ })
+ .start();
+ }
}
}
}
@@ -1345,7 +1348,7 @@
}
updateClockAppearance();
mQsController.updateQsState();
- if (!KeyguardShadeMigrationNssl.isEnabled()) {
+ if (!KeyguardShadeMigrationNssl.isEnabled() && !FooterViewRefactor.isEnabled()) {
mNotificationStackScrollLayoutController.updateFooter();
}
}
@@ -2083,6 +2086,7 @@
mKeyguardStateController.notifyPanelFlingStart(!expand /* flingingToDismiss */);
setClosingWithAlphaFadeout(!expand && !isKeyguardShowing() && getFadeoutAlpha() == 1.0f);
mNotificationStackScrollLayoutController.setPanelFlinging(true);
+ mShadeRepository.setCurrentFling(new FlingInfo(expand, vel));
if (target == mExpandedHeight && mOverExpansion == 0.0f) {
// We're at the target and didn't fling and there's no overshoot
onFlingEnd(false /* cancelled */);
@@ -2199,6 +2203,7 @@
mShadeLog.d("onFlingEnd called"); // TODO(b/277909752): remove log when bug is fixed
// expandImmediate should be always reset at the end of animation
mQsController.setExpandImmediate(false);
+ mShadeRepository.setCurrentFling(null);
}
private boolean isInContentBounds(float x, float y) {
@@ -2472,11 +2477,9 @@
return 0;
}
if (!mKeyguardBypassController.getBypassEnabled()) {
- if (migrateClocksToBlueprint()) {
- View nsslPlaceholder = mView.getRootView().findViewById(R.id.nssl_placeholder);
- if (!mSplitShadeEnabled && nsslPlaceholder != null) {
- return nsslPlaceholder.getTop();
- }
+ if (migrateClocksToBlueprint() && !mSplitShadeEnabled) {
+ return (int) mKeyguardInteractor.getNotificationContainerBounds()
+ .getValue().getTop();
}
return mClockPositionResult.stackScrollerPadding;
@@ -3228,7 +3231,7 @@
@Override
public void applyLaunchAnimationProgress(float linearProgress) {
- boolean hideIcons = LaunchAnimator.getProgress(ActivityLaunchAnimator.TIMINGS,
+ boolean hideIcons = TransitionAnimator.getProgress(ActivityTransitionAnimator.TIMINGS,
linearProgress, ANIMATION_DELAY_ICON_FADE_IN, 100) == 0.0f;
if (hideIcons != mHideIconsDuringLaunchAnimation) {
mHideIconsDuringLaunchAnimation = hideIcons;
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java
index a01ac70..f7fed53 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java
@@ -183,7 +183,8 @@
mBackgroundExecutor = backgroundExecutor;
mColorExtractor = colorExtractor;
mScreenOffAnimationController = screenOffAnimationController;
- dumpManager.registerDumpable(this);
+ // prefix with {slow} to make sure this dumps at the END of the critical section.
+ dumpManager.registerCriticalDumpable("{slow}NotificationShadeWindowControllerImpl", this);
mAuthController = authController;
mUserInteractor = userInteractor;
mSceneContainerFlags = sceneContainerFlags;
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java
index ef820f3..aa2d606 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java
@@ -34,7 +34,7 @@
import com.android.keyguard.AuthKeyguardMessageArea;
import com.android.keyguard.LockIconViewController;
import com.android.systemui.Dumpable;
-import com.android.systemui.animation.ActivityLaunchAnimator;
+import com.android.systemui.animation.ActivityTransitionAnimator;
import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor;
import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor;
import com.android.systemui.bouncer.ui.binder.BouncerViewBinder;
@@ -679,7 +679,7 @@
void setExpandAnimationRunning(boolean running) {
if (mExpandAnimationRunning != running) {
// TODO(b/288507023): Remove this log.
- if (ActivityLaunchAnimator.DEBUG_LAUNCH_ANIMATION) {
+ if (ActivityTransitionAnimator.DEBUG_TRANSITION_ANIMATION) {
Log.d(TAG, "Setting mExpandAnimationRunning=" + running);
}
if (running) {
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerSceneImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerSceneImpl.kt
index 10b9db0..4e8b403 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerSceneImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerSceneImpl.kt
@@ -20,6 +20,7 @@
import com.android.systemui.assist.AssistManager
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor
import com.android.systemui.log.LogBuffer
import com.android.systemui.log.dagger.ShadeTouchLog
@@ -34,11 +35,13 @@
import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager
import dagger.Lazy
import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
/**
* Implementation of ShadeController backed by scenes instead of NPVC.
@@ -50,6 +53,7 @@
class ShadeControllerSceneImpl
@Inject
constructor(
+ @Main private val mainDispatcher: CoroutineDispatcher,
@Background private val scope: CoroutineScope,
private val shadeInteractor: ShadeInteractor,
private val sceneInteractor: SceneInteractor,
@@ -193,7 +197,11 @@
}
override fun setVisibilityListener(listener: ShadeVisibilityListener) {
- scope.launch { sceneInteractor.isVisible.collect { listener.expandedVisibleChanged(it) } }
+ scope.launch {
+ sceneInteractor.isVisible.collect { isVisible ->
+ withContext(mainDispatcher) { listener.expandedVisibleChanged(isVisible) }
+ }
+ }
}
@ExperimentalCoroutinesApi
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeEmptyImplModule.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeEmptyImplModule.kt
index 67bb814..f89a9c70 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ShadeEmptyImplModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeEmptyImplModule.kt
@@ -21,6 +21,7 @@
import com.android.systemui.shade.data.repository.ShadeRepositoryImpl
import com.android.systemui.shade.domain.interactor.ShadeAnimationInteractor
import com.android.systemui.shade.domain.interactor.ShadeAnimationInteractorEmptyImpl
+import com.android.systemui.shade.domain.interactor.ShadeBackActionInteractor
import com.android.systemui.shade.domain.interactor.ShadeInteractor
import com.android.systemui.shade.domain.interactor.ShadeInteractorEmptyImpl
import dagger.Binds
@@ -35,6 +36,10 @@
@Binds
@SysUISingleton
+ abstract fun bindsShadeBack(sbai: ShadeViewControllerEmptyImpl): ShadeBackActionInteractor
+
+ @Binds
+ @SysUISingleton
abstract fun bindsShadeController(sc: ShadeControllerEmptyImpl): ShadeController
@Binds
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeModule.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeModule.kt
index fc2c3ee..e4d5d22 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ShadeModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeModule.kt
@@ -24,6 +24,8 @@
import com.android.systemui.shade.domain.interactor.ShadeAnimationInteractor
import com.android.systemui.shade.domain.interactor.ShadeAnimationInteractorLegacyImpl
import com.android.systemui.shade.domain.interactor.ShadeAnimationInteractorSceneContainerImpl
+import com.android.systemui.shade.domain.interactor.ShadeBackActionInteractor
+import com.android.systemui.shade.domain.interactor.ShadeBackActionInteractorImpl
import com.android.systemui.shade.domain.interactor.ShadeInteractor
import com.android.systemui.shade.domain.interactor.ShadeInteractorImpl
import com.android.systemui.shade.domain.interactor.ShadeInteractorLegacyImpl
@@ -78,6 +80,20 @@
sceneContainerOff.get()
}
}
+
+ @Provides
+ @SysUISingleton
+ fun provideShadeBackActionInteractor(
+ sceneContainerFlags: SceneContainerFlags,
+ sceneContainerOn: Provider<ShadeBackActionInteractorImpl>,
+ sceneContainerOff: Provider<NotificationPanelViewController>
+ ): ShadeBackActionInteractor {
+ return if (sceneContainerFlags.isEnabled()) {
+ sceneContainerOn.get()
+ } else {
+ sceneContainerOff.get()
+ }
+ }
}
@Binds
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeSurface.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeSurface.kt
index 4f970b3..0befb61 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ShadeSurface.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeSurface.kt
@@ -16,6 +16,7 @@
package com.android.systemui.shade
import android.view.ViewPropertyAnimator
+import com.android.systemui.shade.domain.interactor.ShadeBackActionInteractor
import com.android.systemui.statusbar.GestureRecorder
import com.android.systemui.statusbar.phone.CentralSurfaces
import com.android.systemui.statusbar.policy.HeadsUpManager
@@ -25,7 +26,7 @@
* this class. If any method in this class is needed outside of CentralSurfacesImpl, it must be
* pulled up into ShadeViewController.
*/
-interface ShadeSurface : ShadeViewController {
+interface ShadeSurface : ShadeViewController, ShadeBackActionInteractor {
/** Initialize objects instead of injecting to avoid circular dependencies. */
fun initDependencies(
centralSurfaces: CentralSurfaces,
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeViewController.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeViewController.kt
index 3430eed..74035bd 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ShadeViewController.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeViewController.kt
@@ -79,15 +79,8 @@
/** Collapses the shade instantly without animation. */
fun instantCollapse()
- /**
- * Animate QS collapse by flinging it. If QS is expanded, it will collapse into QQS and stop. If
- * in split shade, it will collapse the whole shade.
- *
- * @param fullyCollapse Do not stop when QS becomes QQS. Fling until QS isn't visible anymore.
- */
- fun animateCollapseQs(fullyCollapse: Boolean)
-
/** Returns whether the shade can be collapsed. */
+ @Deprecated("Do not use outside of the shade package. Not supported by scenes.")
fun canBeCollapsed(): Boolean
/** Returns whether the shade is in the process of collapsing. */
@@ -142,22 +135,6 @@
/** Sets the amount of progress in the status bar launch animation. */
fun applyLaunchAnimationProgress(linearProgress: Float)
- /**
- * Close the keyguard user switcher if it is open and capable of closing.
- *
- * Has no effect if user switcher isn't supported, if the user switcher is already closed, or if
- * the user switcher uses "simple" mode. The simple user switcher cannot be closed.
- *
- * @return true if the keyguard user switcher was open, and is now closed
- */
- fun closeUserSwitcherIfOpen(): Boolean
-
- /** Called when Back gesture has been committed (i.e. a back event has definitely occurred) */
- fun onBackPressed()
-
- /** Sets progress of the predictive back animation. */
- fun onBackProgressed(progressFraction: Float)
-
/** Sets the alpha value of the shade to a value between 0 and 255. */
fun setAlpha(alpha: Int, animate: Boolean)
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeViewControllerEmptyImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeViewControllerEmptyImpl.kt
index 1240c6e..5d966ac 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ShadeViewControllerEmptyImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeViewControllerEmptyImpl.kt
@@ -19,13 +19,15 @@
import android.view.MotionEvent
import android.view.ViewGroup
import android.view.ViewTreeObserver
+import com.android.systemui.shade.domain.interactor.ShadeBackActionInteractor
import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow
import com.android.systemui.statusbar.phone.HeadsUpAppearanceController
import java.util.function.Consumer
import javax.inject.Inject
/** Empty implementation of ShadeViewController for variants with no shade. */
-class ShadeViewControllerEmptyImpl @Inject constructor() : ShadeViewController {
+class ShadeViewControllerEmptyImpl @Inject constructor() :
+ ShadeViewController, ShadeBackActionInteractor {
override fun expand(animate: Boolean) {}
override fun expandToQs() {}
override fun expandToNotifications() {}
diff --git a/packages/SoundPicker2/src/com/android/soundpicker/RingtonePickerApplication.java b/packages/SystemUI/src/com/android/systemui/shade/data/repository/FlingInfo.kt
similarity index 61%
copy from packages/SoundPicker2/src/com/android/soundpicker/RingtonePickerApplication.java
copy to packages/SystemUI/src/com/android/systemui/shade/data/repository/FlingInfo.kt
index 48fd4fe..d7f96e6 100644
--- a/packages/SoundPicker2/src/com/android/soundpicker/RingtonePickerApplication.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/data/repository/FlingInfo.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2023 The Android Open Source Project
+ * Copyright (C) 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,15 +14,13 @@
* limitations under the License.
*/
-package com.android.soundpicker;
-
-import android.app.Application;
-
-import dagger.hilt.android.HiltAndroidApp;
+package com.android.systemui.shade.data.repository
/**
- * The main application class for the project.
+ * Information about a fling on the shade: whether we're flinging expanded or collapsed, and the
+ * velocity of the touch gesture that started the fling (if applicable).
*/
-@HiltAndroidApp(Application.class)
-public class RingtonePickerApplication extends Hilt_RingtonePickerApplication {
-}
+data class FlingInfo(
+ val expand: Boolean,
+ val velocity: Float = 0f,
+)
diff --git a/packages/SystemUI/src/com/android/systemui/shade/data/repository/ShadeRepository.kt b/packages/SystemUI/src/com/android/systemui/shade/data/repository/ShadeRepository.kt
index 2445bdb..e5ff977 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/data/repository/ShadeRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/data/repository/ShadeRepository.kt
@@ -29,6 +29,15 @@
*/
val qsExpansion: StateFlow<Float>
+ /** Amount shade has expanded with regard to the UDFPS location */
+ val udfpsTransitionToFullShadeProgress: StateFlow<Float>
+
+ /**
+ * Information about the currently running fling animation, or null if no fling animation is
+ * running.
+ */
+ val currentFling: StateFlow<FlingInfo?>
+
/**
* The amount the lockscreen shade has dragged down by the user, [0-1]. 0 means fully collapsed,
* 1 means fully expanded. Value resets to 0 when the user finishes dragging.
@@ -132,15 +141,18 @@
@Deprecated("Use ShadeInteractor instead")
fun setLegacyLockscreenShadeTracking(tracking: Boolean)
- /** Amount shade has expanded with regard to the UDFPS location */
- val udfpsTransitionToFullShadeProgress: StateFlow<Float>
-
/** The amount QS has expanded without notifications */
fun setQsExpansion(qsExpansion: Float)
fun setUdfpsTransitionToFullShadeProgress(progress: Float)
/**
+ * Sets the [FlingInfo] of the currently animating fling. If [info] is null, no fling is
+ * animating.
+ */
+ fun setCurrentFling(info: FlingInfo?)
+
+ /**
* Set the amount the shade has dragged down by the user, [0-1]. 0 means fully collapsed, 1
* means fully expanded.
*/
@@ -168,6 +180,9 @@
override val udfpsTransitionToFullShadeProgress: StateFlow<Float> =
_udfpsTransitionToFullShadeProgress.asStateFlow()
+ private val _currentFling: MutableStateFlow<FlingInfo?> = MutableStateFlow(null)
+ override val currentFling: StateFlow<FlingInfo?> = _currentFling.asStateFlow()
+
private val _legacyShadeExpansion = MutableStateFlow(0f)
@Deprecated("Use ShadeInteractor.shadeExpansion instead")
override val legacyShadeExpansion: StateFlow<Float> = _legacyShadeExpansion.asStateFlow()
@@ -247,11 +262,6 @@
_qsExpansion.value = qsExpansion
}
- @Deprecated("Should only be called by NPVC and tests")
- override fun setLegacyShadeExpansion(expandedFraction: Float) {
- _legacyShadeExpansion.value = expandedFraction
- }
-
override fun setLockscreenShadeExpansion(lockscreenShadeExpansion: Float) {
_lockscreenShadeExpansion.value = lockscreenShadeExpansion
}
@@ -260,6 +270,15 @@
_udfpsTransitionToFullShadeProgress.value = progress
}
+ override fun setCurrentFling(info: FlingInfo?) {
+ _currentFling.value = info
+ }
+
+ @Deprecated("Should only be called by NPVC and tests")
+ override fun setLegacyShadeExpansion(expandedFraction: Float) {
+ _legacyShadeExpansion.value = expandedFraction
+ }
+
companion object {
private const val TAG = "ShadeRepository"
}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeBackActionInteractor.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeBackActionInteractor.kt
new file mode 100644
index 0000000..15ea219
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeBackActionInteractor.kt
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.shade.domain.interactor
+
+/** Allows the back action to interact with the shade. */
+interface ShadeBackActionInteractor {
+ /**
+ * Animate QS collapse by flinging it. If QS is expanded, it will collapse into QQS and stop. If
+ * in split shade, it will collapse the whole shade.
+ *
+ * @param fullyCollapse Do not stop when QS becomes QQS. Fling until QS isn't visible anymore.
+ */
+ fun animateCollapseQs(fullyCollapse: Boolean)
+
+ /** Returns whether the shade can be collapsed. */
+ fun canBeCollapsed(): Boolean
+
+ /**
+ * Close the keyguard user switcher if it is open and capable of closing.
+ *
+ * Has no effect if user switcher isn't supported, if the user switcher is already closed, or if
+ * the user switcher uses "simple" mode. The simple user switcher cannot be closed.
+ *
+ * @return true if the keyguard user switcher was open, and is now closed
+ */
+ @Deprecated("Only supported by very old devices that will not adopt scenes.")
+ fun closeUserSwitcherIfOpen(): Boolean
+
+ /** Called when Back gesture has been committed (i.e. a back event has definitely occurred) */
+ fun onBackPressed()
+
+ /** Sets progress of the predictive back animation. */
+ @Deprecated("According to b/318376223, shade predictive back is not be supported.")
+ fun onBackProgressed(progressFraction: Float)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeBackActionInteractorImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeBackActionInteractorImpl.kt
new file mode 100644
index 0000000..9bbe1bd
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeBackActionInteractorImpl.kt
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.shade.domain.interactor
+
+import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor
+import com.android.systemui.scene.domain.interactor.SceneInteractor
+import com.android.systemui.scene.shared.model.SceneKey
+import com.android.systemui.scene.shared.model.SceneModel
+import javax.inject.Inject
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+
+/** Implementation of ShadeBackActionInteractor backed by scenes. */
+@OptIn(ExperimentalCoroutinesApi::class)
+class ShadeBackActionInteractorImpl
+@Inject
+constructor(
+ val shadeInteractor: ShadeInteractor,
+ val sceneInteractor: SceneInteractor,
+ val deviceEntryInteractor: DeviceEntryInteractor,
+) : ShadeBackActionInteractor {
+ override fun animateCollapseQs(fullyCollapse: Boolean) {
+ if (shadeInteractor.isQsExpanded.value) {
+ val key =
+ if (fullyCollapse) {
+ if (deviceEntryInteractor.isDeviceEntered.value) {
+ SceneKey.Gone
+ } else {
+ SceneKey.Lockscreen
+ }
+ } else {
+ SceneKey.Shade
+ }
+ sceneInteractor.changeScene(SceneModel(key), "animateCollapseQs")
+ }
+ }
+
+ override fun canBeCollapsed(): Boolean {
+ return shadeInteractor.isAnyExpanded.value && !shadeInteractor.isUserInteracting.value
+ }
+
+ @Deprecated("Only supported by very old devices that will not adopt scenes.")
+ override fun closeUserSwitcherIfOpen(): Boolean {
+ return false
+ }
+
+ override fun onBackPressed() {
+ animateCollapseQs(false)
+ }
+
+ @Deprecated("According to b/318376223, shade predictive back is not be supported.")
+ override fun onBackProgressed(progressFraction: Float) {
+ // Not supported. Do nothing.
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/LightRevealScrim.kt b/packages/SystemUI/src/com/android/systemui/statusbar/LightRevealScrim.kt
index 6e85074..ac510fe 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/LightRevealScrim.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/LightRevealScrim.kt
@@ -156,6 +156,54 @@
}
}
+data class LinearSideLightRevealEffect(private val isVertical: Boolean) : LightRevealEffect {
+
+ override fun setRevealAmountOnScrim(amount: Float, scrim: LightRevealScrim) {
+ scrim.interpolatedRevealAmount = amount
+ scrim.startColorAlpha =
+ getPercentPastThreshold(1 - amount, threshold = 1 - START_COLOR_REVEAL_PERCENTAGE)
+ scrim.revealGradientEndColorAlpha =
+ 1f -
+ getPercentPastThreshold(
+ amount,
+ threshold = REVEAL_GRADIENT_END_COLOR_ALPHA_START_PERCENTAGE
+ )
+
+ val gradientBoundsAmount = lerp(GRADIENT_START_BOUNDS_PERCENTAGE, 1f, amount)
+ if (isVertical) {
+ scrim.setRevealGradientBounds(
+ left = -(scrim.viewWidth) * gradientBoundsAmount,
+ top = -(scrim.viewHeight) * gradientBoundsAmount,
+ right = (scrim.viewWidth) * gradientBoundsAmount,
+ bottom = (scrim.viewHeight) + (scrim.viewHeight) * gradientBoundsAmount
+ )
+ } else {
+ scrim.setRevealGradientBounds(
+ left = -(scrim.viewWidth) * gradientBoundsAmount,
+ top = -(scrim.viewHeight) * gradientBoundsAmount,
+ right = (scrim.viewWidth) + (scrim.viewWidth) * gradientBoundsAmount,
+ bottom = (scrim.viewHeight) * gradientBoundsAmount
+ )
+ }
+ }
+
+ private companion object {
+ // From which percentage we should start the gradient reveal width
+ // E.g. if 0 - starts with 0px width, 0.6f - starts with 60% width
+ private const val GRADIENT_START_BOUNDS_PERCENTAGE: Float = 1f
+
+ // When to start changing alpha color of the gradient scrim
+ // E.g. if 0.6f - starts fading the gradient away at 60% and becomes completely
+ // transparent at 100%
+ private const val REVEAL_GRADIENT_END_COLOR_ALPHA_START_PERCENTAGE: Float = 1f
+
+ // When to finish displaying start color fill that reveals the content
+ // E.g. if 0.6f - the content won't be visible at 0% and it will gradually
+ // reduce the alpha until 60% (at this point the color fill is invisible)
+ private const val START_COLOR_REVEAL_PERCENTAGE: Float = 1f
+ }
+}
+
data class CircleReveal(
/** X-value of the circle center of the reveal. */
val centerX: Int,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt
index 7f8be1c..ef50265 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt
@@ -14,6 +14,7 @@
import androidx.annotation.VisibleForTesting
import com.android.systemui.Dumpable
import com.android.systemui.ExpandHelper
+import com.android.systemui.Flags.nsslFalsingFix
import com.android.systemui.Gefingerpoken
import com.android.systemui.biometrics.UdfpsKeyguardViewControllerLegacy
import com.android.systemui.classifier.Classifier
@@ -889,7 +890,7 @@
isDraggingDown = false
isTrackpadReverseScroll = false
shadeRepository.setLegacyLockscreenShadeTracking(false)
- if (KeyguardShadeMigrationNssl.isEnabled) {
+ if (nsslFalsingFix() || KeyguardShadeMigrationNssl.isEnabled) {
return true
}
} else {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesDependenciesModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesDependenciesModule.java
index 32cd56c..91340be 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesDependenciesModule.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesDependenciesModule.java
@@ -26,7 +26,7 @@
import com.android.internal.jank.InteractionJankMonitor;
import com.android.internal.statusbar.IStatusBarService;
import com.android.systemui.CoreStartable;
-import com.android.systemui.animation.ActivityLaunchAnimator;
+import com.android.systemui.animation.ActivityTransitionAnimator;
import com.android.systemui.animation.AnimationFeatureFlags;
import com.android.systemui.animation.DialogLaunchAnimator;
import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor;
@@ -56,7 +56,6 @@
import com.android.systemui.statusbar.phone.StatusBarIconController;
import com.android.systemui.statusbar.phone.StatusBarIconControllerImpl;
import com.android.systemui.statusbar.phone.StatusBarIconList;
-import com.android.systemui.statusbar.phone.StatusBarNotificationPresenterModule;
import com.android.systemui.statusbar.phone.StatusBarRemoteInputCallback;
import com.android.systemui.statusbar.policy.KeyguardStateController;
@@ -73,7 +72,7 @@
* their own version of CentralSurfaces can include just dependencies, without injecting
* CentralSurfaces itself.
*/
-@Module(includes = {StatusBarNotificationPresenterModule.class})
+@Module
public interface CentralSurfacesDependenciesModule {
/** */
@@ -191,8 +190,8 @@
/** */
@Provides
@SysUISingleton
- static ActivityLaunchAnimator provideActivityLaunchAnimator() {
- return new ActivityLaunchAnimator();
+ static ActivityTransitionAnimator provideActivityTransitionAnimator() {
+ return new ActivityTransitionAnimator();
}
/** */
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesModule.java
index 99d4b2e..27536bc 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesModule.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesModule.java
@@ -18,12 +18,14 @@
import com.android.systemui.statusbar.notification.dagger.NotificationsModule;
import com.android.systemui.statusbar.notification.row.NotificationRowModule;
+import com.android.systemui.statusbar.phone.StatusBarNotificationPresenterModule;
import com.android.systemui.statusbar.phone.dagger.StatusBarPhoneModule;
import dagger.Module;
-/** */
-@Module(includes = {StatusBarPhoneModule.class, CentralSurfacesDependenciesModule.class,
+/** */
+@Module(includes = {CentralSurfacesDependenciesModule.class,
+ StatusBarNotificationPresenterModule.class, StatusBarPhoneModule.class,
NotificationsModule.class, NotificationRowModule.class})
public interface CentralSurfacesModule {
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/LaunchAnimationParameters.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/LaunchAnimationParameters.kt
index 785e65d..4af8cb9 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/LaunchAnimationParameters.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/LaunchAnimationParameters.kt
@@ -2,9 +2,9 @@
import android.util.MathUtils
import com.android.internal.annotations.VisibleForTesting
-import com.android.systemui.animation.ActivityLaunchAnimator
+import com.android.systemui.animation.ActivityTransitionAnimator
import com.android.app.animation.Interpolators
-import com.android.systemui.animation.LaunchAnimator
+import com.android.systemui.animation.TransitionAnimator
import kotlin.math.min
/** Parameters for the notifications launch expanding animations. */
@@ -16,7 +16,7 @@
topCornerRadius: Float = 0f,
bottomCornerRadius: Float = 0f
-) : LaunchAnimator.State(top, bottom, left, right, topCornerRadius, bottomCornerRadius) {
+) : TransitionAnimator.State(top, bottom, left, right, topCornerRadius, bottomCornerRadius) {
@VisibleForTesting
constructor() : this(
top = 0, bottom = 0, left = 0, right = 0, topCornerRadius = 0f, bottomCornerRadius = 0f
@@ -58,7 +58,11 @@
}
fun getProgress(delay: Long, duration: Long): Float {
- return LaunchAnimator.getProgress(ActivityLaunchAnimator.TIMINGS, linearProgress, delay,
- duration)
+ return TransitionAnimator.getProgress(
+ ActivityTransitionAnimator.TIMINGS,
+ linearProgress,
+ delay,
+ duration
+ )
}
}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationLaunchAnimatorController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationLaunchAnimatorController.kt
index 5983fc1..02d1e6f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationLaunchAnimatorController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationLaunchAnimatorController.kt
@@ -19,8 +19,8 @@
import android.util.Log
import android.view.ViewGroup
import com.android.internal.jank.InteractionJankMonitor
-import com.android.systemui.animation.ActivityLaunchAnimator
-import com.android.systemui.animation.LaunchAnimator
+import com.android.systemui.animation.ActivityTransitionAnimator
+import com.android.systemui.animation.TransitionAnimator
import com.android.systemui.statusbar.notification.domain.interactor.NotificationLaunchAnimationInteractor
import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow
import com.android.systemui.statusbar.notification.stack.NotificationListContainer
@@ -55,9 +55,9 @@
}
/**
- * An [ActivityLaunchAnimator.Controller] that animates an [ExpandableNotificationRow]. An instance
- * of this class can be passed to [ActivityLaunchAnimator.startIntentWithAnimation] to animate a
- * notification expanding into an opening window.
+ * An [ActivityTransitionAnimator.Controller] that animates an [ExpandableNotificationRow]. An
+ * instance of this class can be passed to [ActivityTransitionAnimator.startIntentWithAnimation] to
+ * animate a notification expanding into an opening window.
*/
class NotificationLaunchAnimatorController(
private val notificationLaunchAnimationInteractor: NotificationLaunchAnimationInteractor,
@@ -66,7 +66,7 @@
private val notification: ExpandableNotificationRow,
private val jankMonitor: InteractionJankMonitor,
private val onFinishAnimationCallback: Runnable?
-) : ActivityLaunchAnimator.Controller {
+) : ActivityTransitionAnimator.Controller {
companion object {
const val ANIMATION_DURATION_TOP_ROUNDING = 100L
@@ -75,13 +75,13 @@
private val notificationEntry = notification.entry
private val notificationKey = notificationEntry.sbn.key
- override var launchContainer: ViewGroup
+ override var transitionContainer: ViewGroup
get() = notification.rootView as ViewGroup
set(ignored) {
// Do nothing. Notifications are always animated inside their rootView.
}
- override fun createAnimatorState(): LaunchAnimator.State {
+ override fun createAnimatorState(): TransitionAnimator.State {
// If the notification panel is collapsed, the clip may be larger than the height.
val height = max(0, notification.actualHeight - notification.clipBottomAmount)
val location = notification.locationOnScreen
@@ -140,7 +140,7 @@
}
override fun onIntentStarted(willAnimate: Boolean) {
- if (ActivityLaunchAnimator.DEBUG_LAUNCH_ANIMATION) {
+ if (ActivityTransitionAnimator.DEBUG_TRANSITION_ANIMATION) {
Log.d(TAG, "onIntentStarted(willAnimate=$willAnimate)")
}
notificationLaunchAnimationInteractor.setIsLaunchAnimationRunning(willAnimate)
@@ -173,8 +173,8 @@
headsUpManager.removeNotification(row.entry.key, true /* releaseImmediately */, animate)
}
- override fun onLaunchAnimationCancelled(newKeyguardOccludedState: Boolean?) {
- if (ActivityLaunchAnimator.DEBUG_LAUNCH_ANIMATION) {
+ override fun onTransitionAnimationCancelled(newKeyguardOccludedState: Boolean?) {
+ if (ActivityTransitionAnimator.DEBUG_TRANSITION_ANIMATION) {
Log.d(TAG, "onLaunchAnimationCancelled()")
}
@@ -186,15 +186,15 @@
onFinishAnimationCallback?.run()
}
- override fun onLaunchAnimationStart(isExpandingFullyAbove: Boolean) {
+ override fun onTransitionAnimationStart(isExpandingFullyAbove: Boolean) {
notification.isExpandAnimationRunning = true
notificationListContainer.setExpandingNotification(notification)
jankMonitor.begin(notification, InteractionJankMonitor.CUJ_NOTIFICATION_APP_START)
}
- override fun onLaunchAnimationEnd(isExpandingFullyAbove: Boolean) {
- if (ActivityLaunchAnimator.DEBUG_LAUNCH_ANIMATION) {
+ override fun onTransitionAnimationEnd(isExpandingFullyAbove: Boolean) {
+ if (ActivityTransitionAnimator.DEBUG_TRANSITION_ANIMATION) {
Log.d(TAG, "onLaunchAnimationEnd()")
}
jankMonitor.end(InteractionJankMonitor.CUJ_NOTIFICATION_APP_START)
@@ -213,8 +213,8 @@
notificationListContainer.applyLaunchAnimationParams(params)
}
- override fun onLaunchAnimationProgress(
- state: LaunchAnimator.State,
+ override fun onTransitionAnimationProgress(
+ state: TransitionAnimator.State,
progress: Float,
linearProgress: Float
) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinator.kt
index 3f2c818..7c71864 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinator.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinator.kt
@@ -386,6 +386,7 @@
}
}
+ @Deprecated("As part of b/301915812")
private fun scheduleDelayedDozeAmountAnimation() {
val alreadyRunning = delayedDozeAmountAnimator != null
logger.logStartDelayedDozeAmountAnimation(alreadyRunning)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/SensitiveContentCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/SensitiveContentCoordinator.kt
index ae4ba27..29627e1 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/SensitiveContentCoordinator.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/SensitiveContentCoordinator.kt
@@ -18,7 +18,7 @@
import android.os.UserHandle
import com.android.keyguard.KeyguardUpdateMonitor
-import com.android.systemui.Flags.screenshareNotificationHiding
+import com.android.server.notification.Flags.screenshareNotificationHiding
import com.android.systemui.plugins.statusbar.StatusBarStateController
import com.android.systemui.statusbar.NotificationLockscreenUserManager
import com.android.systemui.statusbar.StatusBarState
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinator.kt
index 6e2beb4..8b0b973 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinator.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinator.kt
@@ -56,8 +56,8 @@
if (FooterViewRefactor.isEnabled) {
activeNotificationsInteractor.setNotifStats(notifStats)
}
- // TODO(b/293167744): This shouldn't be done if the footer flag is on, once the footer
- // visibility is handled in the new stack.
+ // TODO(b/293167744): This shouldn't be done if the footer flag is on, once the silent
+ // section clear action is handled in the new stack.
controller.setNotifStats(notifStats)
if (NotificationIconContainerRefactor.isEnabled || FooterViewRefactor.isEnabled) {
renderListInteractor.setRenderedList(entries)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotificationRowBinderImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotificationRowBinderImpl.java
index cd816ae..954e805 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotificationRowBinderImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotificationRowBinderImpl.java
@@ -16,7 +16,7 @@
package com.android.systemui.statusbar.notification.collection.inflation;
-import static com.android.systemui.Flags.screenshareNotificationHiding;
+import static com.android.server.notification.Flags.screenshareNotificationHiding;
import static com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_CONTRACTED;
import static com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_EXPANDED;
import static com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_PUBLIC;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java
index 6bba72b..92b0c04 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java
@@ -51,6 +51,7 @@
import com.android.systemui.statusbar.notification.collection.render.NotifShadeEventSource;
import com.android.systemui.statusbar.notification.collection.render.NotificationVisibilityProvider;
import com.android.systemui.statusbar.notification.data.NotificationDataLayerModule;
+import com.android.systemui.statusbar.notification.domain.NotificationDomainLayerModule;
import com.android.systemui.statusbar.notification.domain.interactor.NotificationLaunchAnimationInteractor;
import com.android.systemui.statusbar.notification.footer.ui.viewmodel.FooterViewModelModule;
import com.android.systemui.statusbar.notification.icon.ConversationIconManager;
@@ -78,14 +79,14 @@
import com.android.systemui.statusbar.phone.StatusBarNotificationActivityStarter;
import com.android.systemui.statusbar.policy.HeadsUpManager;
-import javax.inject.Provider;
-
import dagger.Binds;
import dagger.Module;
import dagger.Provides;
import dagger.multibindings.ClassKey;
import dagger.multibindings.IntoMap;
+import javax.inject.Provider;
+
/**
* Dagger Module for classes found within the com.android.systemui.statusbar.notification package.
*/
@@ -94,6 +95,7 @@
FooterViewModelModule.class,
KeyguardNotificationVisibilityProviderModule.class,
NotificationDataLayerModule.class,
+ NotificationDomainLayerModule.class,
NotifPipelineChoreographerModule.class,
NotificationSectionHeadersModule.class,
ActivatableNotificationViewModelModule.class,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/NotificationDataLayerModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/NotificationDataLayerModule.kt
index 2cac000..b187cf1 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/NotificationDataLayerModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/NotificationDataLayerModule.kt
@@ -17,4 +17,10 @@
import dagger.Module
-@Module(includes = []) interface NotificationDataLayerModule
+@Module(
+ includes =
+ [
+ NotificationSettingsRepositoryModule::class,
+ ]
+)
+interface NotificationDataLayerModule
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/NotificationSettingsRepositoryModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/NotificationSettingsRepositoryModule.kt
new file mode 100644
index 0000000..a7970c7
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/NotificationSettingsRepositoryModule.kt
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2023 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.notification.data
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.settings.SecureSettingsRepositoryModule
+import com.android.systemui.shared.notifications.data.repository.NotificationSettingsRepository
+import com.android.systemui.shared.settings.data.repository.SecureSettingsRepository
+import dagger.Module
+import dagger.Provides
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+
+@Module(includes = [SecureSettingsRepositoryModule::class])
+object NotificationSettingsRepositoryModule {
+ @Provides
+ @SysUISingleton
+ fun provideNotificationSettingsRepository(
+ @Background backgroundScope: CoroutineScope,
+ @Background backgroundDispatcher: CoroutineDispatcher,
+ secureSettingsRepository: SecureSettingsRepository,
+ ): NotificationSettingsRepository =
+ NotificationSettingsRepository(
+ backgroundScope,
+ backgroundDispatcher,
+ secureSettingsRepository
+ )
+}
diff --git a/packages/SoundPicker2/src/com/android/soundpicker/RingtonePickerApplication.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/NotificationDomainLayerModule.kt
similarity index 67%
rename from packages/SoundPicker2/src/com/android/soundpicker/RingtonePickerApplication.java
rename to packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/NotificationDomainLayerModule.kt
index 48fd4fe..5c49b28 100644
--- a/packages/SoundPicker2/src/com/android/soundpicker/RingtonePickerApplication.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/NotificationDomainLayerModule.kt
@@ -14,15 +14,10 @@
* limitations under the License.
*/
-package com.android.soundpicker;
+package com.android.systemui.statusbar.notification.domain
-import android.app.Application;
+import com.android.systemui.statusbar.notification.domain.interactor.NotificationSettingsInteractorModule
+import dagger.Module
-import dagger.hilt.android.HiltAndroidApp;
-
-/**
- * The main application class for the project.
- */
-@HiltAndroidApp(Application.class)
-public class RingtonePickerApplication extends Hilt_RingtonePickerApplication {
-}
+@Module(includes = [NotificationSettingsInteractorModule::class])
+object NotificationDomainLayerModule
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/NotificationLaunchAnimationInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/NotificationLaunchAnimationInteractor.kt
index 22ce4f1..a3189a0 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/NotificationLaunchAnimationInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/NotificationLaunchAnimationInteractor.kt
@@ -17,7 +17,7 @@
package com.android.systemui.statusbar.notification.domain.interactor
import android.util.Log
-import com.android.systemui.animation.ActivityLaunchAnimator
+import com.android.systemui.animation.ActivityTransitionAnimator
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.statusbar.notification.data.repository.NotificationLaunchAnimationRepository
import javax.inject.Inject
@@ -40,7 +40,7 @@
/** Sets whether the notification expansion launch animation is currently running. */
fun setIsLaunchAnimationRunning(running: Boolean) {
- if (ActivityLaunchAnimator.DEBUG_LAUNCH_ANIMATION) {
+ if (ActivityTransitionAnimator.DEBUG_TRANSITION_ANIMATION) {
Log.d(TAG, "setIsLaunchAnimationRunning(running=$running)")
}
repository.isLaunchAnimationRunning.value = running
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/NotificationSettingsInteractorModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/NotificationSettingsInteractorModule.kt
new file mode 100644
index 0000000..ccf6f40
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/NotificationSettingsInteractorModule.kt
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2024 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.notification.domain.interactor
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.shared.notifications.data.repository.NotificationSettingsRepository
+import com.android.systemui.shared.notifications.domain.interactor.NotificationSettingsInteractor
+import dagger.Module
+import dagger.Provides
+
+@Module
+object NotificationSettingsInteractorModule {
+ @Provides
+ @SysUISingleton
+ fun provideNotificationSettingsInteractor(repository: NotificationSettingsRepository) =
+ NotificationSettingsInteractor(repository)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/view/FooterView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/view/FooterView.java
index 3616fd6d..16f18a3 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/view/FooterView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/view/FooterView.java
@@ -54,7 +54,7 @@
private static final String TAG = "FooterView";
private FooterViewButton mClearAllButton;
- private FooterViewButton mManageButton;
+ private FooterViewButton mManageOrHistoryButton;
private boolean mShowHistory;
// String cache, for performance reasons.
// Reading them from a Resources object can be quite slow sometimes.
@@ -68,6 +68,8 @@
private @StringRes int mClearAllButtonTextId;
private @StringRes int mClearAllButtonDescriptionId;
+ private @StringRes int mManageOrHistoryButtonTextId;
+ private @StringRes int mManageOrHistoryButtonDescriptionId;
private @StringRes int mMessageStringId;
private @DrawableRes int mMessageIconId;
@@ -155,6 +157,43 @@
mClearAllButton.setContentDescription(getContext().getString(mClearAllButtonDescriptionId));
}
+ /** Set the text label for the "Manage"/"History" button. */
+ public void setManageOrHistoryButtonText(@StringRes int textId) {
+ if (FooterViewRefactor.isUnexpectedlyInLegacyMode()) return;
+ if (mManageOrHistoryButtonTextId == textId) {
+ return; // nothing changed
+ }
+ mManageOrHistoryButtonTextId = textId;
+ updateManageOrHistoryButtonText();
+ }
+
+ private void updateManageOrHistoryButtonText() {
+ if (mManageOrHistoryButtonTextId == 0) {
+ return; // not initialized yet
+ }
+ mManageOrHistoryButton.setText(getContext().getString(mManageOrHistoryButtonTextId));
+ }
+
+ /** Set the accessibility content description for the "Clear all" button. */
+ public void setManageOrHistoryButtonDescription(@StringRes int contentDescriptionId) {
+ if (FooterViewRefactor.isUnexpectedlyInLegacyMode()) {
+ return;
+ }
+ if (mManageOrHistoryButtonDescriptionId == contentDescriptionId) {
+ return; // nothing changed
+ }
+ mManageOrHistoryButtonDescriptionId = contentDescriptionId;
+ updateManageOrHistoryButtonDescription();
+ }
+
+ private void updateManageOrHistoryButtonDescription() {
+ if (mManageOrHistoryButtonDescriptionId == 0) {
+ return; // not initialized yet
+ }
+ mManageOrHistoryButton.setContentDescription(
+ getContext().getString(mManageOrHistoryButtonDescriptionId));
+ }
+
/** Set the string for a message to be shown instead of the buttons. */
public void setMessageString(@StringRes int messageId) {
if (FooterViewRefactor.isUnexpectedlyInLegacyMode()) return;
@@ -173,7 +212,6 @@
mSeenNotifsFooterTextView.setText(messageString);
}
-
/** Set the icon to be shown before the message (see {@link #setMessageString(int)}). */
public void setMessageIcon(@DrawableRes int iconId) {
if (FooterViewRefactor.isUnexpectedlyInLegacyMode()) return;
@@ -203,9 +241,11 @@
protected void onFinishInflate() {
super.onFinishInflate();
mClearAllButton = (FooterViewButton) findSecondaryView();
- mManageButton = findViewById(R.id.manage_text);
+ mManageOrHistoryButton = findViewById(R.id.manage_text);
mSeenNotifsFooterTextView = findViewById(R.id.unlock_prompt_footer);
- updateResources();
+ if (!FooterViewRefactor.isEnabled()) {
+ updateResources();
+ }
updateContent();
updateColors();
}
@@ -213,11 +253,11 @@
/** Show a message instead of the footer buttons. */
public void setFooterLabelVisible(boolean isVisible) {
if (isVisible) {
- mManageButton.setVisibility(View.GONE);
+ mManageOrHistoryButton.setVisibility(View.GONE);
mClearAllButton.setVisibility(View.GONE);
mSeenNotifsFooterTextView.setVisibility(View.VISIBLE);
} else {
- mManageButton.setVisibility(View.VISIBLE);
+ mManageOrHistoryButton.setVisibility(View.VISIBLE);
mClearAllButton.setVisibility(View.VISIBLE);
mSeenNotifsFooterTextView.setVisibility(View.GONE);
}
@@ -225,7 +265,7 @@
/** Set onClickListener for the manage/history button. */
public void setManageButtonClickListener(OnClickListener listener) {
- mManageButton.setOnClickListener(listener);
+ mManageOrHistoryButton.setOnClickListener(listener);
}
/** Set onClickListener for the clear all (end) button. */
@@ -252,6 +292,7 @@
/** Show "History" instead of "Manage" on the start button. */
public void showHistory(boolean showHistory) {
+ FooterViewRefactor.assertInLegacyMode();
if (mShowHistory == showHistory) {
return;
}
@@ -260,17 +301,13 @@
}
private void updateContent() {
- if (mShowHistory) {
- mManageButton.setText(mManageNotificationHistoryText);
- mManageButton.setContentDescription(mManageNotificationHistoryText);
- } else {
- mManageButton.setText(mManageNotificationText);
- mManageButton.setContentDescription(mManageNotificationText);
- }
if (FooterViewRefactor.isEnabled()) {
updateClearAllButtonText();
updateClearAllButtonDescription();
+ updateManageOrHistoryButtonText();
+ updateManageOrHistoryButtonDescription();
+
updateMessageString();
updateMessageIcon();
} else {
@@ -285,6 +322,14 @@
// `updateResources`, which will eventually be removed. There are, however, still
// situations in which we want to update the views even if the resource IDs didn't
// change, such as configuration changes.
+ if (mShowHistory) {
+ mManageOrHistoryButton.setText(mManageNotificationHistoryText);
+ mManageOrHistoryButton.setContentDescription(mManageNotificationHistoryText);
+ } else {
+ mManageOrHistoryButton.setText(mManageNotificationText);
+ mManageOrHistoryButton.setContentDescription(mManageNotificationText);
+ }
+
mClearAllButton.setText(R.string.clear_all_notifications_text);
mClearAllButton.setContentDescription(
mContext.getString(R.string.accessibility_clear_all));
@@ -297,6 +342,7 @@
/** Whether the start button shows "History" (true) or "Manage" (false). */
public boolean isHistoryShown() {
+ FooterViewRefactor.assertInLegacyMode();
return mShowHistory;
}
@@ -304,7 +350,9 @@
protected void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
updateColors();
- updateResources();
+ if (!FooterViewRefactor.isEnabled()) {
+ updateResources();
+ }
updateContent();
}
@@ -328,23 +376,22 @@
}
mClearAllButton.setBackground(clearAllBg);
mClearAllButton.setTextColor(onSurface);
- mManageButton.setBackground(manageBg);
- mManageButton.setTextColor(onSurface);
+ mManageOrHistoryButton.setBackground(manageBg);
+ mManageOrHistoryButton.setTextColor(onSurface);
mSeenNotifsFooterTextView.setTextColor(onSurface);
mSeenNotifsFooterTextView.setCompoundDrawableTintList(ColorStateList.valueOf(onSurface));
}
private void updateResources() {
+ FooterViewRefactor.assertInLegacyMode();
mManageNotificationText = getContext().getString(R.string.manage_notifications_text);
mManageNotificationHistoryText = getContext()
.getString(R.string.manage_notifications_history_text);
- if (!FooterViewRefactor.isEnabled()) {
- int unlockIconSize = getResources()
- .getDimensionPixelSize(R.dimen.notifications_unseen_footer_icon_size);
- mSeenNotifsFilteredText = getContext().getString(R.string.unlock_to_see_notif_text);
- mSeenNotifsFilteredIcon = getContext().getDrawable(R.drawable.ic_friction_lock_closed);
- mSeenNotifsFilteredIcon.setBounds(0, 0, unlockIconSize, unlockIconSize);
- }
+ int unlockIconSize = getResources()
+ .getDimensionPixelSize(R.dimen.notifications_unseen_footer_icon_size);
+ mSeenNotifsFilteredText = getContext().getString(R.string.unlock_to_see_notif_text);
+ mSeenNotifsFilteredIcon = getContext().getDrawable(R.drawable.ic_friction_lock_closed);
+ mSeenNotifsFilteredIcon.setBounds(0, 0, unlockIconSize, unlockIconSize);
}
@Override
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewbinder/FooterViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewbinder/FooterViewBinder.kt
index e0eee96..9fb453a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewbinder/FooterViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewbinder/FooterViewBinder.kt
@@ -25,49 +25,136 @@
import com.android.systemui.util.ui.stopAnimating
import com.android.systemui.util.ui.value
import kotlinx.coroutines.DisposableHandle
+import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch
/** Binds a [FooterView] to its [view model][FooterViewModel]. */
object FooterViewBinder {
- fun bind(
+ fun bindWhileAttached(
footer: FooterView,
viewModel: FooterViewModel,
clearAllNotifications: View.OnClickListener,
+ launchNotificationSettings: View.OnClickListener,
+ launchNotificationHistory: View.OnClickListener,
): DisposableHandle {
+ return footer.repeatWhenAttached {
+ lifecycleScope.launch {
+ bind(
+ footer,
+ viewModel,
+ clearAllNotifications,
+ launchNotificationSettings,
+ launchNotificationHistory
+ )
+ }
+ }
+ }
+
+ suspend fun bind(
+ footer: FooterView,
+ viewModel: FooterViewModel,
+ clearAllNotifications: View.OnClickListener,
+ launchNotificationSettings: View.OnClickListener,
+ launchNotificationHistory: View.OnClickListener
+ ) = coroutineScope {
+ launch {
+ bindClearAllButton(
+ footer,
+ viewModel,
+ clearAllNotifications,
+ )
+ }
+ launch {
+ bindManageOrHistoryButton(
+ footer,
+ viewModel,
+ launchNotificationSettings,
+ launchNotificationHistory
+ )
+ }
+ launch { bindMessage(footer, viewModel) }
+ }
+
+ private suspend fun bindClearAllButton(
+ footer: FooterView,
+ viewModel: FooterViewModel,
+ clearAllNotifications: View.OnClickListener,
+ ) = coroutineScope {
+ footer.setClearAllButtonClickListener(clearAllNotifications)
+
+ launch {
+ viewModel.clearAllButton.labelId.collect { textId ->
+ footer.setClearAllButtonText(textId)
+ }
+ }
+
+ launch {
+ viewModel.clearAllButton.accessibilityDescriptionId.collect { textId ->
+ footer.setClearAllButtonDescription(textId)
+ }
+ }
+
+ launch {
+ viewModel.clearAllButton.isVisible.collect { isVisible ->
+ if (isVisible.isAnimating) {
+ footer.setClearAllButtonVisible(
+ isVisible.value,
+ /* animate = */ true,
+ ) { _ ->
+ isVisible.stopAnimating()
+ }
+ } else {
+ footer.setClearAllButtonVisible(
+ isVisible.value,
+ /* animate = */ false,
+ )
+ }
+ }
+ }
+ }
+
+ private suspend fun bindManageOrHistoryButton(
+ footer: FooterView,
+ viewModel: FooterViewModel,
+ launchNotificationSettings: View.OnClickListener,
+ launchNotificationHistory: View.OnClickListener,
+ ) = coroutineScope {
+ launch {
+ viewModel.manageButtonShouldLaunchHistory.collect { shouldLaunchHistory ->
+ if (shouldLaunchHistory) {
+ footer.setManageButtonClickListener(launchNotificationHistory)
+ } else {
+ footer.setManageButtonClickListener(launchNotificationSettings)
+ }
+ }
+ }
+
+ launch {
+ viewModel.manageOrHistoryButton.labelId.collect { textId ->
+ footer.setManageOrHistoryButtonText(textId)
+ }
+ }
+
+ launch {
+ viewModel.clearAllButton.accessibilityDescriptionId.collect { textId ->
+ footer.setManageOrHistoryButtonDescription(textId)
+ }
+ }
+
+ // NOTE: The manage/history button is always visible as long as the footer is visible, no
+ // need to update the visibility here.
+ }
+
+ private suspend fun bindMessage(
+ footer: FooterView,
+ viewModel: FooterViewModel,
+ ) = coroutineScope {
// Bind the resource IDs
footer.setMessageString(viewModel.message.messageId)
footer.setMessageIcon(viewModel.message.iconId)
- footer.setClearAllButtonText(viewModel.clearAllButton.labelId)
- footer.setClearAllButtonDescription(viewModel.clearAllButton.accessibilityDescriptionId)
- // Bind the click listeners
- footer.setClearAllButtonClickListener(clearAllNotifications)
-
- // Listen for visibility changes when the view is attached.
- return footer.repeatWhenAttached {
- lifecycleScope.launch {
- viewModel.clearAllButton.isVisible.collect { isVisible ->
- if (isVisible.isAnimating) {
- footer.setClearAllButtonVisible(
- isVisible.value,
- /* animate = */ true,
- ) { _ ->
- isVisible.stopAnimating()
- }
- } else {
- footer.setClearAllButtonVisible(
- isVisible.value,
- /* animate = */ false,
- )
- }
- }
- }
-
- lifecycleScope.launch {
- viewModel.message.isVisible.collect { visible ->
- footer.setFooterLabelVisible(visible)
- }
- }
+ launch {
+ viewModel.message.isVisible.collect { visible -> footer.setFooterLabelVisible(visible) }
}
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterButtonViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterButtonViewModel.kt
index 244555a..691dc42 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterButtonViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterButtonViewModel.kt
@@ -21,7 +21,7 @@
import kotlinx.coroutines.flow.Flow
data class FooterButtonViewModel(
- @StringRes val labelId: Int,
- @StringRes val accessibilityDescriptionId: Int,
+ @StringRes val labelId: Flow<Int>,
+ @StringRes val accessibilityDescriptionId: Flow<Int>,
val isVisible: Flow<AnimatedValue<Boolean>>,
)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModel.kt
index e6b0abc..b23ef35 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModel.kt
@@ -19,33 +19,42 @@
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.res.R
import com.android.systemui.shade.domain.interactor.ShadeInteractor
+import com.android.systemui.shared.notifications.domain.interactor.NotificationSettingsInteractor
import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor
import com.android.systemui.statusbar.notification.domain.interactor.SeenNotificationsInteractor
import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor
import com.android.systemui.statusbar.notification.footer.ui.view.FooterView
import com.android.systemui.util.kotlin.sample
import com.android.systemui.util.ui.AnimatableEvent
+import com.android.systemui.util.ui.AnimatedValue
import com.android.systemui.util.ui.toAnimatedValueFlow
import dagger.Module
import dagger.Provides
import java.util.Optional
import javax.inject.Provider
+import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onStart
/** ViewModel for [FooterView]. */
class FooterViewModel(
activeNotificationsInteractor: ActiveNotificationsInteractor,
+ notificationSettingsInteractor: NotificationSettingsInteractor,
seenNotificationsInteractor: SeenNotificationsInteractor,
shadeInteractor: ShadeInteractor,
) {
val clearAllButton: FooterButtonViewModel =
FooterButtonViewModel(
- labelId = R.string.clear_all_notifications_text,
- accessibilityDescriptionId = R.string.accessibility_clear_all,
+ labelId = flowOf(R.string.clear_all_notifications_text),
+ accessibilityDescriptionId = flowOf(R.string.accessibility_clear_all),
isVisible =
activeNotificationsInteractor.hasClearableNotifications
.sample(
+ // TODO(b/322167853): This check is currently duplicated in
+ // NotificationListViewModel, but instead it should be a field in
+ // ShadeAnimationInteractor.
combine(
shadeInteractor.isShadeFullyExpanded,
shadeInteractor.isShadeTouchable,
@@ -59,6 +68,22 @@
.toAnimatedValueFlow(),
)
+ val manageButtonShouldLaunchHistory =
+ notificationSettingsInteractor.isNotificationHistoryEnabled
+
+ private val manageOrHistoryButtonText: Flow<Int> =
+ manageButtonShouldLaunchHistory.map { shouldLaunchHistory ->
+ if (shouldLaunchHistory) R.string.manage_notifications_history_text
+ else R.string.manage_notifications_text
+ }
+
+ val manageOrHistoryButton: FooterButtonViewModel =
+ FooterButtonViewModel(
+ labelId = manageOrHistoryButtonText,
+ accessibilityDescriptionId = manageOrHistoryButtonText,
+ isVisible = flowOf(AnimatedValue.NotAnimating(true)),
+ )
+
val message: FooterMessageViewModel =
FooterMessageViewModel(
messageId = R.string.unlock_to_see_notif_text,
@@ -73,6 +98,7 @@
@SysUISingleton
fun provideOptional(
activeNotificationsInteractor: Provider<ActiveNotificationsInteractor>,
+ notificationSettingsInteractor: Provider<NotificationSettingsInteractor>,
seenNotificationsInteractor: Provider<SeenNotificationsInteractor>,
shadeInteractor: Provider<ShadeInteractor>,
): Optional<FooterViewModel> {
@@ -80,6 +106,7 @@
Optional.of(
FooterViewModel(
activeNotificationsInteractor.get(),
+ notificationSettingsInteractor.get(),
seenNotificationsInteractor.get(),
shadeInteractor.get()
)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconContainerShelfViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconContainerShelfViewBinder.kt
index 783488af..4e40888 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconContainerShelfViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconContainerShelfViewBinder.kt
@@ -37,11 +37,12 @@
) {
suspend fun bind(view: NotificationIconContainer) {
viewModel.icons.bindIcons(
- view,
- configuration,
- systemBarUtilsState,
+ logTag = "shelf",
+ view = view,
+ configuration = configuration,
+ systemBarUtilsState = systemBarUtilsState,
notifyBindingFailures = { failureTracker.shelfFailures = it },
- viewStore,
+ viewStore = viewStore,
)
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconContainerViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconContainerViewBinder.kt
index f375ebc..de3a626 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconContainerViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconContainerViewBinder.kt
@@ -72,11 +72,12 @@
val iconColors: StateFlow<NotificationIconColors> =
viewModel.iconColors.mapNotNull { it.iconColors(view.viewBounds) }.stateIn(this)
viewModel.icons.bindIcons(
- view,
- configuration,
- systemBarUtilsState,
+ logTag = "statusbar",
+ view = view,
+ configuration = configuration,
+ systemBarUtilsState = systemBarUtilsState,
notifyBindingFailures = { failureTracker.statusBarFailures = it },
- viewStore,
+ viewStore = viewStore,
) { _, sbiv ->
StatusBarIconViewBinder.bindIconColors(
sbiv,
@@ -124,11 +125,12 @@
val tintAlpha = viewModel.tintAlpha.stateIn(this)
val animsEnabled = viewModel.areIconAnimationsEnabled.stateIn(this)
viewModel.icons.bindIcons(
- view,
- configuration,
- systemBarUtilsState,
+ logTag = "aod",
+ view = view,
+ configuration = configuration,
+ systemBarUtilsState = systemBarUtilsState,
notifyBindingFailures = { failureTracker.aodFailures = it },
- viewStore,
+ viewStore = viewStore,
) { _, sbiv ->
coroutineScope {
launch { StatusBarIconViewBinder.bindColor(sbiv, color) }
@@ -176,6 +178,7 @@
* view is to be unbound.
*/
suspend fun Flow<NotificationIconsViewData>.bindIcons(
+ logTag: String,
view: NotificationIconContainer,
configuration: ConfigurationState,
systemBarUtilsState: SystemBarUtilsState,
@@ -197,7 +200,7 @@
}
.stateIn(this)
try {
- bindIcons(view, layoutParams, notifyBindingFailures, viewStore, bindIcon)
+ bindIcons(logTag, view, layoutParams, notifyBindingFailures, viewStore, bindIcon)
} finally {
// Detach everything so that child SBIVs don't hold onto a reference to the container.
view.detachAllIcons()
@@ -205,6 +208,7 @@
}
private suspend fun Flow<NotificationIconsViewData>.bindIcons(
+ logTag: String,
view: NotificationIconContainer,
layoutParams: StateFlow<FrameLayout.LayoutParams>,
notifyBindingFailures: (Collection<String>) -> Unit,
@@ -214,7 +218,7 @@
val failedBindings = mutableSetOf<String>()
val boundViewsByNotifKey = ArrayMap<String, Pair<StatusBarIconView, Job>>()
var prevIcons = NotificationIconsViewData()
- collectTracingEach("NIC#bindIcons") { iconsData: NotificationIconsViewData ->
+ collectTracingEach({ "NIC($logTag)#bindIcons" }) { iconsData: NotificationIconsViewData ->
val iconsDiff = NotificationIconsViewData.computeDifference(iconsData, prevIcons)
prevIcons = iconsData
@@ -249,7 +253,7 @@
if (this !== view) {
Log.wtf(
TAG,
- "StatusBarIconView($notifKey) has an unexpected parent",
+ "[$logTag] SBIV($notifKey) has an unexpected parent",
)
}
// If the container was re-inflated and re-bound, then SBIVs might still
@@ -266,7 +270,9 @@
sbiv,
launch {
launch {
- layoutParams.collectTracingEach("SBIV#bindLayoutParams") {
+ layoutParams.collectTracingEach(
+ tag = { "[$logTag] SBIV#bindLayoutParams" },
+ ) {
if (it != sbiv.layoutParams) {
sbiv.layoutParams = it
}
@@ -307,7 +313,12 @@
val childCount = view.childCount
for (i in 0 until childCount) {
val actual = view.getChildAt(i)
- val expected = expectedChildren[i]
+ val expected = expectedChildren.getOrNull(i)
+ if (expected == null) {
+ Log.wtf(TAG, "[$logTag] Unexpected child $actual")
+ view.removeView(actual)
+ continue
+ }
if (actual === expected) {
continue
}
@@ -379,3 +390,11 @@
tag: String,
crossinline collector: (T) -> Unit,
) = collect { traceSection(tag) { collector(it) } }
+
+private suspend inline fun <T> Flow<T>.collectTracingEach(
+ noinline tag: () -> String,
+ crossinline collector: (T) -> Unit,
+) {
+ val lazyTag = lazy(mode = LazyThreadSafetyMode.PUBLICATION, tag)
+ collect { traceSection({ lazyTag.value }) { collector(it) } }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerAlwaysOnDisplayViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerAlwaysOnDisplayViewModel.kt
index d903f06..8768ea2 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerAlwaysOnDisplayViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerAlwaysOnDisplayViewModel.kt
@@ -29,6 +29,8 @@
import kotlin.coroutines.CoroutineContext
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.conflate
+import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onStart
@@ -56,6 +58,8 @@
panelTouchesEnabled && isKeyguardVisible
}
.flowOn(bgContext)
+ .conflate()
+ .distinctUntilChanged()
/** Amount of a "white" tint to be applied to the icons. */
val tintAlpha: Flow<Float> =
@@ -70,6 +74,8 @@
aodAmt + dozeAmt // If transitioning between them, they should sum to 1f
}
.flowOn(bgContext)
+ .conflate()
+ .distinctUntilChanged()
/** Are notification icons animated (ex: animated gif)? */
val areIconAnimationsEnabled: Flow<Boolean> =
@@ -78,8 +84,10 @@
// Don't animate icons when we're on AOD / dozing
it != KeyguardState.AOD && it != KeyguardState.DOZING
}
- .flowOn(bgContext)
.onStart { emit(true) }
+ .flowOn(bgContext)
+ .conflate()
+ .distinctUntilChanged()
/** [NotificationIconsViewData] indicating which icons to display in the view. */
val icons: Flow<NotificationIconsViewData> =
@@ -91,4 +99,6 @@
)
}
.flowOn(bgContext)
+ .conflate()
+ .distinctUntilChanged()
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerShelfViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerShelfViewModel.kt
index 3574828..260cccd 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerShelfViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerShelfViewModel.kt
@@ -21,6 +21,8 @@
import javax.inject.Inject
import kotlin.coroutines.CoroutineContext
import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.conflate
+import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
@@ -56,4 +58,6 @@
)
}
.flowOn(bgContext)
+ .conflate()
+ .distinctUntilChanged()
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerStatusBarViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerStatusBarViewModel.kt
index 38921c2..a64f888 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerStatusBarViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerStatusBarViewModel.kt
@@ -35,6 +35,7 @@
import kotlin.coroutines.CoroutineContext
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.conflate
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.flowOn
@@ -64,10 +65,13 @@
panelTouchesEnabled && !isKeyguardShowing
}
.flowOn(bgContext)
+ .conflate()
+ .distinctUntilChanged()
/** The colors with which to display the notification icons. */
val iconColors: Flow<NotificationIconColorLookup> =
- combine(darkIconInteractor.tintAreas, darkIconInteractor.tintColor) { areas, tint ->
+ darkIconInteractor.darkState
+ .map { (areas: Collection<Rect>, tint: Int) ->
NotificationIconColorLookup { viewBounds: Rect ->
if (DarkIconDispatcher.isInAreas(areas, viewBounds)) {
IconColorsImpl(tint, areas)
@@ -77,6 +81,8 @@
}
}
.flowOn(bgContext)
+ .conflate()
+ .distinctUntilChanged()
/** [NotificationIconsViewData] indicating which icons to display in the view. */
val icons: Flow<NotificationIconsViewData> =
@@ -88,6 +94,8 @@
)
}
.flowOn(bgContext)
+ .conflate()
+ .distinctUntilChanged()
/** An Icon to show "isolated" in the IconContainer. */
val isolatedIcon: Flow<AnimatedValue<NotificationIconInfo?>> =
@@ -99,6 +107,8 @@
}
.distinctUntilChanged()
.flowOn(bgContext)
+ .conflate()
+ .distinctUntilChanged()
.pairwise(initialValue = null)
.sample(shadeInteractor.shadeExpansion) { (prev, iconInfo), shadeExpansion ->
val animate =
@@ -113,7 +123,7 @@
/** Location to show an isolated icon, if there is one. */
val isolatedIconLocation: Flow<Rect> =
- headsUpIconInteractor.isolatedIconLocation.filterNotNull()
+ headsUpIconInteractor.isolatedIconLocation.filterNotNull().conflate().distinctUntilChanged()
private class IconColorsImpl(
override val tint: Int,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/AvalancheProvider.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/AvalancheProvider.kt
new file mode 100644
index 0000000..c74c396
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/AvalancheProvider.kt
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2024 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.notification.interruption
+
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import android.util.Log
+import com.android.systemui.broadcast.BroadcastDispatcher
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.util.time.SystemClock
+import javax.inject.Inject
+
+// Class to track avalanche trigger event time.
+@SysUISingleton
+class AvalancheProvider
+@Inject
+constructor(
+ private val broadcastDispatcher: BroadcastDispatcher,
+ private val logger: VisualInterruptionDecisionLogger,
+) {
+ val TAG = "AvalancheProvider"
+ val timeoutMs = 120000
+ var startTime: Long = 0L
+
+ private val avalancheTriggerIntents = mutableSetOf(
+ Intent.ACTION_AIRPLANE_MODE_CHANGED,
+ Intent.ACTION_BOOT_COMPLETED,
+ Intent.ACTION_MANAGED_PROFILE_AVAILABLE,
+ Intent.ACTION_USER_SWITCHED
+ )
+
+ private val broadcastReceiver: BroadcastReceiver = object : BroadcastReceiver() {
+ override fun onReceive(context: Context, intent: Intent) {
+ if (intent.action in avalancheTriggerIntents) {
+
+ // Ignore when airplane mode turned on
+ if (intent.action == Intent.ACTION_AIRPLANE_MODE_CHANGED
+ && intent.getBooleanExtra(/* name= */ "state", /* defaultValue */ false)) {
+ Log.d(TAG, "broadcastReceiver: ignore airplane mode on")
+ return
+ }
+ Log.d(TAG, "broadcastReceiver received intent.action=" + intent.action)
+ startTime = System.currentTimeMillis()
+ }
+ }
+ }
+
+ fun register() {
+ val intentFilter = IntentFilter()
+ for (intent in avalancheTriggerIntents) {
+ intentFilter.addAction(intent)
+ }
+ broadcastDispatcher.registerReceiver(broadcastReceiver, intentFilter)
+ }
+}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/CommonVisualInterruptionSuppressors.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/CommonVisualInterruptionSuppressors.kt
index 8e82442..6836816 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/CommonVisualInterruptionSuppressors.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/CommonVisualInterruptionSuppressors.kt
@@ -16,7 +16,10 @@
package com.android.systemui.statusbar.notification.interruption
+import android.app.Notification
import android.app.Notification.BubbleMetadata
+import android.app.Notification.CATEGORY_EVENT
+import android.app.Notification.CATEGORY_REMINDER
import android.app.Notification.VISIBILITY_PRIVATE
import android.app.NotificationManager.IMPORTANCE_DEFAULT
import android.app.NotificationManager.IMPORTANCE_HIGH
@@ -224,3 +227,71 @@
override fun shouldSuppress(entry: NotificationEntry) =
keyguardNotificationVisibilityProvider.shouldHideNotification(entry)
}
+
+
+class AvalancheSuppressor(
+ private val avalancheProvider: AvalancheProvider,
+ private val systemClock: SystemClock,
+) : VisualInterruptionFilter(
+ types = setOf(PEEK, PULSE),
+ reason = "avalanche",
+ ) {
+ val TAG = "AvalancheSuppressor"
+
+ override var reason: String = "avalanche"
+ protected set
+
+ enum class State {
+ ALLOW_CONVERSATION_AFTER_AVALANCHE,
+ ALLOW_HIGH_PRIORITY_CONVERSATION_ANY_TIME,
+ ALLOW_CALLSTYLE,
+ ALLOW_CATEGORY_REMINDER,
+ ALLOW_CATEGORY_EVENT,
+ ALLOW_FSI_WITH_PERMISSION_ON,
+ ALLOW_COLORIZED,
+ SUPPRESS
+ }
+
+ override fun shouldSuppress(entry: NotificationEntry): Boolean {
+ val timeSinceAvalanche = systemClock.currentTimeMillis() - avalancheProvider.startTime
+ val isActive = timeSinceAvalanche < avalancheProvider.timeoutMs
+ val state = calculateState(entry)
+ val suppress = isActive && state == State.SUPPRESS
+ reason = "avalanche suppress=$suppress isActive=$isActive state=$state"
+ return suppress
+ }
+
+ private fun calculateState(entry: NotificationEntry): State {
+ if (
+ entry.ranking.isConversation &&
+ entry.sbn.notification.`when` > avalancheProvider.startTime
+ ) {
+ return State.ALLOW_CONVERSATION_AFTER_AVALANCHE
+ }
+
+ if (entry.channel?.isImportantConversation == true) {
+ return State.ALLOW_HIGH_PRIORITY_CONVERSATION_ANY_TIME
+ }
+
+ if (entry.sbn.notification.isStyle(Notification.CallStyle::class.java)) {
+ return State.ALLOW_CALLSTYLE
+ }
+
+ if (entry.sbn.notification.category == CATEGORY_REMINDER) {
+ return State.ALLOW_CATEGORY_REMINDER
+ }
+
+ if (entry.sbn.notification.category == CATEGORY_EVENT) {
+ return State.ALLOW_CATEGORY_EVENT
+ }
+
+ if (entry.sbn.notification.fullScreenIntent != null) {
+ return State.ALLOW_FSI_WITH_PERMISSION_ON
+ }
+
+ if (entry.sbn.notification.isColorized) {
+ return State.ALLOW_COLORIZED
+ }
+ return State.SUPPRESS
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderImpl.kt
index 6878a1e..dabb18b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderImpl.kt
@@ -33,6 +33,7 @@
import com.android.systemui.statusbar.notification.interruption.VisualInterruptionType.BUBBLE
import com.android.systemui.statusbar.notification.interruption.VisualInterruptionType.PEEK
import com.android.systemui.statusbar.notification.interruption.VisualInterruptionType.PULSE
+import com.android.systemui.statusbar.notification.shared.NotificationAvalancheSuppression
import com.android.systemui.statusbar.policy.BatteryController
import com.android.systemui.statusbar.policy.DeviceProvisionedController
import com.android.systemui.statusbar.policy.HeadsUpManager
@@ -45,22 +46,25 @@
class VisualInterruptionDecisionProviderImpl
@Inject
constructor(
- private val ambientDisplayConfiguration: AmbientDisplayConfiguration,
- private val batteryController: BatteryController,
- deviceProvisionedController: DeviceProvisionedController,
- private val eventLog: EventLog,
- private val globalSettings: GlobalSettings,
- private val headsUpManager: HeadsUpManager,
- private val keyguardNotificationVisibilityProvider: KeyguardNotificationVisibilityProvider,
- keyguardStateController: KeyguardStateController,
- private val logger: VisualInterruptionDecisionLogger,
- @Main private val mainHandler: Handler,
- private val powerManager: PowerManager,
- private val statusBarStateController: StatusBarStateController,
- private val systemClock: SystemClock,
- private val uiEventLogger: UiEventLogger,
- private val userTracker: UserTracker,
+ private val ambientDisplayConfiguration: AmbientDisplayConfiguration,
+ private val batteryController: BatteryController,
+ deviceProvisionedController: DeviceProvisionedController,
+ private val eventLog: EventLog,
+ private val globalSettings: GlobalSettings,
+ private val headsUpManager: HeadsUpManager,
+ private val keyguardNotificationVisibilityProvider: KeyguardNotificationVisibilityProvider,
+ keyguardStateController: KeyguardStateController,
+ private val logger: VisualInterruptionDecisionLogger,
+ @Main private val mainHandler: Handler,
+ private val powerManager: PowerManager,
+ private val statusBarStateController: StatusBarStateController,
+ private val systemClock: SystemClock,
+ private val uiEventLogger: UiEventLogger,
+ private val userTracker: UserTracker,
+ private val avalancheProvider: AvalancheProvider
+
) : VisualInterruptionDecisionProvider {
+
init {
check(!VisualInterruptionRefactor.isUnexpectedlyInLegacyMode())
}
@@ -166,6 +170,10 @@
addFilter(HunJustLaunchedFsiSuppressor())
addFilter(AlertKeyguardVisibilitySuppressor(keyguardNotificationVisibilityProvider))
+ if (NotificationAvalancheSuppression.isEnabled) {
+ addFilter(AvalancheSuppressor(avalancheProvider, systemClock))
+ avalancheProvider.register()
+ }
started = true
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotifRemoteViewsFactoryContainer.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotifRemoteViewsFactoryContainer.kt
index 99177c2..195fe78 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotifRemoteViewsFactoryContainer.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotifRemoteViewsFactoryContainer.kt
@@ -16,6 +16,7 @@
package com.android.systemui.statusbar.notification.row
+import android.widget.flags.Flags.notifLinearlayoutOptimized
import com.android.systemui.flags.FeatureFlags
import com.android.systemui.flags.Flags
import javax.inject.Inject
@@ -31,6 +32,7 @@
precomputedTextViewFactory: PrecomputedTextViewFactory,
bigPictureLayoutInflaterFactory: BigPictureLayoutInflaterFactory,
callLayoutSetDataAsyncFactory: CallLayoutSetDataAsyncFactory,
+ optimizedLinearLayoutFactory: NotificationOptimizedLinearLayoutFactory
) : NotifRemoteViewsFactoryContainer {
override val factories: Set<NotifRemoteViewsFactory> = buildSet {
add(precomputedTextViewFactory)
@@ -40,5 +42,8 @@
if (featureFlags.isEnabled(Flags.CALL_LAYOUT_ASYNC_SET_DATA)) {
add(callLayoutSetDataAsyncFactory)
}
+ if (notifLinearlayoutOptimized()) {
+ add(optimizedLinearLayoutFactory)
+ }
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationOptimizedLinearLayoutFactory.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationOptimizedLinearLayoutFactory.kt
new file mode 100644
index 0000000..f231fbc
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationOptimizedLinearLayoutFactory.kt
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2023 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.notification.row
+
+import android.content.Context
+import android.util.AttributeSet
+import android.view.View
+import android.widget.LinearLayout
+import com.android.internal.widget.NotificationOptimizedLinearLayout
+import javax.inject.Inject
+
+class NotificationOptimizedLinearLayoutFactory @Inject constructor() : NotifRemoteViewsFactory {
+ override fun instantiate(
+ row: ExpandableNotificationRow,
+ @NotificationRowContentBinder.InflationFlag layoutType: Int,
+ parent: View?,
+ name: String,
+ context: Context,
+ attrs: AttributeSet
+ ): View? {
+ return when (name) {
+ LinearLayout::class.java.name,
+ LinearLayout::class.java.simpleName -> NotificationOptimizedLinearLayout(context, attrs)
+ else -> null
+ }
+ }
+}
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 b9afb14..ebe7fb0 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
@@ -233,7 +233,7 @@
private final ArrayList<AnimationEvent> mAnimationEvents = new ArrayList<>();
private final ArrayList<View> mSwipedOutViews = new ArrayList<>();
private NotificationStackSizeCalculator mNotificationStackSizeCalculator;
- private final StackStateAnimator mStateAnimator = new StackStateAnimator(this);
+ private final StackStateAnimator mStateAnimator;
private boolean mAnimationsEnabled;
private boolean mChangePositionInProgress;
private boolean mChildTransferInProgress;
@@ -670,6 +670,7 @@
mExpandHelper.setScrollAdapter(mScrollAdapter);
mStackScrollAlgorithm = createStackScrollAlgorithm(context);
+ mStateAnimator = new StackStateAnimator(context, this);
mShouldDrawNotificationBackground =
res.getBoolean(R.bool.config_drawNotificationBackground);
setOutlineProvider(mOutlineProvider);
@@ -752,6 +753,7 @@
}
public void setIsRemoteInputActive(boolean isActive) {
+ FooterViewRefactor.assertInLegacyMode();
mIsRemoteInputActive = isActive;
updateFooter();
}
@@ -764,6 +766,7 @@
@VisibleForTesting
public void updateFooter() {
+ FooterViewRefactor.assertInLegacyMode();
if (mFooterView == null || mController == null) {
return;
}
@@ -776,10 +779,12 @@
}
private boolean shouldShowDismissView() {
+ FooterViewRefactor.assertInLegacyMode();
return mController.hasActiveClearableNotifications(ROWS_ALL);
}
private boolean shouldShowFooterView(boolean showDismissView) {
+ FooterViewRefactor.assertInLegacyMode();
return (showDismissView || mController.getVisibleNotificationCount() > 0)
&& mIsCurrentUserSetup // see: b/193149550
&& !onKeyguard()
@@ -1079,6 +1084,7 @@
}
mGapHeight = res.getDimensionPixelSize(R.dimen.notification_section_divider_height);
mStackScrollAlgorithm.initView(context);
+ mStateAnimator.initView(context);
mAmbientState.reload(context);
mPaddingBetweenElements = Math.max(1,
res.getDimensionPixelSize(R.dimen.notification_divider_height));
@@ -1445,12 +1451,12 @@
if (mAmbientState.isBouncerInTransit() && mQsExpansionFraction > 0f) {
fraction = BouncerPanelExpansionCalculator.aboutToShowBouncerProgress(fraction);
}
- final float stackY = MathUtils.lerp(0, endTopPosition, fraction);
// TODO(b/322228881): Clean up scene container vs legacy behavior in NSSL
if (SceneContainerFlag.isEnabled()) {
// stackY should be driven by scene container, not NSSL
mAmbientState.setStackY(mTopPadding);
} else {
+ final float stackY = MathUtils.lerp(0, endTopPosition, fraction);
mAmbientState.setStackY(stackY);
}
@@ -4359,6 +4365,12 @@
layoutEnd -= mShelf.getIntrinsicHeight() + mPaddingBetweenElements;
}
if (endPosition > layoutEnd) {
+ // if Scene Container is active, send bottom notification expansion delta
+ // to it so that it can scroll the stack and scrim accordingly.
+ if (SceneContainerFlag.isEnabled()) {
+ float diff = endPosition - layoutEnd;
+ mController.sendSyntheticScrollToSceneFramework(diff);
+ }
setOwnScrollY((int) (mOwnScrollY + endPosition - layoutEnd));
mDisallowScrollingInThisMotion = true;
}
@@ -4698,6 +4710,7 @@
* this will return false.
**/
public boolean isHistoryShown() {
+ FooterViewRefactor.assertInLegacyMode();
return mFooterView != null && mFooterView.isHistoryShown();
}
@@ -4710,10 +4723,10 @@
}
mFooterView = footerView;
addView(mFooterView, index);
- if (mManageButtonClickListener != null) {
- mFooterView.setManageButtonClickListener(mManageButtonClickListener);
- }
if (!FooterViewRefactor.isEnabled()) {
+ if (mManageButtonClickListener != null) {
+ mFooterView.setManageButtonClickListener(mManageButtonClickListener);
+ }
mFooterView.setClearAllButtonClickListener(v -> {
if (mFooterClearAllListener != null) {
mFooterClearAllListener.onClearAll();
@@ -4722,9 +4735,6 @@
footerView.setClearAllButtonVisible(false /* visible */, true /* animate */);
});
}
- if (FooterViewRefactor.isEnabled()) {
- updateFooter();
- }
}
public void setEmptyShadeView(EmptyShadeView emptyShadeView) {
@@ -4789,16 +4799,15 @@
}
public void updateFooterView(boolean visible, boolean showDismissView, boolean showHistory) {
+ FooterViewRefactor.assertInLegacyMode();
if (mFooterView == null || mNotificationStackSizeCalculator == null) {
return;
}
boolean animate = mIsExpanded && mAnimationsEnabled;
mFooterView.setVisible(visible, animate);
mFooterView.showHistory(showHistory);
- if (!FooterViewRefactor.isEnabled()) {
- mFooterView.setClearAllButtonVisible(showDismissView, animate);
- mFooterView.setFooterLabelVisible(mHasFilteredOutSeenNotifications);
- }
+ mFooterView.setClearAllButtonVisible(showDismissView, animate);
+ mFooterView.setFooterLabelVisible(mHasFilteredOutSeenNotifications);
}
@VisibleForTesting
@@ -5069,7 +5078,7 @@
if (mOwnScrollY > 0) {
setOwnScrollY((int) MathUtils.lerp(mOwnScrollY, 0, mQsExpansionFraction));
}
- if (footerAffected) {
+ if (!FooterViewRefactor.isEnabled() && footerAffected) {
updateFooter();
}
}
@@ -5080,6 +5089,10 @@
}
private void setOwnScrollY(int ownScrollY, boolean animateStackYChangeListener) {
+ // If scene container is active, NSSL should not control its own scrolling.
+ if (SceneContainerFlag.isEnabled()) {
+ return;
+ }
// Avoid Flicking during clear all
// when the shade finishes closing, onExpansionStopped will call
// resetScrollPosition to setOwnScrollY to 0
@@ -5175,6 +5188,7 @@
}
void setUpcomingStatusBarState(int upcomingStatusBarState) {
+ FooterViewRefactor.assertInLegacyMode();
mUpcomingStatusBarState = upcomingStatusBarState;
if (mUpcomingStatusBarState != mStatusBarState) {
updateFooter();
@@ -5192,7 +5206,9 @@
setDimmed(onKeyguard, fromShadeLocked);
setExpandingEnabled(!onKeyguard);
- updateFooter();
+ if (!FooterViewRefactor.isEnabled()) {
+ updateFooter();
+ }
requestChildrenUpdate();
onUpdateRowStates();
updateVisibility();
@@ -5269,8 +5285,11 @@
for (int i = 0; i < childCount; i++) {
ExpandableView child = getChildAtIndex(i);
child.dump(pw, args);
- if (child instanceof FooterView) {
- DumpUtilsKt.withIncreasedIndent(pw, () -> dumpFooterViewVisibility(pw));
+ if (!FooterViewRefactor.isEnabled()) {
+ if (child instanceof FooterView) {
+ DumpUtilsKt.withIncreasedIndent(pw,
+ () -> dumpFooterViewVisibility(pw));
+ }
}
pw.println();
}
@@ -5289,6 +5308,7 @@
}
private void dumpFooterViewVisibility(IndentingPrintWriter pw) {
+ FooterViewRefactor.assertInLegacyMode();
final boolean showDismissView = shouldShowDismissView();
pw.println("showFooterView: " + shouldShowFooterView(showDismissView));
@@ -5490,6 +5510,7 @@
* Register a {@link View.OnClickListener} to be invoked when the Manage button is clicked.
*/
public void setManageButtonClickListener(@Nullable OnClickListener listener) {
+ FooterViewRefactor.assertInLegacyMode();
mManageButtonClickListener = listener;
if (mFooterView != null) {
mFooterView.setManageButtonClickListener(mManageButtonClickListener);
@@ -5986,6 +6007,7 @@
* Sets whether the current user is set up, which is required to show the footer (b/193149550)
*/
public void setCurrentUserSetup(boolean isCurrentUserSetup) {
+ FooterViewRefactor.assertInLegacyMode();
if (mIsCurrentUserSetup != isCurrentUserSetup) {
mIsCurrentUserSetup = isCurrentUserSetup;
updateFooter();
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 ed26677..5fa0624 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
@@ -21,8 +21,9 @@
import static com.android.app.animation.Interpolators.STANDARD;
import static com.android.internal.jank.InteractionJankMonitor.CUJ_NOTIFICATION_SHADE_SCROLL_FLING;
+import static com.android.server.notification.Flags.screenshareNotificationHiding;
import static com.android.systemui.Dependency.ALLOW_NOTIFICATION_LONG_PRESS_NAME;
-import static com.android.systemui.Flags.screenshareNotificationHiding;
+import static com.android.systemui.Flags.nsslFalsingFix;
import static com.android.systemui.statusbar.StatusBarState.KEYGUARD;
import static com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout.OnEmptySpaceClickListener;
import static com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout.OnOverscrollTopChangedListener;
@@ -313,8 +314,10 @@
// The bottom might change because we're using the final actual height of the view
mView.setAnimateBottomOnLayout(true);
}
- // Let's update the footer once the notifications have been updated (in the next frame)
- mView.post(this::updateFooter);
+ if (!FooterViewRefactor.isEnabled()) {
+ // Let's update the footer once the notifications have been updated (in the next frame)
+ mView.post(this::updateFooter);
+ }
};
@VisibleForTesting
@@ -341,8 +344,8 @@
mView.reinflateViews();
if (!FooterViewRefactor.isEnabled()) {
updateShowEmptyShadeView();
+ updateFooter();
}
- updateFooter();
}
@Override
@@ -388,7 +391,9 @@
@Override
public void onUpcomingStateChanged(int newState) {
- mView.setUpcomingStatusBarState(newState);
+ if (!FooterViewRefactor.isEnabled()) {
+ mView.setUpcomingStatusBarState(newState);
+ }
}
@Override
@@ -406,7 +411,9 @@
public void onUserChanged(int userId) {
updateSensitivenessWithAnimation(false);
mHistoryEnabled = null;
- updateFooter();
+ if (!FooterViewRefactor.isEnabled()) {
+ updateFooter();
+ }
}
};
@@ -809,14 +816,14 @@
if (!FooterViewRefactor.isEnabled()) {
mView.setFooterClearAllListener(() ->
mMetricsLogger.action(MetricsEvent.ACTION_DISMISS_ALL_NOTES));
+ mView.setIsRemoteInputActive(mRemoteInputManager.isRemoteInputActive());
+ mRemoteInputManager.addControllerCallback(new RemoteInputController.Callback() {
+ @Override
+ public void onRemoteInputActive(boolean active) {
+ mView.setIsRemoteInputActive(active);
+ }
+ });
}
- mView.setIsRemoteInputActive(mRemoteInputManager.isRemoteInputActive());
- mRemoteInputManager.addControllerCallback(new RemoteInputController.Callback() {
- @Override
- public void onRemoteInputActive(boolean active) {
- mView.setIsRemoteInputActive(active);
- }
- });
mView.setClearAllFinishedWhilePanelExpandedRunnable(()-> {
final Runnable doCollapseRunnable = () ->
mShadeController.animateCollapseShade(CommandQueue.FLAG_EXCLUDE_NONE);
@@ -845,11 +852,13 @@
mView.setKeyguardBypassEnabled(mKeyguardBypassController.getBypassEnabled());
mKeyguardBypassController
.registerOnBypassStateChangedListener(mView::setKeyguardBypassEnabled);
- mView.setManageButtonClickListener(v -> {
- if (mNotificationActivityStarter != null) {
- mNotificationActivityStarter.startHistoryIntent(v, mView.isHistoryShown());
- }
- });
+ if (!FooterViewRefactor.isEnabled()) {
+ mView.setManageButtonClickListener(v -> {
+ if (mNotificationActivityStarter != null) {
+ mNotificationActivityStarter.startHistoryIntent(v, mView.isHistoryShown());
+ }
+ });
+ }
mHeadsUpManager.addListener(mOnHeadsUpChangedListener);
mHeadsUpManager.setAnimationStateHandler(mView::setHeadsUpGoingAwayAnimationsAllowed);
@@ -868,7 +877,9 @@
switch (key) {
case Settings.Secure.NOTIFICATION_HISTORY_ENABLED:
mHistoryEnabled = null; // invalidate
- updateFooter();
+ if (!FooterViewRefactor.isEnabled()) {
+ updateFooter();
+ }
break;
case HIGH_PRIORITY:
mView.setHighPriorityBeforeSpeedBump("1".equals(newValue));
@@ -890,9 +901,11 @@
return kotlin.Unit.INSTANCE;
});
- // attach callback, and then call it to update mView immediately
- mDeviceProvisionedController.addCallback(mDeviceProvisionedListener);
- mDeviceProvisionedListener.onDeviceProvisionedChanged();
+ if (!FooterViewRefactor.isEnabled()) {
+ // attach callback, and then call it to update mView immediately
+ mDeviceProvisionedController.addCallback(mDeviceProvisionedListener);
+ mDeviceProvisionedListener.onDeviceProvisionedChanged();
+ }
if (screenshareNotificationHiding()) {
mSensitiveNotificationProtectionController
@@ -1103,8 +1116,7 @@
}
public int getVisibleNotificationCount() {
- // TODO(b/293167744): FooterViewRefactor.assertInLegacyMode() once we handle footer
- // visibility in the refactored code
+ FooterViewRefactor.assertInLegacyMode();
return mNotifStats.getNumActiveNotifs();
}
@@ -1139,6 +1151,11 @@
}
}
+ /** Send internal notification expansion to the scene container framework. */
+ public void sendSyntheticScrollToSceneFramework(Float delta) {
+ mStackAppearanceInteractor.setSyntheticScroll(delta);
+ }
+
/** Get the y-coordinate of the top bound of the stack. */
public float getPlaceholderTop() {
return mStackAppearanceInteractor.getStackBounds().getValue().getTop();
@@ -1506,14 +1523,14 @@
* Return whether there are any clearable notifications
*/
public boolean hasActiveClearableNotifications(@SelectedRows int selection) {
- // TODO(b/293167744): FooterViewRefactor.assertInLegacyMode() once we handle the footer
- // visibility in the refactored code
+ // TODO(b/293167744): FooterViewRefactor.assertInLegacyMode() once we handle the silent
+ // section clear action in the new stack.
return hasNotifications(selection, true /* clearable */);
}
public boolean hasNotifications(@SelectedRows int selection, boolean isClearable) {
- // TODO(b/293167744): FooterViewRefactor.assertInLegacyMode() once we handle the footer
- // visibility in the refactored code
+ // TODO(b/293167744): FooterViewRefactor.assertInLegacyMode() once we handle the silent
+ // section clear action in the new stack.
boolean hasAlertingMatchingClearable = isClearable
? mNotifStats.getHasClearableAlertingNotifs()
: mNotifStats.getHasNonClearableAlertingNotifs();
@@ -1555,7 +1572,9 @@
boolean remoteInputActive) {
mHeadsUpManager.setRemoteInputActive(entry, remoteInputActive);
entry.notifyHeightChanged(true /* needsAnimation */);
- updateFooter();
+ if (!FooterViewRefactor.isEnabled()) {
+ updateFooter();
+ }
}
public void lockScrollTo(NotificationEntry entry) {
@@ -1570,6 +1589,7 @@
}
public void updateFooter() {
+ FooterViewRefactor.assertInLegacyMode();
Trace.beginSection("NSSLC.updateFooter");
mView.updateFooter();
Trace.endSection();
@@ -2052,7 +2072,7 @@
}
boolean horizontalSwipeWantsIt = false;
boolean scrollerWantsIt = false;
- if (KeyguardShadeMigrationNssl.isEnabled()) {
+ if (nsslFalsingFix() || KeyguardShadeMigrationNssl.isEnabled()) {
// Reverse the order relative to the else statement. onScrollTouch will reset on an
// UP event, causing horizontalSwipeWantsIt to be set to true on vertical swipes.
if (mLongPressedView == null && !mView.isBeingDragged()
@@ -2131,19 +2151,16 @@
private class NotifStackControllerImpl implements NotifStackController {
@Override
public void setNotifStats(@NonNull NotifStats notifStats) {
- // TODO(b/293167744): FooterViewRefactor.assertInLegacyMode() once footer visibility
- // is handled in the refactored stack.
+ // TODO(b/293167744): FooterViewRefactor.assertInLegacyMode() once we handle the silent
+ // section clear action in the new stack.
mNotifStats = notifStats;
if (!FooterViewRefactor.isEnabled()) {
mView.setHasFilteredOutSeenNotifications(
mSeenNotificationsInteractor
.getHasFilteredOutSeenNotifications().getValue());
- }
- updateFooter();
-
- if (!FooterViewRefactor.isEnabled()) {
+ updateFooter();
updateShowEmptyShadeView();
updateImportantForAccessibility();
}
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 664a6b6..15fde0e 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
@@ -33,6 +33,7 @@
import com.android.systemui.statusbar.EmptyShadeView;
import com.android.systemui.statusbar.NotificationShelf;
import com.android.systemui.statusbar.notification.SourceType;
+import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor;
import com.android.systemui.statusbar.notification.footer.ui.view.FooterView;
import com.android.systemui.statusbar.notification.row.ActivatableNotificationView;
import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
@@ -137,7 +138,7 @@
}
private void updateAlphaState(StackScrollAlgorithmState algorithmState,
- AmbientState ambientState) {
+ AmbientState ambientState) {
for (ExpandableView view : algorithmState.visibleChildren) {
final ViewState viewState = view.getViewState();
final boolean isHunGoingToShade = ambientState.isShadeExpanded()
@@ -295,7 +296,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++) {
@@ -322,7 +323,7 @@
}
private void updateClipping(StackScrollAlgorithmState algorithmState,
- AmbientState ambientState) {
+ AmbientState ambientState) {
float drawStart = ambientState.isOnKeyguard() ? 0
: ambientState.getStackY() - ambientState.getScrollY();
float clipStart = 0;
@@ -454,7 +455,7 @@
}
private int updateNotGoneIndex(StackScrollAlgorithmState state, int notGoneIndex,
- ExpandableView v) {
+ ExpandableView v) {
ExpandableViewState viewState = v.getViewState();
viewState.notGoneIndex = notGoneIndex;
state.visibleChildren.add(v);
@@ -480,7 +481,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;
@@ -494,7 +495,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;
@@ -598,18 +599,31 @@
viewEnd, /* hunMax */ ambientState.getMaxHeadsUpTranslation()
);
if (view instanceof FooterView) {
- final boolean shadeClosed = !ambientState.isShadeExpanded();
- final boolean isShelfShowing = algorithmState.firstViewInShelf != null;
- if (shadeClosed) {
- viewState.hidden = true;
- } else {
+ if (FooterViewRefactor.isEnabled()) {
final float footerEnd = algorithmState.mCurrentExpandedYPosition
+ view.getIntrinsicHeight();
final boolean noSpaceForFooter = footerEnd > ambientState.getStackEndHeight();
+ // TODO(b/293167744): May be able to keep only noSpaceForFooter here if we add an
+ // emission when clearAllNotifications is called, and then use that in the footer
+ // visibility flow.
((FooterView.FooterViewState) viewState).hideContent =
- isShelfShowing || noSpaceForFooter
- || (ambientState.isClearAllInProgress()
+ noSpaceForFooter || (ambientState.isClearAllInProgress()
&& !hasNonClearableNotifs(algorithmState));
+
+ } else {
+ final boolean shadeClosed = !ambientState.isShadeExpanded();
+ final boolean isShelfShowing = algorithmState.firstViewInShelf != null;
+ if (shadeClosed) {
+ viewState.hidden = true;
+ } else {
+ final float footerEnd = algorithmState.mCurrentExpandedYPosition
+ + view.getIntrinsicHeight();
+ final boolean noSpaceForFooter = footerEnd > ambientState.getStackEndHeight();
+ ((FooterView.FooterViewState) viewState).hideContent =
+ isShelfShowing || noSpaceForFooter
+ || (ambientState.isClearAllInProgress()
+ && !hasNonClearableNotifs(algorithmState));
+ }
}
} else {
if (view instanceof EmptyShadeView) {
@@ -731,7 +745,7 @@
@VisibleForTesting
void updatePulsingStates(StackScrollAlgorithmState algorithmState,
- AmbientState ambientState) {
+ AmbientState ambientState) {
int childCount = algorithmState.visibleChildren.size();
ExpandableNotificationRow pulsingRow = null;
for (int i = 0; i < childCount; i++) {
@@ -761,7 +775,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
@@ -870,18 +884,18 @@
boolean shouldHunBeVisibleWhenScrolled(boolean mustStayOnScreen, boolean headsUpIsVisible,
boolean showingPulsing, boolean isOnKeyguard, boolean headsUpOnKeyguard) {
return mustStayOnScreen && !headsUpIsVisible
- && !showingPulsing
- && (!isOnKeyguard || headsUpOnKeyguard);
+ && !showingPulsing
+ && (!isOnKeyguard || headsUpOnKeyguard);
}
- /**
+ /**
* When shade is open and we are scrolled to the bottom of notifications,
* clamp incoming HUN in its collapsed form, right below qs offset.
* Transition pinned collapsed HUN to full height when scrolling back up.
*/
@VisibleForTesting
void clampHunToTop(float clampInset, float stackTranslation, float collapsedHeight,
- ExpandableViewState viewState) {
+ ExpandableViewState viewState) {
final float newTranslation = Math.max(clampInset + stackTranslation,
viewState.getYTranslation());
@@ -896,7 +910,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();
@@ -919,7 +933,7 @@
@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
@@ -948,7 +962,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;
@@ -976,13 +990,13 @@
* vertically top of screen. Top HUNs should have drop shadows
* @param childrenOnTop It is greater than 0 when there's an existing HUN that is elevated
* @return childrenOnTop The decimal part represents the fraction of the elevated HUN's height
- * that overlaps with QQS Panel. The integer part represents the count of
- * previous HUNs whose Z positions are greater than 0.
+ * that overlaps with QQS Panel. The integer part represents the count of
+ * 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();
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackStateAnimator.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackStateAnimator.java
index a3e0941..ab62ed6 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackStateAnimator.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackStateAnimator.java
@@ -23,6 +23,7 @@
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ValueAnimator;
+import android.content.Context;
import android.util.Property;
import android.view.View;
@@ -59,9 +60,6 @@
public static final int ANIMATION_DURATION_HEADS_UP_DISAPPEAR = 400;
public static final int ANIMATION_DURATION_FOLD_TO_AOD =
AnimatableClockView.ANIMATION_DURATION_FOLD_TO_AOD;
- public static final int ANIMATION_DURATION_PULSE_APPEAR =
- KeyguardSliceView.DEFAULT_ANIM_DURATION;
- public static final int ANIMATION_DURATION_BLOCKING_HELPER_FADE = 240;
public static final int ANIMATION_DURATION_PRIORITY_CHANGE = 500;
public static final int ANIMATION_DELAY_PER_ELEMENT_INTERRUPTING = 80;
public static final int ANIMATION_DELAY_PER_ELEMENT_MANUAL = 32;
@@ -69,10 +67,8 @@
public static final int DELAY_EFFECT_MAX_INDEX_DIFFERENCE = 2;
private static final int MAX_STAGGER_COUNT = 5;
- private final int mGoToFullShadeAppearingTranslation;
- private final int mPulsingAppearingTranslation;
- @VisibleForTesting
- float mHeadsUpAppearStartAboveScreen;
+ @VisibleForTesting int mGoToFullShadeAppearingTranslation;
+ @VisibleForTesting float mHeadsUpAppearStartAboveScreen;
private final ExpandableViewState mTmpState = new ExpandableViewState();
private final AnimationProperties mAnimationProperties;
public NotificationStackScrollLayout mHostLayout;
@@ -96,17 +92,9 @@
private NotificationShelf mShelf;
private StackStateLogger mLogger;
- public StackStateAnimator(NotificationStackScrollLayout hostLayout) {
+ public StackStateAnimator(Context context, NotificationStackScrollLayout hostLayout) {
mHostLayout = hostLayout;
- // TODO(b/317061579) reload on configuration changes
- mGoToFullShadeAppearingTranslation =
- hostLayout.getContext().getResources().getDimensionPixelSize(
- R.dimen.go_to_full_shade_appearing_translation);
- mPulsingAppearingTranslation =
- hostLayout.getContext().getResources().getDimensionPixelSize(
- R.dimen.pulsing_notification_appear_translation);
- mHeadsUpAppearStartAboveScreen = hostLayout.getContext().getResources()
- .getDimensionPixelSize(R.dimen.heads_up_appear_y_above_screen);
+ initView(context);
mAnimationProperties = new AnimationProperties() {
@Override
public AnimationFilter getAnimationFilter() {
@@ -125,6 +113,21 @@
};
}
+ /**
+ * Needs to be called on configuration changes, to update cached resource values.
+ */
+ public void initView(Context context) {
+ updateResources(context);
+ }
+
+ private void updateResources(Context context) {
+ mGoToFullShadeAppearingTranslation =
+ context.getResources().getDimensionPixelSize(
+ R.dimen.go_to_full_shade_appearing_translation);
+ mHeadsUpAppearStartAboveScreen = context.getResources()
+ .getDimensionPixelSize(R.dimen.heads_up_appear_y_above_screen);
+ }
+
protected void setLogger(StackStateLogger logger) {
mLogger = logger;
}
@@ -467,15 +470,8 @@
mHeadsUpAppearChildren.add(changingView);
mTmpState.copyFrom(changingView.getViewState());
- if (event.headsUpFromBottom) {
- // start from the bottom of the screen
- mTmpState.setYTranslation(
- mHeadsUpAppearHeightBottom + mHeadsUpAppearStartAboveScreen);
- } else {
- // start from the top of the screen
- mTmpState.setYTranslation(
- -mStackTopMargin - mHeadsUpAppearStartAboveScreen);
- }
+ // translate the HUN in from the top, or the bottom of the screen
+ mTmpState.setYTranslation(getHeadsUpYTranslationStart(event.headsUpFromBottom));
// set the height and the initial position
mTmpState.applyToView(changingView);
mAnimationProperties.setCustomInterpolator(View.TRANSLATION_Y,
@@ -519,12 +515,20 @@
|| event.animationType == ANIMATION_TYPE_HEADS_UP_DISAPPEAR_CLICK) {
mHeadsUpDisappearChildren.add(changingView);
Runnable endRunnable = null;
+ mTmpState.copyFrom(changingView.getViewState());
if (changingView.getParent() == null) {
// This notification was actually removed, so we need to add it
// transiently
mHostLayout.addTransientView(changingView, 0);
changingView.setTransientContainer(mHostLayout);
- mTmpState.initFrom(changingView);
+ if (NotificationsImprovedHunAnimation.isEnabled()) {
+ // StackScrollAlgorithm cannot find this view because it has been removed
+ // from the NSSL. To correctly translate the view to the top or bottom of
+ // the screen (where it animated from), we need to update its translation.
+ mTmpState.setYTranslation(
+ getHeadsUpYTranslationStart(event.headsUpFromBottom)
+ );
+ }
endRunnable = changingView::removeFromTransientContainer;
}
@@ -572,16 +576,19 @@
changingView.setInRemovalAnimation(true);
};
}
- if (NotificationsImprovedHunAnimation.isEnabled()) {
- mAnimationProperties.setCustomInterpolator(View.TRANSLATION_Y,
- Interpolators.FAST_OUT_SLOW_IN_REVERSE);
- }
long removeAnimationDelay = changingView.performRemoveAnimation(
ANIMATION_DURATION_HEADS_UP_DISAPPEAR,
0, 0.0f, true /* isHeadsUpAppear */,
startAnimation, postAnimation,
getGlobalAnimationFinishedListener());
mAnimationProperties.delay += removeAnimationDelay;
+ if (NotificationsImprovedHunAnimation.isEnabled()) {
+ mAnimationProperties.duration = ANIMATION_DURATION_HEADS_UP_DISAPPEAR;
+ mAnimationProperties.setCustomInterpolator(View.TRANSLATION_Y,
+ Interpolators.FAST_OUT_SLOW_IN_REVERSE);
+ mAnimationProperties.getAnimationFilter().animateY = true;
+ mTmpState.animateTo(changingView, mAnimationProperties);
+ }
} else if (endRunnable != null) {
endRunnable.run();
}
@@ -592,6 +599,15 @@
return needsCustomAnimation;
}
+ private float getHeadsUpYTranslationStart(boolean headsUpFromBottom) {
+ if (headsUpFromBottom) {
+ // start from the bottom of the screen
+ return mHeadsUpAppearHeightBottom + mHeadsUpAppearStartAboveScreen;
+ }
+ // start from the top of the screen
+ return -mStackTopMargin - mHeadsUpAppearStartAboveScreen;
+ }
+
public void animateOverScrollToAmount(float targetAmount, final boolean onTop,
final boolean isRubberbanded) {
final float startOverScrollAmount = mHostLayout.getCurrentOverScrollAmount(onTop);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/data/repository/NotificationStackAppearanceRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/data/repository/NotificationStackAppearanceRepository.kt
index 0197264..9efe632 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/data/repository/NotificationStackAppearanceRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/data/repository/NotificationStackAppearanceRepository.kt
@@ -28,9 +28,6 @@
/** The bounds of the notification stack in the current scene. */
val stackBounds = MutableStateFlow(NotificationContainerBounds())
- /** The corner radius of the notification stack, in dp. */
- val cornerRadiusDp = MutableStateFlow(32f)
-
/**
* The height in px of the contents of notification stack. Depending on the number of
* notifications, this can exceed the space available on screen to show notifications, at which
@@ -50,4 +47,11 @@
* further.
*/
val scrolledToTop = MutableStateFlow(true)
+
+ /**
+ * The amount in px that the notification stack should scroll due to internal expansion. This
+ * should only happen when a notification expansion hits the bottom of the screen, so it is
+ * necessary to scroll up to keep expanding the notification.
+ */
+ val syntheticScroll = MutableStateFlow(0f)
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractor.kt
index 8307397..08df473 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractor.kt
@@ -21,6 +21,7 @@
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.statusbar.notification.stack.data.repository.NotificationStackAppearanceRepository
import javax.inject.Inject
+import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
@@ -34,9 +35,6 @@
/** The bounds of the notification stack in the current scene. */
val stackBounds: StateFlow<NotificationContainerBounds> = repository.stackBounds.asStateFlow()
- /** The corner radius of the notification stack, in dp. */
- val cornerRadiusDp: StateFlow<Float> = repository.cornerRadiusDp.asStateFlow()
-
/**
* The height in px of the contents of notification stack. Depending on the number of
* notifications, this can exceed the space available on screen to show notifications, at which
@@ -53,6 +51,13 @@
*/
val scrolledToTop: StateFlow<Boolean> = repository.scrolledToTop.asStateFlow()
+ /**
+ * The amount in px that the notification stack should scroll due to internal expansion. This
+ * should only happen when a notification expansion hits the bottom of the screen, so it is
+ * necessary to scroll up to keep expanding the notification.
+ */
+ val syntheticScroll: Flow<Float> = repository.syntheticScroll.asStateFlow()
+
/** Sets the position of the notification stack in the current scene. */
fun setStackBounds(bounds: NotificationContainerBounds) {
check(bounds.top <= bounds.bottom) { "Invalid bounds: $bounds" }
@@ -73,4 +78,9 @@
fun setScrolledToTop(scrolledToTop: Boolean) {
repository.scrolledToTop.value = scrolledToTop
}
+
+ /** Sets the amount (px) that the notification stack should scroll due to internal expansion. */
+ fun setSyntheticScroll(delta: Float) {
+ repository.syntheticScroll.value = delta
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt
index 44a7e7e..883aa9b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt
@@ -18,20 +18,21 @@
import android.view.LayoutInflater
import androidx.lifecycle.lifecycleScope
-import com.android.app.tracing.traceSection
+import com.android.app.tracing.TraceUtils.traceAsync
import com.android.internal.logging.MetricsLogger
import com.android.internal.logging.nano.MetricsProto
import com.android.systemui.common.ui.ConfigurationState
-import com.android.systemui.common.ui.reinflateAndBindLatest
import com.android.systemui.common.ui.view.setImportantForAccessibilityYesNo
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.lifecycle.repeatWhenAttached
import com.android.systemui.plugins.FalsingManager
import com.android.systemui.res.R
import com.android.systemui.statusbar.NotificationShelf
+import com.android.systemui.statusbar.notification.NotificationActivityStarter
import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor
import com.android.systemui.statusbar.notification.footer.ui.view.FooterView
import com.android.systemui.statusbar.notification.footer.ui.viewbinder.FooterViewBinder
+import com.android.systemui.statusbar.notification.footer.ui.viewmodel.FooterViewModel
import com.android.systemui.statusbar.notification.icon.ui.viewbinder.NotificationIconContainerShelfViewBinder
import com.android.systemui.statusbar.notification.shared.NotificationsLiveDataStoreRefactor
import com.android.systemui.statusbar.notification.shelf.ui.viewbinder.NotificationShelfViewBinder
@@ -42,11 +43,18 @@
import com.android.systemui.statusbar.notification.stack.ui.viewbinder.HideNotificationsBinder.bindHideList
import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationListViewModel
import com.android.systemui.statusbar.phone.NotificationIconAreaController
+import com.android.systemui.util.kotlin.awaitCancellationThenDispose
import com.android.systemui.util.kotlin.getOrNull
+import com.android.systemui.util.ui.isAnimating
+import com.android.systemui.util.ui.value
import java.util.Optional
import javax.inject.Inject
+import javax.inject.Provider
import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.launch
/** Binds a [NotificationStackScrollLayout] to its [view model][NotificationListViewModel]. */
@@ -58,9 +66,11 @@
private val configuration: ConfigurationState,
private val falsingManager: FalsingManager,
private val iconAreaController: NotificationIconAreaController,
+ private val loggerOptional: Optional<NotificationStatsLogger>,
private val metricsLogger: MetricsLogger,
private val nicBinder: NotificationIconContainerShelfViewBinder,
- private val loggerOptional: Optional<NotificationStatsLogger>,
+ // Using a provider to avoid a circular dependency.
+ private val notificationActivityStarter: Provider<NotificationActivityStarter>,
private val viewModel: NotificationListViewModel,
) {
@@ -79,7 +89,7 @@
bindHideList(viewController, viewModel, hiderTracker)
if (FooterViewRefactor.isEnabled) {
- launch { bindFooter(view) }
+ launch { reinflateAndBindFooter(view) }
launch { bindEmptyShade(view) }
launch {
viewModel.isImportantForAccessibility.collect { isImportantForAccessibility
@@ -104,32 +114,61 @@
)
}
- private suspend fun bindFooter(parentView: NotificationStackScrollLayout) {
+ private suspend fun reinflateAndBindFooter(parentView: NotificationStackScrollLayout) {
viewModel.footer.getOrNull()?.let { footerViewModel ->
// The footer needs to be re-inflated every time the theme or the font size changes.
- configuration.reinflateAndBindLatest(
- R.layout.status_bar_notification_footer,
- parentView,
- attachToRoot = false,
- backgroundDispatcher,
- ) { footerView: FooterView ->
- traceSection("bind FooterView") {
- val disposableHandle =
- FooterViewBinder.bind(
- footerView,
- footerViewModel,
- clearAllNotifications = {
- metricsLogger.action(
- MetricsProto.MetricsEvent.ACTION_DISMISS_ALL_NOTES
- )
- parentView.clearAllNotifications()
- },
- )
- parentView.setFooterView(footerView)
- return@reinflateAndBindLatest disposableHandle
+ configuration
+ .inflateLayout<FooterView>(
+ R.layout.status_bar_notification_footer,
+ parentView,
+ attachToRoot = false,
+ )
+ .flowOn(backgroundDispatcher)
+ .collectLatest { footerView: FooterView ->
+ traceAsync("bind FooterView") {
+ parentView.setFooterView(footerView)
+ bindFooter(footerView, footerViewModel, parentView)
+ }
}
+ }
+ }
+
+ /**
+ * Binds the footer (including its visibility) and dispose of the [DisposableHandle] when done.
+ */
+ private suspend fun bindFooter(
+ footerView: FooterView,
+ footerViewModel: FooterViewModel,
+ parentView: NotificationStackScrollLayout
+ ): Unit = coroutineScope {
+ val disposableHandle =
+ FooterViewBinder.bindWhileAttached(
+ footerView,
+ footerViewModel,
+ clearAllNotifications = {
+ metricsLogger.action(MetricsProto.MetricsEvent.ACTION_DISMISS_ALL_NOTES)
+ parentView.clearAllNotifications()
+ },
+ launchNotificationSettings = { view ->
+ notificationActivityStarter
+ .get()
+ .startHistoryIntent(view, /* showHistory = */ false)
+ },
+ launchNotificationHistory = { view ->
+ notificationActivityStarter
+ .get()
+ .startHistoryIntent(view, /* showHistory = */ true)
+ },
+ )
+ launch {
+ viewModel.shouldShowFooterView.collect { animatedVisibility ->
+ footerView.setVisible(
+ /* visible = */ animatedVisibility.value,
+ /* animate = */ animatedVisibility.isAnimating,
+ )
}
}
+ disposableHandle.awaitCancellationThenDispose()
}
private suspend fun bindEmptyShade(parentView: NotificationStackScrollLayout) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationStackAppearanceViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationStackAppearanceViewBinder.kt
index 6c2cbbe..a157785 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationStackAppearanceViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationStackAppearanceViewBinder.kt
@@ -26,10 +26,12 @@
import com.android.systemui.statusbar.notification.stack.ui.view.SharedNotificationContainer
import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationStackAppearanceViewModel
import kotlin.math.roundToInt
+import kotlinx.coroutines.DisposableHandle
import kotlinx.coroutines.launch
/** Binds the shared notification container to its view-model. */
object NotificationStackAppearanceViewBinder {
+ const val SCRIM_CORNER_RADIUS = 32f
@JvmStatic
fun bind(
@@ -38,8 +40,8 @@
viewModel: NotificationStackAppearanceViewModel,
ambientState: AmbientState,
controller: NotificationStackScrollLayoutController,
- ) {
- view.repeatWhenAttached {
+ ): DisposableHandle {
+ return view.repeatWhenAttached {
repeatOnLifecycle(Lifecycle.State.CREATED) {
launch {
viewModel.stackBounds.collect { bounds ->
@@ -48,8 +50,8 @@
bounds.top.roundToInt(),
bounds.right.roundToInt(),
bounds.bottom.roundToInt(),
- viewModel.cornerRadiusDp.value.dpToPx(context),
- viewModel.cornerRadiusDp.value.dpToPx(context),
+ SCRIM_CORNER_RADIUS.dpToPx(context),
+ 0,
)
}
}
@@ -67,6 +69,9 @@
controller.setMaxAlphaForExpansion(
((expandFraction - 0.5f) / 0.5f).coerceAtLeast(0f)
)
+ if (expandFraction == 0f || expandFraction == 1f) {
+ controller.onExpansionStopped()
+ }
}
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt
index fe5bdd4..f3d0d2c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt
@@ -136,8 +136,12 @@
.collect { y -> controller.setTranslationY(y) }
}
- launch {
- viewModel.expansionAlpha.collect { controller.setMaxAlphaForExpansion(it) }
+ if (!sceneContainerFlags.isEnabled()) {
+ launch {
+ viewModel.expansionAlpha.collect {
+ controller.setMaxAlphaForExpansion(it)
+ }
+ }
}
launch {
viewModel.glanceableHubAlpha.collect {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModel.kt
index 86c0a678..a6c6586 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModel.kt
@@ -16,21 +16,32 @@
package com.android.systemui.statusbar.notification.stack.ui.viewmodel
+import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.keyguard.shared.model.StatusBarState
+import com.android.systemui.power.domain.interactor.PowerInteractor
import com.android.systemui.shade.domain.interactor.ShadeInteractor
+import com.android.systemui.statusbar.domain.interactor.RemoteInputInteractor
import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor
import com.android.systemui.statusbar.notification.domain.interactor.SeenNotificationsInteractor
import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor
import com.android.systemui.statusbar.notification.footer.ui.viewmodel.FooterViewModel
import com.android.systemui.statusbar.notification.shelf.ui.viewmodel.NotificationShelfViewModel
+import com.android.systemui.statusbar.policy.domain.interactor.UserSetupInteractor
import com.android.systemui.statusbar.policy.domain.interactor.ZenModeInteractor
+import com.android.systemui.util.kotlin.combine
+import com.android.systemui.util.kotlin.sample
+import com.android.systemui.util.ui.AnimatableEvent
+import com.android.systemui.util.ui.AnimatedValue
+import com.android.systemui.util.ui.toAnimatedValueFlow
import java.util.Optional
import javax.inject.Inject
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onStart
/** ViewModel for the list of notifications. */
@@ -42,9 +53,13 @@
val footer: Optional<FooterViewModel>,
val logger: Optional<NotificationLoggerViewModel>,
activeNotificationsInteractor: ActiveNotificationsInteractor,
+ keyguardInteractor: KeyguardInteractor,
keyguardTransitionInteractor: KeyguardTransitionInteractor,
+ powerInteractor: PowerInteractor,
+ remoteInputInteractor: RemoteInputInteractor,
seenNotificationsInteractor: SeenNotificationsInteractor,
shadeInteractor: ShadeInteractor,
+ userSetupInteractor: UserSetupInteractor,
zenModeInteractor: ZenModeInteractor,
) {
/**
@@ -76,6 +91,10 @@
combine(
activeNotificationsInteractor.areAnyNotificationsPresent,
shadeInteractor.isQsFullscreen,
+ // TODO(b/293167744): It looks like we're essentially trying to check the same
+ // things for the empty shade visibility as we do for the footer, just in a
+ // slightly different way. We should change this so we also check
+ // statusBarState and isAwake instead of specific keyguard transitions.
keyguardTransitionInteractor.isInTransitionToState(KeyguardState.AOD).onStart {
emit(false)
},
@@ -97,6 +116,80 @@
}
}
+ val shouldShowFooterView: Flow<AnimatedValue<Boolean>> by lazy {
+ if (FooterViewRefactor.isUnexpectedlyInLegacyMode()) {
+ flowOf(AnimatedValue.NotAnimating(false))
+ } else {
+ combine(
+ activeNotificationsInteractor.areAnyNotificationsPresent,
+ userSetupInteractor.isUserSetUp,
+ keyguardInteractor.statusBarState.map { it == StatusBarState.KEYGUARD },
+ shadeInteractor.qsExpansion,
+ shadeInteractor.isQsFullscreen,
+ powerInteractor.isAsleep,
+ remoteInputInteractor.isRemoteInputActive,
+ shadeInteractor.shadeExpansion.map { it == 0f }
+ ) {
+ hasNotifications,
+ isUserSetUp,
+ isOnKeyguard,
+ qsExpansion,
+ qsFullScreen,
+ isAsleep,
+ isRemoteInputActive,
+ isShadeClosed ->
+ Pair(
+ // Should the footer be visible?
+ when {
+ !hasNotifications -> false
+ // Hide the footer until the user setup is complete, to prevent access
+ // to settings (b/193149550).
+ !isUserSetUp -> false
+ // Do not show the footer if the lockscreen is visible (incl. AOD),
+ // except if the shade is opened on top. See also b/219680200.
+ isOnKeyguard -> false
+ // Make sure we're not showing the footer in the transition to AOD while
+ // going to sleep (b/190227875). The StatusBarState is unfortunately not
+ // updated quickly enough when the power button is pressed, so this is
+ // necessary in addition to the isOnKeyguard check.
+ isAsleep -> false
+ // Do not show the footer if quick settings are fully expanded (except
+ // for the foldable split shade view). See b/201427195 && b/222699879.
+ qsExpansion == 1f && qsFullScreen -> false
+ // Hide the footer if remote input is active (i.e. user is replying to a
+ // notification). See b/75984847.
+ isRemoteInputActive -> false
+ // Never show the footer if the shade is collapsed (e.g. when HUNing).
+ isShadeClosed -> false
+ else -> true
+ },
+ // This could in theory be in the .sample below, but it tends to be
+ // inconsistent, so we're passing it on to make sure we have the same state.
+ isOnKeyguard
+ )
+ }
+ .distinctUntilChanged()
+ // Should we animate the visibility change?
+ .sample(
+ // TODO(b/322167853): This check is currently duplicated in FooterViewModel,
+ // but instead it should be a field in ShadeAnimationInteractor.
+ combine(
+ shadeInteractor.isShadeFullyExpanded,
+ shadeInteractor.isShadeTouchable,
+ ::Pair
+ )
+ .onStart { emit(Pair(false, false)) }
+ ) { (visible, isOnKeyguard), (isShadeFullyExpanded, animationsEnabled) ->
+ // Animate if the shade is interactive, but NOT on the lockscreen. Having
+ // animations enabled while on the lockscreen makes the footer appear briefly
+ // when transitioning between the shade and keyguard.
+ val shouldAnimate = isShadeFullyExpanded && animationsEnabled && !isOnKeyguard
+ AnimatableEvent(visible, shouldAnimate)
+ }
+ .toAnimatedValueFlow()
+ }
+ }
+
// TODO(b/308591475): This should be tracked separately by the empty shade.
val areNotificationsHiddenInShade: Flow<Boolean> by lazy {
if (FooterViewRefactor.isUnexpectedlyInLegacyMode()) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationStackAppearanceViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationStackAppearanceViewModel.kt
index 56ff7f9..3a0f03f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationStackAppearanceViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationStackAppearanceViewModel.kt
@@ -28,6 +28,7 @@
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.distinctUntilChanged
/** ViewModel which represents the state of the NSSL/Controller in the world of flexiglass */
@SysUISingleton
@@ -45,38 +46,36 @@
*/
val expandFraction: Flow<Float> =
combine(
- shadeInteractor.shadeExpansion,
- sceneInteractor.transitionState,
- ) { shadeExpansion, transitionState ->
- when (transitionState) {
- is ObservableTransitionState.Idle -> {
- if (transitionState.scene == SceneKey.Lockscreen) {
- 1f
- } else {
- shadeExpansion
+ shadeInteractor.shadeExpansion,
+ sceneInteractor.transitionState,
+ ) { shadeExpansion, transitionState ->
+ when (transitionState) {
+ is ObservableTransitionState.Idle -> {
+ if (transitionState.scene == SceneKey.Lockscreen) {
+ 1f
+ } else {
+ shadeExpansion
+ }
}
- }
- is ObservableTransitionState.Transition -> {
- if (
- (transitionState.fromScene == SceneKey.Shade &&
- transitionState.toScene == SceneKey.QuickSettings) ||
- (transitionState.fromScene == SceneKey.QuickSettings &&
- transitionState.toScene == SceneKey.Shade)
- ) {
- 1f
- } else {
- shadeExpansion
+ is ObservableTransitionState.Transition -> {
+ if (
+ (transitionState.fromScene == SceneKey.Shade &&
+ transitionState.toScene == SceneKey.QuickSettings) ||
+ (transitionState.fromScene == SceneKey.QuickSettings &&
+ transitionState.toScene == SceneKey.Shade)
+ ) {
+ 1f
+ } else {
+ shadeExpansion
+ }
}
}
}
- }
+ .distinctUntilChanged()
/** The bounds of the notification stack in the current scene. */
val stackBounds: Flow<NotificationContainerBounds> = stackAppearanceInteractor.stackBounds
- /** The corner radius of the notification stack, in dp. */
- val cornerRadiusDp: StateFlow<Float> = stackAppearanceInteractor.cornerRadiusDp
-
/** The y-coordinate in px of top of the contents of the notification stack. */
val contentTop: StateFlow<Float> = stackAppearanceInteractor.contentTop
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModel.kt
index a436f17..7ac5cd4 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModel.kt
@@ -20,13 +20,14 @@
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.flags.FeatureFlagsClassic
import com.android.systemui.flags.Flags
+import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
+import com.android.systemui.scene.shared.flag.SceneContainerFlag
import com.android.systemui.scene.shared.flag.SceneContainerFlags
import com.android.systemui.shade.domain.interactor.ShadeInteractor
import com.android.systemui.statusbar.notification.stack.domain.interactor.NotificationStackAppearanceInteractor
import com.android.systemui.statusbar.notification.stack.shared.flexiNotifsEnabled
import javax.inject.Inject
import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.StateFlow
/**
* ViewModel used by the Notification placeholders inside the scene container to update the
@@ -40,9 +41,11 @@
shadeInteractor: ShadeInteractor,
flags: SceneContainerFlags,
featureFlags: FeatureFlagsClassic,
+ private val keyguardInteractor: KeyguardInteractor,
) {
/** DEBUG: whether the placeholder "Notifications" text should be shown. */
- val isPlaceholderTextVisible: Boolean = !flags.flexiNotifsEnabled()
+ val isPlaceholderTextVisible: Boolean =
+ !flags.flexiNotifsEnabled() && SceneContainerFlag.isEnabled
/** DEBUG: whether the placeholder should be made slightly visible for positional debugging. */
val isVisualDebuggingEnabled: Boolean = featureFlags.isEnabled(Flags.NSSL_DEBUG_LINES)
@@ -64,12 +67,12 @@
right: Float,
bottom: Float,
) {
- interactor.setStackBounds(NotificationContainerBounds(left, top, right, bottom))
+ val notificationContainerBounds =
+ NotificationContainerBounds(top = top, bottom = bottom, left = left, right = right)
+ keyguardInteractor.setNotificationContainerBounds(notificationContainerBounds)
+ interactor.setStackBounds(notificationContainerBounds)
}
- /** The corner radius of the placeholder, in dp. */
- val cornerRadiusDp: StateFlow<Float> = interactor.cornerRadiusDp
-
/**
* The height in px of the contents of notification stack. Depending on the number of
* notifications, this can exceed the space available on screen to show notifications, at which
@@ -83,6 +86,13 @@
*/
val expandFraction: Flow<Float> = shadeInteractor.shadeExpansion
+ /**
+ * The amount in px that the notification stack should scroll due to internal expansion. This
+ * should only happen when a notification expansion hits the bottom of the screen, so it is
+ * necessary to scroll up to keep expanding the notification.
+ */
+ val syntheticScroll: Flow<Float> = interactor.syntheticScroll
+
/** Sets the y-coord in px of the top of the contents of the notification stack. */
fun onContentTopChanged(padding: Float) {
interactor.setContentTop(padding)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt
index 3915c376..811da51 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt
@@ -21,6 +21,7 @@
import com.android.systemui.common.shared.model.NotificationContainerBounds
import com.android.systemui.communal.domain.interactor.CommunalInteractor
+import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
@@ -31,20 +32,24 @@
import com.android.systemui.keyguard.shared.model.KeyguardState.DOZING
import com.android.systemui.keyguard.shared.model.KeyguardState.DREAMING
import com.android.systemui.keyguard.shared.model.KeyguardState.GLANCEABLE_HUB
+import com.android.systemui.keyguard.shared.model.KeyguardState.GONE
import com.android.systemui.keyguard.shared.model.KeyguardState.LOCKSCREEN
import com.android.systemui.keyguard.shared.model.KeyguardState.OCCLUDED
import com.android.systemui.keyguard.shared.model.KeyguardState.PRIMARY_BOUNCER
import com.android.systemui.keyguard.shared.model.StatusBarState.SHADE_LOCKED
import com.android.systemui.keyguard.shared.model.TransitionState.RUNNING
import com.android.systemui.keyguard.shared.model.TransitionState.STARTED
+import com.android.systemui.keyguard.ui.viewmodel.AlternateBouncerToGoneTransitionViewModel
import com.android.systemui.keyguard.ui.viewmodel.AodBurnInViewModel
import com.android.systemui.keyguard.ui.viewmodel.BurnInParameters
import com.android.systemui.keyguard.ui.viewmodel.DreamingToLockscreenTransitionViewModel
import com.android.systemui.keyguard.ui.viewmodel.GlanceableHubToLockscreenTransitionViewModel
import com.android.systemui.keyguard.ui.viewmodel.LockscreenToDreamingTransitionViewModel
import com.android.systemui.keyguard.ui.viewmodel.LockscreenToGlanceableHubTransitionViewModel
+import com.android.systemui.keyguard.ui.viewmodel.LockscreenToGoneTransitionViewModel
import com.android.systemui.keyguard.ui.viewmodel.LockscreenToOccludedTransitionViewModel
import com.android.systemui.keyguard.ui.viewmodel.OccludedToLockscreenTransitionViewModel
+import com.android.systemui.keyguard.ui.viewmodel.PrimaryBouncerToGoneTransitionViewModel
import com.android.systemui.shade.domain.interactor.ShadeInteractor
import com.android.systemui.statusbar.notification.stack.domain.interactor.SharedNotificationContainerInteractor
import com.android.systemui.util.kotlin.Utils.Companion.sample as sampleCombine
@@ -70,6 +75,7 @@
import kotlinx.coroutines.isActive
/** View-model for the shared notification container, used by both the shade and keyguard spaces */
+@SysUISingleton
class SharedNotificationContainerViewModel
@Inject
constructor(
@@ -80,6 +86,9 @@
private val shadeInteractor: ShadeInteractor,
communalInteractor: CommunalInteractor,
private val occludedToLockscreenTransitionViewModel: OccludedToLockscreenTransitionViewModel,
+ lockscreenToGoneTransitionViewModel: LockscreenToGoneTransitionViewModel,
+ alternateBouncerToGoneTransitionViewModel: AlternateBouncerToGoneTransitionViewModel,
+ primaryBouncerToGoneTransitionViewModel: PrimaryBouncerToGoneTransitionViewModel,
lockscreenToOccludedTransitionViewModel: LockscreenToOccludedTransitionViewModel,
dreamingToLockscreenTransitionViewModel: DreamingToLockscreenTransitionViewModel,
lockscreenToDreamingTransitionViewModel: LockscreenToDreamingTransitionViewModel,
@@ -94,6 +103,12 @@
mapOf<Edge?, Flow<Float>>(
Edge(from = LOCKSCREEN, to = DREAMING) to
lockscreenToDreamingTransitionViewModel.lockscreenAlpha,
+ Edge(from = LOCKSCREEN, to = GONE) to
+ lockscreenToGoneTransitionViewModel.lockscreenAlpha,
+ Edge(from = ALTERNATE_BOUNCER, to = GONE) to
+ alternateBouncerToGoneTransitionViewModel.lockscreenAlpha,
+ Edge(from = PRIMARY_BOUNCER, to = GONE) to
+ primaryBouncerToGoneTransitionViewModel.lockscreenAlpha,
Edge(from = DREAMING, to = LOCKSCREEN) to
dreamingToLockscreenTransitionViewModel.lockscreenAlpha,
Edge(from = LOCKSCREEN, to = OCCLUDED) to
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ActivityStarterImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ActivityStarterImpl.kt
index 8a56da3..80d45fb 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ActivityStarterImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ActivityStarterImpl.kt
@@ -30,8 +30,8 @@
import android.view.WindowManager
import com.android.keyguard.KeyguardUpdateMonitor
import com.android.systemui.ActivityIntentHelper
-import com.android.systemui.animation.ActivityLaunchAnimator
-import com.android.systemui.animation.ActivityLaunchAnimator.PendingIntentStarter
+import com.android.systemui.animation.ActivityTransitionAnimator
+import com.android.systemui.animation.ActivityTransitionAnimator.PendingIntentStarter
import com.android.systemui.animation.DelegateLaunchAnimatorController
import com.android.systemui.assist.AssistManager
import com.android.systemui.camera.CameraIntents.Companion.isInsecureCameraIntent
@@ -75,7 +75,7 @@
private val shadeAnimationInteractor: ShadeAnimationInteractor,
private val statusBarKeyguardViewManagerLazy: Lazy<StatusBarKeyguardViewManager>,
private val notifShadeWindowControllerLazy: Lazy<NotificationShadeWindowController>,
- private val activityLaunchAnimator: ActivityLaunchAnimator,
+ private val activityTransitionAnimator: ActivityTransitionAnimator,
private val context: Context,
@DisplayId private val displayId: Int,
private val lockScreenUserManager: NotificationLockscreenUserManager,
@@ -127,7 +127,7 @@
override fun startPendingIntentDismissingKeyguard(
intent: PendingIntent,
intentSentUiThreadCallback: Runnable?,
- animationController: ActivityLaunchAnimator.Controller?,
+ animationController: ActivityTransitionAnimator.Controller?,
) {
activityStarterInternal.startPendingIntentDismissingKeyguard(
intent = intent,
@@ -139,7 +139,7 @@
override fun startPendingIntentMaybeDismissingKeyguard(
intent: PendingIntent,
intentSentUiThreadCallback: Runnable?,
- animationController: ActivityLaunchAnimator.Controller?
+ animationController: ActivityTransitionAnimator.Controller?
) {
activityStarterInternal.startPendingIntentDismissingKeyguard(
intent = intent,
@@ -209,7 +209,7 @@
override fun startActivity(
intent: Intent,
dismissShade: Boolean,
- animationController: ActivityLaunchAnimator.Controller?,
+ animationController: ActivityTransitionAnimator.Controller?,
showOverLockscreenWhenLocked: Boolean,
) {
activityStarterInternal.startActivity(
@@ -222,7 +222,7 @@
override fun startActivity(
intent: Intent,
dismissShade: Boolean,
- animationController: ActivityLaunchAnimator.Controller?,
+ animationController: ActivityTransitionAnimator.Controller?,
showOverLockscreenWhenLocked: Boolean,
userHandle: UserHandle?,
) {
@@ -245,7 +245,7 @@
override fun postStartActivityDismissingKeyguard(
intent: PendingIntent,
- animationController: ActivityLaunchAnimator.Controller?
+ animationController: ActivityTransitionAnimator.Controller?
) {
postOnUiThread {
activityStarterInternal.startPendingIntentDismissingKeyguard(
@@ -268,7 +268,7 @@
override fun postStartActivityDismissingKeyguard(
intent: Intent,
delay: Int,
- animationController: ActivityLaunchAnimator.Controller?,
+ animationController: ActivityTransitionAnimator.Controller?,
) {
postOnUiThread(delay) {
activityStarterInternal.startActivityDismissingKeyguard(
@@ -283,7 +283,7 @@
override fun postStartActivityDismissingKeyguard(
intent: Intent,
delay: Int,
- animationController: ActivityLaunchAnimator.Controller?,
+ animationController: ActivityTransitionAnimator.Controller?,
customMessage: String?,
) {
postOnUiThread(delay) {
@@ -342,7 +342,7 @@
disallowEnterPictureInPictureWhileLaunching: Boolean,
callback: ActivityStarter.Callback?,
flags: Int,
- animationController: ActivityLaunchAnimator.Controller?,
+ animationController: ActivityTransitionAnimator.Controller?,
userHandle: UserHandle?,
) {
activityStarterInternal.startActivityDismissingKeyguard(
@@ -430,7 +430,7 @@
disallowEnterPictureInPictureWhileLaunching: Boolean = false,
callback: ActivityStarter.Callback? = null,
flags: Int = 0,
- animationController: ActivityLaunchAnimator.Controller? = null,
+ animationController: ActivityTransitionAnimator.Controller? = null,
userHandle: UserHandle? = null,
customMessage: String? = null,
) {
@@ -464,7 +464,7 @@
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
intent.addFlags(flags)
val result = intArrayOf(ActivityManager.START_CANCELED)
- activityLaunchAnimator.startIntentWithAnimation(
+ activityTransitionAnimator.startIntentWithAnimation(
animController,
animate,
intent.getPackage()
@@ -552,7 +552,7 @@
intent: PendingIntent,
intentSentUiThreadCallback: Runnable? = null,
associatedView: View? = null,
- animationController: ActivityLaunchAnimator.Controller? = null,
+ animationController: ActivityTransitionAnimator.Controller? = null,
showOverLockscreen: Boolean = false,
) {
val animationController =
@@ -602,7 +602,7 @@
val collapse = !animate
val runnable = Runnable {
try {
- activityLaunchAnimator.startPendingIntentWithAnimation(
+ activityTransitionAnimator.startPendingIntentWithAnimation(
controller,
animate,
intent.creatorPackage,
@@ -670,7 +670,7 @@
fun startActivity(
intent: Intent,
dismissShade: Boolean = false,
- animationController: ActivityLaunchAnimator.Controller? = null,
+ animationController: ActivityTransitionAnimator.Controller? = null,
showOverLockscreenWhenLocked: Boolean = false,
userHandle: UserHandle? = null,
) {
@@ -698,7 +698,7 @@
showOverLockscreenWhenLocked
) == true
- var controller: ActivityLaunchAnimator.Controller? = null
+ var controller: ActivityTransitionAnimator.Controller? = null
if (animate) {
// Wrap the animation controller to dismiss the shade and set
// mIsLaunchingActivityOverLockscreen during the animation.
@@ -721,7 +721,7 @@
centralSurfaces?.awakenDreams()
}
- activityLaunchAnimator.startIntentWithAnimation(
+ activityTransitionAnimator.startIntentWithAnimation(
controller,
animate,
intent.getPackage(),
@@ -815,7 +815,7 @@
}
/**
- * Return a [ActivityLaunchAnimator.Controller] wrapping `animationController` so that:
+ * Return a [ActivityTransitionAnimator.Controller] wrapping `animationController` so that:
* - if it launches in the notification shade window and `dismissShade` is true, then the
* shade will be instantly dismissed at the end of the animation.
* - if it launches in status bar window, it will make the status bar window match the
@@ -830,15 +830,15 @@
* @param isLaunchForActivity whether the launch is for an activity.
*/
private fun wrapAnimationControllerForShadeOrStatusBar(
- animationController: ActivityLaunchAnimator.Controller?,
+ animationController: ActivityTransitionAnimator.Controller?,
dismissShade: Boolean,
isLaunchForActivity: Boolean,
- ): ActivityLaunchAnimator.Controller? {
+ ): ActivityTransitionAnimator.Controller? {
if (animationController == null) {
return null
}
- val rootView = animationController.launchContainer.rootView
- val controllerFromStatusBar: Optional<ActivityLaunchAnimator.Controller> =
+ val rootView = animationController.transitionContainer.rootView
+ val controllerFromStatusBar: Optional<ActivityTransitionAnimator.Controller> =
statusBarWindowController.wrapAnimationControllerIfInStatusBar(
rootView,
animationController
@@ -870,8 +870,8 @@
* lockscreen, the correct flags are set for it to be occluded.
*/
private fun wrapAnimationControllerForLockscreen(
- animationController: ActivityLaunchAnimator.Controller?
- ): ActivityLaunchAnimator.Controller? {
+ animationController: ActivityTransitionAnimator.Controller?
+ ): ActivityTransitionAnimator.Controller? {
return animationController?.let {
object : DelegateLaunchAnimatorController(it) {
override fun onIntentStarted(willAnimate: Boolean) {
@@ -881,8 +881,8 @@
}
}
- override fun onLaunchAnimationStart(isExpandingFullyAbove: Boolean) {
- super.onLaunchAnimationStart(isExpandingFullyAbove)
+ override fun onTransitionAnimationStart(isExpandingFullyAbove: Boolean) {
+ super.onTransitionAnimationStart(isExpandingFullyAbove)
// Double check that the keyguard is still showing and not going
// away, but if so set the keyguard occluded. Typically, WM will let
@@ -902,17 +902,19 @@
}
}
- override fun onLaunchAnimationEnd(isExpandingFullyAbove: Boolean) {
+ override fun onTransitionAnimationEnd(isExpandingFullyAbove: Boolean) {
// Set mIsLaunchingActivityOverLockscreen to false before actually
// finishing the animation so that we can assume that
// mIsLaunchingActivityOverLockscreen being true means that we will
// collapse the shade (or at least run the post collapse runnables)
// later on.
centralSurfaces?.setIsLaunchingActivityOverLockscreen(false)
- delegate.onLaunchAnimationEnd(isExpandingFullyAbove)
+ delegate.onTransitionAnimationEnd(isExpandingFullyAbove)
}
- override fun onLaunchAnimationCancelled(newKeyguardOccludedState: Boolean?) {
+ override fun onTransitionAnimationCancelled(
+ newKeyguardOccludedState: Boolean?
+ ) {
if (newKeyguardOccludedState != null) {
keyguardViewMediatorLazy
.get()
@@ -925,7 +927,7 @@
// collapse the shade (or at least run the // post collapse
// runnables) later on.
centralSurfaces?.setIsLaunchingActivityOverLockscreen(false)
- delegate.onLaunchAnimationCancelled(newKeyguardOccludedState)
+ delegate.onTransitionAnimationCancelled(newKeyguardOccludedState)
}
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java
index 4019436..9052409 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java
@@ -38,7 +38,7 @@
import com.android.internal.annotations.VisibleForTesting;
import com.android.keyguard.AuthKeyguardMessageArea;
import com.android.systemui.Dumpable;
-import com.android.systemui.animation.ActivityLaunchAnimator;
+import com.android.systemui.animation.ActivityTransitionAnimator;
import com.android.systemui.display.data.repository.DisplayMetricsRepository;
import com.android.systemui.navigationbar.NavigationBarView;
import com.android.systemui.plugins.ActivityStarter.OnDismissAction;
@@ -334,6 +334,6 @@
/**
* Gets an animation controller from a notification row.
*/
- ActivityLaunchAnimator.Controller getAnimatorControllerFromNotification(
+ ActivityTransitionAnimator.Controller getAnimatorControllerFromNotification(
ExpandableNotificationRow associatedView);
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacks.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacks.java
index 39ca7b2..3669ba8 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacks.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacks.java
@@ -352,7 +352,7 @@
}
if (!mKeyguardStateController.isShowing()) {
- final Intent cameraIntent = CameraIntents.getInsecureCameraIntent(mContext);
+ final Intent cameraIntent = CameraIntents.getInsecureCameraIntent(mContext, mUserTracker.getUserId());
cameraIntent.putExtra(CameraIntents.EXTRA_LAUNCH_SOURCE, source);
mActivityStarter.startActivityDismissingKeyguard(cameraIntent,
false /* onlyProvisioned */, true /* dismissShade */,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesEmptyImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesEmptyImpl.kt
index 60dfaa7..8af7ee8 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesEmptyImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesEmptyImpl.kt
@@ -20,7 +20,7 @@
import android.view.MotionEvent
import androidx.lifecycle.LifecycleRegistry
import com.android.keyguard.AuthKeyguardMessageArea
-import com.android.systemui.animation.ActivityLaunchAnimator
+import com.android.systemui.animation.ActivityTransitionAnimator
import com.android.systemui.navigationbar.NavigationBarView
import com.android.systemui.plugins.ActivityStarter.OnDismissAction
import com.android.systemui.qs.QSPanelController
@@ -99,5 +99,5 @@
override fun setIsLaunchingActivityOverLockscreen(isLaunchingActivityOverLockscreen: Boolean) {}
override fun getAnimatorControllerFromNotification(
associatedView: ExpandableNotificationRow?,
- ): ActivityLaunchAnimator.Controller? = null
+ ): ActivityTransitionAnimator.Controller? = null
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
index 64fcef5..35aa3df 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
@@ -28,6 +28,7 @@
import static com.android.systemui.Dependency.TIME_TICK_HANDLER_NAME;
import static com.android.systemui.Flags.lightRevealMigration;
+import static com.android.systemui.Flags.newAodTransition;
import static com.android.systemui.Flags.predictiveBackSysui;
import static com.android.systemui.charging.WirelessChargingAnimation.UNKNOWN_BATTERY_LEVEL;
import static com.android.systemui.statusbar.NotificationLockscreenUserManager.PERMISSION_SELF;
@@ -114,7 +115,7 @@
import com.android.systemui.InitController;
import com.android.systemui.Prefs;
import com.android.systemui.accessibility.floatingmenu.AccessibilityFloatingMenuController;
-import com.android.systemui.animation.ActivityLaunchAnimator;
+import com.android.systemui.animation.ActivityTransitionAnimator;
import com.android.systemui.assist.AssistManager;
import com.android.systemui.back.domain.interactor.BackActionInteractor;
import com.android.systemui.biometrics.AuthRippleController;
@@ -575,7 +576,7 @@
private boolean mNoAnimationOnNextBarModeChange;
private final SysuiStatusBarStateController mStatusBarStateController;
- private final ActivityLaunchAnimator mActivityLaunchAnimator;
+ private final ActivityTransitionAnimator mActivityTransitionAnimator;
private final NotificationLaunchAnimatorControllerProvider mNotificationAnimationProvider;
private final Lazy<NotificationPresenter> mPresenterLazy;
private final Lazy<NotificationActivityStarter> mNotificationActivityStarterLazy;
@@ -693,7 +694,7 @@
@Main MessageRouter messageRouter,
WallpaperManager wallpaperManager,
Optional<StartingSurface> startingSurfaceOptional,
- ActivityLaunchAnimator activityLaunchAnimator,
+ ActivityTransitionAnimator activityTransitionAnimator,
DeviceStateManager deviceStateManager,
WiredChargingRippleController wiredChargingRippleController,
IDreamManager dreamManager,
@@ -815,7 +816,7 @@
shadeExpansionListener.onPanelExpansionChanged(currentState);
mActivityIntentHelper = new ActivityIntentHelper(mContext);
- mActivityLaunchAnimator = activityLaunchAnimator;
+ mActivityTransitionAnimator = activityTransitionAnimator;
// TODO(b/190746471): Find a better home for this.
DateTimeView.setReceiverHandler(timeTickHandler);
@@ -1423,8 +1424,8 @@
private void setUpPresenter() {
// Set up the initial notification state.
- mActivityLaunchAnimator.setCallback(mActivityLaunchAnimatorCallback);
- mActivityLaunchAnimator.addListener(mActivityLaunchAnimatorListener);
+ mActivityTransitionAnimator.setCallback(mActivityTransitionAnimatorCallback);
+ mActivityTransitionAnimator.addListener(mActivityTransitionAnimatorListener);
mRemoteInputManager.addControllerCallback(mNotificationShadeWindowController);
mStackScrollerController.setNotificationActivityStarter(
mNotificationActivityStarterLazy.get());
@@ -1803,10 +1804,10 @@
}
pw.println("Camera gesture intents:");
- pw.println(" Insecure camera: " + CameraIntents.getInsecureCameraIntent(mContext));
- pw.println(" Secure camera: " + CameraIntents.getSecureCameraIntent(mContext));
+ pw.println(" Insecure camera: " + CameraIntents.getInsecureCameraIntent(mContext, mUserTracker.getUserId()));
+ pw.println(" Secure camera: " + CameraIntents.getSecureCameraIntent(mContext, mUserTracker.getUserId()));
pw.println(" Override package: "
- + CameraIntents.getOverrideCameraPackage(mContext));
+ + CameraIntents.getOverrideCameraPackage(mContext, mUserTracker.getUserId()));
}
private void createAndAddWindows(@Nullable RegisterStatusBarResult result) {
@@ -2497,7 +2498,8 @@
mNotificationShadeWindowController.batchApplyWindowLayoutParams(()-> {
mDeviceInteractive = true;
- if (shouldAnimateDozeWakeup()) {
+ boolean isFlaggedOff = newAodTransition() && KeyguardShadeMigrationNssl.isEnabled();
+ if (!isFlaggedOff && shouldAnimateDozeWakeup()) {
// If this is false, the power button must be physically pressed in order to
// trigger fingerprint authentication.
final boolean touchToUnlockAnytime = Settings.Secure.getIntForUser(
@@ -3174,8 +3176,8 @@
}
};
- private final ActivityLaunchAnimator.Callback mActivityLaunchAnimatorCallback =
- new ActivityLaunchAnimator.Callback() {
+ private final ActivityTransitionAnimator.Callback mActivityTransitionAnimatorCallback =
+ new ActivityTransitionAnimator.Callback() {
@Override
public boolean isOnKeyguard() {
return mKeyguardStateController.isShowing();
@@ -3203,15 +3205,15 @@
}
};
- private final ActivityLaunchAnimator.Listener mActivityLaunchAnimatorListener =
- new ActivityLaunchAnimator.Listener() {
+ private final ActivityTransitionAnimator.Listener mActivityTransitionAnimatorListener =
+ new ActivityTransitionAnimator.Listener() {
@Override
- public void onLaunchAnimationStart() {
+ public void onTransitionAnimationStart() {
mKeyguardViewMediator.setBlursDisabledForAppLaunch(true);
}
@Override
- public void onLaunchAnimationEnd() {
+ public void onTransitionAnimationEnd() {
mKeyguardViewMediator.setBlursDisabledForAppLaunch(false);
}
};
@@ -3265,7 +3267,7 @@
}
@Override
- public ActivityLaunchAnimator.Controller getAnimatorControllerFromNotification(
+ public ActivityTransitionAnimator.Controller getAnimatorControllerFromNotification(
ExpandableNotificationRow associatedView) {
return mNotificationAnimationProvider.getAnimatorController(associatedView);
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconContainer.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconContainer.java
index be5c6b3..8e3d678 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconContainer.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconContainer.java
@@ -349,8 +349,12 @@
}
}
if (child instanceof StatusBarIconView) {
- ((StatusBarIconView) child).updateIconDimens();
- if (!NotificationIconContainerRefactor.isEnabled()) {
+ if (NotificationIconContainerRefactor.isEnabled()) {
+ if (!mChangingViewPositions) {
+ ((StatusBarIconView) child).updateIconDimens();
+ }
+ } else {
+ ((StatusBarIconView) child).updateIconDimens();
((StatusBarIconView) child).setDozing(mDozing, false, 0);
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarContentInsetsProvider.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarContentInsetsProvider.kt
index 877bd7c..e84b7a0 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarContentInsetsProvider.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarContentInsetsProvider.kt
@@ -44,6 +44,8 @@
import com.android.app.tracing.traceSection
import com.android.systemui.BottomMarginCommand
import com.android.systemui.StatusBarInsetsCommand
+import com.android.systemui.SysUICutoutInformation
+import com.android.systemui.SysUICutoutProvider
import com.android.systemui.statusbar.commandline.CommandRegistry
import java.io.PrintWriter
import java.lang.Math.max
@@ -69,6 +71,7 @@
val configurationController: ConfigurationController,
val dumpManager: DumpManager,
val commandRegistry: CommandRegistry,
+ val sysUICutoutProvider: SysUICutoutProvider,
) : CallbackController<StatusBarContentInsetsChangedListener>,
ConfigurationController.ConfigurationListener,
Dumpable {
@@ -176,7 +179,8 @@
*/
fun getStatusBarContentInsetsForRotation(@Rotation rotation: Int): Insets =
traceSection(tag = "StatusBarContentInsetsProvider.getStatusBarContentInsetsForRotation") {
- val displayCutout = checkNotNull(context.display).cutout
+ val sysUICutout = sysUICutoutProvider.cutoutInfoForCurrentDisplay()
+ val displayCutout = sysUICutout?.cutout
val key = getCacheKey(rotation, displayCutout)
val screenBounds = context.resources.configuration.windowConfiguration.maxBounds
@@ -187,7 +191,7 @@
val width = point.logicalWidth(rotation)
val area = insetsCache[key] ?: getAndSetCalculatedAreaForRotation(
- rotation, displayCutout, getResourcesForRotation(rotation, context), key)
+ rotation, sysUICutout, getResourcesForRotation(rotation, context), key)
Insets.of(area.left, area.top, /* right= */ width - area.right, /* bottom= */ 0)
}
@@ -212,10 +216,11 @@
fun getStatusBarContentAreaForRotation(
@Rotation rotation: Int
): Rect {
- val displayCutout = checkNotNull(context.display).cutout
+ val sysUICutout = sysUICutoutProvider.cutoutInfoForCurrentDisplay()
+ val displayCutout = sysUICutout?.cutout
val key = getCacheKey(rotation, displayCutout)
return insetsCache[key] ?: getAndSetCalculatedAreaForRotation(
- rotation, displayCutout, getResourcesForRotation(rotation, context), key)
+ rotation, sysUICutout, getResourcesForRotation(rotation, context), key)
}
/**
@@ -228,18 +233,18 @@
private fun getAndSetCalculatedAreaForRotation(
@Rotation targetRotation: Int,
- displayCutout: DisplayCutout?,
+ sysUICutout: SysUICutoutInformation?,
rotatedResources: Resources,
key: CacheKey
): Rect {
- return getCalculatedAreaForRotation(displayCutout, targetRotation, rotatedResources)
+ return getCalculatedAreaForRotation(sysUICutout, targetRotation, rotatedResources)
.also {
insetsCache.put(key, it)
}
}
private fun getCalculatedAreaForRotation(
- displayCutout: DisplayCutout?,
+ sysUICutout: SysUICutoutInformation?,
@Rotation targetRotation: Int,
rotatedResources: Resources
): Rect {
@@ -271,7 +276,7 @@
return calculateInsetsForRotationWithRotatedResources(
currentRotation,
targetRotation,
- displayCutout,
+ sysUICutout,
context.resources.configuration.windowConfiguration.maxBounds,
SystemBarUtils.getStatusBarHeightForRotation(context, targetRotation),
minLeft,
@@ -415,7 +420,7 @@
fun calculateInsetsForRotationWithRotatedResources(
@Rotation currentRotation: Int,
@Rotation targetRotation: Int,
- displayCutout: DisplayCutout?,
+ sysUICutout: SysUICutoutInformation?,
maxBounds: Rect,
statusBarHeight: Int,
minLeft: Int,
@@ -434,7 +439,7 @@
val rotZeroBounds = getRotationZeroDisplayBounds(maxBounds, currentRotation)
return getStatusBarContentBounds(
- displayCutout,
+ sysUICutout,
statusBarHeight,
rotZeroBounds.right,
rotZeroBounds.bottom,
@@ -470,7 +475,7 @@
* rotation
*/
private fun getStatusBarContentBounds(
- displayCutout: DisplayCutout?,
+ sysUICutout: SysUICutoutInformation?,
sbHeight: Int,
width: Int,
height: Int,
@@ -489,19 +494,17 @@
val logicalDisplayWidth = if (targetRotation.isHorizontal()) height else width
- val cutoutRects = displayCutout?.boundingRects
- if (cutoutRects == null || cutoutRects.isEmpty()) {
- return Rect(minLeft,
- insetTop,
- logicalDisplayWidth - minRight,
- sbHeight)
+ val cutoutRects = sysUICutout?.cutout?.boundingRects
+ if (cutoutRects.isNullOrEmpty()) {
+ return Rect(minLeft, insetTop, logicalDisplayWidth - minRight, sbHeight)
}
- val relativeRotation = if (currentRotation - targetRotation < 0) {
- currentRotation - targetRotation + 4
- } else {
- currentRotation - targetRotation
- }
+ val relativeRotation =
+ if (currentRotation - targetRotation < 0) {
+ currentRotation - targetRotation + 4
+ } else {
+ currentRotation - targetRotation
+ }
// Size of the status bar window for the given rotation relative to our exact rotation
val sbRect = sbRect(relativeRotation, sbHeight, Pair(cWidth, cHeight))
@@ -509,19 +512,26 @@
var leftMargin = minLeft
var rightMargin = minRight
for (cutoutRect in cutoutRects) {
+ val protectionRect = sysUICutout.cameraProtection?.cutoutBounds
+ val actualCutoutRect =
+ if (protectionRect?.intersects(cutoutRect) == true) {
+ rectUnion(cutoutRect, protectionRect)
+ } else {
+ cutoutRect
+ }
// There is at most one non-functional area per short edge of the device. So if the status
// bar doesn't share a short edge with the cutout, we can ignore its insets because there
// will be no letter-boxing to worry about
- if (!shareShortEdge(sbRect, cutoutRect, cWidth, cHeight)) {
+ if (!shareShortEdge(sbRect, actualCutoutRect, cWidth, cHeight)) {
continue
}
- if (cutoutRect.touchesLeftEdge(relativeRotation, cWidth, cHeight)) {
- var logicalWidth = cutoutRect.logicalWidth(relativeRotation)
+ if (actualCutoutRect.touchesLeftEdge(relativeRotation, cWidth, cHeight)) {
+ var logicalWidth = actualCutoutRect.logicalWidth(relativeRotation)
if (isRtl) logicalWidth += dotWidth
leftMargin = max(logicalWidth, leftMargin)
- } else if (cutoutRect.touchesRightEdge(relativeRotation, cWidth, cHeight)) {
- var logicalWidth = cutoutRect.logicalWidth(relativeRotation)
+ } else if (actualCutoutRect.touchesRightEdge(relativeRotation, cWidth, cHeight)) {
+ var logicalWidth = actualCutoutRect.logicalWidth(relativeRotation)
if (!isRtl) logicalWidth += dotWidth
rightMargin = max(rightMargin, logicalWidth)
}
@@ -532,6 +542,11 @@
return Rect(leftMargin, insetTop, logicalDisplayWidth - rightMargin, sbHeight)
}
+private fun rectUnion(first: Rect, second: Rect) = Rect(first).apply { union(second) }
+
+private fun Rect.intersects(other: Rect): Boolean =
+ intersects(other.left, other.top, other.right, other.bottom)
+
/*
* Returns the inset top of the status bar.
*
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
index 4c83ca2..3ad60d9 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
@@ -24,6 +24,7 @@
import static com.android.systemui.statusbar.phone.BiometricUnlockController.MODE_WAKE_AND_UNLOCK;
import static com.android.systemui.statusbar.phone.BiometricUnlockController.MODE_WAKE_AND_UNLOCK_PULSING;
import static com.android.systemui.util.kotlin.JavaAdapterKt.collectFlow;
+import static com.android.systemui.util.kotlin.JavaAdapterKt.combineFlows;
import android.content.Context;
import android.content.res.ColorStateList;
@@ -71,6 +72,7 @@
import com.android.systemui.flags.Flags;
import com.android.systemui.keyguard.KeyguardWmStateRefactor;
import com.android.systemui.keyguard.domain.interactor.KeyguardDismissActionInteractor;
+import com.android.systemui.keyguard.domain.interactor.KeyguardSurfaceBehindInteractor;
import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor;
import com.android.systemui.keyguard.domain.interactor.WindowManagerLockscreenVisibilityInteractor;
import com.android.systemui.keyguard.shared.model.DismissAction;
@@ -345,6 +347,7 @@
}
};
private Lazy<WindowManagerLockscreenVisibilityInteractor> mWmLockscreenVisibilityInteractor;
+ private Lazy<KeyguardSurfaceBehindInteractor> mSurfaceBehindInteractor;
private Lazy<KeyguardDismissActionInteractor> mKeyguardDismissActionInteractor;
@Inject
@@ -377,7 +380,8 @@
@Main CoroutineDispatcher mainDispatcher,
Lazy<WindowManagerLockscreenVisibilityInteractor> wmLockscreenVisibilityInteractor,
Lazy<KeyguardDismissActionInteractor> keyguardDismissActionInteractorLazy,
- SelectedUserInteractor selectedUserInteractor
+ SelectedUserInteractor selectedUserInteractor,
+ Lazy<KeyguardSurfaceBehindInteractor> surfaceBehindInteractor
) {
mContext = context;
mViewMediatorCallback = callback;
@@ -410,6 +414,7 @@
mWmLockscreenVisibilityInteractor = wmLockscreenVisibilityInteractor;
mKeyguardDismissActionInteractor = keyguardDismissActionInteractorLazy;
mSelectedUserInteractor = selectedUserInteractor;
+ mSurfaceBehindInteractor = surfaceBehindInteractor;
}
KeyguardTransitionInteractor mKeyguardTransitionInteractor;
@@ -480,7 +485,16 @@
mShadeViewController.postToView(() ->
collectFlow(
getViewRootImpl().getView(),
- mWmLockscreenVisibilityInteractor.get().getLockscreenVisibility(),
+ combineFlows(
+ mWmLockscreenVisibilityInteractor.get().getLockscreenVisibility(),
+ mSurfaceBehindInteractor.get().isAnimatingSurface(),
+ (lockscreenVis, animatingSurface) ->
+ // TODO(b/322546110): Waiting until we're not animating the
+ // surface is a workaround to avoid jank. We should actually
+ // fix the source of the jank, and then hide the keyguard
+ // view without waiting for the animation to end.
+ lockscreenVis || animatingSurface
+ ),
this::consumeShowStatusBarKeyguardView));
}
}
@@ -1428,10 +1442,6 @@
hideAlternateBouncer(false);
executeAfterKeyguardGoneAction();
}
-
- if (KeyguardWmStateRefactor.isEnabled()) {
- mKeyguardTransitionInteractor.startDismissKeyguardTransition();
- }
}
/** Display security message to relevant KeyguardMessageArea. */
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarLaunchAnimatorController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarLaunchAnimatorController.kt
index 8ca5bfc..7ff5f6e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarLaunchAnimatorController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarLaunchAnimatorController.kt
@@ -1,25 +1,25 @@
package com.android.systemui.statusbar.phone
import android.view.View
-import com.android.systemui.animation.ActivityLaunchAnimator
-import com.android.systemui.animation.LaunchAnimator
+import com.android.systemui.animation.ActivityTransitionAnimator
+import com.android.systemui.animation.TransitionAnimator
import com.android.systemui.shade.ShadeController
import com.android.systemui.shade.ShadeViewController
import com.android.systemui.shade.domain.interactor.ShadeAnimationInteractor
import com.android.systemui.statusbar.NotificationShadeWindowController
/**
- * A [ActivityLaunchAnimator.Controller] that takes care of collapsing the status bar at the right
+ * A [ActivityTransitionAnimator.Controller] that takes care of collapsing the status bar at the right
* time.
*/
class StatusBarLaunchAnimatorController(
- private val delegate: ActivityLaunchAnimator.Controller,
+ private val delegate: ActivityTransitionAnimator.Controller,
private val shadeViewController: ShadeViewController,
private val shadeAnimationInteractor: ShadeAnimationInteractor,
private val shadeController: ShadeController,
private val notificationShadeWindowController: NotificationShadeWindowController,
private val isLaunchForActivity: Boolean = true
-) : ActivityLaunchAnimator.Controller by delegate {
+) : ActivityTransitionAnimator.Controller by delegate {
// Always sync the opening window with the shade, given that we draw a hole punch in the shade
// of the same size and position as the opening app to make it visible.
override val openingWindowSyncView: View?
@@ -34,32 +34,32 @@
}
}
- override fun onLaunchAnimationStart(isExpandingFullyAbove: Boolean) {
- delegate.onLaunchAnimationStart(isExpandingFullyAbove)
+ override fun onTransitionAnimationStart(isExpandingFullyAbove: Boolean) {
+ delegate.onTransitionAnimationStart(isExpandingFullyAbove)
shadeAnimationInteractor.setIsLaunchingActivity(true)
if (!isExpandingFullyAbove) {
shadeViewController.collapseWithDuration(
- ActivityLaunchAnimator.TIMINGS.totalDuration.toInt())
+ ActivityTransitionAnimator.TIMINGS.totalDuration.toInt())
}
}
- override fun onLaunchAnimationEnd(isExpandingFullyAbove: Boolean) {
- delegate.onLaunchAnimationEnd(isExpandingFullyAbove)
+ override fun onTransitionAnimationEnd(isExpandingFullyAbove: Boolean) {
+ delegate.onTransitionAnimationEnd(isExpandingFullyAbove)
shadeAnimationInteractor.setIsLaunchingActivity(false)
shadeController.onLaunchAnimationEnd(isExpandingFullyAbove)
}
- override fun onLaunchAnimationProgress(
- state: LaunchAnimator.State,
+ override fun onTransitionAnimationProgress(
+ state: TransitionAnimator.State,
progress: Float,
linearProgress: Float
) {
- delegate.onLaunchAnimationProgress(state, progress, linearProgress)
+ delegate.onTransitionAnimationProgress(state, progress, linearProgress)
shadeViewController.applyLaunchAnimationProgress(linearProgress)
}
- override fun onLaunchAnimationCancelled(newKeyguardOccludedState: Boolean?) {
- delegate.onLaunchAnimationCancelled()
+ override fun onTransitionAnimationCancelled(newKeyguardOccludedState: Boolean?) {
+ delegate.onTransitionAnimationCancelled()
shadeAnimationInteractor.setIsLaunchingActivity(false)
shadeController.onLaunchAnimationCancelled(isLaunchForActivity)
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java
index 4ee061d..2737580 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java
@@ -52,7 +52,7 @@
import com.android.internal.widget.LockPatternUtils;
import com.android.systemui.ActivityIntentHelper;
import com.android.systemui.EventLogTags;
-import com.android.systemui.animation.ActivityLaunchAnimator;
+import com.android.systemui.animation.ActivityTransitionAnimator;
import com.android.systemui.assist.AssistManager;
import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.dagger.qualifiers.DisplayId;
@@ -125,7 +125,7 @@
private final NotificationPresenter mPresenter;
private final ShadeViewController mShadeViewController;
private final NotificationShadeWindowController mNotificationShadeWindowController;
- private final ActivityLaunchAnimator mActivityLaunchAnimator;
+ private final ActivityTransitionAnimator mActivityTransitionAnimator;
private final NotificationLaunchAnimatorControllerProvider mNotificationAnimationProvider;
private final PowerInteractor mPowerInteractor;
private final UserTracker mUserTracker;
@@ -161,7 +161,7 @@
NotificationPresenter presenter,
ShadeViewController shadeViewController,
NotificationShadeWindowController notificationShadeWindowController,
- ActivityLaunchAnimator activityLaunchAnimator,
+ ActivityTransitionAnimator activityTransitionAnimator,
ShadeAnimationInteractor shadeAnimationInteractor,
NotificationLaunchAnimatorControllerProvider notificationAnimationProvider,
LaunchFullScreenIntentProvider launchFullScreenIntentProvider,
@@ -194,7 +194,7 @@
mOnUserInteractionCallback = onUserInteractionCallback;
mPresenter = presenter;
mShadeViewController = shadeViewController;
- mActivityLaunchAnimator = activityLaunchAnimator;
+ mActivityTransitionAnimator = activityTransitionAnimator;
mNotificationAnimationProvider = notificationAnimationProvider;
mPowerInteractor = powerInteractor;
mUserTracker = userTracker;
@@ -440,7 +440,7 @@
boolean isActivityIntent) {
mLogger.logStartNotificationIntent(entry);
try {
- ActivityLaunchAnimator.Controller animationController =
+ ActivityTransitionAnimator.Controller animationController =
new StatusBarLaunchAnimatorController(
mNotificationAnimationProvider.getAnimatorController(row, null),
mShadeViewController,
@@ -448,7 +448,7 @@
mShadeController,
mNotificationShadeWindowController,
isActivityIntent);
- mActivityLaunchAnimator.startPendingIntentWithAnimation(
+ mActivityTransitionAnimator.startPendingIntentWithAnimation(
animationController,
animate,
intent.getCreatorPackage(),
@@ -482,7 +482,7 @@
@Override
public boolean onDismiss() {
AsyncTask.execute(() -> {
- ActivityLaunchAnimator.Controller animationController =
+ ActivityTransitionAnimator.Controller animationController =
new StatusBarLaunchAnimatorController(
mNotificationAnimationProvider.getAnimatorController(row),
mShadeViewController,
@@ -491,7 +491,7 @@
mNotificationShadeWindowController,
true /* isActivityIntent */);
- mActivityLaunchAnimator.startIntentWithAnimation(
+ mActivityTransitionAnimator.startIntentWithAnimation(
animationController, animate, intent.getPackage(),
(adapter) -> TaskStackBuilder.create(mContext)
.addNextIntentWithParentStack(intent)
@@ -528,11 +528,11 @@
tsb.addNextIntent(intent);
}
- ActivityLaunchAnimator.Controller viewController =
- ActivityLaunchAnimator.Controller.fromView(view,
+ ActivityTransitionAnimator.Controller viewController =
+ ActivityTransitionAnimator.Controller.fromView(view,
InteractionJankMonitor.CUJ_SHADE_APP_LAUNCH_FROM_HISTORY_BUTTON
);
- ActivityLaunchAnimator.Controller animationController =
+ ActivityTransitionAnimator.Controller animationController =
viewController == null ? null
: new StatusBarLaunchAnimatorController(
viewController,
@@ -542,8 +542,8 @@
mNotificationShadeWindowController,
true /* isActivityIntent */);
- mActivityLaunchAnimator.startIntentWithAnimation(animationController, animate,
- intent.getPackage(),
+ mActivityTransitionAnimator.startIntentWithAnimation(
+ animationController, animate, intent.getPackage(),
(adapter) -> tsb.startActivities(
getActivityOptions(mDisplayId, adapter),
mUserTracker.getUserHandle()));
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationController.kt
index 1a17e7c..665a571 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationController.kt
@@ -32,7 +32,7 @@
import com.android.systemui.statusbar.notification.stack.AnimationProperties
import com.android.systemui.statusbar.notification.stack.StackStateAnimator
import com.android.systemui.statusbar.policy.KeyguardStateController
-import com.android.app.tracing.TraceUtils
+import com.android.app.tracing.namedRunnable
import com.android.systemui.util.settings.GlobalSettings
import javax.inject.Inject
@@ -125,7 +125,7 @@
}
// FrameCallback used to delay starting the light reveal animation until the next frame
- private val startLightRevealCallback = TraceUtils.namedRunnable("startLightReveal") {
+ private val startLightRevealCallback = namedRunnable("startLightReveal") {
lightRevealAnimationPlaying = true
lightRevealAnimator.start()
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/domain/interactor/DarkIconInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/domain/interactor/DarkIconInteractor.kt
index 246645e..72f4540 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/domain/interactor/DarkIconInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/domain/interactor/DarkIconInteractor.kt
@@ -15,20 +15,14 @@
*/
package com.android.systemui.statusbar.phone.domain.interactor
-import android.graphics.Rect
import com.android.systemui.statusbar.phone.data.repository.DarkIconRepository
+import com.android.systemui.statusbar.phone.domain.model.DarkState
import javax.inject.Inject
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
/** States pertaining to calculating colors for icons in dark mode. */
class DarkIconInteractor @Inject constructor(repository: DarkIconRepository) {
- /** @see com.android.systemui.statusbar.phone.SysuiDarkIconDispatcher.DarkChange.areas */
- val tintAreas: Flow<Collection<Rect>> = repository.darkState.map { it.areas }
- /**
- * @see com.android.systemui.statusbar.phone.SysuiDarkIconDispatcher.DarkChange.darkIntensity
- */
- val darkIntensity: Flow<Float> = repository.darkState.map { it.darkIntensity }
- /** @see com.android.systemui.statusbar.phone.SysuiDarkIconDispatcher.DarkChange.tint */
- val tintColor: Flow<Int> = repository.darkState.map { it.tint }
+ /** Dark-mode state for tinting icons. */
+ val darkState: Flow<DarkState> = repository.darkState.map { DarkState(it.areas, it.tint) }
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/domain/model/DarkState.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/domain/model/DarkState.kt
new file mode 100644
index 0000000..3cab7cf
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/domain/model/DarkState.kt
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.phone.domain.model
+
+import android.graphics.Rect
+
+/** Dark mode visual states. */
+data class DarkState(
+ /** Areas on screen that require a dark-mode adjustment. */
+ val areas: Collection<Rect>,
+ /** Tint color to apply to UI elements that fall within [areas]. */
+ val tint: Int,
+)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/dagger/StatusBarFragmentModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/dagger/StatusBarFragmentModule.java
index 3741f14..c0e36b2 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/dagger/StatusBarFragmentModule.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/dagger/StatusBarFragmentModule.java
@@ -91,7 +91,9 @@
@StatusBarFragmentScope
@Named(OPERATOR_NAME_VIEW)
static View provideOperatorNameView(@RootView PhoneStatusBarView view) {
- return ((ViewStub) view.findViewById(R.id.operator_name_stub)).inflate();
+ View operatorName = ((ViewStub) view.findViewById(R.id.operator_name_stub)).inflate();
+ operatorName.setVisibility(View.GONE);
+ return operatorName;
}
/** */
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallController.kt
index 0bdd1a5..a20468f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallController.kt
@@ -30,7 +30,7 @@
import com.android.systemui.CoreStartable
import com.android.systemui.Dumpable
import com.android.systemui.res.R
-import com.android.systemui.animation.ActivityLaunchAnimator
+import com.android.systemui.animation.ActivityTransitionAnimator
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.dagger.qualifiers.Main
@@ -233,7 +233,7 @@
logger.logChipClicked()
activityStarter.postStartActivityDismissingKeyguard(
intent,
- ActivityLaunchAnimator.Controller.fromView(
+ ActivityTransitionAnimator.Controller.fromView(
backgroundView,
InteractionJankMonitor.CUJ_STATUS_BAR_APP_LAUNCH_FROM_CALL_CHIP)
)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/ui/model/SatelliteIconModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/ui/model/SatelliteIconModel.kt
index 63566ee..e1798d3 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/ui/model/SatelliteIconModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/ui/model/SatelliteIconModel.kt
@@ -16,6 +16,7 @@
package com.android.systemui.statusbar.pipeline.satellite.ui.model
+import com.android.systemui.common.shared.model.ContentDescription
import com.android.systemui.common.shared.model.Icon
import com.android.systemui.res.R
import com.android.systemui.statusbar.pipeline.satellite.shared.model.SatelliteConnectionState
@@ -36,7 +37,10 @@
SatelliteConnectionState.On ->
Icon.Resource(
res = R.drawable.ic_satellite_not_connected,
- contentDescription = null,
+ contentDescription =
+ ContentDescription.Resource(
+ R.string.accessibility_status_bar_satellite_available
+ ),
)
SatelliteConnectionState.Connected -> fromSignalStrength(signalStrength)
}
@@ -51,15 +55,36 @@
// TODO(b/316634365): these need content descriptions
when (signalStrength) {
// No signal
- 0 -> Icon.Resource(res = R.drawable.ic_satellite_connected_0, contentDescription = null)
+ 0 ->
+ Icon.Resource(
+ res = R.drawable.ic_satellite_connected_0,
+ contentDescription =
+ ContentDescription.Resource(
+ R.string.accessibility_status_bar_satellite_no_connection
+ )
+ )
// Poor -> Moderate
1,
- 2 -> Icon.Resource(res = R.drawable.ic_satellite_connected_1, contentDescription = null)
+ 2 ->
+ Icon.Resource(
+ res = R.drawable.ic_satellite_connected_1,
+ contentDescription =
+ ContentDescription.Resource(
+ R.string.accessibility_status_bar_satellite_poor_connection
+ )
+ )
// Good -> Great
3,
- 4 -> Icon.Resource(res = R.drawable.ic_satellite_connected_2, contentDescription = null)
+ 4 ->
+ Icon.Resource(
+ res = R.drawable.ic_satellite_connected_2,
+ contentDescription =
+ ContentDescription.Resource(
+ R.string.accessibility_status_bar_satellite_good_connection
+ )
+ )
else -> null
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionControllerImpl.java
index b0192c0..d4c180d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionControllerImpl.java
@@ -16,27 +16,38 @@
package com.android.systemui.statusbar.policy;
-import static com.android.systemui.Flags.screenshareNotificationHiding;
+import static com.android.server.notification.Flags.screenshareNotificationHiding;
+import android.annotation.MainThread;
+import android.app.IActivityManager;
+import android.content.Context;
import android.media.projection.MediaProjectionInfo;
import android.media.projection.MediaProjectionManager;
import android.os.Handler;
+import android.os.RemoteException;
import android.os.Trace;
import android.service.notification.StatusBarNotification;
+import android.util.ArraySet;
+import android.util.Log;
import com.android.internal.annotations.VisibleForTesting;
import com.android.systemui.dagger.SysUISingleton;
+import com.android.systemui.dagger.qualifiers.Background;
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.statusbar.notification.collection.NotificationEntry;
+import com.android.systemui.util.Assert;
import com.android.systemui.util.ListenerSet;
+import java.util.concurrent.Executor;
+
import javax.inject.Inject;
/** Implementation of SensitiveNotificationProtectionController. **/
@SysUISingleton
public class SensitiveNotificationProtectionControllerImpl
implements SensitiveNotificationProtectionController {
- private final MediaProjectionManager mMediaProjectionManager;
+ private static final String LOG_TAG = "SNPC";
+ private final ArraySet<String> mExemptPackages = new ArraySet<>();
private final ListenerSet<Runnable> mListeners = new ListenerSet<>();
private volatile MediaProjectionInfo mProjection;
@@ -45,44 +56,100 @@
new MediaProjectionManager.Callback() {
@Override
public void onStart(MediaProjectionInfo info) {
- Trace.beginSection(
- "SNPC.onProjectionStart");
- // Only enable sensitive content protection if sharing full screen
- // Launch cookie only set (non-null) if sharing single app/task
- updateProjectionState((info.getLaunchCookie() == null) ? info : null);
- Trace.endSection();
+ Trace.beginSection("SNPC.onProjectionStart");
+ try {
+ // Only enable sensitive content protection if sharing full screen
+ // Launch cookie only set (non-null) if sharing single app/task
+ updateProjectionStateAndNotifyListeners(
+ (info.getLaunchCookie() == null) ? info : null);
+ } finally {
+ Trace.endSection();
+ }
}
@Override
public void onStop(MediaProjectionInfo info) {
- Trace.beginSection(
- "SNPC.onProjectionStop");
- updateProjectionState(null);
- Trace.endSection();
- }
-
- private void updateProjectionState(MediaProjectionInfo info) {
- // capture previous state
- boolean wasSensitive = isSensitiveStateActive();
-
- // update internal state
- mProjection = info;
-
- // if either previous or new state is sensitive, notify listeners.
- if (wasSensitive || isSensitiveStateActive()) {
- mListeners.forEach(Runnable::run);
+ Trace.beginSection("SNPC.onProjectionStop");
+ try {
+ updateProjectionStateAndNotifyListeners(null);
+ } finally {
+ Trace.endSection();
}
}
};
@Inject
public SensitiveNotificationProtectionControllerImpl(
+ Context context,
MediaProjectionManager mediaProjectionManager,
- @Main Handler mainHandler) {
- mMediaProjectionManager = mediaProjectionManager;
+ IActivityManager activityManager,
+ @Main Handler mainHandler,
+ @Background Executor bgExecutor) {
+ if (!screenshareNotificationHiding()) {
+ return;
+ }
- if (screenshareNotificationHiding()) {
- mMediaProjectionManager.addCallback(mMediaProjectionCallback, mainHandler);
+ bgExecutor.execute(() -> {
+ ArraySet<String> exemptPackages = new ArraySet<>();
+ // Exempt SystemUI
+ exemptPackages.add(context.getPackageName());
+
+ // Exempt approved bug report handlers
+ try {
+ exemptPackages.addAll(activityManager.getBugreportWhitelistedPackages());
+ } catch (RemoteException e) {
+ Log.e(
+ LOG_TAG,
+ "Error adding bug report handlers to exemption, continuing without",
+ e);
+ // silent failure, skip adding packages to exemption
+ }
+
+ // if currently projecting, notify listeners of exemption changes
+ mainHandler.post(() -> {
+ Trace.beginSection("SNPC.exemptPackagesUpdated");
+ try {
+ updateExemptPackagesAndNotifyListeners(exemptPackages);
+ } finally {
+ Trace.endSection();
+ }
+ });
+ });
+
+ mediaProjectionManager.addCallback(mMediaProjectionCallback, mainHandler);
+ }
+
+ /**
+ * Notify listeners of possible ProjectionState change regardless of current
+ * isSensitiveStateActive value. Method used to ensure updates occur after mExemptPackages gets
+ * updated, which directly changes the outcome of isSensitiveStateActive
+ */
+ @MainThread
+ private void updateExemptPackagesAndNotifyListeners(ArraySet<String> exemptPackages) {
+ Assert.isMainThread();
+ mExemptPackages.addAll(exemptPackages);
+
+ if (mProjection != null) {
+ mListeners.forEach(Runnable::run);
+ }
+ }
+
+ /**
+ * Update ProjectionState respecting current isSensitiveStateActive value. Only notifies
+ * listeners
+ */
+ @MainThread
+ private void updateProjectionStateAndNotifyListeners(MediaProjectionInfo info) {
+ Assert.isMainThread();
+ // capture previous state
+ boolean wasSensitive = isSensitiveStateActive();
+
+ // update internal state
+ mProjection = info;
+
+ // if either previous or new state is sensitive, notify listeners.
+ if (wasSensitive || isSensitiveStateActive()) {
+ mListeners.forEach(Runnable::run);
}
}
@@ -96,11 +163,17 @@
mListeners.remove(onSensitiveStateChanged);
}
+ // TODO(b/323396693): opportunity for optimization
@Override
public boolean isSensitiveStateActive() {
+ MediaProjectionInfo projection = mProjection;
+ if (projection == null) {
+ return false;
+ }
+
// TODO(b/316955558): Add disabled by developer option
- // TODO(b/316955306): Add feature exemption for sysui and bug handlers
- return mProjection != null;
+
+ return !mExemptPackages.contains(projection.getPackageName());
}
@Override
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowController.java b/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowController.java
index b598782..65c2e20 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowController.java
@@ -47,13 +47,13 @@
import android.view.WindowManager;
import com.android.internal.policy.SystemBarUtils;
-import com.android.systemui.res.R;
-import com.android.systemui.animation.ActivityLaunchAnimator;
+import com.android.systemui.animation.ActivityTransitionAnimator;
import com.android.systemui.animation.DelegateLaunchAnimatorController;
import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.fragments.FragmentHostManager;
import com.android.systemui.fragments.FragmentService;
+import com.android.systemui.res.R;
import com.android.systemui.statusbar.phone.StatusBarContentInsetsProvider;
import com.android.systemui.unfold.UnfoldTransitionProgressProvider;
import com.android.systemui.unfold.util.JankMonitorTransitionProgressListener;
@@ -188,23 +188,23 @@
* updated animation controller that handles status-bar-related animation details. Returns an
* empty optional if the animation is *not* on a view in the status bar.
*/
- public Optional<ActivityLaunchAnimator.Controller> wrapAnimationControllerIfInStatusBar(
- View rootView, ActivityLaunchAnimator.Controller animationController) {
+ public Optional<ActivityTransitionAnimator.Controller> wrapAnimationControllerIfInStatusBar(
+ View rootView, ActivityTransitionAnimator.Controller animationController) {
if (rootView != mStatusBarWindowView) {
return Optional.empty();
}
- animationController.setLaunchContainer(mLaunchAnimationContainer);
+ animationController.setTransitionContainer(mLaunchAnimationContainer);
return Optional.of(new DelegateLaunchAnimatorController(animationController) {
@Override
- public void onLaunchAnimationStart(boolean isExpandingFullyAbove) {
- getDelegate().onLaunchAnimationStart(isExpandingFullyAbove);
+ public void onTransitionAnimationStart(boolean isExpandingFullyAbove) {
+ getDelegate().onTransitionAnimationStart(isExpandingFullyAbove);
setLaunchAnimationRunning(true);
}
@Override
- public void onLaunchAnimationEnd(boolean isExpandingFullyAbove) {
- getDelegate().onLaunchAnimationEnd(isExpandingFullyAbove);
+ public void onTransitionAnimationEnd(boolean isExpandingFullyAbove) {
+ getDelegate().onTransitionAnimationEnd(isExpandingFullyAbove);
setLaunchAnimationRunning(false);
}
});
diff --git a/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayApplier.java b/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayApplier.java
index 3376e23..147e158 100644
--- a/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayApplier.java
+++ b/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayApplier.java
@@ -15,6 +15,8 @@
*/
package com.android.systemui.theme;
+import static com.android.systemui.shared.Flags.enableHomeDelay;
+
import android.annotation.AnyThread;
import android.content.om.FabricatedOverlay;
import android.content.om.OverlayIdentifier;
@@ -32,6 +34,7 @@
import com.android.systemui.Dumpable;
import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.dagger.qualifiers.Background;
+import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.dump.DumpManager;
import com.google.android.collect.Lists;
@@ -142,6 +145,7 @@
private final Map<String, String> mCategoryToTargetPackage = new ArrayMap<>();
private final OverlayManager mOverlayManager;
private final Executor mBgExecutor;
+ private final Executor mMainExecutor;
private final String mLauncherPackage;
private final String mThemePickerPackage;
@@ -150,9 +154,11 @@
@Background Executor bgExecutor,
@Named(ThemeModule.LAUNCHER_PACKAGE) String launcherPackage,
@Named(ThemeModule.THEME_PICKER_PACKAGE) String themePickerPackage,
- DumpManager dumpManager) {
+ DumpManager dumpManager,
+ @Main Executor mainExecutor) {
mOverlayManager = overlayManager;
mBgExecutor = bgExecutor;
+ mMainExecutor = mainExecutor;
mLauncherPackage = launcherPackage;
mThemePickerPackage = themePickerPackage;
mTargetPackageToCategories.put(ANDROID_PACKAGE, Sets.newHashSet(
@@ -184,12 +190,21 @@
/**
* Apply the set of overlay packages to the set of {@code UserHandle}s provided. Overlays that
* affect sysui will also be applied to the system user.
+ *
+ * @param categoryToPackage Overlay packages to be applied
+ * @param pendingCreation Overlays yet to be created
+ * @param currentUser Current User ID
+ * @param managedProfiles Profiles get overlays
+ * @param onComplete Callback for when resources are ready. Runs in the main thread.
*/
public void applyCurrentUserOverlays(
Map<String, OverlayIdentifier> categoryToPackage,
FabricatedOverlay[] pendingCreation,
int currentUser,
- Set<UserHandle> managedProfiles) {
+ Set<UserHandle> managedProfiles,
+ Runnable onComplete
+ ) {
+
mBgExecutor.execute(() -> {
// Disable all overlays that have not been specified in the user setting.
@@ -236,6 +251,10 @@
try {
mOverlayManager.commit(transaction.build());
+ if (enableHomeDelay() && onComplete != null) {
+ Log.d(TAG, "Executing onComplete runnable");
+ mMainExecutor.execute(onComplete);
+ }
} catch (SecurityException | IllegalStateException e) {
Log.e(TAG, "setEnabled failed", e);
}
diff --git a/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java b/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java
index 2b9ad50..585ab72 100644
--- a/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java
+++ b/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java
@@ -20,6 +20,7 @@
import static com.android.systemui.Flags.themeOverlayControllerWakefulnessDeprecation;
import static com.android.systemui.keyguard.WakefulnessLifecycle.WAKEFULNESS_ASLEEP;
+import static com.android.systemui.shared.Flags.enableHomeDelay;
import static com.android.systemui.theme.ThemeOverlayApplier.COLOR_SOURCE_HOME;
import static com.android.systemui.theme.ThemeOverlayApplier.COLOR_SOURCE_LOCK;
import static com.android.systemui.theme.ThemeOverlayApplier.COLOR_SOURCE_PRESET;
@@ -31,6 +32,7 @@
import static com.android.systemui.theme.ThemeOverlayApplier.OVERLAY_COLOR_SOURCE;
import static com.android.systemui.theme.ThemeOverlayApplier.TIMESTAMP_FIELD;
+import android.app.ActivityManager;
import android.app.UiModeManager;
import android.app.WallpaperColors;
import android.app.WallpaperManager;
@@ -140,6 +142,7 @@
// Current wallpaper colors associated to a user.
private final SparseArray<WallpaperColors> mCurrentColors = new SparseArray<>();
private final WallpaperManager mWallpaperManager;
+ private final ActivityManager mActivityManager;
@VisibleForTesting
protected ColorScheme mColorScheme;
// If fabricated overlays were already created for the current theme.
@@ -414,7 +417,8 @@
WakefulnessLifecycle wakefulnessLifecycle,
JavaAdapter javaAdapter,
KeyguardTransitionInteractor keyguardTransitionInteractor,
- UiModeManager uiModeManager) {
+ UiModeManager uiModeManager,
+ ActivityManager activityManager) {
mContext = context;
mIsMonetEnabled = featureFlags.isEnabled(Flags.MONET);
mIsFidelityEnabled = featureFlags.isEnabled(Flags.COLOR_FIDELITY);
@@ -433,6 +437,7 @@
mJavaAdapter = javaAdapter;
mKeyguardTransitionInteractor = keyguardTransitionInteractor;
mUiModeManager = uiModeManager;
+ mActivityManager = activityManager;
dumpManager.registerDumpable(TAG, this);
}
@@ -806,8 +811,16 @@
}
}
+ final Runnable onCompleteCallback = !enableHomeDelay()
+ ? () -> {}
+ : () -> {
+ Log.d(TAG, "ThemeHomeDelay: ThemeOverlayController ready");
+ mActivityManager.setThemeOverlayReady(true);
+ };
+
if (colorSchemeIsApplied(managedProfiles)) {
Log.d(TAG, "Skipping overlay creation. Theme was already: " + mColorScheme);
+ onCompleteCallback.run();
return;
}
@@ -816,15 +829,19 @@
.map(key -> key + " -> " + categoryToPackage.get(key)).collect(
Collectors.joining(", ")));
}
+
+ FabricatedOverlay[] fOverlays = null;
+
if (mNeedsOverlayCreation) {
mNeedsOverlayCreation = false;
- mThemeManager.applyCurrentUserOverlays(categoryToPackage, new FabricatedOverlay[]{
+ fOverlays = new FabricatedOverlay[]{
mSecondaryOverlay, mNeutralOverlay, mDynamicOverlay
- }, currentUser, managedProfiles);
- } else {
- mThemeManager.applyCurrentUserOverlays(categoryToPackage, null, currentUser,
- managedProfiles);
+ };
}
+
+ mThemeManager.applyCurrentUserOverlays(categoryToPackage, fOverlays, currentUser,
+ managedProfiles, onCompleteCallback);
+
}
private Style fetchThemeStyleFromSetting() {
diff --git a/packages/SystemUI/src/com/android/systemui/unfold/DisplaySwitchLatencyTracker.kt b/packages/SystemUI/src/com/android/systemui/unfold/DisplaySwitchLatencyTracker.kt
index 92a64a6..4dfd5a1 100644
--- a/packages/SystemUI/src/com/android/systemui/unfold/DisplaySwitchLatencyTracker.kt
+++ b/packages/SystemUI/src/com/android/systemui/unfold/DisplaySwitchLatencyTracker.kt
@@ -18,8 +18,8 @@
import android.content.Context
import android.util.Log
-import com.android.app.tracing.TraceUtils.instantForTrack
import com.android.app.tracing.TraceUtils.traceAsync
+import com.android.app.tracing.instantForTrack
import com.android.systemui.CoreStartable
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
@@ -81,8 +81,8 @@
.pairwise()
.filter {
// Start tracking only when the foldable device is
- //folding(UNFOLDED/HALF_FOLDED -> FOLDED) or
- //unfolding(FOLDED -> HALF_FOLD/UNFOLDED)
+ // folding(UNFOLDED/HALF_FOLDED -> FOLDED) or
+ // unfolding(FOLDED -> HALF_FOLD/UNFOLDED)
foldableDeviceState ->
foldableDeviceState.previousValue == DeviceState.FOLDED ||
foldableDeviceState.newValue == DeviceState.FOLDED
@@ -172,7 +172,7 @@
fromFoldableDeviceState: Int
): DisplaySwitchLatencyEvent {
log { "fromFoldableDeviceState=$fromFoldableDeviceState" }
- instantForTrack(TAG, "fromFoldableDeviceState=$fromFoldableDeviceState")
+ instantForTrack(TAG) { "fromFoldableDeviceState=$fromFoldableDeviceState" }
return copy(fromFoldableDeviceState = fromFoldableDeviceState)
}
@@ -187,7 +187,7 @@
"toState=$toState, " +
"latencyMs=$displaySwitchTimeMs"
}
- instantForTrack(TAG, "toFoldableDeviceState=$toFoldableDeviceState, toState=$toState")
+ instantForTrack(TAG) { "toFoldableDeviceState=$toFoldableDeviceState, toState=$toState" }
return copy(
toFoldableDeviceState = toFoldableDeviceState,
diff --git a/packages/SystemUI/src/com/android/systemui/unfold/FoldLightRevealOverlayAnimation.kt b/packages/SystemUI/src/com/android/systemui/unfold/FoldLightRevealOverlayAnimation.kt
new file mode 100644
index 0000000..5c53ff9
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/unfold/FoldLightRevealOverlayAnimation.kt
@@ -0,0 +1,153 @@
+/*
+ * Copyright (C) 2024 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.unfold
+
+import android.animation.ValueAnimator
+import android.annotation.BinderThread
+import android.content.Context
+import android.os.Handler
+import android.os.SystemProperties
+import android.util.Log
+import android.view.animation.DecelerateInterpolator
+import androidx.core.animation.addListener
+import com.android.internal.foldables.FoldLockSettingAvailabilityProvider
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.display.data.repository.DeviceStateRepository
+import com.android.systemui.power.domain.interactor.PowerInteractor
+import com.android.systemui.power.shared.model.ScreenPowerState
+import com.android.systemui.statusbar.LinearSideLightRevealEffect
+import com.android.systemui.unfold.FullscreenLightRevealAnimationController.Companion.ALPHA_OPAQUE
+import com.android.systemui.unfold.FullscreenLightRevealAnimationController.Companion.ALPHA_TRANSPARENT
+import com.android.systemui.unfold.FullscreenLightRevealAnimationController.Companion.isVerticalRotation
+import com.android.systemui.unfold.dagger.UnfoldBg
+import com.android.systemui.util.animation.data.repository.AnimationStatusRepository
+import javax.inject.Inject
+import kotlinx.coroutines.CompletableDeferred
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.TimeoutCancellationException
+import kotlinx.coroutines.android.asCoroutineDispatcher
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.filter
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withTimeout
+
+class FoldLightRevealOverlayAnimation
+@Inject
+constructor(
+ private val context: Context,
+ @UnfoldBg private val bgHandler: Handler,
+ private val deviceStateRepository: DeviceStateRepository,
+ private val powerInteractor: PowerInteractor,
+ @Background private val applicationScope: CoroutineScope,
+ private val animationStatusRepository: AnimationStatusRepository,
+ private val controllerFactory: FullscreenLightRevealAnimationController.Factory
+) : FullscreenLightRevealAnimation {
+
+ private val revealProgressValueAnimator: ValueAnimator =
+ ValueAnimator.ofFloat(ALPHA_OPAQUE, ALPHA_TRANSPARENT)
+ private lateinit var controller: FullscreenLightRevealAnimationController
+ @Volatile private var readyCallback: CompletableDeferred<Runnable>? = null
+
+ override fun init() {
+ // This method will be called only on devices where this animation is enabled,
+ // so normally this thread won't be created
+ if (!FoldLockSettingAvailabilityProvider(context.resources).isFoldLockBehaviorAvailable) {
+ return
+ }
+
+ controller =
+ controllerFactory.create(
+ displaySelector = { minByOrNull { it.naturalWidth } },
+ effectFactory = { LinearSideLightRevealEffect(it.isVerticalRotation()) },
+ overlayContainerName = SURFACE_CONTAINER_NAME
+ )
+ controller.init()
+
+ applicationScope.launch(bgHandler.asCoroutineDispatcher()) {
+ powerInteractor.screenPowerState.collect {
+ if (it == ScreenPowerState.SCREEN_ON) {
+ readyCallback = null
+ }
+ }
+ }
+
+ applicationScope.launch(bgHandler.asCoroutineDispatcher()) {
+ deviceStateRepository.state
+ .map { it != DeviceStateRepository.DeviceState.FOLDED }
+ .distinctUntilChanged()
+ .filter { isUnfolded -> isUnfolded }
+ .collect { controller.ensureOverlayRemoved() }
+ }
+
+ applicationScope.launch(bgHandler.asCoroutineDispatcher()) {
+ deviceStateRepository.state
+ .filter {
+ animationStatusRepository.areAnimationsEnabled().first() &&
+ it == DeviceStateRepository.DeviceState.FOLDED
+ }
+ .collect {
+ try {
+ withTimeout(WAIT_FOR_ANIMATION_TIMEOUT_MS) {
+ readyCallback = CompletableDeferred()
+ val onReady = readyCallback?.await()
+ readyCallback = null
+ controller.addOverlay(ALPHA_OPAQUE, onReady)
+ waitForScreenTurnedOn()
+ playFoldLightRevealOverlayAnimation()
+ }
+ } catch (e: TimeoutCancellationException) {
+ Log.e(TAG, "Fold light reveal animation timed out")
+ ensureOverlayRemovedInternal()
+ }
+ }
+ }
+ }
+
+ @BinderThread
+ override fun onScreenTurningOn(onOverlayReady: Runnable) {
+ readyCallback?.complete(onOverlayReady) ?: onOverlayReady.run()
+ }
+
+ private suspend fun waitForScreenTurnedOn() {
+ powerInteractor.screenPowerState.filter { it == ScreenPowerState.SCREEN_ON }.first()
+ }
+
+ private fun ensureOverlayRemovedInternal() {
+ revealProgressValueAnimator.cancel()
+ controller.ensureOverlayRemoved()
+ }
+
+ private fun playFoldLightRevealOverlayAnimation() {
+ revealProgressValueAnimator.duration = ANIMATION_DURATION
+ revealProgressValueAnimator.interpolator = DecelerateInterpolator()
+ revealProgressValueAnimator.addUpdateListener { animation ->
+ controller.updateRevealAmount(animation.animatedFraction)
+ }
+ revealProgressValueAnimator.addListener(onEnd = { controller.ensureOverlayRemoved() })
+ revealProgressValueAnimator.start()
+ }
+
+ private companion object {
+ const val TAG = "FoldLightRevealOverlayAnimation"
+ const val WAIT_FOR_ANIMATION_TIMEOUT_MS = 2000L
+ const val SURFACE_CONTAINER_NAME = "fold-overlay-container"
+ val ANIMATION_DURATION: Long
+ get() = SystemProperties.getLong("persist.fold_animation_duration", 200L)
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/unfold/FullscreenLightRevealAnimation.kt b/packages/SystemUI/src/com/android/systemui/unfold/FullscreenLightRevealAnimation.kt
new file mode 100644
index 0000000..668b143
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/unfold/FullscreenLightRevealAnimation.kt
@@ -0,0 +1,271 @@
+/*
+ * Copyright (C) 2024 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.unfold
+
+import android.content.Context
+import android.graphics.PixelFormat
+import android.hardware.display.DisplayManager
+import android.os.Handler
+import android.os.Looper
+import android.os.Trace
+import android.view.Choreographer
+import android.view.Display
+import android.view.DisplayInfo
+import android.view.Surface
+import android.view.Surface.Rotation
+import android.view.SurfaceControl
+import android.view.SurfaceControlViewHost
+import android.view.SurfaceSession
+import android.view.WindowManager
+import android.view.WindowlessWindowManager
+import com.android.app.tracing.traceSection
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.settings.DisplayTracker
+import com.android.systemui.statusbar.LightRevealEffect
+import com.android.systemui.statusbar.LightRevealScrim
+import com.android.systemui.unfold.dagger.UnfoldBg
+import com.android.systemui.unfold.updates.RotationChangeProvider
+import com.android.systemui.util.concurrency.ThreadFactory
+import com.android.wm.shell.displayareahelper.DisplayAreaHelper
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+import java.lang.IllegalArgumentException
+import java.util.Optional
+import java.util.concurrent.Executor
+import java.util.function.Consumer
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.asCoroutineDispatcher
+import kotlinx.coroutines.launch
+
+interface FullscreenLightRevealAnimation {
+ fun init()
+
+ fun onScreenTurningOn(onOverlayReady: Runnable)
+}
+
+class FullscreenLightRevealAnimationController
+@AssistedInject
+constructor(
+ private val context: Context,
+ private val displayManager: DisplayManager,
+ private val threadFactory: ThreadFactory,
+ @UnfoldBg private val bgHandler: Handler,
+ @UnfoldBg private val rotationChangeProvider: RotationChangeProvider,
+ private val displayAreaHelper: Optional<DisplayAreaHelper>,
+ private val displayTracker: DisplayTracker,
+ @Background private val applicationScope: CoroutineScope,
+ @Main private val executor: Executor,
+ @Assisted private val displaySelector: Sequence<DisplayInfo>.() -> DisplayInfo?,
+ @Assisted private val lightRevealEffectFactory: (rotation: Int) -> LightRevealEffect,
+ @Assisted private val overlayContainerName: String
+) {
+
+ private lateinit var bgExecutor: Executor
+ private lateinit var wwm: WindowlessWindowManager
+
+ private var currentRotation: Int = context.display.rotation
+ private var root: SurfaceControlViewHost? = null
+ private var scrimView: LightRevealScrim? = null
+
+ private val rotationWatcher = RotationWatcher()
+ private val internalDisplayInfos: Sequence<DisplayInfo>
+ get() =
+ displayManager
+ .getDisplays(DisplayManager.DISPLAY_CATEGORY_ALL_INCLUDING_DISABLED)
+ .asSequence()
+ .map { DisplayInfo().apply { it.getDisplayInfo(this) } }
+ .filter { it.type == Display.TYPE_INTERNAL }
+
+ var isTouchBlocked: Boolean = false
+ set(value) {
+ if (value != field) {
+ traceSection("$TAG#relayoutToUpdateTouch") { root?.relayout(getLayoutParams()) }
+ field = value
+ }
+ }
+
+ fun init() {
+ bgExecutor = threadFactory.buildDelayableExecutorOnHandler(bgHandler)
+ rotationChangeProvider.addCallback(rotationWatcher)
+
+ buildSurface { builder ->
+ applicationScope.launch(executor.asCoroutineDispatcher()) {
+ val overlayContainer = builder.build()
+
+ SurfaceControl.Transaction()
+ .setLayer(overlayContainer, OVERLAY_LAYER_Z_INDEX)
+ .show(overlayContainer)
+ .apply()
+
+ wwm =
+ WindowlessWindowManager(context.resources.configuration, overlayContainer, null)
+ }
+ }
+ }
+
+ fun addOverlay(
+ initialAlpha: Float,
+ onOverlayReady: Runnable? = null,
+ ) {
+ if (!::wwm.isInitialized) {
+ // Surface overlay is not created yet on the first SysUI launch
+ onOverlayReady?.run()
+ return
+ }
+ ensureInBackground()
+ ensureOverlayRemoved()
+ prepareOverlay(onOverlayReady, wwm, bgExecutor, initialAlpha)
+ }
+
+ fun ensureOverlayRemoved() {
+ ensureInBackground()
+
+ traceSection("ensureOverlayRemoved") {
+ root?.release()
+ root = null
+ scrimView = null
+ }
+ }
+
+ fun isOverlayVisible(): Boolean {
+ return scrimView == null
+ }
+
+ fun updateRevealAmount(revealAmount: Float) {
+ scrimView?.revealAmount = revealAmount
+ }
+
+ private fun buildSurface(onUpdated: Consumer<SurfaceControl.Builder>) {
+ val containerBuilder =
+ SurfaceControl.Builder(SurfaceSession())
+ .setContainerLayer()
+ .setName(overlayContainerName)
+
+ displayAreaHelper
+ .get()
+ .attachToRootDisplayArea(displayTracker.defaultDisplayId, containerBuilder, onUpdated)
+ }
+
+ private fun prepareOverlay(
+ onOverlayReady: Runnable? = null,
+ wwm: WindowlessWindowManager,
+ bgExecutor: Executor,
+ initialAlpha: Float,
+ ) {
+ val newRoot = SurfaceControlViewHost(context, context.display, wwm, javaClass.simpleName)
+
+ val params = getLayoutParams()
+ val newView =
+ LightRevealScrim(
+ context,
+ attrs = null,
+ initialWidth = params.width,
+ initialHeight = params.height
+ )
+ .apply {
+ revealEffect = lightRevealEffectFactory(currentRotation)
+ revealAmount = initialAlpha
+ }
+
+ newRoot.setView(newView, params)
+
+ if (onOverlayReady != null) {
+ Trace.beginAsyncSection("$TAG#relayout", 0)
+
+ newRoot.relayout(params) { transaction ->
+ val vsyncId = Choreographer.getSfInstance().vsyncId
+ transaction.setFrameTimelineVsync(vsyncId).apply()
+
+ transaction
+ .setFrameTimelineVsync(vsyncId + 1)
+ .addTransactionCommittedListener(bgExecutor) {
+ Trace.endAsyncSection("$TAG#relayout", 0)
+ onOverlayReady.run()
+ }
+ .apply()
+ }
+ }
+ root = newRoot
+ scrimView = newView
+ }
+
+ private fun ensureInBackground() {
+ check(Looper.myLooper() == bgHandler.looper) { "Not being executed in the background!" }
+ }
+
+ private fun getLayoutParams(): WindowManager.LayoutParams {
+ val displayInfo =
+ internalDisplayInfos.displaySelector()
+ ?: throw IllegalArgumentException("No internal displays found!")
+ return WindowManager.LayoutParams().apply {
+ if (currentRotation.isVerticalRotation()) {
+ height = displayInfo.naturalHeight
+ width = displayInfo.naturalWidth
+ } else {
+ height = displayInfo.naturalWidth
+ width = displayInfo.naturalHeight
+ }
+ format = PixelFormat.TRANSLUCENT
+ type = WindowManager.LayoutParams.TYPE_DISPLAY_OVERLAY
+ title = javaClass.simpleName
+ layoutInDisplayCutoutMode =
+ WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS
+ fitInsetsTypes = 0
+
+ flags =
+ WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or
+ WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE
+ setTrustedOverlay()
+
+ packageName = context.opPackageName
+ }
+ }
+
+ private inner class RotationWatcher : RotationChangeProvider.RotationListener {
+ override fun onRotationChanged(newRotation: Int) {
+ traceSection("$TAG#onRotationChanged") {
+ if (currentRotation != newRotation) {
+ currentRotation = newRotation
+ scrimView?.revealEffect = lightRevealEffectFactory(currentRotation)
+ root?.relayout(getLayoutParams())
+ }
+ }
+ }
+ }
+
+ @AssistedFactory
+ interface Factory {
+ fun create(
+ displaySelector: Sequence<DisplayInfo>.() -> DisplayInfo?,
+ effectFactory: (rotation: Int) -> LightRevealEffect,
+ overlayContainerName: String
+ ): FullscreenLightRevealAnimationController
+ }
+
+ companion object {
+ private const val TAG = "FullscreenLightRevealAnimation"
+ private const val ROTATION_ANIMATION_OVERLAY_Z_INDEX = Integer.MAX_VALUE
+ private const val OVERLAY_LAYER_Z_INDEX = ROTATION_ANIMATION_OVERLAY_Z_INDEX - 1
+ const val ALPHA_TRANSPARENT = 1f
+ const val ALPHA_OPAQUE = 0f
+
+ fun @receiver:Rotation Int.isVerticalRotation(): Boolean =
+ this == Surface.ROTATION_0 || this == Surface.ROTATION_180
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/unfold/SysUIUnfoldModule.kt b/packages/SystemUI/src/com/android/systemui/unfold/SysUIUnfoldModule.kt
index 0016d95..139ac7e 100644
--- a/packages/SystemUI/src/com/android/systemui/unfold/SysUIUnfoldModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/unfold/SysUIUnfoldModule.kt
@@ -17,6 +17,7 @@
package com.android.systemui.unfold
import com.android.keyguard.KeyguardUnfoldTransition
+import com.android.systemui.CoreStartable
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.shade.NotificationPanelUnfoldAnimationController
import com.android.systemui.statusbar.phone.StatusBarMoveFromCenterAnimationController
@@ -25,10 +26,14 @@
import com.android.systemui.unfold.util.ScopedUnfoldTransitionProgressProvider
import com.android.systemui.unfold.util.UnfoldKeyguardVisibilityManager
import com.android.systemui.util.kotlin.getOrNull
+import dagger.Binds
import dagger.BindsInstance
import dagger.Module
import dagger.Provides
import dagger.Subcomponent
+import dagger.multibindings.ClassKey
+import dagger.multibindings.IntoMap
+import dagger.multibindings.IntoSet
import java.util.Optional
import javax.inject.Named
import javax.inject.Scope
@@ -70,8 +75,33 @@
}
}
+@Module
+interface SysUIUnfoldStartableModule {
+ @Binds
+ @IntoMap
+ @ClassKey(UnfoldInitializationStartable::class)
+ fun bindsUnfoldInitializationStartable(impl: UnfoldInitializationStartable): CoreStartable
+}
+
+@Module
+abstract class SysUIUnfoldInternalModule {
+ @Binds
+ @IntoSet
+ @SysUIUnfoldScope
+ abstract fun bindsUnfoldLightRevealOverlayAnimation(
+ anim: UnfoldLightRevealOverlayAnimation
+ ): FullscreenLightRevealAnimation
+
+ @Binds
+ @IntoSet
+ @SysUIUnfoldScope
+ abstract fun bindsFoldLightRevealOverlayAnimation(
+ anim: FoldLightRevealOverlayAnimation
+ ): FullscreenLightRevealAnimation
+}
+
@SysUIUnfoldScope
-@Subcomponent
+@Subcomponent(modules = [SysUIUnfoldInternalModule::class])
interface SysUIUnfoldComponent {
@Subcomponent.Factory
@@ -92,12 +122,12 @@
fun getFoldAodAnimationController(): FoldAodAnimationController
+ fun getFullScreenLightRevealAnimations(): Set<FullscreenLightRevealAnimation>
+
fun getUnfoldTransitionWallpaperController(): UnfoldTransitionWallpaperController
fun getUnfoldHapticsPlayer(): UnfoldHapticsPlayer
- fun getUnfoldLightRevealOverlayAnimation(): UnfoldLightRevealOverlayAnimation
-
fun getUnfoldKeyguardVisibilityManager(): UnfoldKeyguardVisibilityManager
fun getUnfoldLatencyTracker(): UnfoldLatencyTracker
diff --git a/packages/SystemUI/src/com/android/systemui/unfold/UnfoldInitializationStartable.kt b/packages/SystemUI/src/com/android/systemui/unfold/UnfoldInitializationStartable.kt
new file mode 100644
index 0000000..75d8a58
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/unfold/UnfoldInitializationStartable.kt
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2024 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.unfold
+
+import com.android.systemui.CoreStartable
+import com.android.systemui.Flags
+import com.android.systemui.unfold.dagger.UnfoldBg
+import com.android.systemui.unfold.progress.UnfoldTransitionProgressForwarder
+import java.util.Optional
+import javax.inject.Inject
+
+class UnfoldInitializationStartable
+@Inject
+constructor(
+ private val unfoldComponentOptional: Optional<SysUIUnfoldComponent>,
+ private val foldStateLoggingProviderOptional: Optional<FoldStateLoggingProvider>,
+ private val foldStateLoggerOptional: Optional<FoldStateLogger>,
+ @UnfoldBg
+ private val unfoldBgTransitionProgressProviderOptional:
+ Optional<UnfoldTransitionProgressProvider>,
+ private val unfoldTransitionProgressProviderOptional:
+ Optional<UnfoldTransitionProgressProvider>,
+ private val unfoldTransitionProgressForwarder: Optional<UnfoldTransitionProgressForwarder>
+) : CoreStartable {
+ override fun start() {
+ unfoldComponentOptional.ifPresent { c: SysUIUnfoldComponent ->
+ c.getFullScreenLightRevealAnimations().forEach { it: FullscreenLightRevealAnimation ->
+ it.init()
+ }
+ c.getUnfoldTransitionWallpaperController().init()
+ c.getUnfoldHapticsPlayer()
+ c.getNaturalRotationUnfoldProgressProvider().init()
+ c.getUnfoldLatencyTracker().init()
+ }
+
+ foldStateLoggingProviderOptional.ifPresent { obj: FoldStateLoggingProvider -> obj.init() }
+ foldStateLoggerOptional.ifPresent { obj: FoldStateLogger -> obj.init() }
+
+ val unfoldTransitionProgressProvider: Optional<UnfoldTransitionProgressProvider> =
+ if (Flags.unfoldAnimationBackgroundProgress()) {
+ unfoldBgTransitionProgressProviderOptional
+ } else {
+ unfoldTransitionProgressProviderOptional
+ }
+ unfoldTransitionProgressProvider.ifPresent {
+ progressProvider: UnfoldTransitionProgressProvider ->
+ unfoldTransitionProgressForwarder.ifPresent {
+ listener: UnfoldTransitionProgressForwarder ->
+ progressProvider.addCallback(listener)
+ }
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/unfold/UnfoldLightRevealOverlayAnimation.kt b/packages/SystemUI/src/com/android/systemui/unfold/UnfoldLightRevealOverlayAnimation.kt
index b72c6f1..f355dd8 100644
--- a/packages/SystemUI/src/com/android/systemui/unfold/UnfoldLightRevealOverlayAnimation.kt
+++ b/packages/SystemUI/src/com/android/systemui/unfold/UnfoldLightRevealOverlayAnimation.kt
@@ -18,42 +18,22 @@
import android.annotation.BinderThread
import android.content.ContentResolver
import android.content.Context
-import android.graphics.PixelFormat
import android.hardware.devicestate.DeviceStateManager
-import android.hardware.devicestate.DeviceStateManager.FoldStateListener
-import android.hardware.display.DisplayManager
import android.hardware.input.InputManagerGlobal
import android.os.Handler
-import android.os.Looper
import android.os.Trace
-import android.view.Choreographer
-import android.view.Display
-import android.view.DisplayInfo
-import android.view.Surface
-import android.view.SurfaceControl
-import android.view.SurfaceControlViewHost
-import android.view.SurfaceSession
-import android.view.WindowManager
-import android.view.WindowlessWindowManager
-import com.android.app.tracing.traceSection
-import com.android.keyguard.logging.ScrimLogger
import com.android.systemui.Flags.unfoldAnimationBackgroundProgress
-import com.android.systemui.dagger.qualifiers.Main
-import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.flags.FeatureFlagsClassic
import com.android.systemui.flags.Flags
-import com.android.systemui.settings.DisplayTracker
-import com.android.systemui.statusbar.LightRevealEffect
-import com.android.systemui.statusbar.LightRevealScrim
import com.android.systemui.statusbar.LinearLightRevealEffect
+import com.android.systemui.unfold.FullscreenLightRevealAnimationController.Companion.ALPHA_OPAQUE
+import com.android.systemui.unfold.FullscreenLightRevealAnimationController.Companion.ALPHA_TRANSPARENT
+import com.android.systemui.unfold.FullscreenLightRevealAnimationController.Companion.isVerticalRotation
import com.android.systemui.unfold.UnfoldLightRevealOverlayAnimation.AddOverlayReason.FOLD
import com.android.systemui.unfold.UnfoldLightRevealOverlayAnimation.AddOverlayReason.UNFOLD
-import com.android.systemui.unfold.UnfoldTransitionProgressProvider.TransitionProgressListener
import com.android.systemui.unfold.dagger.UnfoldBg
-import com.android.systemui.unfold.updates.RotationChangeProvider
import com.android.systemui.unfold.util.ScaleAwareTransitionProgressProvider.Companion.areAnimationsEnabled
import com.android.systemui.util.concurrency.ThreadFactory
-import com.android.wm.shell.displayareahelper.DisplayAreaHelper
-import java.util.Optional
import java.util.concurrent.Executor
import java.util.function.Consumer
import javax.inject.Inject
@@ -64,80 +44,43 @@
@Inject
constructor(
private val context: Context,
- private val featureFlags: FeatureFlags,
- private val deviceStateManager: DeviceStateManager,
+ private val featureFlags: FeatureFlagsClassic,
private val contentResolver: ContentResolver,
- private val displayManager: DisplayManager,
+ @UnfoldBg private val unfoldProgressHandler: Handler,
@UnfoldBg
private val unfoldTransitionBgProgressProvider: Provider<UnfoldTransitionProgressProvider>,
private val unfoldTransitionProgressProvider: Provider<UnfoldTransitionProgressProvider>,
- private val displayAreaHelper: Optional<DisplayAreaHelper>,
- @Main private val executor: Executor,
+ private val deviceStateManager: DeviceStateManager,
private val threadFactory: ThreadFactory,
- @UnfoldBg private val rotationChangeProvider: RotationChangeProvider,
- @UnfoldBg private val unfoldProgressHandler: Handler,
- private val displayTracker: DisplayTracker,
- private val scrimLogger: ScrimLogger,
-) {
+ private val fullscreenLightRevealAnimationControllerFactory:
+ FullscreenLightRevealAnimationController.Factory
+) : FullscreenLightRevealAnimation {
private val transitionListener = TransitionListener()
- private val rotationWatcher = RotationWatcher()
-
- private lateinit var bgHandler: Handler
- private lateinit var bgExecutor: Executor
-
- private lateinit var wwm: WindowlessWindowManager
- private lateinit var unfoldedDisplayInfo: DisplayInfo
- private lateinit var overlayContainer: SurfaceControl
-
- private var root: SurfaceControlViewHost? = null
- private var scrimView: LightRevealScrim? = null
private var isFolded: Boolean = false
private var isUnfoldHandled: Boolean = true
- private var overlayAddReason: AddOverlayReason? = null
- private var isTouchBlocked: Boolean = true
+ private var overlayAddReason: AddOverlayReason = UNFOLD
+ private lateinit var controller: FullscreenLightRevealAnimationController
+ private lateinit var bgExecutor: Executor
- private var currentRotation: Int = context.display!!.rotation
-
- fun init() {
+ override fun init() {
// This method will be called only on devices where this animation is enabled,
// so normally this thread won't be created
- bgHandler = unfoldProgressHandler
- bgExecutor = threadFactory.buildDelayableExecutorOnHandler(bgHandler)
+ controller =
+ fullscreenLightRevealAnimationControllerFactory.create(
+ displaySelector = { maxByOrNull { it.naturalWidth } },
+ effectFactory = { LinearLightRevealEffect(it.isVerticalRotation()) },
+ overlayContainerName = SURFACE_CONTAINER_NAME,
+ )
+ controller.init()
+ bgExecutor = threadFactory.buildDelayableExecutorOnHandler(unfoldProgressHandler)
deviceStateManager.registerCallback(bgExecutor, FoldListener())
if (unfoldAnimationBackgroundProgress()) {
unfoldTransitionBgProgressProvider.get().addCallback(transitionListener)
} else {
unfoldTransitionProgressProvider.get().addCallback(transitionListener)
}
- rotationChangeProvider.addCallback(rotationWatcher)
-
- val containerBuilder =
- SurfaceControl.Builder(SurfaceSession())
- .setContainerLayer()
- .setName("unfold-overlay-container")
-
- displayAreaHelper.get().attachToRootDisplayArea(
- displayTracker.defaultDisplayId,
- containerBuilder
- ) { builder ->
- executor.execute {
- overlayContainer = builder.build()
-
- SurfaceControl.Transaction()
- .setLayer(overlayContainer, UNFOLD_OVERLAY_LAYER_Z_INDEX)
- .show(overlayContainer)
- .apply()
-
- wwm =
- WindowlessWindowManager(context.resources.configuration, overlayContainer, null)
- }
- }
-
- // Get unfolded display size immediately as 'current display info' might be
- // not up-to-date during unfolding
- unfoldedDisplayInfo = getUnfoldedDisplayInfo()
}
/**
@@ -148,17 +91,18 @@
* @see [com.android.systemui.keyguard.KeyguardViewMediator]
*/
@BinderThread
- fun onScreenTurningOn(onOverlayReady: Runnable) {
+ override fun onScreenTurningOn(onOverlayReady: Runnable) {
executeInBackground {
Trace.beginSection("$TAG#onScreenTurningOn")
try {
// Add the view only if we are unfolding and this is the first screen on
if (!isFolded && !isUnfoldHandled && contentResolver.areAnimationsEnabled()) {
- addOverlay(onOverlayReady, reason = UNFOLD)
+ overlayAddReason = UNFOLD
+ controller.addOverlay(calculateRevealAmount(), onOverlayReady)
isUnfoldHandled = true
} else {
// No unfold transition, immediately report that overlay is ready
- ensureOverlayRemoved()
+ controller.ensureOverlayRemoved()
onOverlayReady.run()
}
} finally {
@@ -167,78 +111,15 @@
}
}
- private fun addOverlay(onOverlayReady: Runnable? = null, reason: AddOverlayReason) {
- if (!::wwm.isInitialized) {
- // Surface overlay is not created yet on the first SysUI launch
- onOverlayReady?.run()
- return
- }
-
- ensureInBackground()
- ensureOverlayRemoved()
-
- overlayAddReason = reason
-
- val newRoot =
- SurfaceControlViewHost(
- context,
- context.display,
- wwm,
- "UnfoldLightRevealOverlayAnimation"
- )
- val params = getLayoutParams()
- val newView =
- LightRevealScrim(
- context,
- attrs = null,
- initialWidth = params.width,
- initialHeight = params.height
- )
- .apply {
- revealEffect = createLightRevealEffect()
- revealAmount = calculateRevealAmount()
- scrimLogger = this@UnfoldLightRevealOverlayAnimation.scrimLogger
- }
-
- newRoot.setView(newView, params)
-
- if (onOverlayReady != null) {
- Trace.beginAsyncSection("$TAG#relayout", 0)
-
- newRoot.relayout(params) { transaction ->
- val vsyncId = Choreographer.getSfInstance().vsyncId
-
- // Apply the transaction that contains the first frame of the overlay and apply
- // another empty transaction with 'vsyncId + 1' to make sure that it is actually
- // displayed on the screen. The second transaction is necessary to remove the screen
- // blocker (turn on the brightness) only when the content is actually visible as it
- // might be presented only in the next frame.
- // See b/197538198
- transaction.setFrameTimelineVsync(vsyncId).apply()
-
- transaction
- .setFrameTimelineVsync(vsyncId + 1)
- .addTransactionCommittedListener(bgExecutor) {
- Trace.endAsyncSection("$TAG#relayout", 0)
- onOverlayReady.run()
- }
- .apply()
- }
- }
-
- scrimView = newView
- root = newRoot
- }
-
private fun calculateRevealAmount(animationProgress: Float? = null): Float {
- val overlayAddReason = overlayAddReason ?: UNFOLD
+ val overlayAddReason = overlayAddReason
if (animationProgress == null) {
- // Animation progress is unknown, calculate the initial value based on the overlay
+ // Animation progress unknown, calculate the initial value based on the overlay
// add reason
return when (overlayAddReason) {
- FOLD -> TRANSPARENT
- UNFOLD -> BLACK
+ FOLD -> ALPHA_TRANSPARENT
+ UNFOLD -> ALPHA_OPAQUE
}
}
@@ -249,144 +130,57 @@
// Do not darken the content when SHOW_VIGNETTE_WHEN_FOLDING flag is off
// and we are folding the device. We still add the overlay to block touches
// while the animation is running but the overlay is transparent.
- TRANSPARENT
+ ALPHA_TRANSPARENT
} else {
animationProgress
}
}
- private fun getLayoutParams(): WindowManager.LayoutParams {
- val params: WindowManager.LayoutParams = WindowManager.LayoutParams()
+ private inner class TransitionListener :
+ UnfoldTransitionProgressProvider.TransitionProgressListener {
- val rotation = currentRotation
- val isNatural = rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_180
-
- params.height =
- if (isNatural) unfoldedDisplayInfo.naturalHeight else unfoldedDisplayInfo.naturalWidth
- params.width =
- if (isNatural) unfoldedDisplayInfo.naturalWidth else unfoldedDisplayInfo.naturalHeight
-
- params.format = PixelFormat.TRANSLUCENT
- params.type = WindowManager.LayoutParams.TYPE_DISPLAY_OVERLAY
- params.title = "Unfold Light Reveal Animation"
- params.layoutInDisplayCutoutMode =
- WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS
- params.fitInsetsTypes = 0
-
- val touchFlags =
- if (isTouchBlocked) {
- // Touchable by default, so it will block the touches
- 0
- } else {
- WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE
- }
- params.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or touchFlags
- params.setTrustedOverlay()
-
- val packageName: String = context.opPackageName
- params.packageName = packageName
-
- return params
- }
-
- private fun updateTouchBlockIfNeeded(progress: Float) {
- // When unfolding unblock touches a bit earlier than the animation end as the
- // interpolation has a long tail of very slight movement at the end which should not
- // affect much the usage of the device
- val shouldBlockTouches =
- if (overlayAddReason == UNFOLD) {
- progress < UNFOLD_BLOCK_TOUCHES_UNTIL_PROGRESS
- } else {
- true
- }
-
- if (isTouchBlocked != shouldBlockTouches) {
- isTouchBlocked = shouldBlockTouches
-
- traceSection("$TAG#relayoutToUpdateTouch") { root?.relayout(getLayoutParams()) }
- }
- }
-
- private fun createLightRevealEffect(): LightRevealEffect {
- val isVerticalFold =
- currentRotation == Surface.ROTATION_0 || currentRotation == Surface.ROTATION_180
- return LinearLightRevealEffect(isVertical = isVerticalFold)
- }
-
- private fun ensureOverlayRemoved() {
- ensureInBackground()
- traceSection("ensureOverlayRemoved") {
- root?.release()
- root = null
- scrimView = null
- }
- }
-
- private fun getUnfoldedDisplayInfo(): DisplayInfo =
- displayManager
- .getDisplays(DisplayManager.DISPLAY_CATEGORY_ALL_INCLUDING_DISABLED)
- .asSequence()
- .map { DisplayInfo().apply { it.getDisplayInfo(this) } }
- .filter { it.type == Display.TYPE_INTERNAL }
- .maxByOrNull { it.naturalWidth }!!
-
- private inner class TransitionListener : TransitionProgressListener {
-
- override fun onTransitionProgress(progress: Float) {
- executeInBackground {
- scrimView?.revealAmount = calculateRevealAmount(progress)
- updateTouchBlockIfNeeded(progress)
- }
+ override fun onTransitionProgress(progress: Float) = executeInBackground {
+ controller.updateRevealAmount(calculateRevealAmount(progress))
+ // When unfolding unblock touches a bit earlier than the animation end as the
+ // interpolation has a long tail of very slight movement at the end which should not
+ // affect much the usage of the device
+ controller.isTouchBlocked =
+ overlayAddReason == FOLD || progress < UNFOLD_BLOCK_TOUCHES_UNTIL_PROGRESS
}
- override fun onTransitionFinished() {
- executeInBackground { ensureOverlayRemoved() }
+ override fun onTransitionFinished() = executeInBackground {
+ controller.ensureOverlayRemoved()
}
override fun onTransitionStarted() {
// Add view for folding case (when unfolding the view is added earlier)
- if (scrimView == null) {
- executeInBackground { addOverlay(reason = FOLD) }
+ if (controller.isOverlayVisible()) {
+ executeInBackground {
+ overlayAddReason = FOLD
+ controller.addOverlay(calculateRevealAmount())
+ }
}
// Disable input dispatching during transition.
InputManagerGlobal.getInstance().cancelCurrentTouch()
}
}
- private inner class RotationWatcher : RotationChangeProvider.RotationListener {
- override fun onRotationChanged(newRotation: Int) {
- executeInBackground {
- traceSection("$TAG#onRotationChanged") {
- if (currentRotation != newRotation) {
- currentRotation = newRotation
- scrimView?.revealEffect = createLightRevealEffect()
- root?.relayout(getLayoutParams())
- }
- }
- }
- }
- }
-
private fun executeInBackground(f: () -> Unit) {
// This is needed to allow progresses to be received both from the main thread (that will
// schedule a runnable on the bg thread), and from the bg thread directly (no reposting).
- if (bgHandler.looper.isCurrentThread) {
+ if (unfoldProgressHandler.looper.isCurrentThread) {
f()
} else {
- bgHandler.post(f)
+ unfoldProgressHandler.post(f)
}
}
- private fun ensureInBackground() {
- check(Looper.myLooper() == bgHandler.looper) { "Not being executed in the background!" }
- }
-
private inner class FoldListener :
- FoldStateListener(
+ DeviceStateManager.FoldStateListener(
context,
Consumer { isFolded ->
if (isFolded) {
- ensureOverlayRemoved()
+ controller.ensureOverlayRemoved()
isUnfoldHandled = false
}
this.isFolded = isFolded
@@ -400,16 +194,7 @@
private companion object {
const val TAG = "UnfoldLightRevealOverlayAnimation"
- const val ROTATION_ANIMATION_OVERLAY_Z_INDEX = Integer.MAX_VALUE
-
- // Put the unfold overlay below the rotation animation screenshot to hide the moment
- // when it is rotated but the rotation of the other windows hasn't happen yet
- const val UNFOLD_OVERLAY_LAYER_Z_INDEX = ROTATION_ANIMATION_OVERLAY_Z_INDEX - 1
-
- // constants for revealAmount.
- const val TRANSPARENT = 1f
- const val BLACK = 0f
-
- private const val UNFOLD_BLOCK_TOUCHES_UNTIL_PROGRESS = 0.8f
+ const val SURFACE_CONTAINER_NAME = "unfold-overlay-container"
+ const val UNFOLD_BLOCK_TOUCHES_UNTIL_PROGRESS = 0.8f
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/user/domain/interactor/SelectedUserInteractor.kt b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/SelectedUserInteractor.kt
index 0fb4b43..38b381a 100644
--- a/packages/SystemUI/src/com/android/systemui/user/domain/interactor/SelectedUserInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/SelectedUserInteractor.kt
@@ -1,6 +1,7 @@
package com.android.systemui.user.domain.interactor
import android.annotation.UserIdInt
+import android.content.pm.UserInfo
import com.android.keyguard.KeyguardUpdateMonitor
import com.android.systemui.Flags.refactorGetCurrentUser
import com.android.systemui.dagger.SysUISingleton
@@ -16,6 +17,9 @@
/** Flow providing the ID of the currently selected user. */
val selectedUser = repository.selectedUserInfo.map { it.id }.distinctUntilChanged()
+ /** Flow providing the [UserInfo] of the currently selected user. */
+ val selectedUserInfo = repository.selectedUserInfo
+
/**
* Returns the ID of the currently-selected user.
*
diff --git a/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserSwitcherInteractor.kt b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserSwitcherInteractor.kt
index c170eb5..a122311 100644
--- a/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserSwitcherInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserSwitcherInteractor.kt
@@ -27,7 +27,6 @@
import android.graphics.drawable.BitmapDrawable
import android.graphics.drawable.Drawable
import android.graphics.drawable.Icon
-import android.os.Process
import android.os.RemoteException
import android.os.UserHandle
import android.os.UserManager
@@ -50,6 +49,7 @@
import com.android.systemui.flags.Flags
import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
import com.android.systemui.plugins.ActivityStarter
+import com.android.systemui.process.ProcessWrapper
import com.android.systemui.qs.user.UserSwitchDialogController
import com.android.systemui.res.R
import com.android.systemui.telephony.domain.interactor.TelephonyInteractor
@@ -108,6 +108,7 @@
private val guestUserInteractor: GuestUserInteractor,
private val uiEventLogger: UiEventLogger,
private val userRestrictionChecker: UserRestrictionChecker,
+ private val processWrapper: ProcessWrapper
) {
/**
* Defines interface for classes that can be notified when the state of users on the device is
@@ -669,7 +670,7 @@
// Connect to the new secondary user's service (purely to ensure that a persistent
// SystemUI application is created for that user)
- if (userId != Process.myUserHandle().identifier) {
+ if (userId != processWrapper.myUserHandle().identifier && !processWrapper.isSystemUser) {
applicationContext.startServiceAsUser(
intent,
UserHandle.of(userId),
diff --git a/packages/SystemUI/src/com/android/systemui/util/concurrency/PendingTasksContainer.kt b/packages/SystemUI/src/com/android/systemui/util/concurrency/PendingTasksContainer.kt
index ceebcb7..e5179dd 100644
--- a/packages/SystemUI/src/com/android/systemui/util/concurrency/PendingTasksContainer.kt
+++ b/packages/SystemUI/src/com/android/systemui/util/concurrency/PendingTasksContainer.kt
@@ -37,19 +37,13 @@
*/
fun registerTask(name: String): Runnable {
pendingTasksCount.incrementAndGet()
-
- if (ENABLE_TRACE) {
- Trace.beginAsyncSection("PendingTasksContainer#$name", 0)
- }
+ Trace.beginAsyncSection("PendingTasksContainer#$name", 0)
return Runnable {
+ Trace.endAsyncSection("PendingTasksContainer#$name", 0)
if (pendingTasksCount.decrementAndGet() == 0) {
val onComplete = completionCallback.getAndSet(null)
onComplete?.run()
-
- if (ENABLE_TRACE) {
- Trace.endAsyncSection("PendingTasksContainer#$name", 0)
- }
}
}
}
@@ -82,4 +76,3 @@
fun getPendingCount(): Int = pendingTasksCount.get()
}
-private const val ENABLE_TRACE = false
diff --git a/packages/SystemUI/src/com/android/systemui/util/kotlin/SharedPreferencesExt.kt b/packages/SystemUI/src/com/android/systemui/util/kotlin/SharedPreferencesExt.kt
new file mode 100644
index 0000000..ab6a37b
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/util/kotlin/SharedPreferencesExt.kt
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.util.kotlin
+
+import android.content.SharedPreferences
+import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.mapNotNull
+
+object SharedPreferencesExt {
+ /**
+ * Returns a flow of [Unit] that is invoked each time shared preference is updated.
+ *
+ * @param key Optional key to limit updates to a particular key.
+ */
+ fun SharedPreferences.observe(key: String? = null): Flow<Unit> =
+ conflatedCallbackFlow {
+ val listener =
+ SharedPreferences.OnSharedPreferenceChangeListener { _, key -> trySend(key) }
+ registerOnSharedPreferenceChangeListener(listener)
+ awaitClose { unregisterOnSharedPreferenceChangeListener(listener) }
+ }
+ .mapNotNull { changedKey -> if ((key ?: changedKey) == changedKey) Unit else null }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/util/kotlin/Utils.kt b/packages/SystemUI/src/com/android/systemui/util/kotlin/Utils.kt
index 6993c96..fa0d030 100644
--- a/packages/SystemUI/src/com/android/systemui/util/kotlin/Utils.kt
+++ b/packages/SystemUI/src/com/android/systemui/util/kotlin/Utils.kt
@@ -16,6 +16,7 @@
package com.android.systemui.util.kotlin
+import android.content.Context
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
@@ -110,3 +111,11 @@
val fifth: E,
val sixth: F,
)
+
+fun Int.toPx(context: Context): Int {
+ return (this * context.resources.displayMetrics.density).toInt()
+}
+
+fun Int.toDp(context: Context): Int {
+ return (this / context.resources.displayMetrics.density).toInt()
+}
diff --git a/packages/SystemUI/src/com/android/systemui/util/settings/SettingsUtilModule.java b/packages/SystemUI/src/com/android/systemui/util/settings/SettingsUtilModule.java
index f36c335e..d509b2d 100644
--- a/packages/SystemUI/src/com/android/systemui/util/settings/SettingsUtilModule.java
+++ b/packages/SystemUI/src/com/android/systemui/util/settings/SettingsUtilModule.java
@@ -16,6 +16,9 @@
package com.android.systemui.util.settings;
+import com.android.systemui.util.settings.repository.UserAwareSecureSettingsRepository;
+import com.android.systemui.util.settings.repository.UserAwareSecureSettingsRepositoryImpl;
+
import dagger.Binds;
import dagger.Module;
@@ -36,4 +39,9 @@
/** Bind GlobalSettingsImpl to GlobalSettings. */
@Binds
GlobalSettings bindsGlobalSettings(GlobalSettingsImpl impl);
+
+ /** Bind UserAwareSecureSettingsRepositoryImpl to UserAwareSecureSettingsRepository. */
+ @Binds
+ UserAwareSecureSettingsRepository bindsUserAwareSecureSettingsRepository(
+ UserAwareSecureSettingsRepositoryImpl impl);
}
diff --git a/packages/SystemUI/src/com/android/systemui/util/settings/repository/UserAwareSecureSettingsRepository.kt b/packages/SystemUI/src/com/android/systemui/util/settings/repository/UserAwareSecureSettingsRepository.kt
new file mode 100644
index 0000000..d3e5080
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/util/settings/repository/UserAwareSecureSettingsRepository.kt
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.util.settings.repository
+
+import android.provider.Settings
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.user.data.repository.UserRepository
+import com.android.systemui.util.settings.SecureSettings
+import com.android.systemui.util.settings.SettingsProxy
+import com.android.systemui.util.settings.SettingsProxyExt.observerFlow
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.onStart
+import javax.inject.Inject
+
+/**
+ * Repository for observing values of [Settings.Secure] for the currently active user. That means
+ * when user is switched and the new user has different value, flow will emit new value.
+ */
+interface UserAwareSecureSettingsRepository {
+
+ /**
+ * Emits boolean value of the setting for active user. Also emits starting value when
+ * subscribed.
+ * See: [SettingsProxy.getBool].
+ */
+ fun boolSettingForActiveUser(name: String, defaultValue: Boolean = false): Flow<Boolean>
+}
+
+@SysUISingleton
+@OptIn(ExperimentalCoroutinesApi::class)
+class UserAwareSecureSettingsRepositoryImpl @Inject constructor(
+ private val secureSettings: SecureSettings,
+ private val userRepository: UserRepository,
+ @Background private val backgroundDispatcher: CoroutineDispatcher,
+) : UserAwareSecureSettingsRepository {
+
+ override fun boolSettingForActiveUser(name: String, defaultValue: Boolean): Flow<Boolean> =
+ userRepository.selectedUserInfo
+ .flatMapLatest { userInfo -> settingObserver(name, defaultValue, userInfo.id) }
+ .distinctUntilChanged()
+ .flowOn(backgroundDispatcher)
+
+ private fun settingObserver(name: String, defaultValue: Boolean, userId: Int): Flow<Boolean> {
+ return secureSettings
+ .observerFlow(userId, name)
+ .onStart { emit(Unit) }
+ .map { secureSettings.getBoolForUser(name, defaultValue, userId) }
+ }
+}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/util/view/DisposableHandleExt.kt b/packages/SystemUI/src/com/android/systemui/util/view/DisposableHandleExt.kt
deleted file mode 100644
index d3653b4..0000000
--- a/packages/SystemUI/src/com/android/systemui/util/view/DisposableHandleExt.kt
+++ /dev/null
@@ -1,35 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.util.view
-
-import android.view.View
-import com.android.systemui.util.kotlin.awaitCancellationThenDispose
-import kotlinx.coroutines.DisposableHandle
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.collectLatest
-
-/**
- * Use the [bind] method to bind the view every time this flow emits, and suspend to await for more
- * updates. New emissions lead to the previous binding call being cancelled if not completed.
- * Dispose of the [DisposableHandle] returned by [bind] when done.
- */
-suspend fun <T : View> Flow<T>.bindLatest(bind: (T) -> DisposableHandle?) {
- this.collectLatest { view ->
- val disposableHandle = bind(view)
- disposableHandle?.awaitCancellationThenDispose()
- }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java
index ce6d740..90c5c62 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java
@@ -116,9 +116,8 @@
import com.android.settingslib.Utils;
import com.android.systemui.Dumpable;
import com.android.systemui.Prefs;
-import com.android.systemui.dagger.qualifiers.Application;
-import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.dump.DumpManager;
+import com.android.systemui.haptics.slider.HapticSliderViewBinder;
import com.android.systemui.haptics.slider.SeekableSliderHapticPlugin;
import com.android.systemui.haptics.slider.SliderHapticFeedbackConfig;
import com.android.systemui.media.dialog.MediaOutputDialogFactory;
@@ -145,9 +144,6 @@
import java.util.List;
import java.util.function.Consumer;
-import kotlinx.coroutines.CoroutineDispatcher;
-import kotlinx.coroutines.CoroutineScope;
-
/**
* Visual presentation of the volume dialog.
*
@@ -311,8 +307,6 @@
private int mOrientation;
private final Lazy<SecureSettings> mSecureSettings;
private int mDialogTimeoutMillis;
- private final CoroutineDispatcher mMainDispatcher;
- private final CoroutineScope mApplicationScope;
private final VibratorHelper mVibratorHelper;
private final com.android.systemui.util.time.SystemClock mSystemClock;
@@ -333,14 +327,10 @@
DumpManager dumpManager,
Lazy<SecureSettings> secureSettings,
VibratorHelper vibratorHelper,
- @Main CoroutineDispatcher mainDispatcher,
- @Application CoroutineScope applicationScope,
com.android.systemui.util.time.SystemClock systemClock) {
mContext =
new ContextThemeWrapper(context, R.style.volume_dialog_theme);
mHandler = new H(looper);
- mMainDispatcher = mainDispatcher;
- mApplicationScope = applicationScope;
mVibratorHelper = vibratorHelper;
mSystemClock = systemClock;
mShouldListenForJank = shouldListenForJank;
@@ -858,7 +848,10 @@
row.header.setFilters(new InputFilter[] {new InputFilter.LengthFilter(13)});
}
row.slider = row.view.findViewById(R.id.volume_row_slider);
- row.createPlugin(mVibratorHelper, mSystemClock, mMainDispatcher, mApplicationScope);
+ if (hapticVolumeSlider()) {
+ row.createPlugin(mVibratorHelper, mSystemClock);
+ HapticSliderViewBinder.bind(row.slider, row.mHapticPlugin);
+ }
row.slider.setOnSeekBarChangeListener(new VolumeSeekBarChangeListener(row));
row.number = row.view.findViewById(R.id.volume_number);
@@ -1498,7 +1491,7 @@
for (int i = 0; i < mRows.size(); i++) {
VolumeRow row = mRows.get(i);
if (row.slider.getVisibility() == VISIBLE) {
- row.addHaptics();
+ row.addTouchListener();
}
}
Trace.endSection();
@@ -2620,17 +2613,13 @@
void createPlugin(
VibratorHelper vibratorHelper,
- com.android.systemui.util.time.SystemClock systemClock,
- CoroutineDispatcher mainDispatcher,
- CoroutineScope applicationScope) {
- if (!hapticVolumeSlider() || mHapticPlugin != null) return;
+ com.android.systemui.util.time.SystemClock systemClock) {
+ if (mHapticPlugin != null) return;
mHapticPlugin = new SeekableSliderHapticPlugin(
- vibratorHelper,
- systemClock,
- mainDispatcher,
- applicationScope,
- sSliderHapticFeedbackConfig);
+ vibratorHelper,
+ systemClock,
+ sSliderHapticFeedbackConfig);
}
@@ -2647,19 +2636,9 @@
});
}
- void addHaptics() {
- if (mHapticPlugin != null) {
- addTouchListener();
- mHapticPlugin.start();
- }
- }
-
@SuppressLint("ClickableViewAccessibility")
void removeHaptics() {
slider.setOnTouchListener(null);
- if (mHapticPlugin != null) {
- mHapticPlugin.stop();
- }
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dagger/AudioModule.kt b/packages/SystemUI/src/com/android/systemui/volume/dagger/AudioModule.kt
index ff1daea..1af5c46 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dagger/AudioModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/dagger/AudioModule.kt
@@ -18,9 +18,14 @@
import android.content.Context
import android.media.AudioManager
+import com.android.settingslib.media.data.repository.SpatializerRepository
+import com.android.settingslib.media.data.repository.SpatializerRepositoryImpl
+import com.android.settingslib.media.domain.interactor.SpatializerInteractor
import com.android.settingslib.volume.data.repository.AudioRepository
import com.android.settingslib.volume.data.repository.AudioRepositoryImpl
import com.android.settingslib.volume.domain.interactor.AudioModeInteractor
+import com.android.settingslib.volume.shared.AudioManagerIntentsReceiver
+import com.android.settingslib.volume.shared.AudioManagerIntentsReceiverImpl
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.dagger.qualifiers.Background
import dagger.Module
@@ -35,16 +40,33 @@
companion object {
@Provides
- fun provideAudioRepository(
+ fun provideAudioManagerIntentsReceiver(
@Application context: Context,
+ @Application coroutineScope: CoroutineScope,
+ ): AudioManagerIntentsReceiver = AudioManagerIntentsReceiverImpl(context, coroutineScope)
+
+ @Provides
+ fun provideAudioRepository(
+ intentsReceiver: AudioManagerIntentsReceiver,
audioManager: AudioManager,
@Background coroutineContext: CoroutineContext,
@Application coroutineScope: CoroutineScope,
): AudioRepository =
- AudioRepositoryImpl(context, audioManager, coroutineContext, coroutineScope)
+ AudioRepositoryImpl(intentsReceiver, audioManager, coroutineContext, coroutineScope)
@Provides
fun provideAudioModeInteractor(repository: AudioRepository): AudioModeInteractor =
AudioModeInteractor(repository)
+
+ @Provides
+ fun provdieSpatializerRepository(
+ audioManager: AudioManager,
+ @Background backgroundContext: CoroutineContext,
+ ): SpatializerRepository =
+ SpatializerRepositoryImpl(audioManager.spatializer, backgroundContext)
+
+ @Provides
+ fun provideSpatializerInetractor(repository: SpatializerRepository): SpatializerInteractor =
+ SpatializerInteractor(repository)
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dagger/CaptioningModule.kt b/packages/SystemUI/src/com/android/systemui/volume/dagger/CaptioningModule.kt
new file mode 100644
index 0000000..ea67eea
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/dagger/CaptioningModule.kt
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2024 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.volume.dagger
+
+import android.view.accessibility.CaptioningManager
+import com.android.settingslib.view.accessibility.data.repository.CaptioningRepository
+import com.android.settingslib.view.accessibility.data.repository.CaptioningRepositoryImpl
+import com.android.settingslib.view.accessibility.domain.interactor.CaptioningInteractor
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.Background
+import dagger.Module
+import dagger.Provides
+import kotlin.coroutines.CoroutineContext
+import kotlinx.coroutines.CoroutineScope
+
+@Module
+interface CaptioningModule {
+
+ companion object {
+
+ @Provides
+ fun provideCaptioningRepository(
+ captioningManager: CaptioningManager,
+ @Background coroutineContext: CoroutineContext,
+ @Application coroutineScope: CoroutineScope,
+ ): CaptioningRepository =
+ CaptioningRepositoryImpl(captioningManager, coroutineContext, coroutineScope)
+
+ @Provides
+ fun provideCaptioningInteractor(repository: CaptioningRepository): CaptioningInteractor =
+ CaptioningInteractor(repository)
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dagger/MediaDevicesModule.kt b/packages/SystemUI/src/com/android/systemui/volume/dagger/MediaDevicesModule.kt
new file mode 100644
index 0000000..ab76d45
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/dagger/MediaDevicesModule.kt
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2024 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.volume.dagger
+
+import android.media.session.MediaSessionManager
+import com.android.settingslib.bluetooth.LocalBluetoothManager
+import com.android.settingslib.volume.data.repository.MediaControllerRepository
+import com.android.settingslib.volume.data.repository.MediaControllerRepositoryImpl
+import com.android.settingslib.volume.shared.AudioManagerIntentsReceiver
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.Background
+import dagger.Module
+import dagger.Provides
+import kotlin.coroutines.CoroutineContext
+import kotlinx.coroutines.CoroutineScope
+
+@Module
+interface MediaDevicesModule {
+
+ companion object {
+
+ @Provides
+ @SysUISingleton
+ fun provideMediaDeviceSessionRepository(
+ intentsReceiver: AudioManagerIntentsReceiver,
+ mediaSessionManager: MediaSessionManager,
+ localBluetoothManager: LocalBluetoothManager?,
+ @Application coroutineScope: CoroutineScope,
+ @Background backgroundContext: CoroutineContext,
+ ): MediaControllerRepository =
+ MediaControllerRepositoryImpl(
+ intentsReceiver,
+ mediaSessionManager,
+ localBluetoothManager,
+ coroutineScope,
+ backgroundContext,
+ )
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dagger/VolumeModule.java b/packages/SystemUI/src/com/android/systemui/volume/dagger/VolumeModule.java
index c842e5f..3285637 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dagger/VolumeModule.java
+++ b/packages/SystemUI/src/com/android/systemui/volume/dagger/VolumeModule.java
@@ -23,8 +23,6 @@
import com.android.internal.jank.InteractionJankMonitor;
import com.android.systemui.CoreStartable;
-import com.android.systemui.dagger.qualifiers.Application;
-import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.dump.DumpManager;
import com.android.systemui.media.dialog.MediaOutputDialogFactory;
import com.android.systemui.plugins.VolumeDialog;
@@ -55,13 +53,12 @@
import dagger.multibindings.IntoMap;
import dagger.multibindings.IntoSet;
-import kotlinx.coroutines.CoroutineDispatcher;
-import kotlinx.coroutines.CoroutineScope;
-
/** Dagger Module for code in the volume package. */
@Module(
includes = {
AudioModule.class,
+ CaptioningModule.class,
+ MediaDevicesModule.class
},
subcomponents = {
VolumePanelComponent.class
@@ -110,8 +107,6 @@
DumpManager dumpManager,
Lazy<SecureSettings> secureSettings,
VibratorHelper vibratorHelper,
- @Main CoroutineDispatcher mainDispatcher,
- @Application CoroutineScope applicationScope,
SystemClock systemClock) {
VolumeDialogImpl impl = new VolumeDialogImpl(
context,
@@ -130,8 +125,6 @@
dumpManager,
secureSettings,
vibratorHelper,
- mainDispatcher,
- applicationScope,
systemClock);
impl.setStreamImportant(AudioManager.STREAM_SYSTEM, false);
impl.setAutomute(true);
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/data/repository/LocalMediaRepositoryFactory.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/data/repository/LocalMediaRepositoryFactory.kt
new file mode 100644
index 0000000..0a1ee24
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/data/repository/LocalMediaRepositoryFactory.kt
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2024 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.volume.panel.component.mediaoutput.data.repository
+
+import android.media.MediaRouter2Manager
+import com.android.settingslib.volume.data.repository.LocalMediaRepository
+import com.android.settingslib.volume.data.repository.LocalMediaRepositoryImpl
+import com.android.settingslib.volume.shared.AudioManagerIntentsReceiver
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.media.controls.pipeline.LocalMediaManagerFactory
+import javax.inject.Inject
+import kotlin.coroutines.CoroutineContext
+import kotlinx.coroutines.CoroutineScope
+
+class LocalMediaRepositoryFactory
+@Inject
+constructor(
+ private val intentsReceiver: AudioManagerIntentsReceiver,
+ private val mediaRouter2Manager: MediaRouter2Manager,
+ private val localMediaManagerFactory: LocalMediaManagerFactory,
+ @Application private val coroutineScope: CoroutineScope,
+ @Background private val backgroundCoroutineContext: CoroutineContext,
+) {
+
+ fun create(packageName: String?): LocalMediaRepository =
+ LocalMediaRepositoryImpl(
+ intentsReceiver,
+ localMediaManagerFactory.create(packageName),
+ mediaRouter2Manager,
+ coroutineScope,
+ backgroundCoroutineContext,
+ )
+}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputInteractor.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputInteractor.kt
new file mode 100644
index 0000000..6c456f9
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputInteractor.kt
@@ -0,0 +1,98 @@
+/*
+ * Copyright (C) 2024 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.volume.panel.component.mediaoutput.domain.interactor
+
+import android.content.pm.PackageManager
+import android.util.Log
+import com.android.settingslib.media.MediaDevice
+import com.android.settingslib.volume.data.repository.LocalMediaRepository
+import com.android.settingslib.volume.data.repository.MediaControllerRepository
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.volume.panel.component.mediaoutput.data.repository.LocalMediaRepositoryFactory
+import com.android.systemui.volume.panel.component.mediaoutput.domain.model.MediaDeviceSession
+import com.android.systemui.volume.panel.dagger.scope.VolumePanelScope
+import javax.inject.Inject
+import kotlin.coroutines.CoroutineContext
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.mapNotNull
+import kotlinx.coroutines.flow.shareIn
+import kotlinx.coroutines.withContext
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@VolumePanelScope
+class MediaOutputInteractor
+@Inject
+constructor(
+ private val localMediaRepositoryFactory: LocalMediaRepositoryFactory,
+ private val packageManager: PackageManager,
+ @VolumePanelScope private val coroutineScope: CoroutineScope,
+ @Background private val backgroundCoroutineContext: CoroutineContext,
+ mediaControllerRepository: MediaControllerRepository
+) {
+
+ val mediaDeviceSession: Flow<MediaDeviceSession> =
+ mediaControllerRepository.activeMediaController.mapNotNull { mediaController ->
+ if (mediaController == null) {
+ MediaDeviceSession.Inactive
+ } else {
+ MediaDeviceSession.Active(
+ appLabel = getApplicationLabel(mediaController.packageName)
+ ?: return@mapNotNull null,
+ packageName = mediaController.packageName,
+ sessionToken = mediaController.sessionToken,
+ )
+ }
+ }
+ private val localMediaRepository: Flow<LocalMediaRepository> =
+ mediaDeviceSession
+ .map { (it as? MediaDeviceSession.Active)?.packageName }
+ .distinctUntilChanged()
+ .map { localMediaRepositoryFactory.create(it) }
+ .shareIn(coroutineScope, SharingStarted.WhileSubscribed(), replay = 1)
+
+ val currentConnectedDevice: Flow<MediaDevice?> =
+ localMediaRepository.flatMapLatest { it.currentConnectedDevice }
+
+ val mediaDevices: Flow<Collection<MediaDevice>> =
+ localMediaRepository.flatMapLatest { it.mediaDevices }
+
+ private suspend fun getApplicationLabel(packageName: String): CharSequence? {
+ return try {
+ withContext(backgroundCoroutineContext) {
+ val appInfo =
+ packageManager.getApplicationInfo(
+ packageName,
+ PackageManager.MATCH_DISABLED_COMPONENTS or PackageManager.MATCH_ANY_USER
+ )
+ appInfo.loadLabel(packageManager)
+ }
+ } catch (e: PackageManager.NameNotFoundException) {
+ Log.e(TAG, "Unable to find info for package: $packageName")
+ null
+ }
+ }
+
+ private companion object {
+ const val TAG = "MediaOutputInteractor"
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/model/MediaDeviceSession.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/model/MediaDeviceSession.kt
new file mode 100644
index 0000000..f250308
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/model/MediaDeviceSession.kt
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2024 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.volume.panel.component.mediaoutput.domain.model
+
+import android.media.session.MediaSession
+
+/** Represents media playing on the connected device. */
+sealed interface MediaDeviceSession {
+
+ /** Media is playing. */
+ data class Active(
+ val appLabel: CharSequence,
+ val packageName: String,
+ val sessionToken: MediaSession.Token,
+ ) : MediaDeviceSession
+
+ /** Media is not playing. */
+ data object Inactive : MediaDeviceSession
+
+ /** Current media state is unknown yet. */
+ data object Unknown : MediaDeviceSession
+}
diff --git a/packages/SystemUI/src/com/android/systemui/wallet/controller/QuickAccessWalletController.java b/packages/SystemUI/src/com/android/systemui/wallet/controller/QuickAccessWalletController.java
index e0228d9..1d9b90a 100644
--- a/packages/SystemUI/src/com/android/systemui/wallet/controller/QuickAccessWalletController.java
+++ b/packages/SystemUI/src/com/android/systemui/wallet/controller/QuickAccessWalletController.java
@@ -34,7 +34,7 @@
import android.service.quickaccesswallet.QuickAccessWalletClientImpl;
import android.util.Log;
-import com.android.systemui.animation.ActivityLaunchAnimator;
+import com.android.systemui.animation.ActivityTransitionAnimator;
import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.dagger.qualifiers.Background;
import com.android.systemui.dagger.qualifiers.Main;
@@ -236,12 +236,12 @@
* that too is null, then fall back to {@link WalletActivity}.
*
* @param activityStarter an {@link ActivityStarter} to launch the Intent or PendingIntent.
- * @param animationController an {@link ActivityLaunchAnimator.Controller} to provide a
+ * @param animationController an {@link ActivityTransitionAnimator.Controller} to provide a
* smooth animation for the activity launch.
* @param hasCard whether the service returns any cards.
*/
public void startQuickAccessUiIntent(ActivityStarter activityStarter,
- ActivityLaunchAnimator.Controller animationController,
+ ActivityTransitionAnimator.Controller animationController,
boolean hasCard) {
mQuickAccessWalletClient.getWalletPendingIntent(mExecutor,
walletPendingIntent -> {
@@ -271,7 +271,7 @@
private void startQuickAccessViaIntent(Intent intent,
boolean hasCard,
ActivityStarter activityStarter,
- ActivityLaunchAnimator.Controller animationController) {
+ ActivityTransitionAnimator.Controller animationController) {
if (hasCard) {
activityStarter.startActivity(intent, true /* dismissShade */,
animationController, true /* showOverLockscreenWhenLocked */);
@@ -285,7 +285,7 @@
private void startQuickAccessViaPendingIntent(PendingIntent pendingIntent,
ActivityStarter activityStarter,
- ActivityLaunchAnimator.Controller animationController) {
+ ActivityTransitionAnimator.Controller animationController) {
activityStarter.postStartActivityDismissingKeyguard(
pendingIntent,
animationController);
diff --git a/packages/SystemUI/src/com/android/systemui/wmshell/BubblesManager.java b/packages/SystemUI/src/com/android/systemui/wmshell/BubblesManager.java
index 2acd4b9..139d190 100644
--- a/packages/SystemUI/src/com/android/systemui/wmshell/BubblesManager.java
+++ b/packages/SystemUI/src/com/android/systemui/wmshell/BubblesManager.java
@@ -111,6 +111,7 @@
// TODO (b/145659174): allow for multiple callbacks to support the "shadow" new notif pipeline
private final List<NotifCallback> mCallbacks = new ArrayList<>();
private final StatusBarWindowCallback mStatusBarWindowCallback;
+ private boolean mPanelExpanded;
/**
* Creates {@link BubblesManager}, returns {@code null} if Optional {@link Bubbles} not present
@@ -242,8 +243,12 @@
// Store callback in a field so it won't get GC'd
mStatusBarWindowCallback =
(keyguardShowing, keyguardOccluded, keyguardGoingAway, bouncerShowing, isDozing,
- panelExpanded, isDreaming) ->
+ panelExpanded, isDreaming) -> {
+ if (panelExpanded != mPanelExpanded) {
+ mPanelExpanded = panelExpanded;
mBubbles.onNotificationPanelExpandedChanged(panelExpanded);
+ }
+ };
notificationShadeWindowController.registerCallback(mStatusBarWindowCallback);
mSysuiProxy = new Bubbles.SysuiProxy() {
diff --git a/packages/SystemUI/tests/Android.bp b/packages/SystemUI/tests/Android.bp
index ec0414e..88939a2 100644
--- a/packages/SystemUI/tests/Android.bp
+++ b/packages/SystemUI/tests/Android.bp
@@ -15,6 +15,7 @@
//
package {
+ default_team: "trendy_team_system_ui_please_use_a_more_specific_subteam_if_possible_",
default_applicable_licenses: ["frameworks_base_packages_SystemUI_license"],
}
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPinViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPinViewControllerTest.kt
index c2efc05..d048cbe 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPinViewControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPinViewControllerTest.kt
@@ -28,6 +28,7 @@
import com.android.internal.widget.LockscreenCredential
import com.android.keyguard.KeyguardPinViewController.PinBouncerUiEvent
import com.android.keyguard.KeyguardSecurityModel.SecurityMode
+import com.android.keyguard.domain.interactor.KeyguardKeyboardInteractor
import com.android.systemui.SysuiTestCase
import com.android.systemui.classifier.FalsingCollector
import com.android.systemui.classifier.FalsingCollectorFake
@@ -88,6 +89,7 @@
@Mock private val mEmergencyButtonController: EmergencyButtonController? = null
private val falsingCollector: FalsingCollector = FalsingCollectorFake()
+ private val keyguardKeyboardInteractor = KeyguardKeyboardInteractor(FakeKeyboardRepository())
@Mock lateinit var postureController: DevicePostureController
@Mock lateinit var mSelectedUserInteractor: SelectedUserInteractor
@@ -143,7 +145,7 @@
featureFlags,
mSelectedUserInteractor,
uiEventLogger,
- FakeKeyboardRepository()
+ keyguardKeyboardInteractor
)
}
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSimPinViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSimPinViewControllerTest.kt
index 0959f1b..4a2554e 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSimPinViewControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSimPinViewControllerTest.kt
@@ -23,6 +23,7 @@
import androidx.test.filters.SmallTest
import com.android.internal.util.LatencyTracker
import com.android.internal.widget.LockPatternUtils
+import com.android.keyguard.domain.interactor.KeyguardKeyboardInteractor
import com.android.systemui.Flags
import com.android.systemui.SysuiTestCase
import com.android.systemui.classifier.FalsingCollector
@@ -80,6 +81,7 @@
LayoutInflater.from(context).inflate(R.layout.keyguard_sim_pin_view, null)
as KeyguardSimPinView
val fakeFeatureFlags = FakeFeatureFlags()
+ val keyguardKeyboardInteractor = KeyguardKeyboardInteractor(FakeKeyboardRepository())
mSetFlagsRule.enableFlags(Flags.FLAG_REVAMPED_BOUNCER_MESSAGES)
underTest =
@@ -97,7 +99,7 @@
emergencyButtonController,
fakeFeatureFlags,
mSelectedUserInteractor,
- FakeKeyboardRepository()
+ keyguardKeyboardInteractor
)
underTest.init()
underTest.onViewAttached()
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSimPukViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSimPukViewControllerTest.kt
index 1281e44..4f46184 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSimPukViewControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSimPukViewControllerTest.kt
@@ -24,6 +24,7 @@
import androidx.test.filters.SmallTest
import com.android.internal.util.LatencyTracker
import com.android.internal.widget.LockPatternUtils
+import com.android.keyguard.domain.interactor.KeyguardKeyboardInteractor
import com.android.systemui.Flags
import com.android.systemui.SysuiTestCase
import com.android.systemui.classifier.FalsingCollector
@@ -75,6 +76,7 @@
simPukView =
LayoutInflater.from(context).inflate(R.layout.keyguard_sim_puk_view, null)
as KeyguardSimPukView
+ val keyguardKeyboardInteractor = KeyguardKeyboardInteractor(FakeKeyboardRepository())
val fakeFeatureFlags = FakeFeatureFlags()
mSetFlagsRule.enableFlags(Flags.FLAG_REVAMPED_BOUNCER_MESSAGES)
underTest =
@@ -92,7 +94,7 @@
emergencyButtonController,
fakeFeatureFlags,
mSelectedUserInteractor,
- FakeKeyboardRepository()
+ keyguardKeyboardInteractor
)
underTest.init()
}
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/LockIconViewControllerBaseTest.java b/packages/SystemUI/tests/src/com/android/keyguard/LockIconViewControllerBaseTest.java
index da6bfe8..d742da7 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/LockIconViewControllerBaseTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/LockIconViewControllerBaseTest.java
@@ -47,7 +47,6 @@
import com.android.systemui.dump.DumpManager;
import com.android.systemui.flags.FakeFeatureFlags;
import com.android.systemui.keyguard.domain.interactor.KeyguardInteractorFactory;
-import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractorFactory;
import com.android.systemui.kosmos.KosmosJavaAdapter;
import com.android.systemui.plugins.FalsingManager;
import com.android.systemui.plugins.statusbar.StatusBarStateController;
@@ -167,9 +166,7 @@
mVibrator,
mAuthRippleController,
mResources,
- KeyguardTransitionInteractorFactory.create(
- TestScopeProvider.getTestScope().getBackgroundScope())
- .getKeyguardTransitionInteractor(),
+ mKosmos.getKeyguardTransitionInteractor(),
KeyguardInteractorFactory.create(mFeatureFlags).getKeyguardInteractor(),
mFeatureFlags,
mPrimaryBouncerInteractor,
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/mediator/ScreenOnCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/mediator/ScreenOnCoordinatorTest.kt
index b45c894..fb649c8 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/mediator/ScreenOnCoordinatorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/keyguard/mediator/ScreenOnCoordinatorTest.kt
@@ -24,8 +24,8 @@
import com.android.systemui.Flags
import com.android.systemui.SysuiTestCase
import com.android.systemui.unfold.FoldAodAnimationController
+import com.android.systemui.unfold.FullscreenLightRevealAnimation
import com.android.systemui.unfold.SysUIUnfoldComponent
-import com.android.systemui.unfold.UnfoldLightRevealOverlayAnimation
import com.android.systemui.util.mockito.capture
import com.android.systemui.utils.os.FakeHandler
import com.android.systemui.utils.os.FakeHandler.Mode.QUEUEING
@@ -53,7 +53,9 @@
@Mock
private lateinit var foldAodAnimationController: FoldAodAnimationController
@Mock
- private lateinit var unfoldAnimation: UnfoldLightRevealOverlayAnimation
+ private lateinit var fullscreenLightRevealAnimation: FullscreenLightRevealAnimation
+ @Mock
+ private lateinit var fullScreenLightRevealAnimations: Set<FullscreenLightRevealAnimation>
@Captor
private lateinit var readyCaptor: ArgumentCaptor<Runnable>
@@ -67,9 +69,9 @@
@Before
fun setUp() {
MockitoAnnotations.initMocks(this)
-
- `when`(unfoldComponent.getUnfoldLightRevealOverlayAnimation())
- .thenReturn(unfoldAnimation)
+ fullScreenLightRevealAnimations = setOf(fullscreenLightRevealAnimation)
+ `when`(unfoldComponent.getFullScreenLightRevealAnimations())
+ .thenReturn(fullScreenLightRevealAnimations)
`when`(unfoldComponent.getFoldAodAnimationController())
.thenReturn(foldAodAnimationController)
@@ -164,7 +166,7 @@
}
private fun onUnfoldOverlayReady() {
- verify(unfoldAnimation).onScreenTurningOn(capture(readyCaptor))
+ verify(fullscreenLightRevealAnimation).onScreenTurningOn(capture(readyCaptor))
readyCaptor.value.run()
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/CameraAvailabilityListenerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/CameraAvailabilityListenerTest.kt
index e921a59..f776a63 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/CameraAvailabilityListenerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/CameraAvailabilityListenerTest.kt
@@ -344,10 +344,15 @@
}
private fun createAndStartSut(): CameraAvailabilityListener {
- return CameraAvailabilityListener.build(context, context.mainExecutor).also {
- it.addTransitionCallback(cameraTransitionCallback)
- it.startListening()
- }
+ return CameraAvailabilityListener.build(
+ context,
+ context.mainExecutor,
+ CameraProtectionLoaderImpl((context))
+ )
+ .also {
+ it.addTransitionCallback(cameraTransitionCallback)
+ it.startListening()
+ }
}
private class TestCameraTransitionCallback :
diff --git a/packages/SystemUI/tests/src/com/android/systemui/CameraProtectionLoaderImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/CameraProtectionLoaderImplTest.kt
new file mode 100644
index 0000000..a19a0c7
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/CameraProtectionLoaderImplTest.kt
@@ -0,0 +1,134 @@
+/*
+ * Copyright (C) 2024 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.graphics.Rect
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.res.R
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class CameraProtectionLoaderImplTest : SysuiTestCase() {
+
+ private val loader = CameraProtectionLoaderImpl(context)
+
+ @Before
+ fun setUp() {
+ overrideResource(R.string.config_protectedCameraId, OUTER_CAMERA_LOGICAL_ID)
+ overrideResource(R.string.config_protectedPhysicalCameraId, OUTER_CAMERA_PHYSICAL_ID)
+ overrideResource(
+ R.string.config_frontBuiltInDisplayCutoutProtection,
+ OUTER_CAMERA_PROTECTION_PATH
+ )
+ overrideResource(R.string.config_protectedScreenUniqueId, OUTER_SCREEN_UNIQUE_ID)
+ overrideResource(R.string.config_protectedInnerCameraId, INNER_CAMERA_LOGICAL_ID)
+ overrideResource(R.string.config_protectedInnerPhysicalCameraId, INNER_CAMERA_PHYSICAL_ID)
+ overrideResource(
+ R.string.config_innerBuiltInDisplayCutoutProtection,
+ INNER_CAMERA_PROTECTION_PATH
+ )
+ overrideResource(R.string.config_protectedInnerScreenUniqueId, INNER_SCREEN_UNIQUE_ID)
+ }
+
+ @Test
+ fun loadCameraProtectionInfoList() {
+ val protectionList = loadProtectionList()
+
+ assertThat(protectionList)
+ .containsExactly(OUTER_CAMERA_PROTECTION_INFO, INNER_CAMERA_PROTECTION_INFO)
+ }
+
+ @Test
+ fun loadCameraProtectionInfoList_outerCameraIdEmpty_onlyReturnsInnerInfo() {
+ overrideResource(R.string.config_protectedCameraId, "")
+
+ val protectionList = loadProtectionList()
+
+ assertThat(protectionList).containsExactly(INNER_CAMERA_PROTECTION_INFO)
+ }
+
+ @Test
+ fun loadCameraProtectionInfoList_innerCameraIdEmpty_onlyReturnsOuterInfo() {
+ overrideResource(R.string.config_protectedInnerCameraId, "")
+
+ val protectionList = loadProtectionList()
+
+ assertThat(protectionList).containsExactly(OUTER_CAMERA_PROTECTION_INFO)
+ }
+
+ @Test
+ fun loadCameraProtectionInfoList_innerAndOuterCameraIdsEmpty_returnsEmpty() {
+ overrideResource(R.string.config_protectedCameraId, "")
+ overrideResource(R.string.config_protectedInnerCameraId, "")
+
+ val protectionList = loadProtectionList()
+
+ assertThat(protectionList).isEmpty()
+ }
+
+ private fun loadProtectionList() =
+ loader.loadCameraProtectionInfoList().map { it.toTestableVersion() }
+
+ private fun CameraProtectionInfo.toTestableVersion() =
+ TestableProtectionInfo(logicalCameraId, physicalCameraId, cutoutBounds, displayUniqueId)
+
+ /**
+ * "Testable" version, because the original version contains a Path property, which doesn't
+ * implement equals.
+ */
+ private data class TestableProtectionInfo(
+ val logicalCameraId: String,
+ val physicalCameraId: String?,
+ val cutoutBounds: Rect,
+ val displayUniqueId: String?,
+ )
+
+ companion object {
+ private const val OUTER_CAMERA_LOGICAL_ID = "1"
+ private const val OUTER_CAMERA_PHYSICAL_ID = "11"
+ private const val OUTER_CAMERA_PROTECTION_PATH = "M 0,0 H 10,10 V 10,10 H 0,10 Z"
+ private val OUTER_CAMERA_PROTECTION_BOUNDS =
+ Rect(/* left = */ 0, /* top = */ 0, /* right = */ 10, /* bottom = */ 10)
+ private const val OUTER_SCREEN_UNIQUE_ID = "111"
+ private val OUTER_CAMERA_PROTECTION_INFO =
+ TestableProtectionInfo(
+ OUTER_CAMERA_LOGICAL_ID,
+ OUTER_CAMERA_PHYSICAL_ID,
+ OUTER_CAMERA_PROTECTION_BOUNDS,
+ OUTER_SCREEN_UNIQUE_ID,
+ )
+
+ private const val INNER_CAMERA_LOGICAL_ID = "2"
+ private const val INNER_CAMERA_PHYSICAL_ID = "22"
+ private const val INNER_CAMERA_PROTECTION_PATH = "M 0,0 H 20,20 V 20,20 H 0,20 Z"
+ private val INNER_CAMERA_PROTECTION_BOUNDS =
+ Rect(/* left = */ 0, /* top = */ 0, /* right = */ 20, /* bottom = */ 20)
+ private const val INNER_SCREEN_UNIQUE_ID = "222"
+ private val INNER_CAMERA_PROTECTION_INFO =
+ TestableProtectionInfo(
+ INNER_CAMERA_LOGICAL_ID,
+ INNER_CAMERA_PHYSICAL_ID,
+ INNER_CAMERA_PROTECTION_BOUNDS,
+ INNER_SCREEN_UNIQUE_ID,
+ )
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/FakeCameraProtectionLoader.kt b/packages/SystemUI/tests/src/com/android/systemui/FakeCameraProtectionLoader.kt
new file mode 100644
index 0000000..f769b4e
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/FakeCameraProtectionLoader.kt
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2024 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 com.android.systemui.res.R
+
+class FakeCameraProtectionLoader(private val context: SysuiTestableContext) :
+ CameraProtectionLoader {
+
+ private val realLoader = CameraProtectionLoaderImpl(context)
+
+ override fun loadCameraProtectionInfoList(): List<CameraProtectionInfo> =
+ realLoader.loadCameraProtectionInfoList()
+
+ fun clearProtectionInfoList() {
+ context.orCreateTestableResources.addOverride(R.string.config_protectedCameraId, "")
+ context.orCreateTestableResources.addOverride(R.string.config_protectedInnerCameraId, "")
+ }
+
+ fun addAllProtections() {
+ addOuterCameraProtection()
+ addInnerCameraProtection()
+ }
+
+ fun addOuterCameraProtection(displayUniqueId: String = "111") {
+ context.orCreateTestableResources.addOverride(R.string.config_protectedCameraId, "1")
+ context.orCreateTestableResources.addOverride(
+ R.string.config_protectedPhysicalCameraId,
+ "11"
+ )
+ context.orCreateTestableResources.addOverride(
+ R.string.config_frontBuiltInDisplayCutoutProtection,
+ "M 0,0 H 10,10 V 10,10 H 0,10 Z"
+ )
+ context.orCreateTestableResources.addOverride(
+ R.string.config_protectedScreenUniqueId,
+ displayUniqueId
+ )
+ }
+
+ fun addInnerCameraProtection(displayUniqueId: String = "222") {
+ context.orCreateTestableResources.addOverride(R.string.config_protectedInnerCameraId, "2")
+ context.orCreateTestableResources.addOverride(
+ R.string.config_protectedInnerPhysicalCameraId,
+ "22"
+ )
+ context.orCreateTestableResources.addOverride(
+ R.string.config_innerBuiltInDisplayCutoutProtection,
+ "M 0,0 H 20,20 V 20,20 H 0,20 Z"
+ )
+ context.orCreateTestableResources.addOverride(
+ R.string.config_protectedInnerScreenUniqueId,
+ displayUniqueId
+ )
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/ScreenDecorationsTest.java b/packages/SystemUI/tests/src/com/android/systemui/ScreenDecorationsTest.java
index c07148b..c20367e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/ScreenDecorationsTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/ScreenDecorationsTest.java
@@ -176,6 +176,8 @@
private FakeFacePropertyRepository mFakeFacePropertyRepository =
new FakeFacePropertyRepository();
private List<DecorProvider> mMockCutoutList;
+ private final CameraProtectionLoader mCameraProtectionLoader =
+ new CameraProtectionLoaderImpl(mContext);
@Before
public void setup() {
@@ -247,7 +249,7 @@
mThreadFactory,
mPrivacyDotDecorProviderFactory, mFaceScanningProviderFactory,
new ScreenDecorationsLogger(logcatLogBuffer("TestLogBuffer")),
- mFakeFacePropertyRepository, mJavaAdapter) {
+ mFakeFacePropertyRepository, mJavaAdapter, mCameraProtectionLoader) {
@Override
public void start() {
super.start();
@@ -1243,7 +1245,7 @@
mDotViewController,
mThreadFactory, mPrivacyDotDecorProviderFactory, mFaceScanningProviderFactory,
new ScreenDecorationsLogger(logcatLogBuffer("TestLogBuffer")),
- mFakeFacePropertyRepository, mJavaAdapter);
+ mFakeFacePropertyRepository, mJavaAdapter, mCameraProtectionLoader);
screenDecorations.start();
when(mContext.getDisplay()).thenReturn(mDisplay);
when(mDisplay.getDisplayInfo(any())).thenAnswer(new Answer<Boolean>() {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/SysUICutoutProviderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/SysUICutoutProviderTest.kt
new file mode 100644
index 0000000..f37c4ae
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/SysUICutoutProviderTest.kt
@@ -0,0 +1,127 @@
+/*
+ * Copyright (C) 2024 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.view.Display
+import android.view.DisplayAdjustments
+import android.view.DisplayCutout
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.whenever
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class SysUICutoutProviderTest : SysuiTestCase() {
+
+ private val fakeProtectionLoader = FakeCameraProtectionLoader(context)
+
+ @Test
+ fun cutoutInfoForCurrentDisplay_noCutout_returnsNull() {
+ val noCutoutDisplay = createDisplay(cutout = null)
+ val noCutoutDisplayContext = context.createDisplayContext(noCutoutDisplay)
+ val provider = SysUICutoutProvider(noCutoutDisplayContext, fakeProtectionLoader)
+
+ val sysUICutout = provider.cutoutInfoForCurrentDisplay()
+
+ assertThat(sysUICutout).isNull()
+ }
+
+ @Test
+ fun cutoutInfoForCurrentDisplay_returnsCutout() {
+ val cutoutDisplay = createDisplay()
+ val cutoutDisplayContext = context.createDisplayContext(cutoutDisplay)
+ val provider = SysUICutoutProvider(cutoutDisplayContext, fakeProtectionLoader)
+
+ val sysUICutout = provider.cutoutInfoForCurrentDisplay()!!
+
+ assertThat(sysUICutout.cutout).isEqualTo(cutoutDisplay.cutout)
+ }
+
+ @Test
+ fun cutoutInfoForCurrentDisplay_noAssociatedProtection_returnsNoProtection() {
+ val cutoutDisplay = createDisplay()
+ val cutoutDisplayContext = context.createDisplayContext(cutoutDisplay)
+ val provider = SysUICutoutProvider(cutoutDisplayContext, fakeProtectionLoader)
+
+ val sysUICutout = provider.cutoutInfoForCurrentDisplay()!!
+
+ assertThat(sysUICutout.cameraProtection).isNull()
+ }
+
+ @Test
+ fun cutoutInfoForCurrentDisplay_outerDisplay_protectionAssociated_returnsProtection() {
+ fakeProtectionLoader.addOuterCameraProtection(displayUniqueId = OUTER_DISPLAY_UNIQUE_ID)
+ val outerDisplayContext = context.createDisplayContext(OUTER_DISPLAY)
+ val provider = SysUICutoutProvider(outerDisplayContext, fakeProtectionLoader)
+
+ val sysUICutout = provider.cutoutInfoForCurrentDisplay()!!
+
+ assertThat(sysUICutout.cameraProtection).isNotNull()
+ }
+
+ @Test
+ fun cutoutInfoForCurrentDisplay_outerDisplay_protectionNotAvailable_returnsNullProtection() {
+ fakeProtectionLoader.clearProtectionInfoList()
+ val outerDisplayContext = context.createDisplayContext(OUTER_DISPLAY)
+ val provider = SysUICutoutProvider(outerDisplayContext, fakeProtectionLoader)
+
+ val sysUICutout = provider.cutoutInfoForCurrentDisplay()!!
+
+ assertThat(sysUICutout.cameraProtection).isNull()
+ }
+
+ @Test
+ fun cutoutInfoForCurrentDisplay_displayWithNullId_protectionsWithNoId_returnsNullProtection() {
+ fakeProtectionLoader.addOuterCameraProtection(displayUniqueId = "")
+ val displayContext = context.createDisplayContext(createDisplay(uniqueId = null))
+ val provider = SysUICutoutProvider(displayContext, fakeProtectionLoader)
+
+ val sysUICutout = provider.cutoutInfoForCurrentDisplay()!!
+
+ assertThat(sysUICutout.cameraProtection).isNull()
+ }
+
+ @Test
+ fun cutoutInfoForCurrentDisplay_displayWithEmptyId_protectionsWithNoId_returnsNullProtection() {
+ fakeProtectionLoader.addOuterCameraProtection(displayUniqueId = "")
+ val displayContext = context.createDisplayContext(createDisplay(uniqueId = ""))
+ val provider = SysUICutoutProvider(displayContext, fakeProtectionLoader)
+
+ val sysUICutout = provider.cutoutInfoForCurrentDisplay()!!
+
+ assertThat(sysUICutout.cameraProtection).isNull()
+ }
+
+ companion object {
+ private const val OUTER_DISPLAY_UNIQUE_ID = "outer"
+ private val OUTER_DISPLAY = createDisplay(uniqueId = OUTER_DISPLAY_UNIQUE_ID)
+
+ private fun createDisplay(
+ uniqueId: String? = "uniqueId",
+ cutout: DisplayCutout? = mock<DisplayCutout>()
+ ) =
+ mock<Display> {
+ whenever(this.displayAdjustments).thenReturn(DisplayAdjustments())
+ whenever(this.cutout).thenReturn(cutout)
+ whenever(this.uniqueId).thenReturn(uniqueId)
+ }
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/SystemUIApplicationTest.kt b/packages/SystemUI/tests/src/com/android/systemui/SystemUIApplicationTest.kt
index 202d9ce..e157fc5 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/SystemUIApplicationTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/SystemUIApplicationTest.kt
@@ -91,7 +91,7 @@
whenever(sysuiComponent.startables)
.thenReturn(mutableMapOf(StartableA::class.java to Provider { startableA }))
app.onCreate()
- app.startServicesIfNeeded()
+ app.startSystemUserServicesIfNeeded()
assertThat(startableA.started).isTrue()
}
@@ -105,7 +105,7 @@
)
)
app.onCreate()
- app.startServicesIfNeeded()
+ app.startSystemUserServicesIfNeeded()
assertThat(startableA.started).isTrue()
assertThat(startableB.started).isTrue()
}
@@ -121,7 +121,7 @@
)
)
app.onCreate()
- app.startServicesIfNeeded()
+ app.startSystemUserServicesIfNeeded()
assertThat(startableA.started).isTrue()
assertThat(startableB.started).isTrue()
assertThat(startableC.started).isTrue()
@@ -141,7 +141,7 @@
)
)
app.onCreate()
- app.startServicesIfNeeded()
+ app.startSystemUserServicesIfNeeded()
assertThat(startableA.started).isTrue()
assertThat(startableB.started).isTrue()
assertThat(startableC.started).isTrue()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationAnimationControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationAnimationControllerTest.java
index 375ebe8..8299acb 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationAnimationControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationAnimationControllerTest.java
@@ -51,7 +51,6 @@
import android.view.accessibility.IRemoteMagnificationAnimationCallback;
import android.view.animation.AccelerateInterpolator;
-import androidx.test.filters.FlakyTest;
import androidx.test.filters.LargeTest;
import com.android.internal.graphics.SfVsyncFrameCallbackProvider;
@@ -80,7 +79,6 @@
@LargeTest
@RunWith(AndroidTestingRunner.class)
-@FlakyTest(bugId = 308501761)
public class WindowMagnificationAnimationControllerTest extends SysuiTestCase {
@Rule
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerTest.java
index ca3eb3e..4a1bdbc 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerTest.java
@@ -69,7 +69,6 @@
import androidx.dynamicanimation.animation.SpringAnimation;
import androidx.test.filters.SmallTest;
-import com.android.internal.accessibility.common.ShortcutConstants;
import com.android.internal.messages.nano.SystemMessageProto;
import com.android.systemui.Flags;
import com.android.systemui.SysuiTestCase;
@@ -238,7 +237,7 @@
final List<String> stubShortcutTargets = new ArrayList<>();
stubShortcutTargets.add(TEST_SELECT_TO_SPEAK_COMPONENT_NAME.flattenToString());
when(mStubAccessibilityManager.getAccessibilityShortcutTargets(
- ShortcutConstants.UserShortcutType.HARDWARE)).thenReturn(stubShortcutTargets);
+ AccessibilityManager.ACCESSIBILITY_SHORTCUT_KEY)).thenReturn(stubShortcutTargets);
mMenuViewLayer.mDismissMenuAction.run();
final String value = Settings.Secure.getString(mSpyContext.getContentResolver(),
diff --git a/packages/SystemUI/tests/src/com/android/systemui/animation/ActivityLaunchAnimatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/animation/ActivityTransitionAnimatorTest.kt
similarity index 72%
rename from packages/SystemUI/tests/src/com/android/systemui/animation/ActivityLaunchAnimatorTest.kt
rename to packages/SystemUI/tests/src/com/android/systemui/animation/ActivityTransitionAnimatorTest.kt
index 8faf715..75a49d7 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/animation/ActivityLaunchAnimatorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/animation/ActivityTransitionAnimatorTest.kt
@@ -44,33 +44,37 @@
@SmallTest
@RunWith(AndroidTestingRunner::class)
@RunWithLooper
-class ActivityLaunchAnimatorTest : SysuiTestCase() {
- private val launchContainer = LinearLayout(mContext)
- private val testLaunchAnimator = fakeLaunchAnimator()
- @Mock lateinit var callback: ActivityLaunchAnimator.Callback
- @Mock lateinit var listener: ActivityLaunchAnimator.Listener
- @Spy private val controller = TestLaunchAnimatorController(launchContainer)
+class ActivityTransitionAnimatorTest : SysuiTestCase() {
+ private val transitionContainer = LinearLayout(mContext)
+ private val testTransitionAnimator = fakeTransitionAnimator()
+ @Mock lateinit var callback: ActivityTransitionAnimator.Callback
+ @Mock lateinit var listener: ActivityTransitionAnimator.Listener
+ @Spy private val controller = TestTransitionAnimatorController(transitionContainer)
@Mock lateinit var iCallback: IRemoteAnimationFinishedCallback
- private lateinit var activityLaunchAnimator: ActivityLaunchAnimator
+ private lateinit var activityTransitionAnimator: ActivityTransitionAnimator
@get:Rule val rule = MockitoJUnit.rule()
@Before
fun setup() {
- activityLaunchAnimator =
- ActivityLaunchAnimator(testLaunchAnimator, testLaunchAnimator, disableWmTimeout = true)
- activityLaunchAnimator.callback = callback
- activityLaunchAnimator.addListener(listener)
+ activityTransitionAnimator =
+ ActivityTransitionAnimator(
+ testTransitionAnimator,
+ testTransitionAnimator,
+ disableWmTimeout = true
+ )
+ activityTransitionAnimator.callback = callback
+ activityTransitionAnimator.addListener(listener)
}
@After
fun tearDown() {
- activityLaunchAnimator.removeListener(listener)
+ activityTransitionAnimator.removeListener(listener)
}
private fun startIntentWithAnimation(
- animator: ActivityLaunchAnimator = this.activityLaunchAnimator,
- controller: ActivityLaunchAnimator.Controller? = this.controller,
+ animator: ActivityTransitionAnimator = this.activityTransitionAnimator,
+ controller: ActivityTransitionAnimator.Controller? = this.controller,
animate: Boolean = true,
intentStarter: (RemoteAnimationAdapter?) -> Int
) {
@@ -134,7 +138,7 @@
val willAnimateCaptor = ArgumentCaptor.forClass(Boolean::class.java)
var animationAdapter: RemoteAnimationAdapter? = null
- startIntentWithAnimation(activityLaunchAnimator) { adapter ->
+ startIntentWithAnimation(activityTransitionAnimator) { adapter ->
animationAdapter = adapter
ActivityManager.START_DELIVERED_TO_TOP
}
@@ -159,50 +163,50 @@
@Test
fun doesNotStartIfAnimationIsCancelled() {
- val runner = activityLaunchAnimator.createRunner(controller)
+ val runner = activityTransitionAnimator.createRunner(controller)
runner.onAnimationCancelled()
runner.onAnimationStart(0, emptyArray(), emptyArray(), emptyArray(), iCallback)
waitForIdleSync()
- verify(controller).onLaunchAnimationCancelled()
- verify(controller, never()).onLaunchAnimationStart(anyBoolean())
- verify(listener).onLaunchAnimationCancelled()
- verify(listener, never()).onLaunchAnimationStart()
+ verify(controller).onTransitionAnimationCancelled()
+ verify(controller, never()).onTransitionAnimationStart(anyBoolean())
+ verify(listener).onTransitionAnimationCancelled()
+ verify(listener, never()).onTransitionAnimationStart()
assertNull(runner.delegate)
}
@Test
fun cancelsIfNoOpeningWindowIsFound() {
- val runner = activityLaunchAnimator.createRunner(controller)
+ val runner = activityTransitionAnimator.createRunner(controller)
runner.onAnimationStart(0, emptyArray(), emptyArray(), emptyArray(), iCallback)
waitForIdleSync()
- verify(controller).onLaunchAnimationCancelled()
- verify(controller, never()).onLaunchAnimationStart(anyBoolean())
- verify(listener).onLaunchAnimationCancelled()
- verify(listener, never()).onLaunchAnimationStart()
+ verify(controller).onTransitionAnimationCancelled()
+ verify(controller, never()).onTransitionAnimationStart(anyBoolean())
+ verify(listener).onTransitionAnimationCancelled()
+ verify(listener, never()).onTransitionAnimationStart()
assertNull(runner.delegate)
}
@Test
fun startsAnimationIfWindowIsOpening() {
- val runner = activityLaunchAnimator.createRunner(controller)
+ val runner = activityTransitionAnimator.createRunner(controller)
runner.onAnimationStart(0, arrayOf(fakeWindow()), emptyArray(), emptyArray(), iCallback)
waitForIdleSync()
- verify(listener).onLaunchAnimationStart()
- verify(controller).onLaunchAnimationStart(anyBoolean())
+ verify(listener).onTransitionAnimationStart()
+ verify(controller).onTransitionAnimationStart(anyBoolean())
}
@Test
fun creatingControllerFromNormalViewThrows() {
assertThrows(IllegalArgumentException::class.java) {
- ActivityLaunchAnimator.Controller.fromView(FrameLayout(mContext))
+ ActivityTransitionAnimator.Controller.fromView(FrameLayout(mContext))
}
}
@Test
fun disposeRunner_delegateDereferenced() {
- val runner = activityLaunchAnimator.createRunner(controller)
+ val runner = activityTransitionAnimator.createRunner(controller)
assertNotNull(runner.delegate)
runner.dispose()
waitForIdleSync()
@@ -237,13 +241,13 @@
}
/**
- * A simple implementation of [ActivityLaunchAnimator.Controller] which throws if it is called
+ * A simple implementation of [ActivityTransitionAnimator.Controller] which throws if it is called
* outside of the main thread.
*/
-private class TestLaunchAnimatorController(override var launchContainer: ViewGroup) :
- ActivityLaunchAnimator.Controller {
+private class TestTransitionAnimatorController(override var transitionContainer: ViewGroup) :
+ ActivityTransitionAnimator.Controller {
override fun createAnimatorState() =
- LaunchAnimator.State(
+ TransitionAnimator.State(
top = 100,
bottom = 200,
left = 300,
@@ -262,23 +266,23 @@
assertOnMainThread()
}
- override fun onLaunchAnimationStart(isExpandingFullyAbove: Boolean) {
+ override fun onTransitionAnimationStart(isExpandingFullyAbove: Boolean) {
assertOnMainThread()
}
- override fun onLaunchAnimationProgress(
- state: LaunchAnimator.State,
+ override fun onTransitionAnimationProgress(
+ state: TransitionAnimator.State,
progress: Float,
linearProgress: Float
) {
assertOnMainThread()
}
- override fun onLaunchAnimationEnd(isExpandingFullyAbove: Boolean) {
+ override fun onTransitionAnimationEnd(isExpandingFullyAbove: Boolean) {
assertOnMainThread()
}
- override fun onLaunchAnimationCancelled(newKeyguardOccludedState: Boolean?) {
+ override fun onTransitionAnimationCancelled(newKeyguardOccludedState: Boolean?) {
assertOnMainThread()
}
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/animation/DialogLaunchAnimatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/animation/DialogLaunchAnimatorTest.kt
index 2233e322..a586421 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/animation/DialogLaunchAnimatorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/animation/DialogLaunchAnimatorTest.kt
@@ -129,14 +129,14 @@
// The dialog shouldn't be dismissable during the animation.
runOnMainThreadAndWaitForIdleSync {
- controller.onLaunchAnimationStart(isExpandingFullyAbove = true)
+ controller.onTransitionAnimationStart(isExpandingFullyAbove = true)
secondDialog.dismiss()
}
assertTrue(secondDialog.isShowing)
// Both dialogs should be dismissed at the end of the animation.
runOnMainThreadAndWaitForIdleSync {
- controller.onLaunchAnimationEnd(isExpandingFullyAbove = true)
+ controller.onTransitionAnimationEnd(isExpandingFullyAbove = true)
}
assertFalse(firstDialog.isShowing)
assertFalse(secondDialog.isShowing)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/animation/GhostedViewLaunchAnimatorControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/animation/GhostedViewLaunchAnimatorControllerTest.kt
index d1ac0e8..8442a62 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/animation/GhostedViewLaunchAnimatorControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/animation/GhostedViewLaunchAnimatorControllerTest.kt
@@ -32,13 +32,13 @@
class GhostedViewLaunchAnimatorControllerTest : SysuiTestCase() {
@Test
fun animatingOrphanViewDoesNotCrash() {
- val state = LaunchAnimator.State(top = 0, bottom = 0, left = 0, right = 0)
+ val state = TransitionAnimator.State(top = 0, bottom = 0, left = 0, right = 0)
val controller = GhostedViewLaunchAnimatorController(LaunchableFrameLayout(mContext))
controller.onIntentStarted(willAnimate = true)
- controller.onLaunchAnimationStart(isExpandingFullyAbove = true)
- controller.onLaunchAnimationProgress(state, progress = 0f, linearProgress = 0f)
- controller.onLaunchAnimationEnd(isExpandingFullyAbove = true)
+ controller.onTransitionAnimationStart(isExpandingFullyAbove = true)
+ controller.onTransitionAnimationProgress(state, progress = 0f, linearProgress = 0f)
+ controller.onTransitionAnimationEnd(isExpandingFullyAbove = true)
}
@Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/back/domain/interactor/BackActionInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/back/domain/interactor/BackActionInteractorTest.kt
index 0ba9abe..f490f3c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/back/domain/interactor/BackActionInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/back/domain/interactor/BackActionInteractorTest.kt
@@ -45,7 +45,7 @@
import com.android.systemui.scene.ui.view.WindowRootView
import com.android.systemui.shade.QuickSettingsController
import com.android.systemui.shade.ShadeController
-import com.android.systemui.shade.ShadeViewController
+import com.android.systemui.shade.domain.interactor.ShadeBackActionInteractor
import com.android.systemui.statusbar.NotificationShadeWindowController
import com.android.systemui.statusbar.StatusBarState
import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationListRepository
@@ -89,7 +89,7 @@
@Mock private lateinit var statusBarKeyguardViewManager: StatusBarKeyguardViewManager
@Mock private lateinit var shadeController: ShadeController
@Mock private lateinit var qsController: QuickSettingsController
- @Mock private lateinit var shadeViewController: ShadeViewController
+ @Mock private lateinit var shadeBackActionInteractor: ShadeBackActionInteractor
@Mock private lateinit var notificationShadeWindowController: NotificationShadeWindowController
@Mock private lateinit var windowRootView: WindowRootView
@Mock private lateinit var viewRootImpl: ViewRootImpl
@@ -123,7 +123,7 @@
notificationShadeWindowController,
windowRootViewVisibilityInteractor
)
- .apply { this.setup(qsController, shadeViewController) }
+ .apply { this.setup(qsController, shadeBackActionInteractor) }
}
private val powerInteractor = PowerInteractorFactory.create().powerInteractor
@@ -165,19 +165,19 @@
val result = backActionInteractor.onBackRequested()
assertTrue(result)
- verify(shadeViewController, atLeastOnce()).animateCollapseQs(anyBoolean())
+ verify(shadeBackActionInteractor, atLeastOnce()).animateCollapseQs(anyBoolean())
verify(statusBarKeyguardViewManager, never()).onBackPressed()
}
@Test
fun testOnBackRequested_closeUserSwitcherIfOpen() {
- whenever(shadeViewController.closeUserSwitcherIfOpen()).thenReturn(true)
+ whenever(shadeBackActionInteractor.closeUserSwitcherIfOpen()).thenReturn(true)
val result = backActionInteractor.onBackRequested()
assertTrue(result)
verify(statusBarKeyguardViewManager, never()).onBackPressed()
- verify(shadeViewController, never()).animateCollapseQs(anyBoolean())
+ verify(shadeBackActionInteractor, never()).animateCollapseQs(anyBoolean())
}
@Test
@@ -189,7 +189,7 @@
assertFalse(result)
verify(statusBarKeyguardViewManager, never()).onBackPressed()
- verify(shadeViewController, never()).animateCollapseQs(anyBoolean())
+ verify(shadeBackActionInteractor, never()).animateCollapseQs(anyBoolean())
}
@Test
@@ -290,11 +290,11 @@
powerInteractor.setAwakeForTest()
val callback = getBackInvokedCallback() as OnBackAnimationCallback
- whenever(shadeViewController.canBeCollapsed()).thenReturn(false)
+ whenever(shadeBackActionInteractor.canBeCollapsed()).thenReturn(false)
callback.onBackProgressed(createBackEvent(0.3f))
- verify(shadeViewController, never()).onBackProgressed(0.3f)
+ verify(shadeBackActionInteractor, never()).onBackProgressed(0.3f)
}
@Test
@@ -305,11 +305,11 @@
powerInteractor.setAwakeForTest()
val callback = getBackInvokedCallback() as OnBackAnimationCallback
- whenever(shadeViewController.canBeCollapsed()).thenReturn(true)
+ whenever(shadeBackActionInteractor.canBeCollapsed()).thenReturn(true)
callback.onBackProgressed(createBackEvent(0.4f))
- verify(shadeViewController).onBackProgressed(0.4f)
+ verify(shadeBackActionInteractor).onBackProgressed(0.4f)
}
private fun getBackInvokedCallback(): OnBackInvokedCallback {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/data/repository/BiometricStatusRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/data/repository/BiometricStatusRepositoryTest.kt
index 8f0e910..8fbeb6f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/data/repository/BiometricStatusRepositoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/data/repository/BiometricStatusRepositoryTest.kt
@@ -17,6 +17,7 @@
package com.android.systemui.biometrics.data.repository
import android.hardware.biometrics.AuthenticationStateListener
+import android.hardware.biometrics.BiometricFingerprintConstants
import android.hardware.biometrics.BiometricManager
import android.hardware.biometrics.BiometricRequestConstants.REASON_AUTH_BP
import android.hardware.biometrics.BiometricRequestConstants.REASON_AUTH_KEYGUARD
@@ -24,11 +25,13 @@
import android.hardware.biometrics.BiometricRequestConstants.REASON_AUTH_SETTINGS
import android.hardware.biometrics.BiometricRequestConstants.REASON_ENROLL_ENROLLING
import android.hardware.biometrics.BiometricRequestConstants.REASON_ENROLL_FIND_SENSOR
+import android.hardware.biometrics.BiometricSourceType
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.biometrics.shared.model.AuthenticationReason
import com.android.systemui.biometrics.shared.model.AuthenticationReason.SettingsOperations
import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.keyguard.shared.model.AcquiredFingerprintAuthenticationStatus
import com.android.systemui.shared.Flags.FLAG_SIDEFPS_CONTROLLER_REFACTOR
import com.android.systemui.util.mockito.withArgCaptor
import com.google.common.truth.Truth.assertThat
@@ -167,6 +170,28 @@
listener.onAuthenticationStopped()
assertThat(fingerprintAuthenticationReason).isEqualTo(AuthenticationReason.NotRunning)
}
+
+ @Test
+ fun updatesFingerprintAcquiredStatusWhenBiometricPromptAuthenticationAcquired() =
+ testScope.runTest {
+ val fingerprintAcquiredStatus by collectLastValue(underTest.fingerprintAcquiredStatus)
+ runCurrent()
+
+ val listener = biometricManager.captureListener()
+ listener.onAuthenticationAcquired(
+ BiometricSourceType.FINGERPRINT,
+ REASON_AUTH_BP,
+ BiometricFingerprintConstants.FINGERPRINT_ACQUIRED_START
+ )
+
+ assertThat(fingerprintAcquiredStatus)
+ .isEqualTo(
+ AcquiredFingerprintAuthenticationStatus(
+ AuthenticationReason.BiometricPromptAuthentication,
+ BiometricFingerprintConstants.FINGERPRINT_ACQUIRED_START
+ )
+ )
+ }
}
private fun BiometricManager.captureListener() =
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/data/repository/DisplayStateRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/data/repository/DisplayStateRepositoryTest.kt
index a84778a..eae953e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/data/repository/DisplayStateRepositoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/data/repository/DisplayStateRepositoryTest.kt
@@ -16,11 +16,9 @@
package com.android.systemui.keyguard.data.repository
-import android.hardware.devicestate.DeviceStateManager
-import android.hardware.display.DisplayManager
-import android.os.Handler
import android.util.Size
import android.view.Display
+import android.view.Display.DEFAULT_DISPLAY
import android.view.DisplayInfo
import android.view.Surface
import androidx.test.filters.SmallTest
@@ -29,61 +27,37 @@
import com.android.systemui.biometrics.data.repository.DisplayStateRepositoryImpl
import com.android.systemui.biometrics.shared.model.DisplayRotation
import com.android.systemui.coroutines.collectLastValue
-import com.android.systemui.util.concurrency.FakeExecutor
+import com.android.systemui.display.data.repository.DeviceStateRepository.DeviceState
+import com.android.systemui.display.data.repository.FakeDeviceStateRepository
+import com.android.systemui.display.data.repository.FakeDisplayRepository
import com.android.systemui.util.mockito.any
-import com.android.systemui.util.mockito.eq
+import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.whenever
-import com.android.systemui.util.mockito.withArgCaptor
-import com.android.systemui.util.time.FakeSystemClock
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.TestScope
-import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Before
-import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
-import org.mockito.ArgumentCaptor
-import org.mockito.ArgumentMatchers.same
-import org.mockito.Captor
-import org.mockito.Mock
import org.mockito.Mockito.spy
-import org.mockito.Mockito.verify
-import org.mockito.junit.MockitoJUnit
-import org.mockito.junit.MockitoRule
-
-private const val NORMAL_DISPLAY_MODE_DEVICE_STATE = 2
-private const val REAR_DISPLAY_MODE_DEVICE_STATE = 3
@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
@RunWith(JUnit4::class)
class DisplayStateRepositoryTest : SysuiTestCase() {
- @JvmField @Rule var mockitoRule: MockitoRule = MockitoJUnit.rule()
- @Mock private lateinit var deviceStateManager: DeviceStateManager
- @Mock private lateinit var displayManager: DisplayManager
- @Mock private lateinit var handler: Handler
- @Mock private lateinit var display: Display
- private lateinit var underTest: DisplayStateRepository
-
+ private val display = mock<Display>()
private val testScope = TestScope(StandardTestDispatcher())
- private val fakeExecutor = FakeExecutor(FakeSystemClock())
+ private val fakeDeviceStateRepository = FakeDeviceStateRepository()
+ private val fakeDisplayRepository = FakeDisplayRepository()
- @Captor
- private lateinit var displayListenerCaptor: ArgumentCaptor<DisplayManager.DisplayListener>
+ private lateinit var underTest: DisplayStateRepository
@Before
fun setUp() {
- val rearDisplayDeviceStates = intArrayOf(REAR_DISPLAY_MODE_DEVICE_STATE)
- mContext.orCreateTestableResources.addOverride(
- com.android.internal.R.array.config_rearDisplayDeviceStates,
- rearDisplayDeviceStates
- )
-
mContext.orCreateTestableResources.addOverride(
com.android.internal.R.bool.config_reverseDefaultRotation,
false
@@ -96,11 +70,8 @@
DisplayStateRepositoryImpl(
testScope.backgroundScope,
mContext,
- deviceStateManager,
- displayManager,
- handler,
- fakeExecutor,
- UnconfinedTestDispatcher(),
+ fakeDeviceStateRepository,
+ fakeDisplayRepository,
)
}
@@ -110,12 +81,10 @@
val isInRearDisplayMode by collectLastValue(underTest.isInRearDisplayMode)
runCurrent()
- val callback = deviceStateManager.captureCallback()
-
- callback.onStateChanged(NORMAL_DISPLAY_MODE_DEVICE_STATE)
+ fakeDeviceStateRepository.emit(DeviceState.FOLDED)
assertThat(isInRearDisplayMode).isFalse()
- callback.onStateChanged(REAR_DISPLAY_MODE_DEVICE_STATE)
+ fakeDeviceStateRepository.emit(DeviceState.REAR_DISPLAY)
assertThat(isInRearDisplayMode).isTrue()
}
@@ -125,19 +94,13 @@
val currentRotation by collectLastValue(underTest.currentRotation)
runCurrent()
- verify(displayManager)
- .registerDisplayListener(
- displayListenerCaptor.capture(),
- same(handler),
- eq(DisplayManager.EVENT_FLAG_DISPLAY_CHANGED)
- )
-
whenever(display.getDisplayInfo(any())).then {
val info = it.getArgument<DisplayInfo>(0)
info.rotation = Surface.ROTATION_90
return@then true
}
- displayListenerCaptor.value.onDisplayChanged(Surface.ROTATION_90)
+
+ fakeDisplayRepository.emitDisplayChangeEvent(DEFAULT_DISPLAY)
assertThat(currentRotation).isEqualTo(DisplayRotation.ROTATION_90)
whenever(display.getDisplayInfo(any())).then {
@@ -145,7 +108,8 @@
info.rotation = Surface.ROTATION_180
return@then true
}
- displayListenerCaptor.value.onDisplayChanged(Surface.ROTATION_180)
+
+ fakeDisplayRepository.emitDisplayChangeEvent(DEFAULT_DISPLAY)
assertThat(currentRotation).isEqualTo(DisplayRotation.ROTATION_180)
}
@@ -155,13 +119,6 @@
val currentSize by collectLastValue(underTest.currentDisplaySize)
runCurrent()
- verify(displayManager)
- .registerDisplayListener(
- displayListenerCaptor.capture(),
- same(handler),
- eq(DisplayManager.EVENT_FLAG_DISPLAY_CHANGED)
- )
-
whenever(display.getDisplayInfo(any())).then {
val info = it.getArgument<DisplayInfo>(0)
info.rotation = Surface.ROTATION_0
@@ -169,7 +126,7 @@
info.logicalHeight = 200
return@then true
}
- displayListenerCaptor.value.onDisplayChanged(Surface.ROTATION_0)
+ fakeDisplayRepository.emitDisplayChangeEvent(DEFAULT_DISPLAY)
assertThat(currentSize).isEqualTo(Size(100, 200))
whenever(display.getDisplayInfo(any())).then {
@@ -179,12 +136,7 @@
info.logicalHeight = 200
return@then true
}
- displayListenerCaptor.value.onDisplayChanged(Surface.ROTATION_180)
+ fakeDisplayRepository.emitDisplayChangeEvent(DEFAULT_DISPLAY)
assertThat(currentSize).isEqualTo(Size(200, 100))
}
}
-
-private fun DeviceStateManager.captureCallback() =
- withArgCaptor<DeviceStateManager.DeviceStateCallback> {
- verify(this@captureCallback).registerCallback(any(), capture())
- }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/BiometricStatusInteractorImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/BiometricStatusInteractorImplTest.kt
index d7b7d79..5c34fd9 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/BiometricStatusInteractorImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/BiometricStatusInteractorImplTest.kt
@@ -19,12 +19,14 @@
import android.app.ActivityManager
import android.app.ActivityTaskManager
import android.content.ComponentName
+import android.hardware.biometrics.BiometricFingerprintConstants
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.biometrics.data.repository.FakeBiometricStatusRepository
import com.android.systemui.biometrics.shared.model.AuthenticationReason
import com.android.systemui.biometrics.shared.model.AuthenticationReason.SettingsOperations
import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.keyguard.shared.model.AcquiredFingerprintAuthenticationStatus
import com.android.systemui.shared.Flags.FLAG_SIDEFPS_CONTROLLER_REFACTOR
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -162,6 +164,27 @@
)
assertThat(sfpsAuthenticationReason).isEqualTo(AuthenticationReason.NotRunning)
}
+
+ @Test
+ fun updatesFingerprintAcquiredStatusWhenBiometricPromptAuthenticationAcquired() =
+ testScope.runTest {
+ val fingerprintAcquiredStatus by collectLastValue(underTest.fingerprintAcquiredStatus)
+ runCurrent()
+
+ biometricStatusRepository.setFingerprintAcquiredStatus(
+ AcquiredFingerprintAuthenticationStatus(
+ AuthenticationReason.BiometricPromptAuthentication,
+ BiometricFingerprintConstants.FINGERPRINT_ACQUIRED_START
+ )
+ )
+ assertThat(fingerprintAcquiredStatus)
+ .isEqualTo(
+ AcquiredFingerprintAuthenticationStatus(
+ AuthenticationReason.BiometricPromptAuthentication,
+ BiometricFingerprintConstants.FINGERPRINT_ACQUIRED_START
+ )
+ )
+ }
}
private fun fpSettingsTask() = settingsTask(".biometrics.fingerprint.FingerprintSettings")
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/LogContextInteractorImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/LogContextInteractorImplTest.kt
index fc34255..3f83ce3 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/LogContextInteractorImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/LogContextInteractorImplTest.kt
@@ -26,7 +26,7 @@
import com.android.systemui.display.data.repository.fakeDeviceStateRepository
import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
-import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractorFactory
+import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor
import com.android.systemui.keyguard.shared.model.KeyguardState
import com.android.systemui.keyguard.shared.model.TransitionState
import com.android.systemui.keyguard.shared.model.TransitionStep
@@ -47,7 +47,6 @@
@SmallTest
@RunWith(JUnit4::class)
class LogContextInteractorImplTest : SysuiTestCase() {
-
@JvmField @Rule var mockitoRule = MockitoJUnit.rule()
private val kosmos = testKosmos()
@@ -64,11 +63,7 @@
LogContextInteractorImpl(
testScope.backgroundScope,
deviceStateRepository,
- KeyguardTransitionInteractorFactory.create(
- repository = keyguardTransitionRepository,
- scope = testScope.backgroundScope,
- )
- .keyguardTransitionInteractor,
+ kosmos.keyguardTransitionInteractor,
udfpsOverlayInteractor,
)
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/binder/SideFpsOverlayViewBinderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/binder/SideFpsOverlayViewBinderTest.kt
index 3603c3c..5509c04 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/binder/SideFpsOverlayViewBinderTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/binder/SideFpsOverlayViewBinderTest.kt
@@ -58,6 +58,7 @@
import com.android.systemui.bouncer.ui.BouncerView
import com.android.systemui.classifier.FalsingCollector
import com.android.systemui.deviceentry.domain.interactor.DeviceEntryFaceAuthInteractor
+import com.android.systemui.deviceentry.domain.interactor.deviceEntryFingerprintAuthInteractor
import com.android.systemui.display.data.repository.FakeDisplayRepository
import com.android.systemui.keyguard.DismissCallbackRegistry
import com.android.systemui.keyguard.data.repository.FakeBiometricSettingsRepository
@@ -100,6 +101,7 @@
import org.mockito.Mock
import org.mockito.Mockito
import org.mockito.Mockito.any
+import org.mockito.Mockito.inOrder
import org.mockito.Mockito.mock
import org.mockito.Mockito.never
import org.mockito.Mockito.spy
@@ -253,7 +255,8 @@
sideFpsProgressBarViewModel =
SideFpsProgressBarViewModel(
mContext,
- mock(),
+ biometricStatusInteractor,
+ kosmos.deviceEntryFingerprintAuthInteractor,
sfpsSensorInteractor,
kosmos.dozeServiceHost,
kosmos.keyguardInteractor,
@@ -426,6 +429,54 @@
}
}
+ // On progress bar shown - hide indicator
+ // On progress bar hidden - show indicator
+ @Test
+ fun verifyIndicatorProgressBarInteraction() {
+ testScope.runTest {
+ // Pre-auth conditions
+ setupTestConfiguration(
+ DeviceConfig.X_ALIGNED,
+ rotation = DisplayRotation.ROTATION_0,
+ isInRearDisplayMode = false
+ )
+ biometricStatusRepository.setFingerprintAuthenticationReason(
+ AuthenticationReason.NotRunning
+ )
+ sideFpsProgressBarViewModel.setVisible(false)
+
+ // Show primary bouncer
+ updatePrimaryBouncer(
+ isShowing = true,
+ isAnimatingAway = false,
+ fpsDetectionRunning = true,
+ isUnlockingWithFpAllowed = true
+ )
+ runCurrent()
+
+ val inOrder = inOrder(windowManager)
+
+ // Verify indicator shown
+ inOrder.verify(windowManager).addView(any(), any())
+
+ // Set progress bar visible
+ sideFpsProgressBarViewModel.setVisible(true)
+
+ runCurrent()
+
+ // Verify indicator hidden
+ inOrder.verify(windowManager).removeView(any())
+
+ // Set progress bar invisible
+ sideFpsProgressBarViewModel.setVisible(false)
+
+ runCurrent()
+
+ // Verify indicator shown
+ inOrder.verify(windowManager).addView(any(), any())
+ }
+ }
+
private fun updatePrimaryBouncer(
isShowing: Boolean,
isAnimatingAway: Boolean,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt
index 6a9c881..2e94d38 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt
@@ -81,6 +81,7 @@
private const val CHALLENGE = 2L
private const val DELAY = 1000L
private const val OP_PACKAGE_NAME = "biometric.testapp"
+private const val OP_PACKAGE_NAME_NO_ICON = "biometric.testapp.noicon"
@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
@@ -1246,6 +1247,14 @@
}
@Test
+ fun logoIsNullIfPackageNameNotFound() =
+ runGenericTest(packageName = OP_PACKAGE_NAME_NO_ICON) {
+ mSetFlagsRule.enableFlags(FLAG_CUSTOM_BIOMETRIC_PROMPT)
+ val logo by collectLastValue(viewModel.logo)
+ assertThat(logo).isNull()
+ }
+
+ @Test
fun defaultLogoIfNoLogoSet() = runGenericTest {
mSetFlagsRule.enableFlags(FLAG_CUSTOM_BIOMETRIC_PROMPT)
val logo by collectLastValue(viewModel.logo)
@@ -1291,7 +1300,8 @@
contentView: PromptContentView? = null,
logoRes: Int = -1,
logoBitmap: Bitmap? = null,
- block: suspend TestScope.() -> Unit
+ packageName: String = OP_PACKAGE_NAME,
+ block: suspend TestScope.() -> Unit,
) {
selector.initializePrompt(
requireConfirmation = testCase.confirmationRequested,
@@ -1302,6 +1312,7 @@
contentViewFromApp = contentView,
logoResFromApp = logoRes,
logoBitmapFromApp = logoBitmap,
+ packageName = packageName,
)
// put the view model in the initial authenticating state, unless explicitly skipped
@@ -1481,6 +1492,7 @@
contentViewFromApp: PromptContentView? = null,
logoResFromApp: Int = -1,
logoBitmapFromApp: Bitmap? = null,
+ packageName: String = OP_PACKAGE_NAME,
) {
val info =
PromptInfo().apply {
@@ -1500,7 +1512,7 @@
USER_ID,
CHALLENGE,
BiometricModalities(fingerprintProperties = fingerprint, faceProperties = face),
- OP_PACKAGE_NAME,
+ packageName,
)
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/SideFpsOverlayViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/SideFpsOverlayViewModelTest.kt
index 3c43031..2014755 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/SideFpsOverlayViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/SideFpsOverlayViewModelTest.kt
@@ -16,7 +16,6 @@
package com.android.systemui.biometrics.ui.viewmodel
-import android.app.ActivityTaskManager
import android.content.res.Configuration.UI_MODE_NIGHT_NO
import android.content.res.Configuration.UI_MODE_NIGHT_YES
import android.graphics.Color
@@ -39,10 +38,10 @@
import com.android.systemui.biometrics.data.repository.FakeBiometricStatusRepository
import com.android.systemui.biometrics.data.repository.FakeDisplayStateRepository
import com.android.systemui.biometrics.data.repository.FakeFingerprintPropertyRepository
-import com.android.systemui.biometrics.domain.interactor.BiometricStatusInteractor
-import com.android.systemui.biometrics.domain.interactor.BiometricStatusInteractorImpl
+import com.android.systemui.biometrics.data.repository.biometricStatusRepository
import com.android.systemui.biometrics.domain.interactor.DisplayStateInteractorImpl
import com.android.systemui.biometrics.domain.interactor.SideFpsSensorInteractor
+import com.android.systemui.biometrics.domain.interactor.biometricStatusInteractor
import com.android.systemui.biometrics.shared.model.AuthenticationReason
import com.android.systemui.biometrics.shared.model.DisplayRotation
import com.android.systemui.biometrics.shared.model.FingerprintSensorType
@@ -80,7 +79,6 @@
import com.android.systemui.unfold.compat.ScreenSizeFoldProvider
import com.android.systemui.user.domain.interactor.SelectedUserInteractor
import com.android.systemui.util.concurrency.FakeExecutor
-import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.whenever
import com.android.systemui.util.time.FakeSystemClock
import com.google.common.truth.Truth.assertThat
@@ -109,7 +107,6 @@
private val kosmos = testKosmos()
@JvmField @Rule var mockitoRule: MockitoRule = MockitoJUnit.rule()
- @Mock private lateinit var activityTaskManager: ActivityTaskManager
@Mock private lateinit var faceAuthInteractor: DeviceEntryFaceAuthInteractor
@Mock
private lateinit var fingerprintInteractiveToAuthProvider: FingerprintInteractiveToAuthProvider
@@ -147,7 +144,6 @@
context.getColor(com.android.settingslib.color.R.color.settingslib_color_blue400)
private lateinit var alternateBouncerInteractor: AlternateBouncerInteractor
- private lateinit var biometricStatusInteractor: BiometricStatusInteractor
private lateinit var deviceEntrySideFpsOverlayInteractor: DeviceEntrySideFpsOverlayInteractor
private lateinit var displayStateInteractor: DisplayStateInteractorImpl
private lateinit var primaryBouncerInteractor: PrimaryBouncerInteractor
@@ -184,6 +180,7 @@
.thenReturn(
Display(mock(DisplayManagerGlobal::class.java), 1, contextDisplayInfo, resources)
)
+ kosmos.biometricStatusRepository = biometricStatusRepository
alternateBouncerInteractor =
AlternateBouncerInteractor(
@@ -197,9 +194,6 @@
testScope.backgroundScope,
)
- biometricStatusInteractor =
- BiometricStatusInteractorImpl(activityTaskManager, biometricStatusRepository)
-
displayStateInteractor =
DisplayStateInteractorImpl(
testScope.backgroundScope,
@@ -256,6 +250,7 @@
sideFpsProgressBarViewModel =
SideFpsProgressBarViewModel(
mContext,
+ kosmos.biometricStatusInteractor,
kosmos.deviceEntryFingerprintAuthInteractor,
sfpsSensorInteractor,
kosmos.dozeServiceHost,
@@ -263,13 +258,13 @@
displayStateInteractor,
kosmos.testDispatcher,
testScope.backgroundScope,
- kosmos.powerInteractor,
+ kosmos.powerInteractor
)
underTest =
SideFpsOverlayViewModel(
mContext,
- biometricStatusInteractor,
+ kosmos.biometricStatusInteractor,
deviceEntrySideFpsOverlayInteractor,
displayStateInteractor,
sfpsSensorInteractor,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/camera/CameraGestureHelperTest.kt b/packages/SystemUI/tests/src/com/android/systemui/camera/CameraGestureHelperTest.kt
index 6d3cc4c..669795b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/camera/CameraGestureHelperTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/camera/CameraGestureHelperTest.kt
@@ -81,10 +81,10 @@
@Before
fun setUp() {
MockitoAnnotations.initMocks(this)
- whenever(cameraIntents.getSecureCameraIntent()).thenReturn(
+ whenever(cameraIntents.getSecureCameraIntent(anyInt())).thenReturn(
Intent(CameraIntents.DEFAULT_SECURE_CAMERA_INTENT_ACTION)
)
- whenever(cameraIntents.getInsecureCameraIntent()).thenReturn(
+ whenever(cameraIntents.getInsecureCameraIntent(anyInt())).thenReturn(
Intent(CameraIntents.DEFAULT_INSECURE_CAMERA_INTENT_ACTION)
)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/common/ui/ConfigurationStateTest.kt b/packages/SystemUI/tests/src/com/android/systemui/common/ui/ConfigurationStateTest.kt
deleted file mode 100644
index 112cec2..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/common/ui/ConfigurationStateTest.kt
+++ /dev/null
@@ -1,170 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
- * except in compliance with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software distributed under the
- * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the specific language governing
- * permissions and limitations under the License.
- *
- */
-
-package com.android.systemui.common.ui
-
-import android.content.Context
-import android.testing.AndroidTestingRunner
-import android.view.LayoutInflater
-import android.view.View
-import android.view.ViewGroup
-import androidx.test.filters.SmallTest
-import com.android.systemui.SysuiTestCase
-import com.android.systemui.statusbar.policy.ConfigurationController
-import com.android.systemui.util.mockito.captureMany
-import com.android.systemui.util.mockito.mock
-import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.DisposableHandle
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.cancelAndJoin
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.test.StandardTestDispatcher
-import kotlinx.coroutines.test.TestScope
-import kotlinx.coroutines.test.runCurrent
-import kotlinx.coroutines.test.runTest
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.mockito.Mockito.atLeastOnce
-import org.mockito.Mockito.verify
-
-@OptIn(ExperimentalCoroutinesApi::class)
-@SmallTest
-@RunWith(AndroidTestingRunner::class)
-class ConfigurationStateTest : SysuiTestCase() {
-
- private val configurationController: ConfigurationController = mock()
- private val layoutInflater = TestLayoutInflater()
- private val backgroundDispatcher = StandardTestDispatcher()
- private val testScope = TestScope(backgroundDispatcher)
-
- val underTest = ConfigurationState(configurationController, context, layoutInflater)
-
- @Test
- fun reinflateAndBindLatest_inflatesWithoutEmission() =
- testScope.runTest {
- var callbackCount = 0
- backgroundScope.launch {
- underTest.reinflateAndBindLatest<View>(
- resource = 0,
- root = null,
- attachToRoot = false,
- backgroundDispatcher,
- ) {
- callbackCount++
- null
- }
- }
-
- // Inflates without an emission
- runCurrent()
- assertThat(layoutInflater.inflationCount).isEqualTo(1)
- assertThat(callbackCount).isEqualTo(1)
- }
-
- @Test
- fun reinflateAndBindLatest_reinflatesOnThemeChanged() =
- testScope.runTest {
- var callbackCount = 0
- backgroundScope.launch {
- underTest.reinflateAndBindLatest<View>(
- resource = 0,
- root = null,
- attachToRoot = false,
- backgroundDispatcher,
- ) {
- callbackCount++
- null
- }
- }
- runCurrent()
-
- val configListeners: List<ConfigurationController.ConfigurationListener> = captureMany {
- verify(configurationController, atLeastOnce()).addCallback(capture())
- }
-
- listOf(1, 2, 3).forEach { count ->
- assertThat(layoutInflater.inflationCount).isEqualTo(count)
- assertThat(callbackCount).isEqualTo(count)
- configListeners.forEach { it.onThemeChanged() }
- runCurrent()
- }
- }
-
- @Test
- fun reinflateAndBindLatest_reinflatesOnDensityOrFontScaleChanged() =
- testScope.runTest {
- var callbackCount = 0
- backgroundScope.launch {
- underTest.reinflateAndBindLatest<View>(
- resource = 0,
- root = null,
- attachToRoot = false,
- backgroundDispatcher,
- ) {
- callbackCount++
- null
- }
- }
- runCurrent()
-
- val configListeners: List<ConfigurationController.ConfigurationListener> = captureMany {
- verify(configurationController, atLeastOnce()).addCallback(capture())
- }
-
- listOf(1, 2, 3).forEach { count ->
- assertThat(layoutInflater.inflationCount).isEqualTo(count)
- assertThat(callbackCount).isEqualTo(count)
- configListeners.forEach { it.onDensityOrFontScaleChanged() }
- runCurrent()
- }
- }
-
- @Test
- fun testReinflateAndBindLatest_disposesOnCancel() =
- testScope.runTest {
- var callbackCount = 0
- var disposed = false
- val job = launch {
- underTest.reinflateAndBindLatest<View>(
- resource = 0,
- root = null,
- attachToRoot = false,
- backgroundDispatcher,
- ) {
- callbackCount++
- DisposableHandle { disposed = true }
- }
- }
-
- runCurrent()
- job.cancelAndJoin()
- assertThat(disposed).isTrue()
- }
-
- inner class TestLayoutInflater : LayoutInflater(context) {
-
- var inflationCount = 0
-
- override fun inflate(resource: Int, root: ViewGroup?, attachToRoot: Boolean): View {
- inflationCount++
- return View(context)
- }
-
- override fun cloneInContext(p0: Context?): LayoutInflater {
- // not needed for this test
- return this
- }
- }
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsControllerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsControllerImplTest.kt
index c98d537..de455f63 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsControllerImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsControllerImplTest.kt
@@ -34,11 +34,12 @@
import com.android.systemui.controls.ControlsServiceInfo
import com.android.systemui.controls.management.ControlsListingController
import com.android.systemui.controls.panels.AuthorizedPanelsRepository
-import com.android.systemui.controls.panels.FakeSelectedComponentRepository
+import com.android.systemui.controls.panels.selectedComponentRepository
import com.android.systemui.controls.ui.ControlsUiController
import com.android.systemui.dump.DumpManager
import com.android.systemui.settings.UserFileManager
import com.android.systemui.settings.UserTracker
+import com.android.systemui.testKosmos
import com.android.systemui.util.concurrency.FakeExecutor
import com.android.systemui.util.mockito.whenever
import com.android.systemui.util.time.FakeSystemClock
@@ -69,12 +70,13 @@
import org.mockito.Mockito.`when`
import org.mockito.MockitoAnnotations
import java.io.File
-import java.util.*
+import java.util.Optional
import java.util.function.Consumer
@SmallTest
@RunWith(AndroidTestingRunner::class)
class ControlsControllerImplTest : SysuiTestCase() {
+ private val kosmos = testKosmos()
@Mock
private lateinit var uiController: ControlsUiController
@@ -109,8 +111,6 @@
private lateinit var listingCallbackCaptor:
ArgumentCaptor<ControlsListingController.ControlsListingCallback>
- private val preferredPanelRepository = FakeSelectedComponentRepository()
-
private lateinit var delayableExecutor: FakeExecutor
private lateinit var controller: ControlsControllerImpl
private lateinit var canceller: DidRunRunnable
@@ -171,7 +171,7 @@
wrapper,
delayableExecutor,
uiController,
- preferredPanelRepository,
+ kosmos.selectedComponentRepository,
bindingController,
listingController,
userFileManager,
@@ -225,7 +225,7 @@
mContext,
delayableExecutor,
uiController,
- preferredPanelRepository,
+ kosmos.selectedComponentRepository,
bindingController,
listingController,
userFileManager,
@@ -245,7 +245,7 @@
mContext,
delayableExecutor,
uiController,
- preferredPanelRepository,
+ kosmos.selectedComponentRepository,
bindingController,
listingController,
userFileManager,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/panels/AuthorizedPanelsRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/panels/AuthorizedPanelsRepositoryImplTest.kt
index 4828ba3..18ce4a8 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/controls/panels/AuthorizedPanelsRepositoryImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/controls/panels/AuthorizedPanelsRepositoryImplTest.kt
@@ -18,36 +18,40 @@
package com.android.systemui.controls.panels
import android.content.SharedPreferences
+import android.content.pm.UserInfo
import android.testing.AndroidTestingRunner
import androidx.test.filters.SmallTest
-import com.android.systemui.res.R
import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.res.R
+import com.android.systemui.settings.FakeUserTracker
import com.android.systemui.settings.UserFileManager
-import com.android.systemui.settings.UserTracker
+import com.android.systemui.settings.fakeUserTracker
+import com.android.systemui.testKosmos
import com.android.systemui.util.FakeSharedPreferences
-import com.android.systemui.util.mockito.whenever
import com.google.common.truth.Truth.assertThat
import java.io.File
+import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
-import org.mockito.Mock
-import org.mockito.MockitoAnnotations
@RunWith(AndroidTestingRunner::class)
@SmallTest
class AuthorizedPanelsRepositoryImplTest : SysuiTestCase() {
+ val kosmos = testKosmos()
+ val testScope = kosmos.testScope
- @Mock private lateinit var userTracker: UserTracker
+ private lateinit var userTracker: FakeUserTracker
@Before
fun setUp() {
- MockitoAnnotations.initMocks(this)
mContext.orCreateTestableResources.addOverride(
R.array.config_controlsPreferredPackages,
arrayOf<String>()
)
- whenever(userTracker.userId).thenReturn(0)
+ userTracker = kosmos.fakeUserTracker.apply { set(listOf(PRIMARY_USER, SECONDARY_USER), 0) }
}
@Test
@@ -91,7 +95,7 @@
val repository = createRepository(fileManager)
assertThat(repository.getAuthorizedPanels()).containsExactly(TEST_PACKAGE)
- whenever(userTracker.userId).thenReturn(1)
+ userTracker.set(listOf(SECONDARY_USER), 0)
assertThat(repository.getAuthorizedPanels()).isEmpty()
}
@@ -127,6 +131,51 @@
assertThat(sharedPrefs.getStringSet(KEY, null)).isEmpty()
}
+ @Test
+ fun observeAuthorizedPanels() =
+ testScope.runTest {
+ val sharedPrefs = FakeSharedPreferences()
+ val fileManager = FakeUserFileManager(mapOf(0 to sharedPrefs))
+ val repository = createRepository(fileManager)
+
+ val authorizedPanels by
+ collectLastValue(repository.observeAuthorizedPanels(PRIMARY_USER.userHandle))
+ assertThat(authorizedPanels).isEmpty()
+
+ repository.addAuthorizedPanels(setOf(TEST_PACKAGE))
+ assertThat(authorizedPanels).containsExactly(TEST_PACKAGE)
+
+ repository.removeAuthorizedPanels(setOf(TEST_PACKAGE))
+ assertThat(authorizedPanels).isEmpty()
+ }
+
+ @Test
+ fun observeAuthorizedPanelsForAnotherUser() =
+ testScope.runTest {
+ val fileManager =
+ FakeUserFileManager(
+ mapOf(
+ 0 to FakeSharedPreferences(),
+ 1 to FakeSharedPreferences(),
+ )
+ )
+ val repository = createRepository(fileManager)
+
+ val authorizedPanels by
+ collectLastValue(repository.observeAuthorizedPanels(SECONDARY_USER.userHandle))
+ assertThat(authorizedPanels).isEmpty()
+
+ // Primary user is active, add authorized panels.
+ repository.addAuthorizedPanels(setOf(TEST_PACKAGE))
+ assertThat(authorizedPanels).isEmpty()
+
+ // Make secondary user active and add authorized panels again.
+ userTracker.set(listOf(PRIMARY_USER, SECONDARY_USER), 1)
+ assertThat(authorizedPanels).isEmpty()
+ repository.addAuthorizedPanels(setOf(TEST_PACKAGE))
+ assertThat(authorizedPanels).containsExactly(TEST_PACKAGE)
+ }
+
private fun createRepository(userFileManager: UserFileManager): AuthorizedPanelsRepositoryImpl {
return AuthorizedPanelsRepositoryImpl(mContext, userFileManager, userTracker)
}
@@ -153,5 +202,9 @@
private const val FILE_NAME = "controls_prefs"
private const val KEY = "authorized_panels"
private const val TEST_PACKAGE = "package"
+ private val PRIMARY_USER =
+ UserInfo(/* id= */ 0, /* name= */ "primary user", /* flags= */ UserInfo.FLAG_MAIN)
+ private val SECONDARY_USER =
+ UserInfo(/* id= */ 1, /* name= */ "secondary user", /* flags= */ 0)
}
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/panels/SelectedComponentRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/panels/SelectedComponentRepositoryTest.kt
index b463adf..a7e7ba9 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/controls/panels/SelectedComponentRepositoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/controls/panels/SelectedComponentRepositoryTest.kt
@@ -23,8 +23,6 @@
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.coroutines.collectLastValue
-import com.android.systemui.flags.FakeFeatureFlags
-import com.android.systemui.kosmos.applicationCoroutineScope
import com.android.systemui.kosmos.testDispatcher
import com.android.systemui.kosmos.testScope
import com.android.systemui.settings.UserFileManager
@@ -74,7 +72,6 @@
@Mock private lateinit var userTracker: UserTracker
private lateinit var userFileManager: UserFileManager
- private val featureFlags = FakeFeatureFlags()
// under test
private lateinit var repository: SelectedComponentRepository
@@ -95,11 +92,9 @@
)
repository =
SelectedComponentRepositoryImpl(
- userFileManager,
- userTracker,
- featureFlags,
+ userFileManager = userFileManager,
+ userTracker = userTracker,
bgDispatcher = testDispatcher,
- applicationScope = applicationCoroutineScope
)
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/start/ControlsStartableTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/start/ControlsStartableTest.kt
index bcef67e..c44429b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/controls/start/ControlsStartableTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/controls/start/ControlsStartableTest.kt
@@ -31,15 +31,15 @@
import com.android.systemui.SysuiTestCase
import com.android.systemui.broadcast.BroadcastDispatcher
import com.android.systemui.common.data.repository.fakePackageChangeRepository
-import com.android.systemui.common.data.repository.packageChangeRepository
-import com.android.systemui.common.data.shared.model.PackageChangeModel
+import com.android.systemui.common.domain.interactor.packageChangeInteractor
+import com.android.systemui.common.shared.model.PackageChangeModel
import com.android.systemui.controls.ControlsServiceInfo
import com.android.systemui.controls.controller.ControlsController
import com.android.systemui.controls.dagger.ControlsComponent
import com.android.systemui.controls.management.ControlsListingController
import com.android.systemui.controls.panels.AuthorizedPanelsRepository
-import com.android.systemui.controls.panels.FakeSelectedComponentRepository
import com.android.systemui.controls.panels.SelectedComponentRepository
+import com.android.systemui.controls.panels.selectedComponentRepository
import com.android.systemui.controls.ui.SelectedItem
import com.android.systemui.kosmos.applicationCoroutineScope
import com.android.systemui.kosmos.testDispatcher
@@ -87,7 +87,7 @@
@Mock private lateinit var userManager: UserManager
@Mock private lateinit var broadcastDispatcher: BroadcastDispatcher
- private lateinit var preferredPanelsRepository: FakeSelectedComponentRepository
+ private lateinit var preferredPanelsRepository: SelectedComponentRepository
private lateinit var fakeExecutor: FakeExecutor
@@ -99,7 +99,7 @@
whenever(userTracker.userHandle).thenReturn(UserHandle.of(1))
fakeExecutor = FakeExecutor(FakeSystemClock())
- preferredPanelsRepository = FakeSelectedComponentRepository()
+ preferredPanelsRepository = kosmos.selectedComponentRepository
}
@Test
@@ -444,7 +444,7 @@
userTracker,
authorizedPanelsRepository,
preferredPanelsRepository,
- kosmos.packageChangeRepository,
+ kosmos.packageChangeInteractor,
userManager,
broadcastDispatcher,
)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/ui/ControlsUiControllerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/ui/ControlsUiControllerImplTest.kt
index 36ae0c7..8f3813d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/controls/ui/ControlsUiControllerImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/controls/ui/ControlsUiControllerImplTest.kt
@@ -43,8 +43,8 @@
import com.android.systemui.controls.management.ControlsListingController
import com.android.systemui.controls.management.ControlsProviderSelectorActivity
import com.android.systemui.controls.panels.AuthorizedPanelsRepository
-import com.android.systemui.controls.panels.FakeSelectedComponentRepository
import com.android.systemui.controls.panels.SelectedComponentRepository
+import com.android.systemui.controls.panels.selectedComponentRepository
import com.android.systemui.controls.settings.FakeControlsSettingsRepository
import com.android.systemui.dump.DumpManager
import com.android.systemui.flags.FeatureFlags
@@ -53,6 +53,7 @@
import com.android.systemui.settings.UserTracker
import com.android.systemui.statusbar.phone.SystemUIDialog
import com.android.systemui.statusbar.policy.KeyguardStateController
+import com.android.systemui.testKosmos
import com.android.systemui.util.FakeSystemUIDialogController
import com.android.systemui.util.concurrency.FakeExecutor
import com.android.systemui.util.mockito.any
@@ -85,6 +86,8 @@
@RunWith(AndroidTestingRunner::class)
@TestableLooper.RunWithLooper
class ControlsUiControllerImplTest : SysuiTestCase() {
+ private val kosmos = testKosmos()
+
@Mock lateinit var controlsController: ControlsController
@Mock lateinit var controlsListingController: ControlsListingController
@Mock lateinit var controlActionCoordinator: ControlActionCoordinator
@@ -100,7 +103,7 @@
@Mock lateinit var packageManager: PackageManager
@Mock lateinit var systemUIDialogFactory: SystemUIDialog.Factory
- private val preferredPanelRepository = FakeSelectedComponentRepository()
+ private val preferredPanelRepository = kosmos.selectedComponentRepository
private lateinit var fakeDialogController: FakeSystemUIDialogController
private val uiExecutor = FakeExecutor(FakeSystemClock())
private val bgExecutor = FakeExecutor(FakeSystemClock())
diff --git a/packages/SystemUI/tests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFaceAuthInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFaceAuthInteractorTest.kt
index 368d1d9..e30dd35d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFaceAuthInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFaceAuthInteractorTest.kt
@@ -16,62 +16,47 @@
package com.android.systemui.deviceentry.domain.interactor
-import android.app.trust.TrustManager
import android.content.pm.UserInfo
import android.hardware.biometrics.BiometricFaceConstants
import android.hardware.biometrics.BiometricSourceType
-import android.os.Handler
import android.os.PowerManager
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
-import com.android.keyguard.KeyguardSecurityModel
-import com.android.keyguard.KeyguardUpdateMonitor
+import com.android.keyguard.keyguardUpdateMonitor
+import com.android.keyguard.trustManager
import com.android.systemui.SysuiTestCase
import com.android.systemui.biometrics.data.repository.FaceSensorInfo
-import com.android.systemui.biometrics.data.repository.FakeFacePropertyRepository
-import com.android.systemui.biometrics.data.repository.FakeFingerprintPropertyRepository
+import com.android.systemui.biometrics.data.repository.facePropertyRepository
import com.android.systemui.biometrics.shared.model.LockoutMode
import com.android.systemui.biometrics.shared.model.SensorStrength
-import com.android.systemui.bouncer.data.repository.FakeKeyguardBouncerRepository
-import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor
-import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerCallbackInteractor
-import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor
-import com.android.systemui.bouncer.ui.BouncerView
-import com.android.systemui.classifier.FalsingCollector
+import com.android.systemui.bouncer.data.repository.fakeKeyguardBouncerRepository
+import com.android.systemui.bouncer.domain.interactor.alternateBouncerInteractor
+import com.android.systemui.bouncer.domain.interactor.primaryBouncerInteractor
import com.android.systemui.coroutines.collectLastValue
-import com.android.systemui.deviceentry.data.repository.FaceWakeUpTriggersConfig
+import com.android.systemui.deviceentry.data.repository.fakeFaceWakeUpTriggersConfig
import com.android.systemui.deviceentry.shared.FaceAuthUiEvent
import com.android.systemui.deviceentry.shared.model.ErrorFaceAuthenticationStatus
-import com.android.systemui.keyguard.DismissCallbackRegistry
-import com.android.systemui.keyguard.data.repository.FakeBiometricSettingsRepository
-import com.android.systemui.keyguard.data.repository.FakeDeviceEntryFaceAuthRepository
-import com.android.systemui.keyguard.data.repository.FakeDeviceEntryFingerprintAuthRepository
-import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
-import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
-import com.android.systemui.keyguard.data.repository.FakeTrustRepository
-import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
-import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractorFactory
+import com.android.systemui.keyguard.data.repository.fakeBiometricSettingsRepository
+import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFaceAuthRepository
+import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFingerprintAuthRepository
+import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
+import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor
import com.android.systemui.keyguard.shared.model.KeyguardState
import com.android.systemui.keyguard.shared.model.TransitionState
import com.android.systemui.keyguard.shared.model.TransitionStep
+import com.android.systemui.kosmos.testDispatcher
+import com.android.systemui.kosmos.testScope
import com.android.systemui.log.FaceAuthenticationLogger
import com.android.systemui.log.logcatLogBuffer
-import com.android.systemui.plugins.statusbar.StatusBarStateController
-import com.android.systemui.power.domain.interactor.PowerInteractor
import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAwakeForTest
-import com.android.systemui.power.domain.interactor.PowerInteractorFactory
+import com.android.systemui.power.domain.interactor.powerInteractor
import com.android.systemui.power.shared.model.WakeSleepReason
-import com.android.systemui.statusbar.policy.KeyguardStateController
+import com.android.systemui.testKosmos
import com.android.systemui.user.data.model.SelectionStatus
-import com.android.systemui.user.data.repository.FakeUserRepository
-import com.android.systemui.user.domain.interactor.SelectedUserInteractor
+import com.android.systemui.user.data.repository.fakeUserRepository
import com.android.systemui.util.mockito.eq
-import com.android.systemui.util.mockito.whenever
-import com.android.systemui.util.time.FakeSystemClock
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.test.StandardTestDispatcher
-import kotlinx.coroutines.test.TestCoroutineScheduler
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
@@ -79,98 +64,47 @@
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentMatchers.anyInt
-import org.mockito.Mock
-import org.mockito.Mockito.mock
import org.mockito.Mockito.never
import org.mockito.Mockito.verify
-import org.mockito.MockitoAnnotations
@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
@RunWith(AndroidJUnit4::class)
class DeviceEntryFaceAuthInteractorTest : SysuiTestCase() {
+ private val kosmos = testKosmos()
+ private val testScope: TestScope = kosmos.testScope
private lateinit var underTest: SystemUIDeviceEntryFaceAuthInteractor
- private lateinit var testScope: TestScope
- private lateinit var bouncerRepository: FakeKeyguardBouncerRepository
- private lateinit var keyguardTransitionRepository: FakeKeyguardTransitionRepository
- private lateinit var keyguardTransitionInteractor: KeyguardTransitionInteractor
- private lateinit var faceAuthRepository: FakeDeviceEntryFaceAuthRepository
- private lateinit var fakeUserRepository: FakeUserRepository
- private lateinit var facePropertyRepository: FakeFacePropertyRepository
- private lateinit var fakeDeviceEntryFingerprintAuthRepository:
- FakeDeviceEntryFingerprintAuthRepository
- private lateinit var fakeKeyguardRepository: FakeKeyguardRepository
- private lateinit var powerInteractor: PowerInteractor
- private lateinit var fakeBiometricSettingsRepository: FakeBiometricSettingsRepository
+ private val bouncerRepository = kosmos.fakeKeyguardBouncerRepository
+ private val keyguardTransitionRepository = kosmos.fakeKeyguardTransitionRepository
+ private val keyguardTransitionInteractor = kosmos.keyguardTransitionInteractor
+ private val faceAuthRepository = kosmos.fakeDeviceEntryFaceAuthRepository
+ private val fakeUserRepository = kosmos.fakeUserRepository
+ private val facePropertyRepository = kosmos.facePropertyRepository
+ private val fakeDeviceEntryFingerprintAuthInteractor =
+ kosmos.deviceEntryFingerprintAuthInteractor
+ private val powerInteractor = kosmos.powerInteractor
+ private val fakeBiometricSettingsRepository = kosmos.fakeBiometricSettingsRepository
- @Mock private lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor
- @Mock private lateinit var faceWakeUpTriggersConfig: FaceWakeUpTriggersConfig
- @Mock private lateinit var selectedUserInteractor: SelectedUserInteractor
- @Mock private lateinit var trustManager: TrustManager
+ private val keyguardUpdateMonitor = kosmos.keyguardUpdateMonitor
+ private val faceWakeUpTriggersConfig = kosmos.fakeFaceWakeUpTriggersConfig
+ private val trustManager = kosmos.trustManager
@Before
fun setup() {
- MockitoAnnotations.initMocks(this)
- val scheduler = TestCoroutineScheduler()
- val dispatcher = StandardTestDispatcher(scheduler)
- testScope = TestScope(dispatcher)
- bouncerRepository = FakeKeyguardBouncerRepository()
- faceAuthRepository = FakeDeviceEntryFaceAuthRepository()
- keyguardTransitionRepository = FakeKeyguardTransitionRepository()
- keyguardTransitionInteractor =
- KeyguardTransitionInteractorFactory.create(
- scope = TestScope().backgroundScope,
- repository = keyguardTransitionRepository,
- )
- .keyguardTransitionInteractor
-
- fakeDeviceEntryFingerprintAuthRepository = FakeDeviceEntryFingerprintAuthRepository()
- fakeUserRepository = FakeUserRepository()
fakeUserRepository.setUserInfos(listOf(primaryUser, secondaryUser))
- facePropertyRepository = FakeFacePropertyRepository()
- fakeKeyguardRepository = FakeKeyguardRepository()
- powerInteractor = PowerInteractorFactory.create().powerInteractor
- fakeBiometricSettingsRepository = FakeBiometricSettingsRepository()
-
underTest =
SystemUIDeviceEntryFaceAuthInteractor(
mContext,
testScope.backgroundScope,
- dispatcher,
+ kosmos.testDispatcher,
faceAuthRepository,
- {
- PrimaryBouncerInteractor(
- bouncerRepository,
- mock(BouncerView::class.java),
- mock(Handler::class.java),
- mock(KeyguardStateController::class.java),
- mock(KeyguardSecurityModel::class.java),
- mock(PrimaryBouncerCallbackInteractor::class.java),
- mock(FalsingCollector::class.java),
- mock(DismissCallbackRegistry::class.java),
- context,
- keyguardUpdateMonitor,
- FakeTrustRepository(),
- testScope.backgroundScope,
- selectedUserInteractor,
- underTest,
- )
- },
- AlternateBouncerInteractor(
- mock(StatusBarStateController::class.java),
- mock(KeyguardStateController::class.java),
- bouncerRepository,
- FakeFingerprintPropertyRepository(),
- fakeBiometricSettingsRepository,
- FakeSystemClock(),
- keyguardUpdateMonitor,
- testScope.backgroundScope,
- ),
+ { kosmos.primaryBouncerInteractor },
+ kosmos.alternateBouncerInteractor,
keyguardTransitionInteractor,
FaceAuthenticationLogger(logcatLogBuffer("faceAuthBuffer")),
keyguardUpdateMonitor,
- fakeDeviceEntryFingerprintAuthRepository,
+ fakeDeviceEntryFingerprintAuthInteractor,
fakeUserRepository,
facePropertyRepository,
faceWakeUpTriggersConfig,
@@ -186,10 +120,9 @@
underTest.start()
powerInteractor.setAwakeForTest(reason = PowerManager.WAKE_REASON_LID)
- whenever(
- faceWakeUpTriggersConfig.shouldTriggerFaceAuthOnWakeUpFrom(WakeSleepReason.LID)
- )
- .thenReturn(true)
+ faceWakeUpTriggersConfig.setTriggerFaceAuthOnWakeUpFrom(
+ setOf(WakeSleepReason.LID.powerManagerWakeReason)
+ )
keyguardTransitionRepository.sendTransitionStep(
TransitionStep(
@@ -228,10 +161,9 @@
underTest.start()
powerInteractor.setAwakeForTest(reason = PowerManager.WAKE_REASON_LID)
- whenever(
- faceWakeUpTriggersConfig.shouldTriggerFaceAuthOnWakeUpFrom(WakeSleepReason.LID)
- )
- .thenReturn(true)
+ faceWakeUpTriggersConfig.setTriggerFaceAuthOnWakeUpFrom(
+ setOf(WakeSleepReason.LID.powerManagerWakeReason)
+ )
keyguardTransitionRepository.sendTransitionStep(
TransitionStep(
@@ -254,10 +186,9 @@
underTest.start()
powerInteractor.setAwakeForTest(reason = PowerManager.WAKE_REASON_LIFT)
- whenever(
- faceWakeUpTriggersConfig.shouldTriggerFaceAuthOnWakeUpFrom(WakeSleepReason.LIFT)
- )
- .thenReturn(false)
+ faceWakeUpTriggersConfig.setTriggerFaceAuthOnWakeUpFrom(
+ setOf(WakeSleepReason.LID.powerManagerWakeReason)
+ )
keyguardTransitionRepository.sendTransitionStep(
TransitionStep(
@@ -277,10 +208,9 @@
underTest.start()
powerInteractor.setAwakeForTest(reason = PowerManager.WAKE_REASON_LID)
- whenever(
- faceWakeUpTriggersConfig.shouldTriggerFaceAuthOnWakeUpFrom(WakeSleepReason.LID)
- )
- .thenReturn(true)
+ faceWakeUpTriggersConfig.setTriggerFaceAuthOnWakeUpFrom(
+ setOf(WakeSleepReason.LID.powerManagerWakeReason)
+ )
keyguardTransitionRepository.sendTransitionStep(
TransitionStep(
@@ -500,7 +430,45 @@
underTest.start()
fakeBiometricSettingsRepository.setIsFaceAuthEnrolledAndEnabled(true)
- fakeDeviceEntryFingerprintAuthRepository.setLockedOut(true)
+ kosmos.fakeDeviceEntryFingerprintAuthRepository.setLockedOut(true)
+ runCurrent()
+
+ assertThat(faceAuthRepository.isLockedOut.value).isTrue()
+ }
+
+ @Test
+ fun faceLockoutStateIsResetWheneverFingerprintIsNotLockedOut() =
+ testScope.runTest {
+ underTest.start()
+ fakeUserRepository.setSelectedUserInfo(primaryUser, SelectionStatus.SELECTION_COMPLETE)
+ fakeBiometricSettingsRepository.setIsFaceAuthEnrolledAndEnabled(true)
+
+ kosmos.fakeDeviceEntryFingerprintAuthRepository.setLockedOut(true)
+ runCurrent()
+
+ assertThat(faceAuthRepository.isLockedOut.value).isTrue()
+ facePropertyRepository.setLockoutMode(primaryUserId, LockoutMode.NONE)
+
+ kosmos.fakeDeviceEntryFingerprintAuthRepository.setLockedOut(false)
+ runCurrent()
+
+ assertThat(faceAuthRepository.isLockedOut.value).isFalse()
+ }
+
+ @Test
+ fun faceLockoutStateIsSetToUsersLockoutStateWheneverFingerprintIsNotLockedOut() =
+ testScope.runTest {
+ underTest.start()
+ fakeUserRepository.setSelectedUserInfo(primaryUser, SelectionStatus.SELECTION_COMPLETE)
+ fakeBiometricSettingsRepository.setIsFaceAuthEnrolledAndEnabled(true)
+
+ kosmos.fakeDeviceEntryFingerprintAuthRepository.setLockedOut(true)
+ runCurrent()
+
+ assertThat(faceAuthRepository.isLockedOut.value).isTrue()
+ facePropertyRepository.setLockoutMode(primaryUserId, LockoutMode.TIMED)
+
+ kosmos.fakeDeviceEntryFingerprintAuthRepository.setLockedOut(false)
runCurrent()
assertThat(faceAuthRepository.isLockedOut.value).isTrue()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/doze/DozeDockHandlerTest.java b/packages/SystemUI/tests/src/com/android/systemui/doze/DozeDockHandlerTest.java
index af027e8..6d2df19 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/doze/DozeDockHandlerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/doze/DozeDockHandlerTest.java
@@ -63,6 +63,7 @@
mDockHandler = new DozeDockHandler(mConfig, mDockManagerFake, mUserTracker);
mDockHandler.setDozeMachine(mMachine);
+ when(mMachine.isExecutingTransition()).thenReturn(false);
when(mUserTracker.getUserId()).thenReturn(ActivityManager.getCurrentUser());
when(mMachine.getState()).thenReturn(State.DOZE_AOD);
doReturn(true).when(mConfig).alwaysOnEnabled(anyInt());
@@ -148,4 +149,13 @@
verify(mMachine, never()).requestState(any(State.class));
}
+
+ @Test
+ public void onEvent_dockedWhileTransitioning_wontRequestStateChange() {
+ when(mMachine.isExecutingTransition()).thenReturn(true);
+
+ mDockManagerFake.setDockEvent(DockManager.STATE_DOCKED_HIDE);
+
+ verify(mMachine, never()).requestState(any(State.class));
+ }
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/haptics/slider/SeekableSliderTrackerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/haptics/slider/SeekableSliderTrackerTest.kt
index db04962..796d6d9 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/haptics/slider/SeekableSliderTrackerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/haptics/slider/SeekableSliderTrackerTest.kt
@@ -20,8 +20,8 @@
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.test.TestCoroutineScheduler
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.advanceTimeBy
import kotlinx.coroutines.test.runTest
@@ -36,6 +36,7 @@
import org.mockito.MockitoAnnotations
@SmallTest
+@OptIn(ExperimentalCoroutinesApi::class)
@RunWith(AndroidJUnit4::class)
class SeekableSliderTrackerTest : SysuiTestCase() {
@@ -51,7 +52,7 @@
@Test
fun initializeSliderTracker_startsTracking() = runTest {
// GIVEN Initialized tracker
- initTracker(testScheduler)
+ initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler)))
// THEN the tracker job is active
assertThat(mSeekableSliderTracker.isTracking).isTrue()
@@ -61,7 +62,7 @@
fun stopTracking_onAnyState_resetsToIdle() = runTest {
enumValues<SliderState>().forEach {
// GIVEN Initialized tracker
- initTracker(testScheduler)
+ initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler)))
// GIVEN a state in the state machine
mSeekableSliderTracker.setState(it)
@@ -79,7 +80,7 @@
@Test
fun initializeSliderTracker_isIdle() = runTest {
// GIVEN Initialized tracker
- initTracker(testScheduler)
+ initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler)))
// THEN The state is idle and the listener is not called to play haptics
assertThat(mSeekableSliderTracker.currentState).isEqualTo(SliderState.IDLE)
@@ -88,7 +89,7 @@
@Test
fun startsTrackingTouch_onIdle_entersWaitState() = runTest {
- initTracker(testScheduler)
+ initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler)))
// GIVEN a start of tracking touch event
val progress = 0f
@@ -106,7 +107,7 @@
@Test
fun waitCompletes_onWait_movesToHandleAcquired() = runTest {
val config = SeekableSliderTrackerConfig()
- initTracker(testScheduler, config)
+ initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler)), config)
// GIVEN a start of tracking touch event that moves the tracker to WAIT
val progress = 0f
@@ -126,7 +127,7 @@
@Test
fun impreciseTouch_onWait_movesToHandleAcquired() = runTest {
val config = SeekableSliderTrackerConfig()
- initTracker(testScheduler, config)
+ initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler)), config)
// GIVEN a start of tracking touch event that moves the tracker to WAIT at the middle of the
// slider
@@ -151,7 +152,7 @@
@Test
fun trackJump_onWait_movesToJumpTrackLocationSelected() = runTest {
val config = SeekableSliderTrackerConfig()
- initTracker(testScheduler, config)
+ initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler)), config)
// GIVEN a start of tracking touch event that moves the tracker to WAIT at the middle of the
// slider
@@ -175,7 +176,7 @@
@Test
fun upperBookendSelection_onWait_movesToBookendSelected() = runTest {
val config = SeekableSliderTrackerConfig()
- initTracker(testScheduler, config)
+ initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler)), config)
// GIVEN a start of tracking touch event that moves the tracker to WAIT at the middle of the
// slider
@@ -197,7 +198,7 @@
@Test
fun lowerBookendSelection_onWait_movesToBookendSelected() = runTest {
val config = SeekableSliderTrackerConfig()
- initTracker(testScheduler, config)
+ initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler)), config)
// GIVEN a start of tracking touch event that moves the tracker to WAIT at the middle of the
// slider
@@ -219,7 +220,7 @@
@Test
fun stopTracking_onWait_whenWaitingJobIsActive_resetsToIdle() = runTest {
val config = SeekableSliderTrackerConfig()
- initTracker(testScheduler, config)
+ initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler)), config)
// GIVEN a start of tracking touch event that moves the tracker to WAIT at the middle of the
// slider
@@ -240,7 +241,7 @@
@Test
fun progressChangeByUser_onJumpTrackLocationSelected_movesToDragHandleDragging() = runTest {
- initTracker(testScheduler)
+ initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler)))
// GIVEN a JUMP_TRACK_LOCATION_SELECTED state
mSeekableSliderTracker.setState(SliderState.JUMP_TRACK_LOCATION_SELECTED)
@@ -256,7 +257,7 @@
@Test
fun touchRelease_onJumpTrackLocationSelected_movesToIdle() = runTest {
- initTracker(testScheduler)
+ initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler)))
// GIVEN a JUMP_TRACK_LOCATION_SELECTED state
mSeekableSliderTracker.setState(SliderState.JUMP_TRACK_LOCATION_SELECTED)
@@ -272,7 +273,7 @@
@Test
fun progressChangeByUser_onJumpBookendSelected_movesToDragHandleDragging() = runTest {
- initTracker(testScheduler)
+ initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler)))
// GIVEN a JUMP_BOOKEND_SELECTED state
mSeekableSliderTracker.setState(SliderState.JUMP_BOOKEND_SELECTED)
@@ -288,7 +289,7 @@
@Test
fun touchRelease_onJumpBookendSelected_movesToIdle() = runTest {
- initTracker(testScheduler)
+ initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler)))
// GIVEN a JUMP_BOOKEND_SELECTED state
mSeekableSliderTracker.setState(SliderState.JUMP_BOOKEND_SELECTED)
@@ -306,7 +307,7 @@
@Test
fun progressChangeByUser_onHandleAcquired_movesToDragHandleDragging() = runTest {
- initTracker(testScheduler)
+ initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler)))
// GIVEN a DRAG_HANDLE_ACQUIRED_BY_TOUCH state
mSeekableSliderTracker.setState(SliderState.DRAG_HANDLE_ACQUIRED_BY_TOUCH)
@@ -325,7 +326,7 @@
@Test
fun touchRelease_onHandleAcquired_movesToIdle() = runTest {
- initTracker(testScheduler)
+ initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler)))
// GIVEN a DRAG_HANDLE_ACQUIRED_BY_TOUCH state
mSeekableSliderTracker.setState(SliderState.DRAG_HANDLE_ACQUIRED_BY_TOUCH)
@@ -344,7 +345,7 @@
@Test
fun progressChangeByUser_onHandleDragging_progressOutsideOfBookends_doesNotChangeState() =
runTest {
- initTracker(testScheduler)
+ initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler)))
// GIVEN a DRAG_HANDLE_DRAGGING state
mSeekableSliderTracker.setState(SliderState.DRAG_HANDLE_DRAGGING)
@@ -366,7 +367,7 @@
fun progressChangeByUser_onHandleDragging_reachesLowerBookend_movesToHandleReachedBookend() =
runTest {
val config = SeekableSliderTrackerConfig()
- initTracker(testScheduler, config)
+ initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler)), config)
// GIVEN a DRAG_HANDLE_DRAGGING state
mSeekableSliderTracker.setState(SliderState.DRAG_HANDLE_DRAGGING)
@@ -389,7 +390,7 @@
fun progressChangeByUser_onHandleDragging_reachesUpperBookend_movesToHandleReachedBookend() =
runTest {
val config = SeekableSliderTrackerConfig()
- initTracker(testScheduler, config)
+ initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler)), config)
// GIVEN a DRAG_HANDLE_DRAGGING state
mSeekableSliderTracker.setState(SliderState.DRAG_HANDLE_DRAGGING)
@@ -410,7 +411,7 @@
@Test
fun touchRelease_onHandleDragging_movesToIdle() = runTest {
- initTracker(testScheduler)
+ initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler)))
// GIVEN a DRAG_HANDLE_DRAGGING state
mSeekableSliderTracker.setState(SliderState.DRAG_HANDLE_DRAGGING)
@@ -430,7 +431,7 @@
fun progressChangeByUser_outsideOfBookendRange_onLowerBookend_movesToDragHandleDragging() =
runTest {
val config = SeekableSliderTrackerConfig()
- initTracker(testScheduler, config)
+ initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler)), config)
// GIVEN a DRAG_HANDLE_REACHED_BOOKEND state
mSeekableSliderTracker.setState(SliderState.DRAG_HANDLE_REACHED_BOOKEND)
@@ -451,7 +452,7 @@
@Test
fun progressChangeByUser_insideOfBookendRange_onLowerBookend_doesNotChangeState() = runTest {
val config = SeekableSliderTrackerConfig()
- initTracker(testScheduler, config)
+ initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler)), config)
// GIVEN a DRAG_HANDLE_REACHED_BOOKEND state
mSeekableSliderTracker.setState(SliderState.DRAG_HANDLE_REACHED_BOOKEND)
@@ -473,7 +474,7 @@
fun progressChangeByUser_outsideOfBookendRange_onUpperBookend_movesToDragHandleDragging() =
runTest {
val config = SeekableSliderTrackerConfig()
- initTracker(testScheduler, config)
+ initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler)), config)
// GIVEN a DRAG_HANDLE_REACHED_BOOKEND state
mSeekableSliderTracker.setState(SliderState.DRAG_HANDLE_REACHED_BOOKEND)
@@ -494,7 +495,7 @@
@Test
fun progressChangeByUser_insideOfBookendRange_onUpperBookend_doesNotChangeState() = runTest {
val config = SeekableSliderTrackerConfig()
- initTracker(testScheduler, config)
+ initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler)), config)
// GIVEN a DRAG_HANDLE_REACHED_BOOKEND state
mSeekableSliderTracker.setState(SliderState.DRAG_HANDLE_REACHED_BOOKEND)
@@ -514,7 +515,7 @@
@Test
fun touchRelease_onHandleReachedBookend_movesToIdle() = runTest {
- initTracker(testScheduler)
+ initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler)))
// GIVEN a DRAG_HANDLE_REACHED_BOOKEND state
mSeekableSliderTracker.setState(SliderState.DRAG_HANDLE_REACHED_BOOKEND)
@@ -531,7 +532,7 @@
@Test
fun onProgressChangeByProgram_atTheMiddle_onIdle_movesToArrowHandleMovedOnce() = runTest {
// GIVEN an initialized tracker in the IDLE state
- initTracker(testScheduler)
+ initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler)))
// GIVEN a progress due to an external source that lands at the middle of the slider
val progress = 0.5f
@@ -550,7 +551,7 @@
fun onProgressChangeByProgram_atUpperBookend_onIdle_movesToIdle() = runTest {
// GIVEN an initialized tracker in the IDLE state
val config = SeekableSliderTrackerConfig()
- initTracker(testScheduler, config)
+ initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler)), config)
// GIVEN a progress due to an external source that lands at the upper bookend
val progress = config.upperBookendThreshold + 0.01f
@@ -567,7 +568,7 @@
fun onProgressChangeByProgram_atLowerBookend_onIdle_movesToIdle() = runTest {
// GIVEN an initialized tracker in the IDLE state
val config = SeekableSliderTrackerConfig()
- initTracker(testScheduler, config)
+ initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler)), config)
// WHEN a progress is recorded due to an external source that lands at the lower bookend
val progress = config.lowerBookendThreshold - 0.01f
@@ -583,7 +584,7 @@
@Test
fun onArrowUp_onArrowMovedOnce_movesToIdle() = runTest {
// GIVEN an initialized tracker in the ARROW_HANDLE_MOVED_ONCE state
- initTracker(testScheduler)
+ initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler)))
mSeekableSliderTracker.setState(SliderState.ARROW_HANDLE_MOVED_ONCE)
// WHEN the external stimulus is released
@@ -598,7 +599,7 @@
@Test
fun onStartTrackingTouch_onArrowMovedOnce_movesToWait() = runTest {
// GIVEN an initialized tracker in the ARROW_HANDLE_MOVED_ONCE state
- initTracker(testScheduler)
+ initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler)))
mSeekableSliderTracker.setState(SliderState.ARROW_HANDLE_MOVED_ONCE)
// WHEN the slider starts tracking touch
@@ -615,7 +616,7 @@
@Test
fun onProgressChangeByProgram_onArrowMovedOnce_movesToArrowMovesContinuously() = runTest {
// GIVEN an initialized tracker in the ARROW_HANDLE_MOVED_ONCE state
- initTracker(testScheduler)
+ initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler)))
mSeekableSliderTracker.setState(SliderState.ARROW_HANDLE_MOVED_ONCE)
// WHEN the slider gets an external progress change
@@ -634,7 +635,7 @@
@Test
fun onArrowUp_onArrowMovesContinuously_movesToIdle() = runTest {
// GIVEN an initialized tracker in the ARROW_HANDLE_MOVES_CONTINUOUSLY state
- initTracker(testScheduler)
+ initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler)))
mSeekableSliderTracker.setState(SliderState.ARROW_HANDLE_MOVES_CONTINUOUSLY)
// WHEN the external stimulus is released
@@ -649,7 +650,7 @@
@Test
fun onStartTrackingTouch_onArrowMovesContinuously_movesToWait() = runTest {
// GIVEN an initialized tracker in the ARROW_HANDLE_MOVES_CONTINUOUSLY state
- initTracker(testScheduler)
+ initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler)))
mSeekableSliderTracker.setState(SliderState.ARROW_HANDLE_MOVES_CONTINUOUSLY)
// WHEN the slider starts tracking touch
@@ -665,7 +666,7 @@
@Test
fun onProgressChangeByProgram_onArrowMovesContinuously_preservesState() = runTest {
// GIVEN an initialized tracker in the ARROW_HANDLE_MOVES_CONTINUOUSLY state
- initTracker(testScheduler)
+ initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler)))
mSeekableSliderTracker.setState(SliderState.ARROW_HANDLE_MOVES_CONTINUOUSLY)
// WHEN the slider changes progress programmatically at the middle
@@ -684,7 +685,7 @@
fun onProgramProgress_atLowerBookend_onArrowMovesContinuously_movesToIdle() = runTest {
// GIVEN an initialized tracker in the ARROW_HANDLE_MOVES_CONTINUOUSLY state
val config = SeekableSliderTrackerConfig()
- initTracker(testScheduler, config)
+ initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler)), config)
mSeekableSliderTracker.setState(SliderState.ARROW_HANDLE_MOVES_CONTINUOUSLY)
// WHEN the slider reaches the lower bookend programmatically
@@ -702,7 +703,7 @@
fun onProgramProgress_atUpperBookend_onArrowMovesContinuously_movesToIdle() = runTest {
// GIVEN an initialized tracker in the ARROW_HANDLE_MOVES_CONTINUOUSLY state
val config = SeekableSliderTrackerConfig()
- initTracker(testScheduler, config)
+ initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler)), config)
mSeekableSliderTracker.setState(SliderState.ARROW_HANDLE_MOVES_CONTINUOUSLY)
// WHEN the slider reaches the lower bookend programmatically
@@ -718,16 +719,11 @@
@OptIn(ExperimentalCoroutinesApi::class)
private fun initTracker(
- scheduler: TestCoroutineScheduler,
+ scope: CoroutineScope,
config: SeekableSliderTrackerConfig = SeekableSliderTrackerConfig(),
) {
mSeekableSliderTracker =
- SeekableSliderTracker(
- sliderStateListener,
- sliderEventProducer,
- UnconfinedTestDispatcher(scheduler),
- config
- )
+ SeekableSliderTracker(sliderStateListener, sliderEventProducer, scope, config)
mSeekableSliderTracker.startTracking()
}
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyboard/stickykeys/data/repository/StickyKeysRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyboard/stickykeys/data/repository/StickyKeysRepositoryImplTest.kt
deleted file mode 100644
index ed80a86..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/keyboard/stickykeys/data/repository/StickyKeysRepositoryImplTest.kt
+++ /dev/null
@@ -1,98 +0,0 @@
-package com.android.systemui.keyboard.stickykeys.data.repository
-
-import android.content.pm.UserInfo
-import android.hardware.input.InputManager
-import android.provider.Settings.Secure.ACCESSIBILITY_STICKY_KEYS
-import androidx.test.filters.SmallTest
-import com.android.systemui.SysuiTestCase
-import com.android.systemui.coroutines.collectLastValue
-import com.android.systemui.coroutines.collectValues
-import com.android.systemui.keyboard.stickykeys.StickyKeysLogger
-import com.android.systemui.kosmos.Kosmos
-import com.android.systemui.user.data.repository.fakeUserRepository
-import com.android.systemui.util.mockito.mock
-import com.android.systemui.util.settings.FakeSettings
-import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.test.StandardTestDispatcher
-import kotlinx.coroutines.test.TestScope
-import kotlinx.coroutines.test.runCurrent
-import kotlinx.coroutines.test.runTest
-import org.junit.Before
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
-
-@OptIn(ExperimentalCoroutinesApi::class)
-@SmallTest
-@RunWith(JUnit4::class)
-class StickyKeysRepositoryImplTest : SysuiTestCase() {
-
- private val dispatcher = StandardTestDispatcher()
- private val testScope = TestScope(dispatcher)
- private val secureSettings = FakeSettings()
- private val userRepository = Kosmos().fakeUserRepository
- private lateinit var stickyKeysRepository: StickyKeysRepositoryImpl
-
- @Before
- fun setup() {
- stickyKeysRepository = StickyKeysRepositoryImpl(
- mock<InputManager>(),
- dispatcher,
- secureSettings,
- userRepository,
- mock<StickyKeysLogger>()
- )
- userRepository.setUserInfos(USER_INFOS)
- setStickyKeySettingForUser(enabled = true, userInfo = SETTING_ENABLED_USER)
- setStickyKeySettingForUser(enabled = false, userInfo = SETTING_DISABLED_USER)
- }
-
- @Test
- fun settingEnabledEmitsValueForCurrentUser() {
- testScope.runTest {
- userRepository.setSelectedUserInfo(SETTING_ENABLED_USER)
-
- val enabled by collectLastValue(stickyKeysRepository.settingEnabled)
-
- assertThat(enabled).isTrue()
- }
- }
-
- @Test
- fun settingEnabledEmitsNewValueWhenSettingChanges() {
- testScope.runTest {
- userRepository.setSelectedUserInfo(SETTING_ENABLED_USER)
- val enabled by collectValues(stickyKeysRepository.settingEnabled)
- runCurrent()
-
- setStickyKeySettingForUser(enabled = false, userInfo = SETTING_ENABLED_USER)
-
- assertThat(enabled).containsExactly(true, false).inOrder()
- }
- }
-
- @Test
- fun settingEnabledEmitsValueForNewUserWhenUserChanges() {
- testScope.runTest {
- userRepository.setSelectedUserInfo(SETTING_ENABLED_USER)
- val enabled by collectLastValue(stickyKeysRepository.settingEnabled)
- runCurrent()
-
- userRepository.setSelectedUserInfo(SETTING_DISABLED_USER)
-
- assertThat(enabled).isFalse()
- }
- }
-
- private fun setStickyKeySettingForUser(enabled: Boolean, userInfo: UserInfo) {
- val newValue = if (enabled) "1" else "0"
- secureSettings.putStringForUser(ACCESSIBILITY_STICKY_KEYS, newValue, userInfo.id)
- }
-
- private companion object {
- val SETTING_ENABLED_USER = UserInfo(/* id= */ 0, "user1", /* flags= */ 0)
- val SETTING_DISABLED_USER = UserInfo(/* id= */ 1, "user2", /* flags= */ 0)
- val USER_INFOS = listOf(SETTING_ENABLED_USER, SETTING_DISABLED_USER)
- }
-}
\ No newline at end of file
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyboard/stickykeys/ui/StickyKeysIndicatorCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyboard/stickykeys/ui/StickyKeysIndicatorCoordinatorTest.kt
index df73cc8..a992956 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyboard/stickykeys/ui/StickyKeysIndicatorCoordinatorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyboard/stickykeys/ui/StickyKeysIndicatorCoordinatorTest.kt
@@ -16,6 +16,7 @@
package com.android.systemui.keyboard.stickykeys.ui
+import android.app.Dialog
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.compose.ComposeFacade
@@ -26,8 +27,6 @@
import com.android.systemui.keyboard.stickykeys.shared.model.ModifierKey.SHIFT
import com.android.systemui.keyboard.stickykeys.ui.viewmodel.StickyKeysIndicatorViewModel
import com.android.systemui.kosmos.Kosmos
-import com.android.systemui.statusbar.phone.ComponentSystemUIDialog
-import com.android.systemui.statusbar.phone.SystemUIDialogFactory
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.whenever
@@ -40,8 +39,6 @@
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
-import org.mockito.Mockito.anyBoolean
-import org.mockito.Mockito.anyInt
import org.mockito.Mockito.verify
import org.mockito.Mockito.verifyZeroInteractions
@@ -53,15 +50,13 @@
private lateinit var coordinator: StickyKeysIndicatorCoordinator
private val testScope = TestScope(StandardTestDispatcher())
private val stickyKeysRepository = FakeStickyKeysRepository()
- private val dialog = mock<ComponentSystemUIDialog>()
+ private val dialog = mock<Dialog>()
@Before
fun setup() {
Assume.assumeTrue(ComposeFacade.isComposeAvailable())
- val dialogFactory = mock<SystemUIDialogFactory> {
- whenever(applicationContext).thenReturn(context)
- whenever(create(any(), anyInt(), anyBoolean())).thenReturn(dialog)
- }
+ val dialogFactory = mock<StickyKeyDialogFactory>()
+ whenever(dialogFactory.create(any())).thenReturn(dialog)
val keyboardRepository = Kosmos().keyboardRepository
val viewModel = StickyKeysIndicatorViewModel(
stickyKeysRepository,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyboard/stickykeys/ui/viewmodel/StickyKeysIndicatorViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyboard/stickykeys/ui/viewmodel/StickyKeysIndicatorViewModelTest.kt
index 6eebb6d..d14d72d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyboard/stickykeys/ui/viewmodel/StickyKeysIndicatorViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyboard/stickykeys/ui/viewmodel/StickyKeysIndicatorViewModelTest.kt
@@ -37,6 +37,7 @@
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.mock
import com.android.systemui.util.settings.FakeSettings
+import com.android.systemui.util.settings.repository.UserAwareSecureSettingsRepositoryImpl
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.StandardTestDispatcher
@@ -68,11 +69,15 @@
@Before
fun setup() {
+ val settingsRepository = UserAwareSecureSettingsRepositoryImpl(
+ secureSettings,
+ userRepository,
+ dispatcher
+ )
val stickyKeysRepository = StickyKeysRepositoryImpl(
inputManager,
dispatcher,
- secureSettings,
- userRepository,
+ settingsRepository,
mock<StickyKeysLogger>()
)
setStickyKeySetting(enabled = false)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java
index 14cae0b..2732047 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java
@@ -86,7 +86,7 @@
import com.android.keyguard.mediator.ScreenOnCoordinator;
import com.android.systemui.DejankUtils;
import com.android.systemui.SysuiTestCase;
-import com.android.systemui.animation.ActivityLaunchAnimator;
+import com.android.systemui.animation.ActivityTransitionAnimator;
import com.android.systemui.biometrics.AuthController;
import com.android.systemui.broadcast.BroadcastDispatcher;
import com.android.systemui.classifier.FalsingCollectorFake;
@@ -94,7 +94,6 @@
import com.android.systemui.dreams.DreamOverlayStateController;
import com.android.systemui.dump.DumpManager;
import com.android.systemui.flags.FakeFeatureFlags;
-import com.android.systemui.flags.Flags;
import com.android.systemui.flags.SystemPropertiesHelper;
import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor;
import com.android.systemui.keyguard.ui.viewmodel.DreamingToLockscreenTransitionViewModel;
@@ -187,7 +186,7 @@
private @Mock ShadeController mShadeController;
private NotificationShadeWindowController mNotificationShadeWindowController;
private @Mock DreamOverlayStateController mDreamOverlayStateController;
- private @Mock ActivityLaunchAnimator mActivityLaunchAnimator;
+ private @Mock ActivityTransitionAnimator mActivityTransitionAnimator;
private @Mock ScrimController mScrimController;
private @Mock IActivityTaskManager mActivityTaskManagerService;
private @Mock SysuiColorExtractor mColorExtractor;
@@ -754,7 +753,7 @@
@Test
public void testUpdateIsKeyguardAfterOccludeAnimationEnds() {
- mViewMediator.mOccludeAnimationController.onLaunchAnimationEnd(
+ mViewMediator.mOccludeAnimationController.onTransitionAnimationEnd(
false /* isExpandingFullyAbove */);
// Since the updateIsKeyguard call is delayed during the animation, ensure it's called once
@@ -764,7 +763,7 @@
@Test
public void testUpdateIsKeyguardAfterOccludeAnimationIsCancelled() {
- mViewMediator.mOccludeAnimationController.onLaunchAnimationCancelled(
+ mViewMediator.mOccludeAnimationController.onTransitionAnimationCancelled(
null /* newKeyguardOccludedState */);
// Since the updateIsKeyguard call is delayed during the animation, ensure it's called if
@@ -1232,7 +1231,7 @@
mWallpaperRepository,
() -> mShadeController,
() -> mNotificationShadeWindowController,
- () -> mActivityLaunchAnimator,
+ () -> mActivityTransitionAnimator,
() -> mScrimController,
mActivityTaskManagerService,
mFeatureFlags,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ResourceTrimmerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ResourceTrimmerTest.kt
index 852f9a5..cf8fe79 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ResourceTrimmerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ResourceTrimmerTest.kt
@@ -5,16 +5,16 @@
import android.testing.AndroidTestingRunner
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
-import com.android.systemui.flags.FakeFeatureFlags
import com.android.systemui.flags.Flags
-import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
-import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
+import com.android.systemui.flags.fakeFeatureFlagsClassic
+import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository
+import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
import com.android.systemui.keyguard.domain.interactor.KeyguardInteractorFactory
-import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractorFactory
+import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor
import com.android.systemui.keyguard.shared.model.KeyguardState
-import com.android.systemui.power.domain.interactor.PowerInteractor
import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAsleepForTest
-import com.android.systemui.power.domain.interactor.PowerInteractorFactory
+import com.android.systemui.power.domain.interactor.powerInteractor
+import com.android.systemui.testKosmos
import com.android.systemui.util.mockito.any
import com.android.systemui.utils.GlobalWindowManager
import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -36,13 +36,14 @@
@RunWith(AndroidTestingRunner::class)
@SmallTest
class ResourceTrimmerTest : SysuiTestCase() {
+ val kosmos = testKosmos()
private val testDispatcher = UnconfinedTestDispatcher()
private val testScope = TestScope(testDispatcher)
- private val keyguardRepository = FakeKeyguardRepository()
- private val featureFlags = FakeFeatureFlags()
- private val keyguardTransitionRepository = FakeKeyguardTransitionRepository()
- private lateinit var powerInteractor: PowerInteractor
+ private val keyguardRepository = kosmos.fakeKeyguardRepository
+ private val featureFlags = kosmos.fakeFeatureFlagsClassic
+ private val keyguardTransitionRepository = kosmos.fakeKeyguardTransitionRepository
+ private val powerInteractor = kosmos.powerInteractor
@Mock private lateinit var globalWindowManager: GlobalWindowManager
private lateinit var resourceTrimmer: ResourceTrimmer
@@ -52,7 +53,6 @@
MockitoAnnotations.initMocks(this)
featureFlags.set(Flags.TRIM_RESOURCES_WITH_BACKGROUND_TRIM_AT_LOCK, true)
featureFlags.set(Flags.TRIM_FONT_CACHES_AT_UNLOCK, true)
- powerInteractor = PowerInteractorFactory.create().powerInteractor
keyguardRepository.setDozeAmount(0f)
keyguardRepository.setKeyguardGoingAway(false)
@@ -66,11 +66,7 @@
ResourceTrimmer(
keyguardInteractor,
powerInteractor,
- KeyguardTransitionInteractorFactory.create(
- scope = TestScope().backgroundScope,
- repository = keyguardTransitionRepository,
- )
- .keyguardTransitionInteractor,
+ kosmos.keyguardTransitionInteractor,
globalWindowManager,
testScope.backgroundScope,
testDispatcher,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractorTest.kt
new file mode 100644
index 0000000..c174cb8
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractorTest.kt
@@ -0,0 +1,99 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyguard.domain.interactor
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
+import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository
+import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
+import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.keyguard.shared.model.TransitionState
+import com.android.systemui.keyguard.shared.model.TransitionStep
+import com.android.systemui.keyguard.util.KeyguardTransitionRepositorySpySubject.Companion.assertThat
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.testKosmos
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.reset
+import org.mockito.Mockito.spy
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class FromGoneTransitionInteractorTest : SysuiTestCase() {
+ private val kosmos =
+ testKosmos().apply {
+ fakeKeyguardTransitionRepository = spy(FakeKeyguardTransitionRepository())
+ }
+ private val testScope = kosmos.testScope
+ private val underTest = kosmos.fromGoneTransitionInteractor
+ private val keyguardTransitionRepository = kosmos.fakeKeyguardTransitionRepository
+
+ @Before
+ fun setUp() {
+ underTest.start()
+ }
+
+ @Test
+ fun testDoesNotTransitionToLockscreen_ifStartedButNotFinishedInGone() =
+ testScope.runTest {
+ keyguardTransitionRepository.sendTransitionSteps(
+ listOf(
+ TransitionStep(
+ from = KeyguardState.LOCKSCREEN,
+ to = KeyguardState.GONE,
+ transitionState = TransitionState.STARTED,
+ ),
+ TransitionStep(
+ from = KeyguardState.LOCKSCREEN,
+ to = KeyguardState.GONE,
+ transitionState = TransitionState.RUNNING,
+ ),
+ ),
+ testScope,
+ )
+ reset(keyguardTransitionRepository)
+ kosmos.fakeKeyguardRepository.setKeyguardShowing(true)
+ runCurrent()
+
+ // We're in the middle of a LOCKSCREEN -> GONE transition.
+ assertThat(keyguardTransitionRepository).noTransitionsStarted()
+ }
+
+ @Test
+ fun testTransitionsToLockscreen_ifFinishedInGone() =
+ testScope.runTest {
+ keyguardTransitionRepository.sendTransitionSteps(
+ from = KeyguardState.LOCKSCREEN,
+ to = KeyguardState.GONE,
+ testScope,
+ )
+ reset(keyguardTransitionRepository)
+ kosmos.fakeKeyguardRepository.setKeyguardShowing(true)
+ runCurrent()
+
+ // We're in the middle of a LOCKSCREEN -> GONE transition.
+ assertThat(keyguardTransitionRepository)
+ .startedTransition(
+ to = KeyguardState.LOCKSCREEN,
+ )
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractorTest.kt
index 722c11d..6d8e7aa 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractorTest.kt
@@ -16,89 +16,58 @@
package com.android.systemui.keyguard.domain.interactor
+import android.platform.test.annotations.DisableFlags
+import android.platform.test.annotations.EnableFlags
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
-import com.android.systemui.SysUITestModule
+import com.android.systemui.Flags
import com.android.systemui.SysuiTestCase
-import com.android.systemui.TestMocksModule
import com.android.systemui.coroutines.collectValues
-import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.keyguard.data.repository.FakeKeyguardSurfaceBehindRepository
import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
-import com.android.systemui.keyguard.data.repository.InWindowLauncherUnlockAnimationRepository
+import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository
+import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
import com.android.systemui.keyguard.shared.model.KeyguardState
import com.android.systemui.keyguard.shared.model.TransitionState
import com.android.systemui.keyguard.shared.model.TransitionStep
-import com.android.systemui.keyguard.util.mockTopActivityClassName
-import com.android.systemui.shared.system.ActivityManagerWrapper
-import dagger.BindsInstance
-import dagger.Component
-import dagger.Lazy
-import junit.framework.Assert.assertEquals
-import junit.framework.Assert.assertTrue
-import junit.framework.Assert.fail
+import com.android.systemui.keyguard.util.KeyguardTransitionRepositorySpySubject.Companion.assertThat as assertThatRepository
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.shade.data.repository.FlingInfo
+import com.android.systemui.shade.data.repository.fakeShadeRepository
+import com.android.systemui.testKosmos
+import com.android.systemui.util.mockito.any
+import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
-import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
-import org.mockito.Mock
-import org.mockito.MockitoAnnotations
+import org.mockito.Mockito.never
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.verify
@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
@RunWith(AndroidJUnit4::class)
-class FromLockscreenTransitionInteractorTest : KeyguardTransitionInteractorTestCase() {
- private lateinit var underTest: FromLockscreenTransitionInteractor
-
- // Override the fromLockscreenTransitionInteractor provider from the superclass so our underTest
- // interactor is provided to any classes that need it.
- override var fromLockscreenTransitionInteractorLazy: Lazy<FromLockscreenTransitionInteractor>? =
- Lazy {
- underTest
+class FromLockscreenTransitionInteractorTest : SysuiTestCase() {
+ private val kosmos =
+ testKosmos().apply {
+ fakeKeyguardTransitionRepository = spy(FakeKeyguardTransitionRepository())
}
- private lateinit var testComponent: TestComponent
- @Mock private lateinit var activityManagerWrapper: ActivityManagerWrapper
-
- private var topActivityClassName = "launcher"
-
- @Before
- override fun setUp() {
- super.setUp()
- MockitoAnnotations.initMocks(this)
-
- testComponent =
- DaggerFromLockscreenTransitionInteractorTest_TestComponent.factory()
- .create(
- test = this,
- mocks =
- TestMocksModule(
- activityManagerWrapper = activityManagerWrapper,
- ),
- )
- underTest = testComponent.underTest
- testScope = testComponent.testScope
- transitionRepository = testComponent.transitionRepository
-
- activityManagerWrapper.mockTopActivityClassName(topActivityClassName)
- }
+ private val testScope = kosmos.testScope
+ private val underTest = kosmos.fromLockscreenTransitionInteractor
+ private val transitionRepository = kosmos.fakeKeyguardTransitionRepository
+ private val shadeRepository = kosmos.fakeShadeRepository
+ private val keyguardRepository = kosmos.fakeKeyguardRepository
@Test
- fun testSurfaceBehindVisibility_nonNullOnlyForRelevantTransitions() =
+ fun testSurfaceBehindVisibility() =
testScope.runTest {
val values by collectValues(underTest.surfaceBehindVisibility)
runCurrent()
// Transition-specific surface visibility should be null ("don't care") initially.
- assertEquals(
- listOf(
- null,
- ),
- values
- )
+ assertThat(values).containsExactly(null)
transitionRepository.sendTransitionStep(
TransitionStep(
@@ -107,15 +76,12 @@
to = KeyguardState.AOD,
)
)
-
runCurrent()
- assertEquals(
- listOf(
+ assertThat(values)
+ .containsExactly(
null, // LOCKSCREEN -> AOD does not have any specific surface visibility.
- ),
- values
- )
+ )
transitionRepository.sendTransitionStep(
TransitionStep(
@@ -124,159 +90,67 @@
to = KeyguardState.GONE,
)
)
-
runCurrent()
- assertEquals(
- listOf(
+ assertThat(values)
+ .containsExactly(
null,
true, // Surface is made visible immediately during LOCKSCREEN -> GONE
- ),
- values
+ )
+ .inOrder()
+
+ transitionRepository.sendTransitionStep(
+ TransitionStep(
+ transitionState = TransitionState.FINISHED,
+ from = KeyguardState.LOCKSCREEN,
+ to = KeyguardState.GONE,
+ )
)
+ runCurrent()
+
+ assertThat(values)
+ .containsExactly(
+ null,
+ true, // Surface remains visible.
+ )
+ .inOrder()
}
@Test
- fun testSurfaceBehindModel() =
+ @EnableFlags(Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR)
+ fun testTransitionsToGone_whenDismissFlingWhileDismissable_flagEnabled() =
testScope.runTest {
- val values by collectValues(underTest.surfaceBehindModel)
+ underTest.start()
+ verify(transitionRepository, never()).startTransition(any())
+
+ keyguardRepository.setKeyguardDismissible(true)
runCurrent()
-
- assertEquals(
- values,
- listOf(
- null, // We should start null ("don't care").
- )
- )
-
- transitionRepository.sendTransitionStep(
- TransitionStep(
- transitionState = TransitionState.STARTED,
- from = KeyguardState.LOCKSCREEN,
- to = KeyguardState.AOD,
- )
+ shadeRepository.setCurrentFling(
+ FlingInfo(expand = true) // Not a dismiss fling (expand = true).
)
runCurrent()
- assertEquals(
- listOf(
- null, // LOCKSCREEN -> AOD does not have specific view params.
- ),
- values
- )
-
- transitionRepository.sendTransitionStep(
- TransitionStep(
- transitionState = TransitionState.STARTED,
+ assertThatRepository(transitionRepository)
+ .startedTransition(
from = KeyguardState.LOCKSCREEN,
to = KeyguardState.GONE,
)
- )
- runCurrent()
-
- transitionRepository.sendTransitionStep(
- TransitionStep(
- transitionState = TransitionState.RUNNING,
- from = KeyguardState.LOCKSCREEN,
- to = KeyguardState.GONE,
- value = 0.01f,
- )
- )
- runCurrent()
-
- transitionRepository.sendTransitionStep(
- TransitionStep(
- transitionState = TransitionState.RUNNING,
- from = KeyguardState.LOCKSCREEN,
- to = KeyguardState.GONE,
- value = 0.99f,
- )
- )
- runCurrent()
-
- assertEquals(3, values.size)
- val model1percent = values[1]
- val model99percent = values[2]
-
- try {
- // We should initially have an alpha of 0f when unlocking, so the surface is not
- // visible
- // while lockscreen UI animates out.
- assertEquals(0f, model1percent!!.alpha)
-
- // By the end it should probably be visible.
- assertTrue(model99percent!!.alpha > 0f)
- } catch (e: NullPointerException) {
- fail("surfaceBehindModel was unexpectedly null.")
- }
}
@Test
- fun testSurfaceBehindModel_alpha1_whenTransitioningWithInWindowAnimation() =
+ @DisableFlags(Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR)
+ fun testDoesNotTransitionToGone_whenDismissFlingWhileDismissable_flagDisabled() =
testScope.runTest {
- testComponent.inWindowLauncherUnlockAnimationRepository.setLauncherActivityClass(
- topActivityClassName
+ underTest.start()
+ verify(transitionRepository, never()).startTransition(any())
+
+ keyguardRepository.setKeyguardDismissible(true)
+ runCurrent()
+ shadeRepository.setCurrentFling(
+ FlingInfo(expand = true) // Not a dismiss fling (expand = true).
)
runCurrent()
- val values by collectValues(underTest.surfaceBehindModel)
- runCurrent()
-
- transitionRepository.sendTransitionStep(
- TransitionStep(
- transitionState = TransitionState.STARTED,
- from = KeyguardState.LOCKSCREEN,
- to = KeyguardState.GONE,
- )
- )
- runCurrent()
-
- assertEquals(1f, values[values.size - 1]?.alpha)
+ assertThatRepository(transitionRepository).noTransitionsStarted()
}
-
- @Test
- fun testSurfaceBehindModel_alphaZero_whenNotTransitioningWithInWindowAnimation() =
- testScope.runTest {
- testComponent.inWindowLauncherUnlockAnimationRepository.setLauncherActivityClass(
- "not_launcher"
- )
- runCurrent()
-
- val values by collectValues(underTest.surfaceBehindModel)
- runCurrent()
-
- transitionRepository.sendTransitionStep(
- TransitionStep(
- transitionState = TransitionState.STARTED,
- from = KeyguardState.LOCKSCREEN,
- to = KeyguardState.GONE,
- )
- )
- runCurrent()
-
- assertEquals(0f, values[values.size - 1]?.alpha)
- }
-
- @SysUISingleton
- @Component(
- modules =
- [
- SysUITestModule::class,
- ]
- )
- interface TestComponent {
- val underTest: FromLockscreenTransitionInteractor
- val testScope: TestScope
- val transitionRepository: FakeKeyguardTransitionRepository
- val surfaceBehindRepository: FakeKeyguardSurfaceBehindRepository
- val inWindowLauncherUnlockAnimationRepository: InWindowLauncherUnlockAnimationRepository
-
- @Component.Factory
- interface Factory {
- fun create(
- @BindsInstance test: SysuiTestCase,
- mocks: TestMocksModule,
- ): TestComponent
- }
- }
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/FromPrimaryBouncerTransitionInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/FromPrimaryBouncerTransitionInteractorTest.kt
index 6092b6b35..f33a5c9 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/FromPrimaryBouncerTransitionInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/FromPrimaryBouncerTransitionInteractorTest.kt
@@ -18,61 +18,33 @@
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
import com.android.systemui.coroutines.collectValues
-import com.android.systemui.flags.FakeFeatureFlags
+import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
import com.android.systemui.keyguard.shared.model.KeyguardState
import com.android.systemui.keyguard.shared.model.TransitionState
import com.android.systemui.keyguard.shared.model.TransitionStep
-import com.android.systemui.power.domain.interactor.PowerInteractorFactory
-import com.android.systemui.user.data.repository.FakeUserRepository
-import com.android.systemui.user.domain.interactor.SelectedUserInteractor
-import com.android.systemui.util.mockito.mock
-import dagger.Lazy
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.testKosmos
+import com.android.systemui.user.domain.interactor.selectedUserInteractor
import junit.framework.Assert.assertEquals
import junit.framework.Assert.assertTrue
import junit.framework.Assert.fail
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
-import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
@RunWith(AndroidJUnit4::class)
-class FromPrimaryBouncerTransitionInteractorTest : KeyguardTransitionInteractorTestCase() {
- private lateinit var underTest: FromPrimaryBouncerTransitionInteractor
-
- private val mSelectedUserInteractor = SelectedUserInteractor(FakeUserRepository())
-
- // Override the fromPrimaryBouncerTransitionInteractor provider from the superclass so our
- // underTest interactor is provided to any classes that need it.
- override var fromPrimaryBouncerTransitionInteractorLazy:
- Lazy<FromPrimaryBouncerTransitionInteractor>? =
- Lazy {
- underTest
- }
-
- @Before
- override fun setUp() {
- super.setUp()
-
- underTest =
- FromPrimaryBouncerTransitionInteractor(
- transitionRepository = super.transitionRepository,
- transitionInteractor = super.transitionInteractor,
- scope = super.testScope.backgroundScope,
- bgDispatcher = super.testDispatcher,
- mainDispatcher = super.testDispatcher,
- keyguardInteractor = super.keyguardInteractor,
- communalInteractor = super.communalInteractor,
- flags = FakeFeatureFlags(),
- keyguardSecurityModel = mock(),
- powerInteractor = PowerInteractorFactory.create().powerInteractor,
- selectedUserInteractor = mSelectedUserInteractor
- )
- }
+class FromPrimaryBouncerTransitionInteractorTest : SysuiTestCase() {
+ val kosmos = testKosmos()
+ val underTest = kosmos.fromPrimaryBouncerTransitionInteractor
+ val testScope = kosmos.testScope
+ val selectedUserInteractor = kosmos.selectedUserInteractor
+ val transitionRepository = kosmos.fakeKeyguardTransitionRepository
@Test
fun testSurfaceBehindVisibility() =
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissActionInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissActionInteractorTest.kt
index e75f557..62855d7 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissActionInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissActionInteractorTest.kt
@@ -21,16 +21,15 @@
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.coroutines.collectLastValue
-import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
-import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
+import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository
+import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
import com.android.systemui.keyguard.shared.model.DismissAction
import com.android.systemui.keyguard.shared.model.KeyguardDone
import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.testKosmos
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.test.StandardTestDispatcher
-import kotlinx.coroutines.test.TestDispatcher
-import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Before
@@ -42,11 +41,12 @@
@SmallTest
@RunWith(AndroidJUnit4::class)
class KeyguardDismissActionInteractorTest : SysuiTestCase() {
- private lateinit var keyguardRepository: FakeKeyguardRepository
- private lateinit var transitionRepository: FakeKeyguardTransitionRepository
+ val kosmos = testKosmos()
- private lateinit var dispatcher: TestDispatcher
- private lateinit var testScope: TestScope
+ private val keyguardRepository = kosmos.fakeKeyguardRepository
+ private val transitionRepository = kosmos.fakeKeyguardTransitionRepository
+
+ private val testScope = kosmos.testScope
private lateinit var dismissInteractorWithDependencies:
KeyguardDismissInteractorFactory.WithDependencies
@@ -55,25 +55,18 @@
@Before
fun setUp() {
MockitoAnnotations.initMocks(this)
- dispatcher = StandardTestDispatcher()
- testScope = TestScope(dispatcher)
dismissInteractorWithDependencies =
KeyguardDismissInteractorFactory.create(
context = context,
testScope = testScope,
+ keyguardRepository = keyguardRepository,
)
- keyguardRepository = dismissInteractorWithDependencies.keyguardRepository
- transitionRepository = FakeKeyguardTransitionRepository()
underTest =
KeyguardDismissActionInteractor(
keyguardRepository,
- KeyguardTransitionInteractorFactory.create(
- scope = testScope.backgroundScope,
- repository = transitionRepository,
- )
- .keyguardTransitionInteractor,
+ kosmos.keyguardTransitionInteractor,
dismissInteractorWithDependencies.interactor,
testScope.backgroundScope,
)
@@ -180,7 +173,7 @@
transitionRepository.sendTransitionSteps(
from = KeyguardState.LOCKSCREEN,
to = KeyguardState.GONE,
- testScope
+ testScope,
)
assertThat(executeDismissAction).isNotNull()
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorParameterizedTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorParameterizedTest.kt
index b4ae7e3..798c7f7 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorParameterizedTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorParameterizedTest.kt
@@ -24,7 +24,7 @@
import androidx.test.filters.SmallTest
import com.android.internal.widget.LockPatternUtils
import com.android.systemui.SysuiTestCase
-import com.android.systemui.animation.ActivityLaunchAnimator
+import com.android.systemui.animation.ActivityTransitionAnimator
import com.android.systemui.animation.DialogLaunchAnimator
import com.android.systemui.animation.Expandable
import com.android.systemui.common.shared.model.ContentDescription
@@ -225,7 +225,7 @@
@Mock private lateinit var lockPatternUtils: LockPatternUtils
@Mock private lateinit var keyguardStateController: KeyguardStateController
@Mock private lateinit var activityStarter: ActivityStarter
- @Mock private lateinit var animationController: ActivityLaunchAnimator.Controller
+ @Mock private lateinit var animationController: ActivityTransitionAnimator.Controller
@Mock private lateinit var expandable: Expandable
@Mock private lateinit var launchAnimator: DialogLaunchAnimator
@Mock private lateinit var devicePolicyManager: DevicePolicyManager
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardSurfaceBehindInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardSurfaceBehindInteractorTest.kt
index 71fcf6f..3f05bfa 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardSurfaceBehindInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardSurfaceBehindInteractorTest.kt
@@ -20,148 +20,211 @@
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.coroutines.collectValues
-import com.android.systemui.keyguard.data.repository.FakeKeyguardSurfaceBehindRepository
-import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
+import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
import com.android.systemui.keyguard.shared.model.KeyguardState
import com.android.systemui.keyguard.shared.model.KeyguardSurfaceBehindModel
import com.android.systemui.keyguard.shared.model.TransitionState
import com.android.systemui.keyguard.shared.model.TransitionStep
-import com.android.systemui.util.mockito.whenever
-import junit.framework.Assert.assertEquals
-import junit.framework.Assert.assertTrue
-import kotlinx.coroutines.flow.flowOf
-import kotlinx.coroutines.test.TestScope
+import com.android.systemui.keyguard.util.mockTopActivityClassName
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.shared.system.activityManagerWrapper
+import com.android.systemui.statusbar.notification.domain.interactor.notificationLaunchAnimationInteractor
+import com.android.systemui.testKosmos
+import com.android.systemui.util.assertValuesMatch
+import com.google.common.truth.Truth.assertThat
+import kotlin.test.Test
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Before
-import org.junit.Test
import org.junit.runner.RunWith
-import org.mockito.Mock
-import org.mockito.MockitoAnnotations.initMocks
@SmallTest
@RunWith(AndroidJUnit4::class)
@kotlinx.coroutines.ExperimentalCoroutinesApi
class KeyguardSurfaceBehindInteractorTest : SysuiTestCase() {
+ private val kosmos = testKosmos()
+ private val testScope = kosmos.testScope
+ private val underTest = kosmos.keyguardSurfaceBehindInteractor
+ private val transitionRepository = kosmos.fakeKeyguardTransitionRepository
+ private val inWindowUnlockInteractor = kosmos.inWindowLauncherUnlockAnimationInteractor
+ private val activityManagerWrapper = kosmos.activityManagerWrapper
- private lateinit var underTest: KeyguardSurfaceBehindInteractor
- private lateinit var repository: FakeKeyguardSurfaceBehindRepository
-
- @Mock
- private lateinit var fromLockscreenTransitionInteractor: FromLockscreenTransitionInteractor
- @Mock
- private lateinit var fromPrimaryBouncerTransitionInteractor:
- FromPrimaryBouncerTransitionInteractor
-
- private val lockscreenSurfaceBehindModel = KeyguardSurfaceBehindModel(alpha = 0.33f)
- private val primaryBouncerSurfaceBehindModel = KeyguardSurfaceBehindModel(alpha = 0.66f)
-
- private val testScope = TestScope()
-
- private lateinit var transitionRepository: FakeKeyguardTransitionRepository
- private lateinit var transitionInteractor: KeyguardTransitionInteractor
+ private val LAUNCHER_ACTIVITY_NAME = "launcher"
@Before
fun setUp() {
- initMocks(this)
+ inWindowUnlockInteractor.setLauncherActivityClass(LAUNCHER_ACTIVITY_NAME)
- whenever(fromLockscreenTransitionInteractor.surfaceBehindModel)
- .thenReturn(flowOf(lockscreenSurfaceBehindModel))
- whenever(fromPrimaryBouncerTransitionInteractor.surfaceBehindModel)
- .thenReturn(flowOf(primaryBouncerSurfaceBehindModel))
-
- transitionRepository = FakeKeyguardTransitionRepository()
-
- transitionInteractor =
- KeyguardTransitionInteractorFactory.create(
- scope = testScope.backgroundScope,
- repository = transitionRepository,
- )
- .keyguardTransitionInteractor
-
- repository = FakeKeyguardSurfaceBehindRepository()
- underTest =
- KeyguardSurfaceBehindInteractor(
- repository = repository,
- fromLockscreenInteractor = fromLockscreenTransitionInteractor,
- fromPrimaryBouncerInteractor = fromPrimaryBouncerTransitionInteractor,
- transitionInteractor = transitionInteractor,
- )
+ // Default to having something other than Launcher on top.
+ activityManagerWrapper.mockTopActivityClassName("not_launcher")
}
@Test
- fun viewParamsSwitchToCorrectFlow() =
+ fun testSurfaceBehindModel_toAppSurface() =
testScope.runTest {
val values by collectValues(underTest.viewParams)
-
- // Start on the LOCKSCREEN.
- transitionRepository.sendTransitionStep(
- TransitionStep(
- transitionState = TransitionState.STARTED,
- from = KeyguardState.AOD,
- to = KeyguardState.LOCKSCREEN,
- )
- )
-
runCurrent()
- transitionRepository.sendTransitionStep(
- TransitionStep(
- transitionState = TransitionState.FINISHED,
- from = KeyguardState.AOD,
- to = KeyguardState.LOCKSCREEN,
+ assertThat(values)
+ .containsExactly(
+ // We're initialized in LOCKSCREEN.
+ KeyguardSurfaceBehindModel(alpha = 0f),
)
- )
-
- runCurrent()
-
- // We're on LOCKSCREEN; we should be using the default params.
- assertEquals(1, values.size)
- assertTrue(values[0].alpha == 0f)
transitionRepository.sendTransitionStep(
TransitionStep(
- transitionState = TransitionState.STARTED,
from = KeyguardState.LOCKSCREEN,
to = KeyguardState.GONE,
- )
- )
-
- runCurrent()
-
- // We're going from LOCKSCREEN -> GONE, we should be using the lockscreen interactor's
- // surface behind model.
- assertEquals(2, values.size)
- assertEquals(values[1], lockscreenSurfaceBehindModel)
-
- transitionRepository.sendTransitionStep(
- TransitionStep(
transitionState = TransitionState.STARTED,
- from = KeyguardState.PRIMARY_BOUNCER,
- to = KeyguardState.GONE,
)
)
-
runCurrent()
- // We're going from PRIMARY_BOUNCER -> GONE, we should be using the bouncer interactor's
- // surface behind model.
- assertEquals(3, values.size)
- assertEquals(values[2], primaryBouncerSurfaceBehindModel)
+ values.assertValuesMatch(
+ { it == KeyguardSurfaceBehindModel(alpha = 0f) },
+ // Once we start a transition to GONE, we should fade in and translate up. The exact
+ // start value depends on screen density, so just look for != 0.
+ {
+ it.animateFromAlpha == 0f &&
+ it.alpha == 1f &&
+ it.animateFromTranslationY != 0f &&
+ it.translationY == 0f
+ }
+ )
transitionRepository.sendTransitionStep(
TransitionStep(
- transitionState = TransitionState.FINISHED,
- from = KeyguardState.PRIMARY_BOUNCER,
+ from = KeyguardState.LOCKSCREEN,
to = KeyguardState.GONE,
+ transitionState = TransitionState.RUNNING,
)
)
-
runCurrent()
- // Once PRIMARY_BOUNCER -> GONE finishes, we should be using default params, which is
- // alpha=1f when we're GONE.
- assertEquals(4, values.size)
- assertEquals(1f, values[3].alpha)
+ values.assertValuesMatch(
+ { it == KeyguardSurfaceBehindModel(alpha = 0f) },
+ // There should be no change as we're RUNNING.
+ {
+ it.animateFromAlpha == 0f &&
+ it.alpha == 1f &&
+ it.animateFromTranslationY != 0f &&
+ it.translationY == 0f
+ }
+ )
+
+ transitionRepository.sendTransitionStep(
+ TransitionStep(
+ from = KeyguardState.LOCKSCREEN,
+ to = KeyguardState.GONE,
+ transitionState = TransitionState.FINISHED,
+ )
+ )
+ runCurrent()
+
+ values.assertValuesMatch(
+ { it == KeyguardSurfaceBehindModel(alpha = 0f) },
+ {
+ it.animateFromAlpha == 0f &&
+ it.alpha == 1f &&
+ it.animateFromTranslationY != 0f &&
+ it.translationY == 0f
+ },
+ // Once the current state is GONE, we should default to alpha = 1f.
+ { it == KeyguardSurfaceBehindModel(alpha = 1f) }
+ )
+ }
+
+ @Test
+ fun testSurfaceBehindModel_toLauncher() =
+ testScope.runTest {
+ val values by collectValues(underTest.viewParams)
+ activityManagerWrapper.mockTopActivityClassName(LAUNCHER_ACTIVITY_NAME)
+ runCurrent()
+
+ assertThat(values)
+ .containsExactly(
+ // We're initialized in LOCKSCREEN.
+ KeyguardSurfaceBehindModel(alpha = 0f),
+ )
+ .inOrder()
+
+ transitionRepository.sendTransitionStep(
+ TransitionStep(
+ from = KeyguardState.LOCKSCREEN,
+ to = KeyguardState.GONE,
+ transitionState = TransitionState.STARTED,
+ )
+ )
+ runCurrent()
+
+ assertThat(values)
+ .containsExactly(
+ KeyguardSurfaceBehindModel(alpha = 0f),
+ // We should instantly set alpha = 1, with no animations, when Launcher is
+ // behind
+ // the keyguard since we're playing in-window animations.
+ KeyguardSurfaceBehindModel(alpha = 1f),
+ )
+ .inOrder()
+
+ transitionRepository.sendTransitionStep(
+ TransitionStep(
+ from = KeyguardState.LOCKSCREEN,
+ to = KeyguardState.GONE,
+ transitionState = TransitionState.RUNNING,
+ )
+ )
+ runCurrent()
+
+ transitionRepository.sendTransitionStep(
+ TransitionStep(
+ from = KeyguardState.LOCKSCREEN,
+ to = KeyguardState.GONE,
+ transitionState = TransitionState.FINISHED,
+ )
+ )
+ runCurrent()
+
+ assertThat(values)
+ .containsExactly(
+ KeyguardSurfaceBehindModel(alpha = 0f),
+ // Should have remained at alpha = 1f through the entire animation.
+ KeyguardSurfaceBehindModel(alpha = 1f),
+ )
+ .inOrder()
+ }
+
+ @Test
+ fun testSurfaceBehindModel_fromNotificationLaunch() =
+ testScope.runTest {
+ val values by collectValues(underTest.viewParams)
+ runCurrent()
+
+ kosmos.notificationLaunchAnimationInteractor.setIsLaunchAnimationRunning(true)
+ runCurrent()
+
+ transitionRepository.sendTransitionStep(
+ TransitionStep(
+ from = KeyguardState.LOCKSCREEN,
+ to = KeyguardState.GONE,
+ transitionState = TransitionState.STARTED,
+ )
+ )
+ runCurrent()
+
+ transitionRepository.sendTransitionStep(
+ TransitionStep(
+ from = KeyguardState.LOCKSCREEN,
+ to = KeyguardState.GONE,
+ transitionState = TransitionState.RUNNING,
+ value = 0.5f,
+ )
+ )
+ runCurrent()
+
+ values.assertValuesMatch(
+ // We should be at alpha = 0f during the animation.
+ { it == KeyguardSurfaceBehindModel(alpha = 0f) },
+ )
}
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorTestCase.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorTestCase.kt
deleted file mode 100644
index a03aed0..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorTestCase.kt
+++ /dev/null
@@ -1,78 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.keyguard.domain.interactor
-
-import com.android.systemui.SysuiTestCase
-import com.android.systemui.communal.domain.interactor.CommunalInteractor
-import com.android.systemui.communal.domain.interactor.communalInteractor
-import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
-import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
-import com.android.systemui.testKosmos
-import com.android.systemui.util.mockito.mock
-import dagger.Lazy
-import kotlinx.coroutines.test.StandardTestDispatcher
-import kotlinx.coroutines.test.TestScope
-
-open class KeyguardTransitionInteractorTestCase : SysuiTestCase() {
- private val kosmos = testKosmos()
- val testDispatcher = StandardTestDispatcher()
- var testScope = TestScope(testDispatcher)
-
- lateinit var keyguardRepository: FakeKeyguardRepository
- lateinit var transitionRepository: FakeKeyguardTransitionRepository
-
- lateinit var keyguardInteractor: KeyguardInteractor
- lateinit var communalInteractor: CommunalInteractor
- lateinit var transitionInteractor: KeyguardTransitionInteractor
-
- /**
- * Replace these lazy providers with non-null ones if you want test dependencies to use a real
- * instance of the interactor for the test.
- */
- open var fromLockscreenTransitionInteractorLazy: Lazy<FromLockscreenTransitionInteractor>? =
- null
- open var fromPrimaryBouncerTransitionInteractorLazy:
- Lazy<FromPrimaryBouncerTransitionInteractor>? =
- null
-
- open fun setUp() {
- keyguardRepository = FakeKeyguardRepository()
- transitionRepository = FakeKeyguardTransitionRepository()
-
- keyguardInteractor =
- KeyguardInteractorFactory.create(repository = keyguardRepository).keyguardInteractor
-
- communalInteractor = kosmos.communalInteractor
-
- transitionInteractor =
- KeyguardTransitionInteractorFactory.create(
- repository = transitionRepository,
- keyguardInteractor = keyguardInteractor,
- scope = testScope.backgroundScope,
- fromLockscreenTransitionInteractor = fromLockscreenTransitionInteractorLazy
- ?: Lazy { mock() },
- fromPrimaryBouncerTransitionInteractor =
- fromPrimaryBouncerTransitionInteractorLazy ?: Lazy { mock() },
- )
- .also {
- fromLockscreenTransitionInteractorLazy = it.fromLockscreenTransitionInteractor
- fromPrimaryBouncerTransitionInteractorLazy =
- it.fromPrimaryBouncerTransitionInteractor
- }
- .keyguardTransitionInteractor
- }
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt
index 2812718..5b93df5 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt
@@ -23,18 +23,13 @@
import com.android.systemui.Flags.FLAG_COMMUNAL_HUB
import com.android.systemui.Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR
import com.android.systemui.SysuiTestCase
-import com.android.systemui.bouncer.data.repository.FakeKeyguardBouncerRepository
import com.android.systemui.bouncer.data.repository.fakeKeyguardBouncerRepository
-import com.android.systemui.communal.domain.interactor.CommunalInteractor
import com.android.systemui.communal.domain.interactor.communalInteractor
import com.android.systemui.communal.shared.model.CommunalSceneKey
import com.android.systemui.communal.shared.model.ObservableCommunalTransitionState
import com.android.systemui.flags.FakeFeatureFlags
-import com.android.systemui.keyguard.data.repository.FakeCommandQueue
-import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
-import com.android.systemui.keyguard.data.repository.FakeKeyguardSurfaceBehindRepository
import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
-import com.android.systemui.keyguard.data.repository.InWindowLauncherUnlockAnimationRepository
+import com.android.systemui.keyguard.data.repository.fakeCommandQueue
import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository
import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
import com.android.systemui.keyguard.shared.model.BiometricUnlockModel
@@ -42,29 +37,26 @@
import com.android.systemui.keyguard.shared.model.DozeTransitionModel
import com.android.systemui.keyguard.shared.model.KeyguardState
import com.android.systemui.keyguard.shared.model.StatusBarState
-import com.android.systemui.keyguard.shared.model.TransitionInfo
import com.android.systemui.keyguard.shared.model.TransitionState
import com.android.systemui.keyguard.shared.model.TransitionStep
+import com.android.systemui.keyguard.util.KeyguardTransitionRepositorySpySubject.Companion.assertThat
import com.android.systemui.kosmos.testDispatcher
import com.android.systemui.kosmos.testScope
-import com.android.systemui.power.domain.interactor.PowerInteractor
import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAsleepForTest
import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAwakeForTest
-import com.android.systemui.power.domain.interactor.PowerInteractorFactory
-import com.android.systemui.shade.data.repository.FakeShadeRepository
+import com.android.systemui.power.domain.interactor.powerInteractor
import com.android.systemui.shade.data.repository.fakeShadeRepository
+import com.android.systemui.statusbar.commandQueue
import com.android.systemui.testKosmos
import com.android.systemui.user.domain.interactor.SelectedUserInteractor
import com.android.systemui.util.mockito.any
-import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.whenever
-import com.android.systemui.util.mockito.withArgCaptor
-import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.cancelChildren
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.advanceTimeBy
import kotlinx.coroutines.test.advanceUntilIdle
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
@@ -89,22 +81,26 @@
@SmallTest
@RunWith(JUnit4::class)
class KeyguardTransitionScenariosTest : SysuiTestCase() {
- private val kosmos = testKosmos()
+ private val kosmos =
+ testKosmos().apply {
+ fakeKeyguardTransitionRepository = spy(FakeKeyguardTransitionRepository())
+ this.commandQueue = fakeCommandQueue
+ }
private val testScope = kosmos.testScope
- private lateinit var keyguardRepository: FakeKeyguardRepository
- private lateinit var bouncerRepository: FakeKeyguardBouncerRepository
- private lateinit var commandQueue: FakeCommandQueue
- private lateinit var shadeRepository: FakeShadeRepository
- private lateinit var transitionRepository: FakeKeyguardTransitionRepository
- private lateinit var transitionInteractor: KeyguardTransitionInteractor
+ private val keyguardRepository = kosmos.fakeKeyguardRepository
+ private val bouncerRepository = kosmos.fakeKeyguardBouncerRepository
+ private var commandQueue = kosmos.fakeCommandQueue
+ private val shadeRepository = kosmos.fakeShadeRepository
+ private val transitionRepository = kosmos.fakeKeyguardTransitionRepository
+ private val transitionInteractor = kosmos.keyguardTransitionInteractor
private lateinit var featureFlags: FakeFeatureFlags
// Used to verify transition requests for test output
@Mock private lateinit var keyguardSecurityModel: KeyguardSecurityModel
@Mock private lateinit var mSelectedUserInteractor: SelectedUserInteractor
- private lateinit var fromLockscreenTransitionInteractor: FromLockscreenTransitionInteractor
+ private val fromLockscreenTransitionInteractor = kosmos.fromLockscreenTransitionInteractor
private lateinit var fromDreamingTransitionInteractor: FromDreamingTransitionInteractor
private lateinit var fromDozingTransitionInteractor: FromDozingTransitionInteractor
private lateinit var fromOccludedTransitionInteractor: FromOccludedTransitionInteractor
@@ -112,48 +108,26 @@
private lateinit var fromAodTransitionInteractor: FromAodTransitionInteractor
private lateinit var fromAlternateBouncerTransitionInteractor:
FromAlternateBouncerTransitionInteractor
- private lateinit var fromPrimaryBouncerTransitionInteractor:
- FromPrimaryBouncerTransitionInteractor
+ private val fromPrimaryBouncerTransitionInteractor =
+ kosmos.fromPrimaryBouncerTransitionInteractor
private lateinit var fromDreamingLockscreenHostedTransitionInteractor:
FromDreamingLockscreenHostedTransitionInteractor
private lateinit var fromGlanceableHubTransitionInteractor:
FromGlanceableHubTransitionInteractor
- private lateinit var powerInteractor: PowerInteractor
- private lateinit var keyguardInteractor: KeyguardInteractor
- private lateinit var communalInteractor: CommunalInteractor
+ private val powerInteractor = kosmos.powerInteractor
+ private val keyguardInteractor = kosmos.keyguardInteractor
+ private val communalInteractor = kosmos.communalInteractor
@Before
fun setUp() {
MockitoAnnotations.initMocks(this)
- keyguardRepository = kosmos.fakeKeyguardRepository
- bouncerRepository = kosmos.fakeKeyguardBouncerRepository
- commandQueue = FakeCommandQueue()
- shadeRepository = kosmos.fakeShadeRepository
- transitionRepository = spy(kosmos.fakeKeyguardTransitionRepository)
- powerInteractor = PowerInteractorFactory.create().powerInteractor
- communalInteractor = kosmos.communalInteractor
-
whenever(keyguardSecurityModel.getSecurityMode(anyInt())).thenReturn(PIN)
mSetFlagsRule.enableFlags(FLAG_COMMUNAL_HUB)
featureFlags = FakeFeatureFlags()
- keyguardInteractor = createKeyguardInteractor()
-
- transitionInteractor =
- KeyguardTransitionInteractorFactory.create(
- scope = testScope,
- repository = transitionRepository,
- keyguardInteractor = keyguardInteractor,
- fromLockscreenTransitionInteractor = { fromLockscreenTransitionInteractor },
- fromPrimaryBouncerTransitionInteractor = {
- fromPrimaryBouncerTransitionInteractor
- },
- )
- .keyguardTransitionInteractor
-
val glanceableHubTransitions =
GlanceableHubTransitions(
scope = testScope,
@@ -162,45 +136,9 @@
transitionRepository = transitionRepository,
communalInteractor = communalInteractor
)
- fromLockscreenTransitionInteractor =
- FromLockscreenTransitionInteractor(
- scope = testScope,
- bgDispatcher = kosmos.testDispatcher,
- mainDispatcher = kosmos.testDispatcher,
- keyguardInteractor = keyguardInteractor,
- transitionRepository = transitionRepository,
- transitionInteractor = transitionInteractor,
- flags = featureFlags,
- shadeRepository = shadeRepository,
- powerInteractor = powerInteractor,
- glanceableHubTransitions = glanceableHubTransitions,
- inWindowLauncherUnlockAnimationInteractor = {
- InWindowLauncherUnlockAnimationInteractor(
- InWindowLauncherUnlockAnimationRepository(),
- testScope,
- transitionInteractor,
- { FakeKeyguardSurfaceBehindRepository() },
- mock(),
- )
- },
- )
- .apply { start() }
- fromPrimaryBouncerTransitionInteractor =
- FromPrimaryBouncerTransitionInteractor(
- scope = testScope,
- bgDispatcher = kosmos.testDispatcher,
- mainDispatcher = kosmos.testDispatcher,
- keyguardInteractor = keyguardInteractor,
- transitionRepository = transitionRepository,
- transitionInteractor = transitionInteractor,
- flags = featureFlags,
- keyguardSecurityModel = keyguardSecurityModel,
- powerInteractor = powerInteractor,
- communalInteractor = communalInteractor,
- selectedUserInteractor = mSelectedUserInteractor,
- )
- .apply { start() }
+ fromLockscreenTransitionInteractor.start()
+ fromPrimaryBouncerTransitionInteractor.start()
fromDreamingTransitionInteractor =
FromDreamingTransitionInteractor(
@@ -315,15 +253,13 @@
bouncerRepository.setPrimaryShow(true)
runCurrent()
- val info =
- withArgCaptor<TransitionInfo> {
- verify(transitionRepository).startTransition(capture())
- }
- // THEN a transition to PRIMARY_BOUNCER should occur
- assertThat(info.ownerName).isEqualTo("FromLockscreenTransitionInteractor")
- assertThat(info.from).isEqualTo(KeyguardState.LOCKSCREEN)
- assertThat(info.to).isEqualTo(KeyguardState.PRIMARY_BOUNCER)
- assertThat(info.animator).isNotNull()
+ assertThat(transitionRepository)
+ .startedTransition(
+ to = KeyguardState.PRIMARY_BOUNCER,
+ from = KeyguardState.LOCKSCREEN,
+ ownerName = "FromLockscreenTransitionInteractor",
+ animatorAssertion = { it.isNotNull() }
+ )
coroutineContext.cancelChildren()
}
@@ -342,15 +278,13 @@
powerInteractor.setAsleepForTest()
runCurrent()
- val info =
- withArgCaptor<TransitionInfo> {
- verify(transitionRepository).startTransition(capture())
- }
- // THEN a transition to DOZING should occur
- assertThat(info.ownerName).isEqualTo("FromOccludedTransitionInteractor")
- assertThat(info.from).isEqualTo(KeyguardState.OCCLUDED)
- assertThat(info.to).isEqualTo(KeyguardState.DOZING)
- assertThat(info.animator).isNotNull()
+ assertThat(transitionRepository)
+ .startedTransition(
+ to = KeyguardState.DOZING,
+ from = KeyguardState.OCCLUDED,
+ ownerName = "FromOccludedTransitionInteractor",
+ animatorAssertion = { it.isNotNull() }
+ )
coroutineContext.cancelChildren()
}
@@ -369,15 +303,13 @@
powerInteractor.setAsleepForTest()
runCurrent()
- val info =
- withArgCaptor<TransitionInfo> {
- verify(transitionRepository).startTransition(capture())
- }
- // THEN a transition to DOZING should occur
- assertThat(info.ownerName).isEqualTo("FromOccludedTransitionInteractor")
- assertThat(info.from).isEqualTo(KeyguardState.OCCLUDED)
- assertThat(info.to).isEqualTo(KeyguardState.AOD)
- assertThat(info.animator).isNotNull()
+ assertThat(transitionRepository)
+ .startedTransition(
+ to = KeyguardState.AOD,
+ from = KeyguardState.OCCLUDED,
+ ownerName = "FromOccludedTransitionInteractor",
+ animatorAssertion = { it.isNotNull() }
+ )
coroutineContext.cancelChildren()
}
@@ -397,17 +329,15 @@
// WHEN the device begins to dream
keyguardRepository.setDreamingWithOverlay(true)
- advanceUntilIdle()
+ advanceTimeBy(100L)
- val info =
- withArgCaptor<TransitionInfo> {
- verify(transitionRepository).startTransition(capture())
- }
- // THEN a transition to DREAMING should occur
- assertThat(info.ownerName).isEqualTo("FromLockscreenTransitionInteractor")
- assertThat(info.from).isEqualTo(KeyguardState.LOCKSCREEN)
- assertThat(info.to).isEqualTo(KeyguardState.DREAMING)
- assertThat(info.animator).isNotNull()
+ assertThat(transitionRepository)
+ .startedTransition(
+ to = KeyguardState.DREAMING,
+ from = KeyguardState.LOCKSCREEN,
+ ownerName = "FromLockscreenTransitionInteractor",
+ animatorAssertion = { it.isNotNull() }
+ )
coroutineContext.cancelChildren()
}
@@ -428,17 +358,15 @@
// WHEN the device begins to dream and the dream is lockscreen hosted
keyguardRepository.setDreamingWithOverlay(true)
keyguardRepository.setIsActiveDreamLockscreenHosted(true)
- advanceUntilIdle()
+ advanceTimeBy(100L)
- val info =
- withArgCaptor<TransitionInfo> {
- verify(transitionRepository).startTransition(capture())
- }
- // THEN a transition to DREAMING_LOCKSCREEN_HOSTED should occur
- assertThat(info.ownerName).isEqualTo("FromLockscreenTransitionInteractor")
- assertThat(info.from).isEqualTo(KeyguardState.LOCKSCREEN)
- assertThat(info.to).isEqualTo(KeyguardState.DREAMING_LOCKSCREEN_HOSTED)
- assertThat(info.animator).isNotNull()
+ assertThat(transitionRepository)
+ .startedTransition(
+ to = KeyguardState.DREAMING_LOCKSCREEN_HOSTED,
+ from = KeyguardState.LOCKSCREEN,
+ ownerName = "FromLockscreenTransitionInteractor",
+ animatorAssertion = { it.isNotNull() }
+ )
coroutineContext.cancelChildren()
}
@@ -457,15 +385,13 @@
powerInteractor.setAsleepForTest()
runCurrent()
- val info =
- withArgCaptor<TransitionInfo> {
- verify(transitionRepository).startTransition(capture())
- }
- // THEN a transition to DOZING should occur
- assertThat(info.ownerName).isEqualTo("FromLockscreenTransitionInteractor")
- assertThat(info.from).isEqualTo(KeyguardState.LOCKSCREEN)
- assertThat(info.to).isEqualTo(KeyguardState.DOZING)
- assertThat(info.animator).isNotNull()
+ assertThat(transitionRepository)
+ .startedTransition(
+ to = KeyguardState.DOZING,
+ from = KeyguardState.LOCKSCREEN,
+ ownerName = "FromLockscreenTransitionInteractor",
+ animatorAssertion = { it.isNotNull() }
+ )
coroutineContext.cancelChildren()
}
@@ -484,15 +410,13 @@
powerInteractor.setAsleepForTest()
runCurrent()
- val info =
- withArgCaptor<TransitionInfo> {
- verify(transitionRepository).startTransition(capture())
- }
- // THEN a transition to DOZING should occur
- assertThat(info.ownerName).isEqualTo("FromLockscreenTransitionInteractor")
- assertThat(info.from).isEqualTo(KeyguardState.LOCKSCREEN)
- assertThat(info.to).isEqualTo(KeyguardState.AOD)
- assertThat(info.animator).isNotNull()
+ assertThat(transitionRepository)
+ .startedTransition(
+ to = KeyguardState.AOD,
+ from = KeyguardState.LOCKSCREEN,
+ ownerName = "FromLockscreenTransitionInteractor",
+ animatorAssertion = { it.isNotNull() }
+ )
coroutineContext.cancelChildren()
}
@@ -515,17 +439,15 @@
// WHEN the lockscreen hosted dream stops
keyguardRepository.setIsActiveDreamLockscreenHosted(false)
- advanceUntilIdle()
+ advanceTimeBy(100L)
- val info =
- withArgCaptor<TransitionInfo> {
- verify(transitionRepository).startTransition(capture())
- }
- // THEN a transition to Lockscreen should occur
- assertThat(info.ownerName).isEqualTo("FromDreamingLockscreenHostedTransitionInteractor")
- assertThat(info.from).isEqualTo(KeyguardState.DREAMING_LOCKSCREEN_HOSTED)
- assertThat(info.to).isEqualTo(KeyguardState.LOCKSCREEN)
- assertThat(info.animator).isNotNull()
+ assertThat(transitionRepository)
+ .startedTransition(
+ to = KeyguardState.LOCKSCREEN,
+ from = KeyguardState.DREAMING_LOCKSCREEN_HOSTED,
+ ownerName = "FromDreamingLockscreenHostedTransitionInteractor",
+ animatorAssertion = { it.isNotNull() }
+ )
coroutineContext.cancelChildren()
}
@@ -545,15 +467,13 @@
)
runCurrent()
- val info =
- withArgCaptor<TransitionInfo> {
- verify(transitionRepository).startTransition(capture())
- }
- // THEN a transition to Gone should occur
- assertThat(info.ownerName).isEqualTo("FromDreamingLockscreenHostedTransitionInteractor")
- assertThat(info.from).isEqualTo(KeyguardState.DREAMING_LOCKSCREEN_HOSTED)
- assertThat(info.to).isEqualTo(KeyguardState.GONE)
- assertThat(info.animator).isNotNull()
+ assertThat(transitionRepository)
+ .startedTransition(
+ to = KeyguardState.GONE,
+ from = KeyguardState.DREAMING_LOCKSCREEN_HOSTED,
+ ownerName = "FromDreamingLockscreenHostedTransitionInteractor",
+ animatorAssertion = { it.isNotNull() }
+ )
coroutineContext.cancelChildren()
}
@@ -575,15 +495,13 @@
bouncerRepository.setPrimaryShow(true)
runCurrent()
- val info =
- withArgCaptor<TransitionInfo> {
- verify(transitionRepository).startTransition(capture())
- }
- // THEN a transition to PRIMARY_BOUNCER should occur
- assertThat(info.ownerName).isEqualTo("FromDreamingLockscreenHostedTransitionInteractor")
- assertThat(info.from).isEqualTo(KeyguardState.DREAMING_LOCKSCREEN_HOSTED)
- assertThat(info.to).isEqualTo(KeyguardState.PRIMARY_BOUNCER)
- assertThat(info.animator).isNotNull()
+ assertThat(transitionRepository)
+ .startedTransition(
+ to = KeyguardState.PRIMARY_BOUNCER,
+ from = KeyguardState.DREAMING_LOCKSCREEN_HOSTED,
+ ownerName = "FromDreamingLockscreenHostedTransitionInteractor",
+ animatorAssertion = { it.isNotNull() }
+ )
coroutineContext.cancelChildren()
}
@@ -608,15 +526,13 @@
)
runCurrent()
- val info =
- withArgCaptor<TransitionInfo> {
- verify(transitionRepository).startTransition(capture())
- }
- // THEN a transition to DOZING should occur
- assertThat(info.ownerName).isEqualTo("FromDreamingLockscreenHostedTransitionInteractor")
- assertThat(info.from).isEqualTo(KeyguardState.DREAMING_LOCKSCREEN_HOSTED)
- assertThat(info.to).isEqualTo(KeyguardState.DOZING)
- assertThat(info.animator).isNotNull()
+ assertThat(transitionRepository)
+ .startedTransition(
+ to = KeyguardState.DOZING,
+ from = KeyguardState.DREAMING_LOCKSCREEN_HOSTED,
+ ownerName = "FromDreamingLockscreenHostedTransitionInteractor",
+ animatorAssertion = { it.isNotNull() }
+ )
coroutineContext.cancelChildren()
}
@@ -640,15 +556,13 @@
keyguardRepository.setKeyguardOccluded(true)
runCurrent()
- val info =
- withArgCaptor<TransitionInfo> {
- verify(transitionRepository).startTransition(capture())
- }
- // THEN a transition to OCCLUDED should occur
- assertThat(info.ownerName).isEqualTo("FromDreamingLockscreenHostedTransitionInteractor")
- assertThat(info.from).isEqualTo(KeyguardState.DREAMING_LOCKSCREEN_HOSTED)
- assertThat(info.to).isEqualTo(KeyguardState.OCCLUDED)
- assertThat(info.animator).isNotNull()
+ assertThat(transitionRepository)
+ .startedTransition(
+ to = KeyguardState.OCCLUDED,
+ from = KeyguardState.DREAMING_LOCKSCREEN_HOSTED,
+ ownerName = "FromDreamingLockscreenHostedTransitionInteractor",
+ animatorAssertion = { it.isNotNull() }
+ )
coroutineContext.cancelChildren()
}
@@ -664,15 +578,13 @@
powerInteractor.setAwakeForTest()
runCurrent()
- val info =
- withArgCaptor<TransitionInfo> {
- verify(transitionRepository).startTransition(capture())
- }
- // THEN a transition to DOZING should occur
- assertThat(info.ownerName).isEqualTo("FromDozingTransitionInteractor")
- assertThat(info.from).isEqualTo(KeyguardState.DOZING)
- assertThat(info.to).isEqualTo(KeyguardState.LOCKSCREEN)
- assertThat(info.animator).isNotNull()
+ assertThat(transitionRepository)
+ .startedTransition(
+ to = KeyguardState.LOCKSCREEN,
+ from = KeyguardState.DOZING,
+ ownerName = "FromDozingTransitionInteractor",
+ animatorAssertion = { it.isNotNull() }
+ )
coroutineContext.cancelChildren()
}
@@ -710,7 +622,7 @@
// WHEN a signal comes that dreaming is enabled
keyguardRepository.setDreamingWithOverlay(true)
- advanceUntilIdle()
+ advanceTimeBy(100L)
// THEN the transition is ignored
verify(transitionRepository, never()).startTransition(any())
@@ -728,15 +640,13 @@
keyguardRepository.setBiometricUnlockState(BiometricUnlockModel.WAKE_AND_UNLOCK)
runCurrent()
- val info =
- withArgCaptor<TransitionInfo> {
- verify(transitionRepository).startTransition(capture())
- }
- // THEN a transition to DOZING should occur
- assertThat(info.ownerName).isEqualTo("FromDozingTransitionInteractor")
- assertThat(info.from).isEqualTo(KeyguardState.DOZING)
- assertThat(info.to).isEqualTo(KeyguardState.GONE)
- assertThat(info.animator).isNotNull()
+ assertThat(transitionRepository)
+ .startedTransition(
+ to = KeyguardState.GONE,
+ from = KeyguardState.DOZING,
+ ownerName = "FromDozingTransitionInteractor",
+ animatorAssertion = { it.isNotNull() }
+ )
coroutineContext.cancelChildren()
}
@@ -760,15 +670,13 @@
powerInteractor.setAwakeForTest()
runCurrent()
- val info =
- withArgCaptor<TransitionInfo> {
- verify(transitionRepository).startTransition(capture())
- }
- // THEN a transition to DOZING should occur
- assertThat(info.ownerName).isEqualTo(FromDozingTransitionInteractor::class.simpleName)
- assertThat(info.from).isEqualTo(KeyguardState.DOZING)
- assertThat(info.to).isEqualTo(KeyguardState.GLANCEABLE_HUB)
- assertThat(info.animator).isNotNull()
+ assertThat(transitionRepository)
+ .startedTransition(
+ to = KeyguardState.GLANCEABLE_HUB,
+ from = KeyguardState.DOZING,
+ ownerName = FromDozingTransitionInteractor::class.simpleName,
+ animatorAssertion = { it.isNotNull() }
+ )
coroutineContext.cancelChildren()
}
@@ -787,15 +695,13 @@
powerInteractor.setAsleepForTest()
runCurrent()
- val info =
- withArgCaptor<TransitionInfo> {
- verify(transitionRepository).startTransition(capture())
- }
- // THEN a transition to DOZING should occur
- assertThat(info.ownerName).isEqualTo("FromGoneTransitionInteractor")
- assertThat(info.from).isEqualTo(KeyguardState.GONE)
- assertThat(info.to).isEqualTo(KeyguardState.DOZING)
- assertThat(info.animator).isNotNull()
+ assertThat(transitionRepository)
+ .startedTransition(
+ to = KeyguardState.DOZING,
+ from = KeyguardState.GONE,
+ ownerName = "FromGoneTransitionInteractor",
+ animatorAssertion = { it.isNotNull() }
+ )
coroutineContext.cancelChildren()
}
@@ -814,15 +720,13 @@
powerInteractor.setAsleepForTest()
runCurrent()
- val info =
- withArgCaptor<TransitionInfo> {
- verify(transitionRepository).startTransition(capture())
- }
- // THEN a transition to AOD should occur
- assertThat(info.ownerName).isEqualTo("FromGoneTransitionInteractor")
- assertThat(info.from).isEqualTo(KeyguardState.GONE)
- assertThat(info.to).isEqualTo(KeyguardState.AOD)
- assertThat(info.animator).isNotNull()
+ assertThat(transitionRepository)
+ .startedTransition(
+ to = KeyguardState.AOD,
+ from = KeyguardState.GONE,
+ ownerName = "FromGoneTransitionInteractor",
+ animatorAssertion = { it.isNotNull() }
+ )
coroutineContext.cancelChildren()
}
@@ -837,15 +741,13 @@
keyguardRepository.setKeyguardShowing(true)
runCurrent()
- val info =
- withArgCaptor<TransitionInfo> {
- verify(transitionRepository).startTransition(capture())
- }
- // THEN a transition to AOD should occur
- assertThat(info.ownerName).isEqualTo("FromGoneTransitionInteractor")
- assertThat(info.from).isEqualTo(KeyguardState.GONE)
- assertThat(info.to).isEqualTo(KeyguardState.LOCKSCREEN)
- assertThat(info.animator).isNotNull()
+ assertThat(transitionRepository)
+ .startedTransition(
+ to = KeyguardState.LOCKSCREEN,
+ from = KeyguardState.GONE,
+ ownerName = "FromGoneTransitionInteractor",
+ animatorAssertion = { it.isNotNull() }
+ )
coroutineContext.cancelChildren()
}
@@ -865,17 +767,15 @@
// WHEN the device begins to dream
keyguardRepository.setDreamingWithOverlay(true)
- advanceUntilIdle()
+ advanceTimeBy(100L)
- val info =
- withArgCaptor<TransitionInfo> {
- verify(transitionRepository).startTransition(capture())
- }
- // THEN a transition to DREAMING should occur
- assertThat(info.ownerName).isEqualTo("FromGoneTransitionInteractor")
- assertThat(info.from).isEqualTo(KeyguardState.GONE)
- assertThat(info.to).isEqualTo(KeyguardState.DREAMING)
- assertThat(info.animator).isNotNull()
+ assertThat(transitionRepository)
+ .startedTransition(
+ to = KeyguardState.DREAMING,
+ from = KeyguardState.GONE,
+ ownerName = "FromGoneTransitionInteractor",
+ animatorAssertion = { it.isNotNull() }
+ )
coroutineContext.cancelChildren()
}
@@ -896,17 +796,15 @@
// WHEN the device begins to dream with the lockscreen hosted dream
keyguardRepository.setDreamingWithOverlay(true)
keyguardRepository.setIsActiveDreamLockscreenHosted(true)
- advanceUntilIdle()
+ advanceTimeBy(100L)
- val info =
- withArgCaptor<TransitionInfo> {
- verify(transitionRepository).startTransition(capture())
- }
- // THEN a transition to DREAMING_LOCKSCREEN_HOSTED should occur
- assertThat(info.ownerName).isEqualTo("FromGoneTransitionInteractor")
- assertThat(info.from).isEqualTo(KeyguardState.GONE)
- assertThat(info.to).isEqualTo(KeyguardState.DREAMING_LOCKSCREEN_HOSTED)
- assertThat(info.animator).isNotNull()
+ assertThat(transitionRepository)
+ .startedTransition(
+ to = KeyguardState.DREAMING_LOCKSCREEN_HOSTED,
+ from = KeyguardState.GONE,
+ ownerName = "FromGoneTransitionInteractor",
+ animatorAssertion = { it.isNotNull() }
+ )
coroutineContext.cancelChildren()
}
@@ -929,15 +827,13 @@
keyguardRepository.setKeyguardShowing(true)
runCurrent()
- val info =
- withArgCaptor<TransitionInfo> {
- verify(transitionRepository).startTransition(capture())
- }
- // THEN a transition to DOZING should occur
- assertThat(info.ownerName).isEqualTo(FromGoneTransitionInteractor::class.simpleName)
- assertThat(info.from).isEqualTo(KeyguardState.GONE)
- assertThat(info.to).isEqualTo(KeyguardState.GLANCEABLE_HUB)
- assertThat(info.animator).isNotNull()
+ assertThat(transitionRepository)
+ .startedTransition(
+ to = KeyguardState.GLANCEABLE_HUB,
+ from = KeyguardState.GONE,
+ ownerName = FromGoneTransitionInteractor::class.simpleName,
+ animatorAssertion = { it.isNotNull() }
+ )
coroutineContext.cancelChildren()
}
@@ -955,21 +851,19 @@
bouncerRepository.setPrimaryShow(true)
runCurrent()
- val info =
- withArgCaptor<TransitionInfo> {
- verify(transitionRepository).startTransition(capture())
- }
- // THEN a transition to PRIMARY_BOUNCER should occur
- assertThat(info.ownerName).isEqualTo("FromAlternateBouncerTransitionInteractor")
- assertThat(info.from).isEqualTo(KeyguardState.ALTERNATE_BOUNCER)
- assertThat(info.to).isEqualTo(KeyguardState.PRIMARY_BOUNCER)
- assertThat(info.animator).isNotNull()
+ assertThat(transitionRepository)
+ .startedTransition(
+ to = KeyguardState.PRIMARY_BOUNCER,
+ from = KeyguardState.ALTERNATE_BOUNCER,
+ ownerName = "FromAlternateBouncerTransitionInteractor",
+ animatorAssertion = { it.isNotNull() }
+ )
coroutineContext.cancelChildren()
}
@Test
- fun alternateBoucnerToAod() =
+ fun alternateBouncerToAod() =
testScope.runTest {
// GIVEN a prior transition has run to ALTERNATE_BOUNCER
bouncerRepository.setAlternateVisible(true)
@@ -985,17 +879,15 @@
// WHEN the alternateBouncer stops showing
bouncerRepository.setAlternateVisible(false)
- advanceUntilIdle()
+ advanceTimeBy(200L)
- val info =
- withArgCaptor<TransitionInfo> {
- verify(transitionRepository).startTransition(capture())
- }
- // THEN a transition to AOD should occur
- assertThat(info.ownerName).isEqualTo("FromAlternateBouncerTransitionInteractor")
- assertThat(info.from).isEqualTo(KeyguardState.ALTERNATE_BOUNCER)
- assertThat(info.to).isEqualTo(KeyguardState.AOD)
- assertThat(info.animator).isNotNull()
+ assertThat(transitionRepository)
+ .startedTransition(
+ to = KeyguardState.AOD,
+ from = KeyguardState.ALTERNATE_BOUNCER,
+ ownerName = "FromAlternateBouncerTransitionInteractor",
+ animatorAssertion = { it.isNotNull() }
+ )
coroutineContext.cancelChildren()
}
@@ -1018,17 +910,15 @@
// WHEN the alternateBouncer stops showing
bouncerRepository.setAlternateVisible(false)
- advanceUntilIdle()
+ advanceTimeBy(200L)
- val info =
- withArgCaptor<TransitionInfo> {
- verify(transitionRepository).startTransition(capture())
- }
- // THEN a transition to DOZING should occur
- assertThat(info.ownerName).isEqualTo("FromAlternateBouncerTransitionInteractor")
- assertThat(info.from).isEqualTo(KeyguardState.ALTERNATE_BOUNCER)
- assertThat(info.to).isEqualTo(KeyguardState.DOZING)
- assertThat(info.animator).isNotNull()
+ assertThat(transitionRepository)
+ .startedTransition(
+ to = KeyguardState.DOZING,
+ from = KeyguardState.ALTERNATE_BOUNCER,
+ ownerName = "FromAlternateBouncerTransitionInteractor",
+ animatorAssertion = { it.isNotNull() }
+ )
coroutineContext.cancelChildren()
}
@@ -1048,17 +938,15 @@
// WHEN the alternateBouncer stops showing
bouncerRepository.setAlternateVisible(false)
- advanceUntilIdle()
+ advanceTimeBy(200L)
- val info =
- withArgCaptor<TransitionInfo> {
- verify(transitionRepository).startTransition(capture())
- }
- // THEN a transition to LOCKSCREEN should occur
- assertThat(info.ownerName).isEqualTo("FromAlternateBouncerTransitionInteractor")
- assertThat(info.from).isEqualTo(KeyguardState.ALTERNATE_BOUNCER)
- assertThat(info.to).isEqualTo(KeyguardState.LOCKSCREEN)
- assertThat(info.animator).isNotNull()
+ assertThat(transitionRepository)
+ .startedTransition(
+ ownerName = "FromAlternateBouncerTransitionInteractor",
+ from = KeyguardState.ALTERNATE_BOUNCER,
+ to = KeyguardState.LOCKSCREEN,
+ animatorAssertion = { it.isNotNull() },
+ )
coroutineContext.cancelChildren()
}
@@ -1086,18 +974,16 @@
// WHEN the alternateBouncer stops showing
bouncerRepository.setAlternateVisible(false)
- advanceUntilIdle()
+ advanceTimeBy(200L)
- val info =
- withArgCaptor<TransitionInfo> {
- verify(transitionRepository).startTransition(capture())
- }
// THEN a transition to LOCKSCREEN should occur
- assertThat(info.ownerName)
- .isEqualTo(FromAlternateBouncerTransitionInteractor::class.simpleName)
- assertThat(info.from).isEqualTo(KeyguardState.ALTERNATE_BOUNCER)
- assertThat(info.to).isEqualTo(KeyguardState.GLANCEABLE_HUB)
- assertThat(info.animator).isNotNull()
+ assertThat(transitionRepository)
+ .startedTransition(
+ ownerName = FromAlternateBouncerTransitionInteractor::class.simpleName,
+ from = KeyguardState.ALTERNATE_BOUNCER,
+ to = KeyguardState.GLANCEABLE_HUB,
+ animatorAssertion = { it.isNotNull() },
+ )
coroutineContext.cancelChildren()
}
@@ -1117,15 +1003,14 @@
bouncerRepository.setPrimaryShow(false)
runCurrent()
- val info =
- withArgCaptor<TransitionInfo> {
- verify(transitionRepository).startTransition(capture())
- }
// THEN a transition to AOD should occur
- assertThat(info.ownerName).isEqualTo("FromPrimaryBouncerTransitionInteractor")
- assertThat(info.from).isEqualTo(KeyguardState.PRIMARY_BOUNCER)
- assertThat(info.to).isEqualTo(KeyguardState.AOD)
- assertThat(info.animator).isNotNull()
+ assertThat(transitionRepository)
+ .startedTransition(
+ ownerName = "FromPrimaryBouncerTransitionInteractor",
+ from = KeyguardState.PRIMARY_BOUNCER,
+ to = KeyguardState.AOD,
+ animatorAssertion = { it.isNotNull() },
+ )
coroutineContext.cancelChildren()
}
@@ -1145,15 +1030,14 @@
bouncerRepository.setPrimaryShow(false)
runCurrent()
- val info =
- withArgCaptor<TransitionInfo> {
- verify(transitionRepository).startTransition(capture())
- }
// THEN a transition to DOZING should occur
- assertThat(info.ownerName).isEqualTo("FromPrimaryBouncerTransitionInteractor")
- assertThat(info.from).isEqualTo(KeyguardState.PRIMARY_BOUNCER)
- assertThat(info.to).isEqualTo(KeyguardState.DOZING)
- assertThat(info.animator).isNotNull()
+ assertThat(transitionRepository)
+ .startedTransition(
+ ownerName = "FromPrimaryBouncerTransitionInteractor",
+ from = KeyguardState.PRIMARY_BOUNCER,
+ to = KeyguardState.DOZING,
+ animatorAssertion = { it.isNotNull() },
+ )
coroutineContext.cancelChildren()
}
@@ -1169,15 +1053,14 @@
bouncerRepository.setPrimaryShow(false)
runCurrent()
- val info =
- withArgCaptor<TransitionInfo> {
- verify(transitionRepository).startTransition(capture())
- }
// THEN a transition to LOCKSCREEN should occur
- assertThat(info.ownerName).isEqualTo("FromPrimaryBouncerTransitionInteractor")
- assertThat(info.from).isEqualTo(KeyguardState.PRIMARY_BOUNCER)
- assertThat(info.to).isEqualTo(KeyguardState.LOCKSCREEN)
- assertThat(info.animator).isNotNull()
+ assertThat(transitionRepository)
+ .startedTransition(
+ ownerName = "FromPrimaryBouncerTransitionInteractor",
+ from = KeyguardState.PRIMARY_BOUNCER,
+ to = KeyguardState.LOCKSCREEN,
+ animatorAssertion = { it.isNotNull() },
+ )
coroutineContext.cancelChildren()
}
@@ -1201,16 +1084,14 @@
bouncerRepository.setPrimaryShow(false)
runCurrent()
- val info =
- withArgCaptor<TransitionInfo> {
- verify(transitionRepository).startTransition(capture())
- }
// THEN a transition to LOCKSCREEN should occur
- assertThat(info.ownerName)
- .isEqualTo(FromPrimaryBouncerTransitionInteractor::class.simpleName)
- assertThat(info.from).isEqualTo(KeyguardState.PRIMARY_BOUNCER)
- assertThat(info.to).isEqualTo(KeyguardState.GLANCEABLE_HUB)
- assertThat(info.animator).isNotNull()
+ assertThat(transitionRepository)
+ .startedTransition(
+ ownerName = FromPrimaryBouncerTransitionInteractor::class.simpleName,
+ from = KeyguardState.PRIMARY_BOUNCER,
+ to = KeyguardState.GLANCEABLE_HUB,
+ animatorAssertion = { it.isNotNull() },
+ )
coroutineContext.cancelChildren()
}
@@ -1232,15 +1113,14 @@
bouncerRepository.setPrimaryShow(false)
runCurrent()
- val info =
- withArgCaptor<TransitionInfo> {
- verify(transitionRepository).startTransition(capture())
- }
// THEN a transition back to DREAMING_LOCKSCREEN_HOSTED should occur
- assertThat(info.ownerName).isEqualTo("FromPrimaryBouncerTransitionInteractor")
- assertThat(info.from).isEqualTo(KeyguardState.PRIMARY_BOUNCER)
- assertThat(info.to).isEqualTo(KeyguardState.DREAMING_LOCKSCREEN_HOSTED)
- assertThat(info.animator).isNotNull()
+ assertThat(transitionRepository)
+ .startedTransition(
+ ownerName = "FromPrimaryBouncerTransitionInteractor",
+ from = KeyguardState.PRIMARY_BOUNCER,
+ to = KeyguardState.DREAMING_LOCKSCREEN_HOSTED,
+ animatorAssertion = { it.isNotNull() },
+ )
coroutineContext.cancelChildren()
}
@@ -1263,15 +1143,14 @@
keyguardRepository.setKeyguardOccluded(false)
runCurrent()
- val info =
- withArgCaptor<TransitionInfo> {
- verify(transitionRepository).startTransition(capture())
- }
// THEN a transition to GONE should occur
- assertThat(info.ownerName).isEqualTo("FromOccludedTransitionInteractor")
- assertThat(info.from).isEqualTo(KeyguardState.OCCLUDED)
- assertThat(info.to).isEqualTo(KeyguardState.GONE)
- assertThat(info.animator).isNotNull()
+ assertThat(transitionRepository)
+ .startedTransition(
+ ownerName = "FromOccludedTransitionInteractor",
+ from = KeyguardState.OCCLUDED,
+ to = KeyguardState.GONE,
+ animatorAssertion = { it.isNotNull() },
+ )
coroutineContext.cancelChildren()
}
@@ -1292,15 +1171,14 @@
keyguardRepository.setKeyguardOccluded(false)
runCurrent()
- val info =
- withArgCaptor<TransitionInfo> {
- verify(transitionRepository).startTransition(capture())
- }
// THEN a transition to LOCKSCREEN should occur
- assertThat(info.ownerName).isEqualTo("FromOccludedTransitionInteractor")
- assertThat(info.from).isEqualTo(KeyguardState.OCCLUDED)
- assertThat(info.to).isEqualTo(KeyguardState.LOCKSCREEN)
- assertThat(info.animator).isNotNull()
+ assertThat(transitionRepository)
+ .startedTransition(
+ ownerName = "FromOccludedTransitionInteractor",
+ from = KeyguardState.OCCLUDED,
+ to = KeyguardState.LOCKSCREEN,
+ animatorAssertion = { it.isNotNull() },
+ )
coroutineContext.cancelChildren()
}
@@ -1329,15 +1207,14 @@
keyguardRepository.setKeyguardOccluded(false)
runCurrent()
- val info =
- withArgCaptor<TransitionInfo> {
- verify(transitionRepository).startTransition(capture())
- }
// THEN a transition to GLANCEABLE_HUB should occur
- assertThat(info.ownerName).isEqualTo(FromOccludedTransitionInteractor::class.simpleName)
- assertThat(info.from).isEqualTo(KeyguardState.OCCLUDED)
- assertThat(info.to).isEqualTo(KeyguardState.GLANCEABLE_HUB)
- assertThat(info.animator).isNotNull()
+ assertThat(transitionRepository)
+ .startedTransition(
+ ownerName = FromOccludedTransitionInteractor::class.simpleName,
+ from = KeyguardState.OCCLUDED,
+ to = KeyguardState.GLANCEABLE_HUB,
+ animatorAssertion = { it.isNotNull() },
+ )
coroutineContext.cancelChildren()
}
@@ -1354,15 +1231,14 @@
bouncerRepository.setAlternateVisible(true)
runCurrent()
- val info =
- withArgCaptor<TransitionInfo> {
- verify(transitionRepository).startTransition(capture())
- }
// THEN a transition to AlternateBouncer should occur
- assertThat(info.ownerName).isEqualTo("FromOccludedTransitionInteractor")
- assertThat(info.from).isEqualTo(KeyguardState.OCCLUDED)
- assertThat(info.to).isEqualTo(KeyguardState.ALTERNATE_BOUNCER)
- assertThat(info.animator).isNotNull()
+ assertThat(transitionRepository)
+ .startedTransition(
+ ownerName = "FromOccludedTransitionInteractor",
+ from = KeyguardState.OCCLUDED,
+ to = KeyguardState.ALTERNATE_BOUNCER,
+ animatorAssertion = { it.isNotNull() },
+ )
coroutineContext.cancelChildren()
}
@@ -1379,15 +1255,14 @@
bouncerRepository.setPrimaryShow(true)
runCurrent()
- val info =
- withArgCaptor<TransitionInfo> {
- verify(transitionRepository).startTransition(capture())
- }
// THEN a transition to AlternateBouncer should occur
- assertThat(info.ownerName).isEqualTo("FromOccludedTransitionInteractor")
- assertThat(info.from).isEqualTo(KeyguardState.OCCLUDED)
- assertThat(info.to).isEqualTo(KeyguardState.PRIMARY_BOUNCER)
- assertThat(info.animator).isNotNull()
+ assertThat(transitionRepository)
+ .startedTransition(
+ ownerName = "FromOccludedTransitionInteractor",
+ from = KeyguardState.OCCLUDED,
+ to = KeyguardState.PRIMARY_BOUNCER,
+ animatorAssertion = { it.isNotNull() },
+ )
coroutineContext.cancelChildren()
}
@@ -1405,15 +1280,14 @@
bouncerRepository.setPrimaryShow(false)
runCurrent()
- val info =
- withArgCaptor<TransitionInfo> {
- verify(transitionRepository).startTransition(capture())
- }
// THEN a transition to OCCLUDED should occur
- assertThat(info.ownerName).isEqualTo("FromPrimaryBouncerTransitionInteractor")
- assertThat(info.from).isEqualTo(KeyguardState.PRIMARY_BOUNCER)
- assertThat(info.to).isEqualTo(KeyguardState.OCCLUDED)
- assertThat(info.animator).isNotNull()
+ assertThat(transitionRepository)
+ .startedTransition(
+ ownerName = "FromPrimaryBouncerTransitionInteractor",
+ from = KeyguardState.PRIMARY_BOUNCER,
+ to = KeyguardState.OCCLUDED,
+ animatorAssertion = { it.isNotNull() },
+ )
coroutineContext.cancelChildren()
}
@@ -1430,15 +1304,14 @@
powerInteractor.setAwakeForTest()
runCurrent()
- val info =
- withArgCaptor<TransitionInfo> {
- verify(transitionRepository).startTransition(capture())
- }
// THEN a transition to OCCLUDED should occur
- assertThat(info.ownerName).isEqualTo("FromDozingTransitionInteractor")
- assertThat(info.from).isEqualTo(KeyguardState.DOZING)
- assertThat(info.to).isEqualTo(KeyguardState.OCCLUDED)
- assertThat(info.animator).isNotNull()
+ assertThat(transitionRepository)
+ .startedTransition(
+ ownerName = "FromDozingTransitionInteractor",
+ from = KeyguardState.DOZING,
+ to = KeyguardState.OCCLUDED,
+ animatorAssertion = { it.isNotNull() },
+ )
coroutineContext.cancelChildren()
}
@@ -1457,15 +1330,14 @@
powerInteractor.setAwakeForTest()
runCurrent()
- val info =
- withArgCaptor<TransitionInfo> {
- verify(transitionRepository).startTransition(capture())
- }
// THEN a transition to OCCLUDED should occur
- assertThat(info.ownerName).isEqualTo("FromDreamingTransitionInteractor")
- assertThat(info.from).isEqualTo(KeyguardState.DREAMING)
- assertThat(info.to).isEqualTo(KeyguardState.OCCLUDED)
- assertThat(info.animator).isNotNull()
+ assertThat(transitionRepository)
+ .startedTransition(
+ ownerName = "FromDreamingTransitionInteractor",
+ from = KeyguardState.DREAMING,
+ to = KeyguardState.OCCLUDED,
+ animatorAssertion = { it.isNotNull() },
+ )
coroutineContext.cancelChildren()
}
@@ -1487,15 +1359,14 @@
)
runCurrent()
- val info =
- withArgCaptor<TransitionInfo> {
- verify(transitionRepository).startTransition(capture())
- }
// THEN a transition to AOD should occur
- assertThat(info.ownerName).isEqualTo("FromDreamingTransitionInteractor")
- assertThat(info.from).isEqualTo(KeyguardState.DREAMING)
- assertThat(info.to).isEqualTo(KeyguardState.AOD)
- assertThat(info.animator).isNotNull()
+ assertThat(transitionRepository)
+ .startedTransition(
+ ownerName = "FromDreamingTransitionInteractor",
+ from = KeyguardState.DREAMING,
+ to = KeyguardState.AOD,
+ animatorAssertion = { it.isNotNull() },
+ )
coroutineContext.cancelChildren()
}
@@ -1511,15 +1382,14 @@
keyguardRepository.setKeyguardOccluded(true)
runCurrent()
- val info =
- withArgCaptor<TransitionInfo> {
- verify(transitionRepository).startTransition(capture())
- }
// THEN a transition to OCCLUDED should occur
- assertThat(info.ownerName).isEqualTo("FromLockscreenTransitionInteractor")
- assertThat(info.from).isEqualTo(KeyguardState.LOCKSCREEN)
- assertThat(info.to).isEqualTo(KeyguardState.OCCLUDED)
- assertThat(info.animator).isNotNull()
+ assertThat(transitionRepository)
+ .startedTransition(
+ ownerName = "FromLockscreenTransitionInteractor",
+ from = KeyguardState.LOCKSCREEN,
+ to = KeyguardState.OCCLUDED,
+ animatorAssertion = { it.isNotNull() },
+ )
coroutineContext.cancelChildren()
}
@@ -1535,15 +1405,14 @@
keyguardRepository.setKeyguardOccluded(true)
runCurrent()
- val info =
- withArgCaptor<TransitionInfo> {
- verify(transitionRepository).startTransition(capture())
- }
// THEN a transition to OCCLUDED should occur
- assertThat(info.ownerName).isEqualTo("FromAodTransitionInteractor")
- assertThat(info.from).isEqualTo(KeyguardState.AOD)
- assertThat(info.to).isEqualTo(KeyguardState.OCCLUDED)
- assertThat(info.animator).isNotNull()
+ assertThat(transitionRepository)
+ .startedTransition(
+ ownerName = "FromAodTransitionInteractor",
+ from = KeyguardState.AOD,
+ to = KeyguardState.OCCLUDED,
+ animatorAssertion = { it.isNotNull() },
+ )
coroutineContext.cancelChildren()
}
@@ -1559,15 +1428,14 @@
bouncerRepository.setPrimaryShow(true)
runCurrent()
- val info =
- withArgCaptor<TransitionInfo> {
- verify(transitionRepository).startTransition(capture())
- }
// THEN a transition to OCCLUDED should occur
- assertThat(info.ownerName).isEqualTo("FromAodTransitionInteractor")
- assertThat(info.from).isEqualTo(KeyguardState.AOD)
- assertThat(info.to).isEqualTo(KeyguardState.PRIMARY_BOUNCER)
- assertThat(info.animator).isNotNull()
+ assertThat(transitionRepository)
+ .startedTransition(
+ ownerName = "FromAodTransitionInteractor",
+ from = KeyguardState.AOD,
+ to = KeyguardState.PRIMARY_BOUNCER,
+ animatorAssertion = { it.isNotNull() },
+ )
coroutineContext.cancelChildren()
}
@@ -1593,14 +1461,13 @@
runCurrent()
// THEN a transition from LOCKSCREEN => OCCLUDED should occur
- val info =
- withArgCaptor<TransitionInfo> {
- verify(transitionRepository).startTransition(capture())
- }
- assertThat(info.ownerName).isEqualTo("FromLockscreenTransitionInteractor")
- assertThat(info.from).isEqualTo(KeyguardState.LOCKSCREEN)
- assertThat(info.to).isEqualTo(KeyguardState.OCCLUDED)
- assertThat(info.animator).isNotNull()
+ assertThat(transitionRepository)
+ .startedTransition(
+ ownerName = "FromLockscreenTransitionInteractor",
+ from = KeyguardState.LOCKSCREEN,
+ to = KeyguardState.OCCLUDED,
+ animatorAssertion = { it.isNotNull() },
+ )
coroutineContext.cancelChildren()
}
@@ -1620,14 +1487,13 @@
runCurrent()
// THEN a transition from LOCKSCREEN => PRIMARY_BOUNCER should occur
- val info =
- withArgCaptor<TransitionInfo> {
- verify(transitionRepository).startTransition(capture())
- }
- assertThat(info.ownerName).isEqualTo("FromLockscreenTransitionInteractor")
- assertThat(info.from).isEqualTo(KeyguardState.LOCKSCREEN)
- assertThat(info.to).isEqualTo(KeyguardState.PRIMARY_BOUNCER)
- assertThat(info.animator).isNull() // dragging should be manually animated
+ assertThat(transitionRepository)
+ .startedTransition(
+ ownerName = "FromLockscreenTransitionInteractor",
+ from = KeyguardState.LOCKSCREEN,
+ to = KeyguardState.PRIMARY_BOUNCER,
+ animatorAssertion = { it.isNull() }, // dragging should be manually animated
+ )
// WHEN the user stops dragging and shade is back to expanded
clearInvocations(transitionRepository)
@@ -1636,14 +1502,13 @@
shadeRepository.setLegacyShadeExpansion(1f)
runCurrent()
- // THEN a transition from PRIMARY_BOUNCER => LOCKSCREEN should occur
- val info2 =
- withArgCaptor<TransitionInfo> {
- verify(transitionRepository).startTransition(capture())
- }
- assertThat(info2.from).isEqualTo(KeyguardState.PRIMARY_BOUNCER)
- assertThat(info2.to).isEqualTo(KeyguardState.LOCKSCREEN)
- assertThat(info2.animator).isNotNull()
+ // THEN a transition from LOCKSCREEN => PRIMARY_BOUNCER should occur
+ assertThat(transitionRepository)
+ .startedTransition(
+ from = KeyguardState.PRIMARY_BOUNCER,
+ to = KeyguardState.LOCKSCREEN,
+ animatorAssertion = { it.isNotNull() },
+ )
coroutineContext.cancelChildren()
}
@@ -1675,15 +1540,13 @@
runCurrent()
// THEN a transition from LOCKSCREEN => GLANCEABLE_HUB should occur
- val info =
- withArgCaptor<TransitionInfo> {
- verify(transitionRepository).startTransition(capture())
- }
- assertThat(info.ownerName)
- .isEqualTo(FromLockscreenTransitionInteractor::class.simpleName)
- assertThat(info.from).isEqualTo(KeyguardState.LOCKSCREEN)
- assertThat(info.to).isEqualTo(KeyguardState.GLANCEABLE_HUB)
- assertThat(info.animator).isNull() // transition should be manually animated
+ assertThat(transitionRepository)
+ .startedTransition(
+ ownerName = FromLockscreenTransitionInteractor::class.simpleName,
+ from = KeyguardState.LOCKSCREEN,
+ to = KeyguardState.GLANCEABLE_HUB,
+ animatorAssertion = { it.isNull() }, // transition should be manually animated
+ )
// WHEN the user stops dragging and the glanceable hub opening is cancelled
clearInvocations(transitionRepository)
@@ -1695,14 +1558,13 @@
communalInteractor.setTransitionState(idleTransitionState)
runCurrent()
- // THEN a transition from GLANCEABLE_HUB => LOCKSCREEN should occur
- val info2 =
- withArgCaptor<TransitionInfo> {
- verify(transitionRepository).startTransition(capture())
- }
- assertThat(info2.from).isEqualTo(KeyguardState.GLANCEABLE_HUB)
- assertThat(info2.to).isEqualTo(KeyguardState.LOCKSCREEN)
- assertThat(info.animator).isNull() // transition should be manually animated
+ // THEN a transition from LOCKSCREEN => GLANCEABLE_HUB should occur
+ assertThat(transitionRepository)
+ .startedTransition(
+ ownerName = FromLockscreenTransitionInteractor::class.simpleName,
+ from = KeyguardState.GLANCEABLE_HUB,
+ to = KeyguardState.LOCKSCREEN,
+ )
coroutineContext.cancelChildren()
}
@@ -1733,16 +1595,13 @@
progress.value = .1f
runCurrent()
- // THEN a transition from GLANCEABLE_HUB => LOCKSCREEN should occur
- val info =
- withArgCaptor<TransitionInfo> {
- verify(transitionRepository).startTransition(capture())
- }
- assertThat(info.ownerName)
- .isEqualTo(FromGlanceableHubTransitionInteractor::class.simpleName)
- assertThat(info.from).isEqualTo(KeyguardState.GLANCEABLE_HUB)
- assertThat(info.to).isEqualTo(KeyguardState.LOCKSCREEN)
- assertThat(info.animator).isNull() // transition should be manually animated
+ assertThat(transitionRepository)
+ .startedTransition(
+ ownerName = FromGlanceableHubTransitionInteractor::class.simpleName,
+ from = KeyguardState.GLANCEABLE_HUB,
+ to = KeyguardState.LOCKSCREEN,
+ animatorAssertion = { it.isNull() }, // transition should be manually animated
+ )
// WHEN the user stops dragging and the glanceable hub closing is cancelled
clearInvocations(transitionRepository)
@@ -1754,14 +1613,11 @@
communalInteractor.setTransitionState(idleTransitionState)
runCurrent()
- // THEN a transition from LOCKSCREEN => GLANCEABLE_HUB should occur
- val info2 =
- withArgCaptor<TransitionInfo> {
- verify(transitionRepository).startTransition(capture())
- }
- assertThat(info2.from).isEqualTo(KeyguardState.LOCKSCREEN)
- assertThat(info2.to).isEqualTo(KeyguardState.GLANCEABLE_HUB)
- assertThat(info.animator).isNull() // transition should be manually animated
+ assertThat(transitionRepository)
+ .startedTransition(
+ from = KeyguardState.LOCKSCREEN,
+ to = KeyguardState.GLANCEABLE_HUB,
+ )
coroutineContext.cancelChildren()
}
@@ -1776,16 +1632,13 @@
powerInteractor.setAsleepForTest()
runCurrent()
- val info =
- withArgCaptor<TransitionInfo> {
- verify(transitionRepository).startTransition(capture())
- }
- // THEN a transition to DOZING should occur
- assertThat(info.ownerName)
- .isEqualTo(FromGlanceableHubTransitionInteractor::class.simpleName)
- assertThat(info.from).isEqualTo(KeyguardState.GLANCEABLE_HUB)
- assertThat(info.to).isEqualTo(KeyguardState.DOZING)
- assertThat(info.animator).isNotNull()
+ assertThat(transitionRepository)
+ .startedTransition(
+ ownerName = FromGlanceableHubTransitionInteractor::class.simpleName,
+ from = KeyguardState.GLANCEABLE_HUB,
+ to = KeyguardState.DOZING,
+ animatorAssertion = { it.isNotNull() },
+ )
coroutineContext.cancelChildren()
}
@@ -1800,16 +1653,13 @@
bouncerRepository.setPrimaryShow(true)
runCurrent()
- val info =
- withArgCaptor<TransitionInfo> {
- verify(transitionRepository).startTransition(capture())
- }
- // THEN a transition to PRIMARY_BOUNCER should occur
- assertThat(info.ownerName)
- .isEqualTo(FromGlanceableHubTransitionInteractor::class.simpleName)
- assertThat(info.from).isEqualTo(KeyguardState.GLANCEABLE_HUB)
- assertThat(info.to).isEqualTo(KeyguardState.PRIMARY_BOUNCER)
- assertThat(info.animator).isNotNull()
+ assertThat(transitionRepository)
+ .startedTransition(
+ ownerName = FromGlanceableHubTransitionInteractor::class.simpleName,
+ from = KeyguardState.GLANCEABLE_HUB,
+ to = KeyguardState.PRIMARY_BOUNCER,
+ animatorAssertion = { it.isNotNull() },
+ )
coroutineContext.cancelChildren()
}
@@ -1824,16 +1674,13 @@
bouncerRepository.setAlternateVisible(true)
runCurrent()
- val info =
- withArgCaptor<TransitionInfo> {
- verify(transitionRepository).startTransition(capture())
- }
- // THEN a transition to PRIMARY_BOUNCER should occur
- assertThat(info.ownerName)
- .isEqualTo(FromGlanceableHubTransitionInteractor::class.simpleName)
- assertThat(info.from).isEqualTo(KeyguardState.GLANCEABLE_HUB)
- assertThat(info.to).isEqualTo(KeyguardState.ALTERNATE_BOUNCER)
- assertThat(info.animator).isNotNull()
+ assertThat(transitionRepository)
+ .startedTransition(
+ ownerName = FromGlanceableHubTransitionInteractor::class.simpleName,
+ from = KeyguardState.GLANCEABLE_HUB,
+ to = KeyguardState.ALTERNATE_BOUNCER,
+ animatorAssertion = { it.isNotNull() },
+ )
coroutineContext.cancelChildren()
}
@@ -1857,16 +1704,13 @@
keyguardRepository.setKeyguardOccluded(true)
runCurrent()
- val info =
- withArgCaptor<TransitionInfo> {
- verify(transitionRepository).startTransition(capture())
- }
- // THEN a transition to OCCLUDED should occur
- assertThat(info.ownerName)
- .isEqualTo(FromGlanceableHubTransitionInteractor::class.simpleName)
- assertThat(info.from).isEqualTo(KeyguardState.GLANCEABLE_HUB)
- assertThat(info.to).isEqualTo(KeyguardState.OCCLUDED)
- assertThat(info.animator).isNotNull()
+ assertThat(transitionRepository)
+ .startedTransition(
+ ownerName = FromGlanceableHubTransitionInteractor::class.simpleName,
+ from = KeyguardState.GLANCEABLE_HUB,
+ to = KeyguardState.OCCLUDED,
+ animatorAssertion = { it.isNotNull() },
+ )
coroutineContext.cancelChildren()
}
@@ -1881,16 +1725,13 @@
keyguardRepository.setKeyguardGoingAway(true)
runCurrent()
- val info =
- withArgCaptor<TransitionInfo> {
- verify(transitionRepository).startTransition(capture())
- }
- // THEN a transition to DOZING should occur
- assertThat(info.ownerName)
- .isEqualTo(FromGlanceableHubTransitionInteractor::class.simpleName)
- assertThat(info.from).isEqualTo(KeyguardState.GLANCEABLE_HUB)
- assertThat(info.to).isEqualTo(KeyguardState.GONE)
- assertThat(info.animator).isNotNull()
+ assertThat(transitionRepository)
+ .startedTransition(
+ ownerName = FromGlanceableHubTransitionInteractor::class.simpleName,
+ from = KeyguardState.GLANCEABLE_HUB,
+ to = KeyguardState.GONE,
+ animatorAssertion = { it.isNotNull() },
+ )
coroutineContext.cancelChildren()
}
@@ -1910,33 +1751,19 @@
// WHEN the device begins to dream
keyguardRepository.setDreamingWithOverlay(true)
- advanceUntilIdle()
+ advanceTimeBy(100L)
- val info =
- withArgCaptor<TransitionInfo> {
- verify(transitionRepository).startTransition(capture())
- }
- // THEN a transition to DREAMING should occur
- assertThat(info.ownerName)
- .isEqualTo(FromGlanceableHubTransitionInteractor::class.simpleName)
- assertThat(info.from).isEqualTo(KeyguardState.GLANCEABLE_HUB)
- assertThat(info.to).isEqualTo(KeyguardState.DREAMING)
- assertThat(info.animator).isNotNull()
+ assertThat(transitionRepository)
+ .startedTransition(
+ ownerName = FromGlanceableHubTransitionInteractor::class.simpleName,
+ from = KeyguardState.GLANCEABLE_HUB,
+ to = KeyguardState.DREAMING,
+ animatorAssertion = { it.isNotNull() },
+ )
coroutineContext.cancelChildren()
}
- private fun createKeyguardInteractor(): KeyguardInteractor {
- return KeyguardInteractorFactory.create(
- featureFlags = featureFlags,
- repository = keyguardRepository,
- commandQueue = commandQueue,
- bouncerRepository = bouncerRepository,
- powerInteractor = powerInteractor,
- )
- .keyguardInteractor
- }
-
private suspend fun TestScope.runTransitionAndSetWakefulness(
from: KeyguardState,
to: KeyguardState
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/SwipeToDismissInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/SwipeToDismissInteractorTest.kt
new file mode 100644
index 0000000..d77519d
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/SwipeToDismissInteractorTest.kt
@@ -0,0 +1,107 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyguard.domain.interactor
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectValues
+import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository
+import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
+import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.shade.data.repository.FlingInfo
+import com.android.systemui.shade.data.repository.fakeShadeRepository
+import com.android.systemui.testKosmos
+import com.google.common.truth.Correspondence
+import com.google.common.truth.Truth.assertThat
+import kotlin.test.Test
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+@kotlinx.coroutines.ExperimentalCoroutinesApi
+class SwipeToDismissInteractorTest : SysuiTestCase() {
+ private val kosmos = testKosmos()
+ private val testScope = kosmos.testScope
+ private val underTest = kosmos.swipeToDismissInteractor
+ private val transitionRepository = kosmos.fakeKeyguardTransitionRepository
+ private val shadeRepository = kosmos.fakeShadeRepository
+ private val keyguardRepository = kosmos.fakeKeyguardRepository
+
+ @Test
+ fun testDismissFling_emitsInLockscreenWhenDismissable() =
+ testScope.runTest {
+ val values by collectValues(underTest.dismissFling)
+ keyguardRepository.setKeyguardDismissible(true)
+ runCurrent()
+ shadeRepository.setCurrentFling(FlingInfo(expand = false))
+ runCurrent()
+
+ assertThat(values)
+ .comparingElementsUsing(
+ Correspondence.transforming(
+ { fling: FlingInfo? -> fling?.expand },
+ "expand equals"
+ )
+ )
+ .containsExactly(null, false)
+ .inOrder()
+ }
+
+ @Test
+ fun testDismissFling_doesNotEmitInWrongStateOrNotDismissable() =
+ testScope.runTest {
+ val values by collectValues(underTest.dismissFling)
+ keyguardRepository.setKeyguardDismissible(false)
+ shadeRepository.setCurrentFling(FlingInfo(expand = false))
+ runCurrent()
+
+ assertThat(values).containsExactly(null)
+
+ // Not in LOCKSCREEN, but dismissable.
+ transitionRepository.sendTransitionSteps(
+ KeyguardState.LOCKSCREEN,
+ KeyguardState.GONE,
+ testScope
+ )
+ keyguardRepository.setKeyguardDismissible(true)
+
+ // Re-emit a valid dismiss fling.
+ shadeRepository.setCurrentFling(null)
+ shadeRepository.setCurrentFling(FlingInfo(expand = false))
+ runCurrent()
+
+ assertThat(values).containsExactly(null)
+ }
+
+ @Test
+ fun testExpandFling_doesNotEmit() =
+ testScope.runTest {
+ val values by collectValues(underTest.dismissFling)
+ keyguardRepository.setKeyguardDismissible(true)
+ runCurrent()
+ shadeRepository.setCurrentFling(
+ FlingInfo(expand = true) // Not a dismiss fling (expand = true).
+ )
+ runCurrent()
+
+ assertThat(values).containsExactly(null)
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/WindowManagerLockscreenVisibilityInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/WindowManagerLockscreenVisibilityInteractorTest.kt
index 6eed427..a4483bd 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/WindowManagerLockscreenVisibilityInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/WindowManagerLockscreenVisibilityInteractorTest.kt
@@ -20,76 +20,46 @@
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.coroutines.collectValues
-import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
+import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
import com.android.systemui.keyguard.shared.model.KeyguardState
import com.android.systemui.keyguard.shared.model.TransitionState
import com.android.systemui.keyguard.shared.model.TransitionStep
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.testKosmos
+import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.whenever
import junit.framework.Assert.assertEquals
import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
-import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
-import org.mockito.Mock
-import org.mockito.MockitoAnnotations.initMocks
@SmallTest
@RunWith(AndroidJUnit4::class)
@kotlinx.coroutines.ExperimentalCoroutinesApi
class WindowManagerLockscreenVisibilityInteractorTest : SysuiTestCase() {
-
- private lateinit var underTest: WindowManagerLockscreenVisibilityInteractor
-
- @Mock private lateinit var surfaceBehindInteractor: KeyguardSurfaceBehindInteractor
- @Mock
- private lateinit var fromLockscreenTransitionInteractor: FromLockscreenTransitionInteractor
- @Mock
- private lateinit var fromPrimaryBouncerTransitionInteractor:
- FromPrimaryBouncerTransitionInteractor
-
private val lockscreenSurfaceVisibilityFlow = MutableStateFlow<Boolean?>(false)
private val primaryBouncerSurfaceVisibilityFlow = MutableStateFlow<Boolean?>(false)
private val surfaceBehindIsAnimatingFlow = MutableStateFlow(false)
- private val testScope = TestScope()
+ private val kosmos =
+ testKosmos().apply {
+ fromLockscreenTransitionInteractor = mock<FromLockscreenTransitionInteractor>()
+ fromPrimaryBouncerTransitionInteractor = mock<FromPrimaryBouncerTransitionInteractor>()
+ keyguardSurfaceBehindInteractor = mock<KeyguardSurfaceBehindInteractor>()
- private lateinit var keyguardInteractor: KeyguardInteractor
- private lateinit var transitionRepository: FakeKeyguardTransitionRepository
- private lateinit var transitionInteractor: KeyguardTransitionInteractor
+ whenever(fromLockscreenTransitionInteractor.surfaceBehindVisibility)
+ .thenReturn(lockscreenSurfaceVisibilityFlow)
+ whenever(fromPrimaryBouncerTransitionInteractor.surfaceBehindVisibility)
+ .thenReturn(primaryBouncerSurfaceVisibilityFlow)
+ whenever(keyguardSurfaceBehindInteractor.isAnimatingSurface)
+ .thenReturn(surfaceBehindIsAnimatingFlow)
+ }
- @Before
- fun setUp() {
- initMocks(this)
-
- whenever(fromLockscreenTransitionInteractor.surfaceBehindVisibility)
- .thenReturn(lockscreenSurfaceVisibilityFlow)
- whenever(fromPrimaryBouncerTransitionInteractor.surfaceBehindVisibility)
- .thenReturn(primaryBouncerSurfaceVisibilityFlow)
- whenever(surfaceBehindInteractor.isAnimatingSurface)
- .thenReturn(surfaceBehindIsAnimatingFlow)
-
- transitionRepository = FakeKeyguardTransitionRepository()
-
- transitionInteractor =
- KeyguardTransitionInteractorFactory.create(
- scope = testScope.backgroundScope,
- repository = transitionRepository,
- )
- .also { keyguardInteractor = it.keyguardInteractor }
- .keyguardTransitionInteractor
-
- underTest =
- WindowManagerLockscreenVisibilityInteractor(
- keyguardInteractor = keyguardInteractor,
- transitionInteractor = transitionInteractor,
- surfaceBehindInteractor = surfaceBehindInteractor,
- fromLockscreenTransitionInteractor,
- fromPrimaryBouncerTransitionInteractor,
- )
- }
+ private val underTest = kosmos.windowManagerLockscreenVisibilityInteractor
+ private val testScope = kosmos.testScope
+ private val transitionRepository = kosmos.fakeKeyguardTransitionRepository
@Test
fun surfaceBehindVisibility_switchesToCorrectFlow() =
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlowTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlowTest.kt
index 2d9d5ed..0e9197e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlowTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlowTest.kt
@@ -181,6 +181,31 @@
}
@Test
+ fun usesOnStepToDoubleValueWithState() =
+ testScope.runTest {
+ val flow =
+ underTest.sharedFlowWithState(
+ duration = 1000.milliseconds,
+ onStep = { it * 2 },
+ )
+ val animationValues by collectLastValue(flow)
+ runCurrent()
+
+ repository.sendTransitionStep(step(0f, TransitionState.STARTED))
+ assertThat(animationValues).isEqualTo(StateToValue(TransitionState.STARTED, 0f))
+ repository.sendTransitionStep(step(0.3f, TransitionState.RUNNING))
+ assertThat(animationValues).isEqualTo(StateToValue(TransitionState.RUNNING, 0.6f))
+ repository.sendTransitionStep(step(0.6f, TransitionState.RUNNING))
+ assertThat(animationValues).isEqualTo(StateToValue(TransitionState.RUNNING, 1.2f))
+ repository.sendTransitionStep(step(0.8f, TransitionState.RUNNING))
+ assertThat(animationValues).isEqualTo(StateToValue(TransitionState.RUNNING, 1.6f))
+ repository.sendTransitionStep(step(1f, TransitionState.RUNNING))
+ assertThat(animationValues).isEqualTo(StateToValue(TransitionState.RUNNING, 2f))
+ repository.sendTransitionStep(step(1f, TransitionState.FINISHED))
+ assertThat(animationValues).isEqualTo(StateToValue(TransitionState.FINISHED, null))
+ }
+
+ @Test
fun sameFloatValueWithTheSameTransitionStateDoesNotEmitTwice() =
testScope.runTest {
val flow =
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/GoneToAodTransitionViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/GoneToAodTransitionViewModelTest.kt
index 1c9c942..bfa8433 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/GoneToAodTransitionViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/GoneToAodTransitionViewModelTest.kt
@@ -26,6 +26,7 @@
import com.android.systemui.keyguard.shared.model.KeyguardState
import com.android.systemui.keyguard.shared.model.TransitionState
import com.android.systemui.keyguard.shared.model.TransitionStep
+import com.android.systemui.keyguard.ui.StateToValue
import com.android.systemui.kosmos.testScope
import com.android.systemui.testKosmos
import com.google.common.collect.Range
@@ -57,17 +58,19 @@
// The animation should only start > .4f way through
repository.sendTransitionStep(step(0f, TransitionState.STARTED))
- assertThat(enterFromTopTranslationY).isEqualTo(pixels)
+ assertThat(enterFromTopTranslationY)
+ .isEqualTo(StateToValue(TransitionState.STARTED, pixels))
- repository.sendTransitionStep(step(0.4f))
- assertThat(enterFromTopTranslationY).isEqualTo(pixels)
+ repository.sendTransitionStep(step(.55f))
+ assertThat(enterFromTopTranslationY!!.value ?: -1f).isIn(Range.closed(pixels, 0f))
repository.sendTransitionStep(step(.85f))
- assertThat(enterFromTopTranslationY).isIn(Range.closed(pixels, 0f))
+ assertThat(enterFromTopTranslationY!!.value ?: -1f).isIn(Range.closed(pixels, 0f))
// At the end, the translation should be complete and set to zero
repository.sendTransitionStep(step(1f))
- assertThat(enterFromTopTranslationY).isEqualTo(0f)
+ assertThat(enterFromTopTranslationY)
+ .isEqualTo(StateToValue(TransitionState.RUNNING, 0f))
}
@Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt
index af38523c..90943de 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt
@@ -46,7 +46,7 @@
import com.android.systemui.keyguard.domain.interactor.KeyguardInteractorFactory
import com.android.systemui.keyguard.domain.interactor.KeyguardLongPressInteractor
import com.android.systemui.keyguard.domain.interactor.KeyguardQuickAffordanceInteractor
-import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractorFactory
+import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor
import com.android.systemui.keyguard.shared.quickaffordance.ActivationState
import com.android.systemui.keyguard.shared.quickaffordance.KeyguardQuickAffordancePosition
import com.android.systemui.keyguard.shared.quickaffordance.KeyguardQuickAffordancesMetricsLogger
@@ -67,7 +67,6 @@
import com.google.common.truth.Truth.assertThat
import kotlin.math.max
import kotlin.math.min
-import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.TestScope
@@ -83,7 +82,6 @@
import org.mockito.Mockito.verifyZeroInteractions
import org.mockito.MockitoAnnotations
-@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
@RunWith(JUnit4::class)
class KeyguardBottomAreaViewModelTest : SysuiTestCase() {
@@ -208,11 +206,7 @@
KeyguardLongPressInteractor(
appContext = mContext,
scope = testScope.backgroundScope,
- transitionInteractor =
- KeyguardTransitionInteractorFactory.create(
- scope = TestScope().backgroundScope,
- )
- .keyguardTransitionInteractor,
+ transitionInteractor = kosmos.keyguardTransitionInteractor,
repository = repository,
logger = UiEventLoggerFake(),
featureFlags = featureFlags,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/util/KeyguardTransitionRepositorySpySubject.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/util/KeyguardTransitionRepositorySpySubject.kt
new file mode 100644
index 0000000..655a551
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/util/KeyguardTransitionRepositorySpySubject.kt
@@ -0,0 +1,112 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyguard.util
+
+import androidx.core.animation.ValueAnimator
+import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository
+import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.keyguard.shared.model.TransitionInfo
+import com.android.systemui.keyguard.shared.model.TransitionModeOnCanceled
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.withArgCaptor
+import com.google.common.truth.FailureMetadata
+import com.google.common.truth.Subject
+import com.google.common.truth.Truth
+import com.google.common.truth.Truth.assertAbout
+import junit.framework.Assert.assertEquals
+import kotlin.test.fail
+import org.mockito.Mockito
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+
+/** [Subject] used to make assertions about a [Mockito.spy] KeyguardTransitionRepository. */
+class KeyguardTransitionRepositorySpySubject
+private constructor(
+ failureMetadata: FailureMetadata,
+ private val repository: KeyguardTransitionRepository,
+) : Subject(failureMetadata, repository) {
+
+ /**
+ * Asserts that we started a transition to the given state, optionally checking additional
+ * parameters. If an animator param or assertion is not provided, we will not assert anything
+ * about the animator.
+ */
+ fun startedTransition(
+ ownerName: String? = null,
+ from: KeyguardState? = null,
+ to: KeyguardState,
+ modeOnCanceled: TransitionModeOnCanceled? = null,
+ ) {
+ startedTransition(ownerName, from, to, {}, modeOnCanceled)
+ }
+
+ /**
+ * Asserts that we started a transition to the given state, optionally verifying additional
+ * params.
+ */
+ fun startedTransition(
+ ownerName: String? = null,
+ from: KeyguardState? = null,
+ to: KeyguardState,
+ animator: ValueAnimator?,
+ modeOnCanceled: TransitionModeOnCanceled? = null,
+ ) {
+ startedTransition(ownerName, from, to, { assertEquals(animator, it) }, modeOnCanceled)
+ }
+
+ /**
+ * Asserts that we started a transition to the given state, optionally verifying additional
+ * params.
+ */
+ fun startedTransition(
+ ownerName: String? = null,
+ from: KeyguardState? = null,
+ to: KeyguardState,
+ animatorAssertion: (Subject) -> Unit,
+ modeOnCanceled: TransitionModeOnCanceled? = null,
+ ) {
+ withArgCaptor<TransitionInfo> { verify(repository).startTransition(capture()) }
+ .also { transitionInfo ->
+ assertEquals(to, transitionInfo.to)
+ animatorAssertion.invoke(Truth.assertThat(transitionInfo.animator))
+ from?.let { assertEquals(it, transitionInfo.from) }
+ ownerName?.let { assertEquals(it, transitionInfo.ownerName) }
+ modeOnCanceled?.let { assertEquals(it, transitionInfo.modeOnCanceled) }
+ }
+ }
+
+ /** Verifies that [KeyguardTransitionRepository.startTransition] was never called. */
+ fun noTransitionsStarted() {
+ verify(repository, never()).startTransition(any())
+ }
+
+ companion object {
+ fun assertThat(
+ repository: KeyguardTransitionRepository
+ ): KeyguardTransitionRepositorySpySubject =
+ assertAbout { failureMetadata, repository: KeyguardTransitionRepository ->
+ if (!Mockito.mockingDetails(repository).isSpy) {
+ fail(
+ "Cannot assert on a non-spy KeyguardTransitionRepository. " +
+ "Use Mockito.spy(keyguardTransitionRepository)."
+ )
+ }
+ KeyguardTransitionRepositorySpySubject(failureMetadata, repository)
+ }
+ .that(repository)
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/OWNERS b/packages/SystemUI/tests/src/com/android/systemui/media/OWNERS
deleted file mode 100644
index 142862d..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/media/OWNERS
+++ /dev/null
@@ -1,2 +0,0 @@
-# Haptics team also works on Ringtones (RingtonePlayer/NotificationPlayer)
-file:/services/core/java/com/android/server/vibrator/OWNERS
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/models/player/SeekBarObserverTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/models/player/SeekBarObserverTest.kt
index 043dae6..100e579 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/models/player/SeekBarObserverTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/models/player/SeekBarObserverTest.kt
@@ -24,9 +24,9 @@
import android.widget.SeekBar
import android.widget.TextView
import androidx.test.filters.SmallTest
-import com.android.systemui.res.R
import com.android.systemui.SysuiTestCase
import com.android.systemui.media.controls.ui.SquigglyProgress
+import com.android.systemui.res.R
import com.google.common.truth.Truth.assertThat
import org.junit.Before
import org.junit.Rule
@@ -86,7 +86,7 @@
fun seekBarGone() {
// WHEN seek bar is disabled
val isEnabled = false
- val data = SeekBarViewModel.Progress(isEnabled, false, false, false, null, 0)
+ val data = SeekBarViewModel.Progress(isEnabled, false, false, false, null, 0, false)
observer.onChanged(data)
// THEN seek bar shows just a thin line with no text
assertThat(seekBarView.isEnabled()).isFalse()
@@ -99,7 +99,7 @@
fun seekBarVisible() {
// WHEN seek bar is enabled
val isEnabled = true
- val data = SeekBarViewModel.Progress(isEnabled, true, false, false, 3000, 12000)
+ val data = SeekBarViewModel.Progress(isEnabled, true, false, false, 3000, 12000, true)
observer.onChanged(data)
// THEN seek bar is visible and thick
assertThat(seekBarView.getVisibility()).isEqualTo(View.VISIBLE)
@@ -109,7 +109,7 @@
@Test
fun seekBarProgress() {
// WHEN part of the track has been played
- val data = SeekBarViewModel.Progress(true, true, true, false, 3000, 120000)
+ val data = SeekBarViewModel.Progress(true, true, true, false, 3000, 120000, true)
observer.onChanged(data)
// THEN seek bar shows the progress
assertThat(seekBarView.progress).isEqualTo(3000)
@@ -123,7 +123,8 @@
fun seekBarDisabledWhenSeekNotAvailable() {
// WHEN seek is not available
val isSeekAvailable = false
- val data = SeekBarViewModel.Progress(true, isSeekAvailable, false, false, 3000, 120000)
+ val data =
+ SeekBarViewModel.Progress(true, isSeekAvailable, false, false, 3000, 120000, false)
observer.onChanged(data)
// THEN seek bar is not enabled
assertThat(seekBarView.isEnabled()).isFalse()
@@ -133,7 +134,8 @@
fun seekBarEnabledWhenSeekNotAvailable() {
// WHEN seek is available
val isSeekAvailable = true
- val data = SeekBarViewModel.Progress(true, isSeekAvailable, false, false, 3000, 120000)
+ val data =
+ SeekBarViewModel.Progress(true, isSeekAvailable, false, false, 3000, 120000, false)
observer.onChanged(data)
// THEN seek bar is not enabled
assertThat(seekBarView.isEnabled()).isTrue()
@@ -144,7 +146,7 @@
// WHEN playing
val isPlaying = true
val isScrubbing = false
- val data = SeekBarViewModel.Progress(true, true, isPlaying, isScrubbing, 3000, 120000)
+ val data = SeekBarViewModel.Progress(true, true, isPlaying, isScrubbing, 3000, 120000, true)
observer.onChanged(data)
// THEN progress drawable is animating
verify(mockSquigglyProgress).animate = true
@@ -155,7 +157,7 @@
// WHEN not playing & not scrubbing
val isPlaying = false
val isScrubbing = false
- val data = SeekBarViewModel.Progress(true, true, isPlaying, isScrubbing, 3000, 120000)
+ val data = SeekBarViewModel.Progress(true, true, isPlaying, isScrubbing, 3000, 120000, true)
observer.onChanged(data)
// THEN progress drawable is not animating
verify(mockSquigglyProgress).animate = false
@@ -166,7 +168,7 @@
// WHEN playing & scrubbing
val isPlaying = true
val isScrubbing = true
- val data = SeekBarViewModel.Progress(true, true, isPlaying, isScrubbing, 3000, 120000)
+ val data = SeekBarViewModel.Progress(true, true, isPlaying, isScrubbing, 3000, 120000, true)
observer.onChanged(data)
// THEN progress drawable is not animating
verify(mockSquigglyProgress).animate = false
@@ -177,7 +179,7 @@
// WHEN playing & scrubbing
val isPlaying = false
val isScrubbing = true
- val data = SeekBarViewModel.Progress(true, true, isPlaying, isScrubbing, 3000, 120000)
+ val data = SeekBarViewModel.Progress(true, true, isPlaying, isScrubbing, 3000, 120000, true)
observer.onChanged(data)
// THEN progress drawable is not animating
verify(mockSquigglyProgress).animate = false
@@ -187,7 +189,7 @@
fun seekBarProgress_enabledAndScrubbing_timeViewsHaveTime() {
val isEnabled = true
val isScrubbing = true
- val data = SeekBarViewModel.Progress(isEnabled, true, true, isScrubbing, 3000, 120000)
+ val data = SeekBarViewModel.Progress(isEnabled, true, true, isScrubbing, 3000, 120000, true)
observer.onChanged(data)
@@ -199,7 +201,7 @@
fun seekBarProgress_disabledAndScrubbing_timeViewsEmpty() {
val isEnabled = false
val isScrubbing = true
- val data = SeekBarViewModel.Progress(isEnabled, true, true, isScrubbing, 3000, 120000)
+ val data = SeekBarViewModel.Progress(isEnabled, true, true, isScrubbing, 3000, 120000, true)
observer.onChanged(data)
@@ -211,7 +213,7 @@
fun seekBarProgress_enabledAndNotScrubbing_timeViewsEmpty() {
val isEnabled = true
val isScrubbing = false
- val data = SeekBarViewModel.Progress(isEnabled, true, true, isScrubbing, 3000, 120000)
+ val data = SeekBarViewModel.Progress(isEnabled, true, true, isScrubbing, 3000, 120000, true)
observer.onChanged(data)
@@ -221,8 +223,8 @@
@Test
fun seekBarJumpAnimation() {
- val data0 = SeekBarViewModel.Progress(true, true, true, false, 4000, 120000)
- val data1 = SeekBarViewModel.Progress(true, true, true, false, 10, 120000)
+ val data0 = SeekBarViewModel.Progress(true, true, true, false, 4000, 120000, true)
+ val data1 = SeekBarViewModel.Progress(true, true, true, false, 10, 120000, true)
// Set initial position of progress bar
observer.onChanged(data0)
@@ -241,7 +243,7 @@
observer.animationEnabled = false
val isPlaying = true
val isScrubbing = false
- val data = SeekBarViewModel.Progress(true, true, isPlaying, isScrubbing, 3000, 120000)
+ val data = SeekBarViewModel.Progress(true, true, isPlaying, isScrubbing, 3000, 120000, true)
observer.onChanged(data)
// THEN progress drawable does not animate
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaCarouselControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaCarouselControllerTest.kt
index 310e0b8..f3b9102 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaCarouselControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaCarouselControllerTest.kt
@@ -30,13 +30,12 @@
import com.android.internal.logging.InstanceId
import com.android.keyguard.KeyguardUpdateMonitor
import com.android.keyguard.KeyguardUpdateMonitorCallback
-import com.android.keyguard.TestScopeProvider
import com.android.systemui.SysuiTestCase
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.dump.DumpManager
-import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
+import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
-import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractorFactory
+import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor
import com.android.systemui.keyguard.shared.model.KeyguardState
import com.android.systemui.media.controls.MediaTestUtils
import com.android.systemui.media.controls.models.player.MediaData
@@ -53,6 +52,7 @@
import com.android.systemui.statusbar.notification.collection.provider.OnReorderingAllowedListener
import com.android.systemui.statusbar.notification.collection.provider.VisualStabilityProvider
import com.android.systemui.statusbar.policy.ConfigurationController
+import com.android.systemui.testKosmos
import com.android.systemui.util.concurrency.DelayableExecutor
import com.android.systemui.util.concurrency.FakeExecutor
import com.android.systemui.util.mockito.any
@@ -93,6 +93,7 @@
@TestableLooper.RunWithLooper(setAsMainLooper = true)
@RunWith(AndroidTestingRunner::class)
class MediaCarouselControllerTest : SysuiTestCase() {
+ val kosmos = testKosmos()
@Mock lateinit var mediaControlPanelFactory: Provider<MediaControlPanel>
@Mock lateinit var panel: MediaControlPanel
@@ -115,7 +116,7 @@
@Mock lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor
@Mock lateinit var keyguardTransitionInteractor: KeyguardTransitionInteractor
@Mock lateinit var globalSettings: GlobalSettings
- private lateinit var transitionRepository: FakeKeyguardTransitionRepository
+ private val transitionRepository = kosmos.fakeKeyguardTransitionRepository
@Captor lateinit var listener: ArgumentCaptor<MediaDataManager.Listener>
@Captor
lateinit var configListener: ArgumentCaptor<ConfigurationController.ConfigurationListener>
@@ -132,7 +133,6 @@
fun setup() {
MockitoAnnotations.initMocks(this)
context.resources.configuration.setLocales(LocaleList(Locale.US, Locale.UK))
- transitionRepository = FakeKeyguardTransitionRepository()
bgExecutor = FakeExecutor(clock)
mediaCarouselController =
MediaCarouselController(
@@ -152,11 +152,7 @@
debugLogger,
mediaFlags,
keyguardUpdateMonitor,
- KeyguardTransitionInteractorFactory.create(
- scope = TestScopeProvider.getTestScope().backgroundScope,
- repository = transitionRepository,
- )
- .keyguardTransitionInteractor,
+ kosmos.keyguardTransitionInteractor,
globalSettings
)
verify(configurationController).addCallback(capture(configListener))
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaHierarchyManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaHierarchyManagerTest.kt
index edba902..87d093f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaHierarchyManagerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaHierarchyManagerTest.kt
@@ -530,58 +530,6 @@
}
@Test
- fun testCommunalLocation_showsOverLockscreen() =
- testScope.runTest {
- // Device is on lock screen.
- whenever(statusBarStateController.state).thenReturn(StatusBarState.KEYGUARD)
-
- // UMO goes to communal even over the lock screen.
- communalInteractor.onSceneChanged(CommunalSceneKey.Communal)
- runCurrent()
- verify(mediaCarouselController)
- .onDesiredLocationChanged(
- eq(MediaHierarchyManager.LOCATION_COMMUNAL_HUB),
- nullable(),
- eq(false),
- anyLong(),
- anyLong()
- )
- }
-
- @Test
- fun testCommunalLocation_showsUntilQsExpands() =
- testScope.runTest {
- // Device is on lock screen.
- whenever(statusBarStateController.state).thenReturn(StatusBarState.KEYGUARD)
-
- communalInteractor.onSceneChanged(CommunalSceneKey.Communal)
- runCurrent()
- verify(mediaCarouselController)
- .onDesiredLocationChanged(
- eq(MediaHierarchyManager.LOCATION_COMMUNAL_HUB),
- nullable(),
- eq(false),
- anyLong(),
- anyLong()
- )
- clearInvocations(mediaCarouselController)
-
- // Start opening the shade.
- mediaHierarchyManager.qsExpansion = 0.1f
- runCurrent()
-
- // UMO goes to the shade instead.
- verify(mediaCarouselController)
- .onDesiredLocationChanged(
- eq(MediaHierarchyManager.LOCATION_QS),
- any(MediaHostState::class.java),
- eq(false),
- anyLong(),
- anyLong()
- )
- }
-
- @Test
fun testQsExpandedChanged_noQqsMedia() {
// When we are looking at QQS with active media
whenever(statusBarStateController.state).thenReturn(StatusBarState.SHADE)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputControllerTest.java
index c6cfabc..32b6f38 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputControllerTest.java
@@ -72,7 +72,7 @@
import com.android.settingslib.media.LocalMediaManager;
import com.android.settingslib.media.MediaDevice;
import com.android.systemui.SysuiTestCase;
-import com.android.systemui.animation.ActivityLaunchAnimator;
+import com.android.systemui.animation.ActivityTransitionAnimator;
import com.android.systemui.animation.DialogLaunchAnimator;
import com.android.systemui.flags.FeatureFlags;
import com.android.systemui.media.nearby.NearbyMediaDevicesManager;
@@ -110,7 +110,7 @@
@Mock
private DialogLaunchAnimator mDialogLaunchAnimator;
@Mock
- private ActivityLaunchAnimator.Controller mActivityLaunchAnimatorController;
+ private ActivityTransitionAnimator.Controller mActivityTransitionAnimatorController;
@Mock
private NearbyMediaDevicesManager mNearbyMediaDevicesManager;
// Mock
@@ -143,7 +143,7 @@
@Mock
private KeyguardManager mKeyguardManager;
@Mock
- private ActivityLaunchAnimator.Controller mController;
+ private ActivityTransitionAnimator.Controller mController;
@Mock
private PowerExemptionManager mPowerExemptionManager;
@Mock
@@ -1122,7 +1122,7 @@
@Test
public void launchBluetoothPairing_isKeyguardLocked_dismissDialog() {
when(mDialogLaunchAnimator.createActivityLaunchController(mDialogLaunchView)).thenReturn(
- mActivityLaunchAnimatorController);
+ mActivityTransitionAnimatorController);
when(mKeyguardManager.isKeyguardLocked()).thenReturn(true);
mMediaOutputController.mCallback = this.mCallback;
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/nearby/NearbyMediaDevicesManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/nearby/NearbyMediaDevicesManagerTest.kt
index 301d887..d9453d6 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/nearby/NearbyMediaDevicesManagerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/nearby/NearbyMediaDevicesManagerTest.kt
@@ -33,6 +33,7 @@
fun setUp() {
MockitoAnnotations.initMocks(this)
manager = NearbyMediaDevicesManager(commandQueue, logger)
+ manager.start()
val callbackCaptor = ArgumentCaptor.forClass(CommandQueue.Callbacks::class.java)
verify(commandQueue).addCallback(callbackCaptor.capture())
diff --git a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/permission/MediaProjectionPermissionDialogDelegateTest.kt b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/permission/MediaProjectionPermissionDialogDelegateTest.kt
new file mode 100644
index 0000000..e044eec
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/permission/MediaProjectionPermissionDialogDelegateTest.kt
@@ -0,0 +1,141 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.mediaprojection.permission
+
+import android.app.AlertDialog
+import android.media.projection.MediaProjectionConfig
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
+import android.view.WindowManager
+import android.widget.Spinner
+import android.widget.TextView
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.flags.FeatureFlagsClassic
+import com.android.systemui.flags.Flags
+import com.android.systemui.mediaprojection.MediaProjectionMetricsLogger
+import com.android.systemui.res.R
+import com.android.systemui.statusbar.phone.AlertDialogWithDelegate
+import com.android.systemui.statusbar.phone.SystemUIDialog
+import com.android.systemui.util.mockito.mock
+import junit.framework.Assert.assertEquals
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.`when` as whenever
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+@TestableLooper.RunWithLooper(setAsMainLooper = true)
+class MediaProjectionPermissionDialogDelegateTest : SysuiTestCase() {
+
+ private lateinit var dialog: AlertDialog
+
+ private val flags = mock<FeatureFlagsClassic>()
+ private val onStartRecordingClicked = mock<Runnable>()
+ private val mediaProjectionMetricsLogger = mock<MediaProjectionMetricsLogger>()
+
+ private val mediaProjectionConfig: MediaProjectionConfig =
+ MediaProjectionConfig.createConfigForDefaultDisplay()
+ private val appName: String = "testApp"
+ private val hostUid: Int = 12345
+
+ private val resIdSingleApp = R.string.screen_share_permission_dialog_option_single_app
+ private val resIdFullScreen = R.string.screen_share_permission_dialog_option_entire_screen
+ private val resIdSingleAppDisabled =
+ R.string.media_projection_entry_app_permission_dialog_single_app_disabled
+
+ @Before
+ fun setUp() {
+ whenever(flags.isEnabled(Flags.WM_ENABLE_PARTIAL_SCREEN_SHARING)).thenReturn(true)
+ }
+
+ @After
+ fun teardown() {
+ if (::dialog.isInitialized) {
+ dialog.dismiss()
+ }
+ }
+
+ @Test
+ fun showDialog_forceShowPartialScreenShareFalse() {
+ // Set up dialog with MediaProjectionConfig.createConfigForDefaultDisplay() and
+ // overrideDisableSingleAppOption = false
+ val overrideDisableSingleAppOption = false
+ setUpAndShowDialog(overrideDisableSingleAppOption)
+
+ val spinner = dialog.requireViewById<Spinner>(R.id.screen_share_mode_spinner)
+ val secondOptionText =
+ spinner.adapter
+ .getDropDownView(1, null, spinner)
+ .findViewById<TextView>(android.R.id.text2)
+ ?.text
+
+ // check that the first option is full screen and enabled
+ assertEquals(context.getString(resIdFullScreen), spinner.selectedItem)
+
+ // check that the second option is single app and disabled
+ assertEquals(context.getString(resIdSingleAppDisabled, appName), secondOptionText)
+ }
+
+ @Test
+ fun showDialog_forceShowPartialScreenShareTrue() {
+ // Set up dialog with MediaProjectionConfig.createConfigForDefaultDisplay() and
+ // overrideDisableSingleAppOption = true
+ val overrideDisableSingleAppOption = true
+ setUpAndShowDialog(overrideDisableSingleAppOption)
+
+ val spinner = dialog.requireViewById<Spinner>(R.id.screen_share_mode_spinner)
+ val secondOptionText =
+ spinner.adapter
+ .getDropDownView(1, null, spinner)
+ .findViewById<TextView>(android.R.id.text1)
+ ?.text
+
+ // check that the first option is single app and enabled
+ assertEquals(context.getString(resIdSingleApp), spinner.selectedItem)
+
+ // check that the second option is full screen and enabled
+ assertEquals(context.getString(resIdFullScreen), secondOptionText)
+ }
+
+ private fun setUpAndShowDialog(overrideDisableSingleAppOption: Boolean) {
+ val delegate =
+ MediaProjectionPermissionDialogDelegate(
+ context,
+ mediaProjectionConfig,
+ {},
+ onStartRecordingClicked,
+ appName,
+ overrideDisableSingleAppOption,
+ hostUid,
+ mediaProjectionMetricsLogger
+ )
+
+ dialog = AlertDialogWithDelegate(context, R.style.Theme_SystemUI_Dialog, delegate)
+ SystemUIDialog.applyFlags(dialog)
+ SystemUIDialog.setDialogSize(dialog)
+
+ dialog.window?.addSystemFlags(
+ WindowManager.LayoutParams.SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS
+ )
+
+ delegate.onCreate(dialog, savedInstanceState = null)
+ dialog.show()
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavBarHelperTest.java b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavBarHelperTest.java
index 28d35ce..e5ba569 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavBarHelperTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavBarHelperTest.java
@@ -41,7 +41,6 @@
import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;
-import com.android.internal.accessibility.common.ShortcutConstants;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.accessibility.AccessibilityButtonModeObserver;
import com.android.systemui.accessibility.AccessibilityButtonTargetsObserver;
@@ -57,8 +56,6 @@
import com.android.systemui.statusbar.phone.CentralSurfaces;
import com.android.systemui.statusbar.policy.KeyguardStateController;
-import dagger.Lazy;
-
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -70,6 +67,8 @@
import java.util.Optional;
import java.util.concurrent.Executor;
+import dagger.Lazy;
+
/**
* Tests for {@link NavBarHelper}.
*/
@@ -271,8 +270,7 @@
@Test
public void initNavBarHelper_buttonModeNavBar_a11yButtonClickableState() {
when(mAccessibilityManager.getAccessibilityShortcutTargets(
- ShortcutConstants.UserShortcutType.SOFTWARE)).thenReturn(
- createFakeShortcutTargets());
+ AccessibilityManager.ACCESSIBILITY_BUTTON)).thenReturn(createFakeShortcutTargets());
mNavBarHelper.registerNavTaskStateUpdater(mNavbarTaskbarStateUpdater);
@@ -297,8 +295,7 @@
ACCESSIBILITY_BUTTON_MODE_NAVIGATION_BAR);
when(mAccessibilityManager.getAccessibilityShortcutTargets(
- ShortcutConstants.UserShortcutType.SOFTWARE)).thenReturn(
- createFakeShortcutTargets());
+ AccessibilityManager.ACCESSIBILITY_BUTTON)).thenReturn(createFakeShortcutTargets());
mAccessibilityServicesStateChangeListener.onAccessibilityServicesStateChanged(
mAccessibilityManager);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarTest.java b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarTest.java
index db5bd9b..0d1e874 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarTest.java
@@ -182,6 +182,8 @@
@Mock
private UiEventLogger mUiEventLogger;
@Mock
+ private NavBarButtonClickLogger mNavBarButtonClickLogger;
+ @Mock
private ViewTreeObserver mViewTreeObserver;
NavBarHelper mNavBarHelper;
@Mock
@@ -596,7 +598,8 @@
mUserContextProvider,
mWakefulnessLifecycle,
mTaskStackChangeListeners,
- new FakeDisplayTracker(mContext)));
+ new FakeDisplayTracker(mContext),
+ mNavBarButtonClickLogger));
}
private void processAllMessages() {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/external/CustomTileTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/external/CustomTileTest.kt
index ae47a7b..33f8f1f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/external/CustomTileTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/external/CustomTileTest.kt
@@ -38,7 +38,7 @@
import android.view.View
import com.android.internal.logging.MetricsLogger
import com.android.systemui.SysuiTestCase
-import com.android.systemui.animation.ActivityLaunchAnimator
+import com.android.systemui.animation.ActivityTransitionAnimator
import com.android.systemui.animation.view.LaunchableFrameLayout
import com.android.systemui.classifier.FalsingManagerFake
import com.android.systemui.plugins.ActivityStarter
@@ -372,7 +372,7 @@
verify(activityStarter)
.startPendingIntentMaybeDismissingKeyguard(
- eq(pi), nullable(), nullable<ActivityLaunchAnimator.Controller>())
+ eq(pi), nullable(), nullable<ActivityTransitionAnimator.Controller>())
}
@Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/footer/domain/interactor/FooterActionsInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/footer/domain/interactor/FooterActionsInteractorTest.kt
index 23466cc..720c25a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/footer/domain/interactor/FooterActionsInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/footer/domain/interactor/FooterActionsInteractorTest.kt
@@ -26,7 +26,7 @@
import com.android.internal.logging.testing.FakeMetricsLogger
import com.android.internal.logging.testing.UiEventLoggerFake
import com.android.systemui.SysuiTestCase
-import com.android.systemui.animation.ActivityLaunchAnimator
+import com.android.systemui.animation.ActivityTransitionAnimator
import com.android.systemui.animation.Expandable
import com.android.systemui.globalactions.GlobalActionsDialogLite
import com.android.systemui.plugins.ActivityStarter
@@ -127,7 +127,7 @@
.startActivity(
intentCaptor.capture(),
/* dismissShade= */ eq(true),
- nullable() as? ActivityLaunchAnimator.Controller,
+ nullable() as? ActivityTransitionAnimator.Controller,
)
assertThat(intentCaptor.value.action).isEqualTo(Settings.ACTION_SETTINGS)
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSIconViewImplTest_311121830.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSIconViewImplTest_311121830.kt
index e8aa8f0..bbae0c9 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSIconViewImplTest_311121830.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSIconViewImplTest_311121830.kt
@@ -34,7 +34,7 @@
import org.junit.Rule
import org.junit.runner.RunWith
-/** Test for regression b/311121830 */
+/** Test for regression b/311121830 and b/323125376 */
@RunWith(AndroidTestingRunner::class)
@UiThreadTest
@SmallTest
@@ -82,6 +82,55 @@
assertThat(iconView.mLastIcon).isEqualTo(secondState.icon)
}
+ @Test
+ fun alwaysLastIcon_twoStateChanges() {
+ // Need to inflate with the correct theme so the colors can be retrieved and the animations
+ // are run
+ val iconView =
+ AnimateQSIconViewImpl(
+ ContextThemeWrapper(context, R.style.Theme_SystemUI_QuickSettings)
+ )
+
+ val initialState =
+ QSTile.State().apply {
+ state = Tile.STATE_ACTIVE
+ icon = QSTileImpl.ResourceIcon.get(WifiIcons.WIFI_FULL_ICONS[4])
+ }
+ val firstState =
+ QSTile.State().apply {
+ state = Tile.STATE_INACTIVE
+ icon = QSTileImpl.ResourceIcon.get(WifiIcons.WIFI_NO_INTERNET_ICONS[4])
+ }
+ val secondState =
+ QSTile.State().apply {
+ state = Tile.STATE_ACTIVE
+ icon = QSTileImpl.ResourceIcon.get(WifiIcons.WIFI_FULL_ICONS[3])
+ }
+ val thirdState =
+ QSTile.State().apply {
+ state = Tile.STATE_INACTIVE
+ icon = QSTileImpl.ResourceIcon.get(WifiIcons.WIFI_NO_NETWORK)
+ }
+
+ // Start with the initial state
+ iconView.setIcon(initialState, /* allowAnimations= */ false)
+
+ // Set the first state to animate, and advance time to one third of the animation
+ iconView.setIcon(firstState, /* allowAnimations= */ true)
+ animatorRule.advanceTimeBy(QSIconViewImpl.QS_ANIM_LENGTH / 3)
+
+ // Set the second state to animate and advance time by another third of animations length
+ iconView.setIcon(secondState, /* allowAnimations= */ true)
+ animatorRule.advanceTimeBy(QSIconViewImpl.QS_ANIM_LENGTH / 3)
+
+ // Set the third state to animate and advance time by two times the animation length
+ // to guarantee that all animations are done
+ iconView.setIcon(thirdState, /* allowAnimations= */ true)
+ animatorRule.advanceTimeBy(QSIconViewImpl.QS_ANIM_LENGTH * 2)
+
+ assertThat(iconView.mLastIcon).isEqualTo(thirdState.icon)
+ }
+
private class AnimateQSIconViewImpl(context: Context) : QSIconViewImpl(context) {
override fun createIcon(): View {
return object : ImageView(context) {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/DeviceControlsTileTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/DeviceControlsTileTest.kt
index 3bf59ca..874368b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/DeviceControlsTileTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/DeviceControlsTileTest.kt
@@ -29,7 +29,7 @@
import com.android.internal.logging.MetricsLogger
import com.android.systemui.res.R
import com.android.systemui.SysuiTestCase
-import com.android.systemui.animation.ActivityLaunchAnimator
+import com.android.systemui.animation.ActivityTransitionAnimator
import com.android.systemui.classifier.FalsingManagerFake
import com.android.systemui.controls.ControlsServiceInfo
import com.android.systemui.controls.controller.ControlInfo
@@ -348,7 +348,7 @@
verify(activityStarter).startActivity(
intentCaptor.capture(),
eq(true) /* dismissShade */,
- nullable(ActivityLaunchAnimator.Controller::class.java),
+ nullable(ActivityTransitionAnimator.Controller::class.java),
eq(true) /* showOverLockscreenWhenLocked */)
assertThat(intentCaptor.value.component?.className).isEqualTo(CONTROLS_ACTIVITY_CLASS_NAME)
}
@@ -379,7 +379,7 @@
verify(activityStarter).startActivity(
intentCaptor.capture(),
anyBoolean() /* dismissShade */,
- nullable(ActivityLaunchAnimator.Controller::class.java),
+ nullable(ActivityTransitionAnimator.Controller::class.java),
eq(false) /* showOverLockscreenWhenLocked */)
assertThat(intentCaptor.value.component?.className).isEqualTo(CONTROLS_ACTIVITY_CLASS_NAME)
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenrecord/RecordingServiceTest.java b/packages/SystemUI/tests/src/com/android/systemui/screenrecord/RecordingServiceTest.java
index 9ce77e5..deecc5b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/screenrecord/RecordingServiceTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/screenrecord/RecordingServiceTest.java
@@ -166,7 +166,7 @@
@Test
public void testLogStopFromNotificationIntent() {
- Intent stopIntent = RecordingService.getNotificationIntent(mContext);
+ Intent stopIntent = mRecordingService.getNotificationIntent(mContext);
mRecordingService.onStartCommand(stopIntent, 0, 0);
// Verify that we log the correct event
diff --git a/packages/SystemUI/tests/src/com/android/systemui/settings/brightness/BrightnessSliderControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/settings/brightness/BrightnessSliderControllerTest.kt
index 707a297..ab90b9b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/settings/brightness/BrightnessSliderControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/settings/brightness/BrightnessSliderControllerTest.kt
@@ -24,10 +24,13 @@
import com.android.settingslib.RestrictedLockUtils
import com.android.systemui.SysuiTestCase
import com.android.systemui.classifier.FalsingManagerFake
+import com.android.systemui.haptics.slider.SeekableSliderHapticPlugin
+import com.android.systemui.statusbar.VibratorHelper
import com.android.systemui.statusbar.policy.BrightnessMirrorController
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.capture
import com.android.systemui.util.mockito.eq
+import com.android.systemui.util.time.FakeSystemClock
import com.google.common.truth.Truth.assertThat
import org.junit.After
import org.junit.Before
@@ -35,6 +38,7 @@
import org.junit.runner.RunWith
import org.mockito.ArgumentCaptor
import org.mockito.ArgumentMatchers.anyBoolean
+import org.mockito.ArgumentMatchers.anyInt
import org.mockito.Captor
import org.mockito.Mock
import org.mockito.Mockito.isNull
@@ -61,7 +65,7 @@
@Mock
private lateinit var listener: ToggleSlider.Listener
@Mock
- private lateinit var mBrightnessSliderHapticPlugin: BrightnessSliderHapticPlugin
+ private lateinit var vibratorHelper: VibratorHelper
@Captor
private lateinit var seekBarChangeCaptor: ArgumentCaptor<SeekBar.OnSeekBarChangeListener>
@@ -69,6 +73,7 @@
private lateinit var seekBar: SeekBar
private val uiEventLogger = UiEventLoggerFake()
private var mFalsingManager: FalsingManagerFake = FalsingManagerFake()
+ private val systemClock = FakeSystemClock()
private lateinit var mController: BrightnessSliderController
@@ -78,13 +83,14 @@
whenever(mirrorController.toggleSlider).thenReturn(mirror)
whenever(motionEvent.copy()).thenReturn(motionEvent)
+ whenever(vibratorHelper.getPrimitiveDurations(anyInt())).thenReturn(intArrayOf(0))
mController =
BrightnessSliderController(
brightnessSliderView,
mFalsingManager,
uiEventLogger,
- mBrightnessSliderHapticPlugin,
+ SeekableSliderHapticPlugin(vibratorHelper, systemClock),
)
mController.init()
mController.setOnChangedListener(listener)
@@ -100,7 +106,6 @@
mController.onViewAttached()
verify(brightnessSliderView).setOnSeekBarChangeListener(notNull())
- verify(mBrightnessSliderHapticPlugin).start()
}
@Test
@@ -110,7 +115,6 @@
verify(brightnessSliderView).setOnSeekBarChangeListener(isNull())
verify(brightnessSliderView).setOnDispatchTouchEventListener(isNull())
- verify(mBrightnessSliderHapticPlugin).stop()
}
@Test
@@ -202,7 +206,7 @@
@Test
fun testSeekBarTrackingStarted() {
whenever(brightnessSliderView.value).thenReturn(42)
- val event = BrightnessSliderEvent.SLIDER_STARTED_TRACKING_TOUCH
+ val event = BrightnessSliderEvent.BRIGHTNESS_SLIDER_STARTED_TRACKING_TOUCH
mController.onViewAttached()
mController.setMirrorControllerAndMirror(mirrorController)
@@ -220,7 +224,7 @@
@Test
fun testSeekBarTrackingStopped() {
whenever(brightnessSliderView.value).thenReturn(23)
- val event = BrightnessSliderEvent.SLIDER_STOPPED_TRACKING_TOUCH
+ val event = BrightnessSliderEvent.BRIGHTNESS_SLIDER_STOPPED_TRACKING_TOUCH
mController.onViewAttached()
mController.setMirrorControllerAndMirror(mirrorController)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/settings/brightness/BrightnessSliderHapticPluginImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/settings/brightness/BrightnessSliderHapticPluginImplTest.kt
deleted file mode 100644
index 51629b5..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/settings/brightness/BrightnessSliderHapticPluginImplTest.kt
+++ /dev/null
@@ -1,102 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.settings.brightness
-
-import android.view.VelocityTracker
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.SmallTest
-import com.android.systemui.SysuiTestCase
-import com.android.systemui.haptics.slider.SeekableSliderEventProducer
-import com.android.systemui.statusbar.VibratorHelper
-import com.android.systemui.util.mockito.whenever
-import com.android.systemui.util.time.FakeSystemClock
-import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.CoroutineDispatcher
-import kotlinx.coroutines.test.UnconfinedTestDispatcher
-import kotlinx.coroutines.test.runTest
-import org.junit.Before
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.mockito.ArgumentMatchers.anyInt
-import org.mockito.Mock
-import org.mockito.MockitoAnnotations
-
-@SmallTest
-@RunWith(AndroidJUnit4::class)
-class BrightnessSliderHapticPluginImplTest : SysuiTestCase() {
-
- @Mock private lateinit var vibratorHelper: VibratorHelper
- @Mock private lateinit var velocityTracker: VelocityTracker
- @Mock private lateinit var mainDispatcher: CoroutineDispatcher
-
- private val systemClock = FakeSystemClock()
- private val sliderEventProducer = SeekableSliderEventProducer()
-
- private lateinit var plugin: BrightnessSliderHapticPluginImpl
-
- @Before
- fun setup() {
- MockitoAnnotations.initMocks(this)
- whenever(vibratorHelper.getPrimitiveDurations(anyInt())).thenReturn(intArrayOf(0))
- }
-
- @Test
- fun start_beginsTrackingSlider() = runTest {
- createPlugin(UnconfinedTestDispatcher(testScheduler))
- plugin.start()
-
- assertThat(plugin.isTracking).isTrue()
- }
-
- @Test
- fun stop_stopsTrackingSlider() = runTest {
- createPlugin(UnconfinedTestDispatcher(testScheduler))
- // GIVEN that the plugin started the tracking component
- plugin.start()
-
- // WHEN called to stop
- plugin.stop()
-
- // THEN the tracking component stops
- assertThat(plugin.isTracking).isFalse()
- }
-
- @Test
- fun start_afterStop_startsTheTrackingAgain() = runTest {
- createPlugin(UnconfinedTestDispatcher(testScheduler))
- // GIVEN that the plugin started the tracking component
- plugin.start()
-
- // WHEN the plugin is restarted
- plugin.stop()
- plugin.start()
-
- // THEN the tracking begins again
- assertThat(plugin.isTracking).isTrue()
- }
-
- private fun createPlugin(dispatcher: CoroutineDispatcher) {
- plugin =
- BrightnessSliderHapticPluginImpl(
- vibratorHelper,
- systemClock,
- dispatcher,
- velocityTracker,
- sliderEventProducer,
- )
- }
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/GlanceableHubContainerControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/GlanceableHubContainerControllerTest.kt
index 0831971..1dc5f7d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/GlanceableHubContainerControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/GlanceableHubContainerControllerTest.kt
@@ -16,12 +16,14 @@
package com.android.systemui.shade
+import android.content.Context
import android.os.PowerManager
import android.testing.AndroidTestingRunner
import android.testing.TestableLooper
import android.testing.ViewUtils
import android.view.MotionEvent
import android.view.View
+import android.view.WindowManager
import android.widget.FrameLayout
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
@@ -33,18 +35,22 @@
import com.android.systemui.communal.ui.viewmodel.CommunalViewModel
import com.android.systemui.compose.ComposeFacade
import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.testDispatcher
import com.android.systemui.res.R
import com.android.systemui.shade.domain.interactor.ShadeInteractor
import com.android.systemui.testKosmos
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.whenever
import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import org.junit.After
import org.junit.Assert.assertThrows
import org.junit.Assume.assumeTrue
import org.junit.Before
import org.junit.BeforeClass
-import org.junit.Ignore
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mock
@@ -52,12 +58,17 @@
import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations
-@Ignore("b/323053208")
+@ExperimentalCoroutinesApi
@RunWith(AndroidTestingRunner::class)
@TestableLooper.RunWithLooper(setAsMainLooper = true)
@SmallTest
class GlanceableHubContainerControllerTest : SysuiTestCase() {
- private val kosmos = testKosmos()
+ private val kosmos: Kosmos =
+ testKosmos().apply {
+ // UnconfinedTestDispatcher makes testing simpler due to CommunalInteractor flows using
+ // SharedFlow
+ testDispatcher = UnconfinedTestDispatcher()
+ }
@Mock private lateinit var communalViewModel: CommunalViewModel
@Mock private lateinit var keyguardTransitionInteractor: KeyguardTransitionInteractor
@@ -92,8 +103,6 @@
)
testableLooper = TestableLooper.get(this)
- communalRepository.setIsCommunalEnabled(true)
-
whenever(keyguardTransitionInteractor.isFinishedInStateWhere(any()))
.thenReturn(bouncerShowingFlow)
whenever(shadeInteractor.isAnyFullyExpanded).thenReturn(shadeShowingFlow)
@@ -104,31 +113,26 @@
R.dimen.communal_bottom_edge_swipe_region_height,
BOTTOM_SWIPE_REGION_WIDTH
)
+
+ initAndAttachContainerView()
}
- @Test
- fun isEnabled_interactorEnabled_interceptsTouches() {
- communalRepository.setIsCommunalEnabled(true)
-
- assertThat(underTest.isEnabled()).isTrue()
- }
-
- @Test
- fun isEnabled_interactorDisabled_doesNotIntercept() {
- communalRepository.setIsCommunalEnabled(false)
-
- assertThat(underTest.isEnabled()).isFalse()
- }
-
- @Test
- fun initView_notEnabled_throwsException() {
- communalRepository.setIsCommunalEnabled(false)
-
- assertThrows(RuntimeException::class.java) { underTest.initView(context) }
+ @After
+ fun tearDown() {
+ ViewUtils.detachView(parentView)
}
@Test
fun initView_calledTwice_throwsException() {
+ underTest =
+ GlanceableHubContainerController(
+ communalInteractor,
+ communalViewModel,
+ keyguardTransitionInteractor,
+ shadeInteractor,
+ powerManager,
+ )
+
// First call succeeds.
underTest.initView(context)
@@ -137,25 +141,20 @@
}
@Test
- fun onTouchEvent_touchInsideGestureRegion_interceptsTouches() {
- // Communal is open.
- communalRepository.setDesiredScene(CommunalSceneKey.Communal)
+ fun onTouchEvent_communalClosed_doesNotIntercept() {
+ // Communal is closed.
+ goToScene(CommunalSceneKey.Blank)
- initAndAttachContainerView()
-
- // Touch events are intercepted.
- assertThat(underTest.onTouchEvent(DOWN_IN_RIGHT_SWIPE_REGION_EVENT)).isTrue()
+ assertThat(underTest.onTouchEvent(DOWN_EVENT)).isFalse()
}
@Test
- fun onTouchEvent_subsequentTouchesAfterGestureStart_interceptsTouches() {
- // Communal is open.
- communalRepository.setDesiredScene(CommunalSceneKey.Communal)
+ fun onTouchEvent_openGesture_interceptsTouches() {
+ // Communal is closed.
+ goToScene(CommunalSceneKey.Blank)
- initAndAttachContainerView()
-
- // Initial touch down is intercepted, and so are touches outside of the region, until an up
- // event is received.
+ // Initial touch down is intercepted, and so are touches outside of the region, until an
+ // up event is received.
assertThat(underTest.onTouchEvent(DOWN_IN_RIGHT_SWIPE_REGION_EVENT)).isTrue()
assertThat(underTest.onTouchEvent(MOVE_EVENT)).isTrue()
assertThat(underTest.onTouchEvent(UP_EVENT)).isTrue()
@@ -165,34 +164,27 @@
@Test
fun onTouchEvent_communalOpen_interceptsTouches() {
// Communal is open.
- communalRepository.setDesiredScene(CommunalSceneKey.Communal)
+ goToScene(CommunalSceneKey.Communal)
- initAndAttachContainerView()
- testableLooper.processAllMessages()
-
- // Touch events are intercepted.
+ // Touch events are intercepted outside of any gesture areas.
assertThat(underTest.onTouchEvent(DOWN_EVENT)).isTrue()
// User activity sent to PowerManager.
verify(powerManager).userActivity(any(), any(), any())
}
@Test
- fun onTouchEvent_topSwipeWhenHubOpen_returnsFalse() {
+ fun onTouchEvent_topSwipeWhenCommunalOpen_doesNotIntercept() {
// Communal is open.
- communalRepository.setDesiredScene(CommunalSceneKey.Communal)
-
- initAndAttachContainerView()
+ goToScene(CommunalSceneKey.Communal)
// Touch event in the top swipe reqgion is not intercepted.
assertThat(underTest.onTouchEvent(DOWN_IN_TOP_SWIPE_REGION_EVENT)).isFalse()
}
@Test
- fun onTouchEvent_bottomSwipeWhenHubOpen_returnsFalse() {
+ fun onTouchEvent_bottomSwipeWhenCommunalOpen_doesNotIntercept() {
// Communal is open.
- communalRepository.setDesiredScene(CommunalSceneKey.Communal)
-
- initAndAttachContainerView()
+ goToScene(CommunalSceneKey.Communal)
// Touch event in the bottom swipe reqgion is not intercepted.
assertThat(underTest.onTouchEvent(DOWN_IN_BOTTOM_SWIPE_REGION_EVENT)).isFalse()
@@ -201,9 +193,7 @@
@Test
fun onTouchEvent_communalAndBouncerShowing_doesNotIntercept() {
// Communal is open.
- communalRepository.setDesiredScene(CommunalSceneKey.Communal)
-
- initAndAttachContainerView()
+ goToScene(CommunalSceneKey.Communal)
// Bouncer is visible.
bouncerShowingFlow.value = true
@@ -218,9 +208,7 @@
@Test
fun onTouchEvent_communalAndShadeShowing_doesNotIntercept() {
// Communal is open.
- communalRepository.setDesiredScene(CommunalSceneKey.Communal)
-
- initAndAttachContainerView()
+ goToScene(CommunalSceneKey.Communal)
shadeShowingFlow.value = true
testableLooper.processAllMessages()
@@ -232,10 +220,7 @@
@Test
fun onTouchEvent_containerViewDisposed_doesNotIntercept() {
// Communal is open.
- communalRepository.setDesiredScene(CommunalSceneKey.Communal)
-
- initAndAttachContainerView()
- testableLooper.processAllMessages()
+ goToScene(CommunalSceneKey.Communal)
// Touch events are intercepted.
assertThat(underTest.onTouchEvent(DOWN_EVENT)).isTrue()
@@ -253,15 +238,24 @@
parentView = FrameLayout(context)
parentView.addView(containerView)
- // Make view clickable so that dispatchTouchEvent returns true.
- containerView.isClickable = true
-
underTest.initView(containerView)
+
// Attach the view so that flows start collecting.
ViewUtils.attachView(parentView)
- // Give the view a size so that determining if a touch starts at the right edge works.
- parentView.layout(0, 0, CONTAINER_WIDTH, CONTAINER_HEIGHT)
- containerView.layout(0, 0, CONTAINER_WIDTH, CONTAINER_HEIGHT)
+
+ // Give the view a fixed size to simplify testing for edge swipes.
+ val lp =
+ parentView.layoutParams.apply {
+ width = CONTAINER_WIDTH
+ height = CONTAINER_HEIGHT
+ }
+ val wm = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
+ wm.updateViewLayout(parentView, lp)
+ }
+
+ private fun goToScene(scene: CommunalSceneKey) {
+ communalRepository.setDesiredScene(scene)
+ testableLooper.processAllMessages()
}
companion object {
@@ -271,13 +265,17 @@
private const val TOP_SWIPE_REGION_WIDTH = 20
private const val BOTTOM_SWIPE_REGION_WIDTH = 20
+ /**
+ * A touch down event right in the middle of the screen, to avoid being in any of the swipe
+ * regions.
+ */
private val DOWN_EVENT =
MotionEvent.obtain(
0L,
0L,
MotionEvent.ACTION_DOWN,
- CONTAINER_WIDTH.toFloat(),
- CONTAINER_HEIGHT.toFloat(),
+ CONTAINER_WIDTH.toFloat() / 2,
+ CONTAINER_HEIGHT.toFloat() / 2,
0
)
private val DOWN_IN_RIGHT_SWIPE_REGION_EVENT =
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java
index 7f4508a..8a22f4c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java
@@ -63,13 +63,8 @@
import com.android.systemui.keyguard.KeyguardViewMediator;
import com.android.systemui.keyguard.data.repository.FakeCommandQueue;
import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository;
-import com.android.systemui.keyguard.data.repository.FakeKeyguardSurfaceBehindRepository;
-import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository;
-import com.android.systemui.keyguard.data.repository.InWindowLauncherUnlockAnimationRepository;
import com.android.systemui.keyguard.domain.interactor.FromLockscreenTransitionInteractor;
import com.android.systemui.keyguard.domain.interactor.FromPrimaryBouncerTransitionInteractor;
-import com.android.systemui.keyguard.domain.interactor.GlanceableHubTransitions;
-import com.android.systemui.keyguard.domain.interactor.InWindowLauncherUnlockAnimationInteractor;
import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor;
import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor;
import com.android.systemui.kosmos.KosmosJavaAdapter;
@@ -87,7 +82,6 @@
import com.android.systemui.shade.domain.interactor.ShadeInteractor;
import com.android.systemui.shade.domain.interactor.ShadeInteractorImpl;
import com.android.systemui.shade.domain.interactor.ShadeInteractorLegacyImpl;
-import com.android.systemui.shared.system.ActivityManagerWrapper;
import com.android.systemui.statusbar.StatusBarState;
import com.android.systemui.statusbar.SysuiStatusBarStateController;
import com.android.systemui.statusbar.disableflags.data.repository.FakeDisableFlagsRepository;
@@ -202,56 +196,12 @@
() -> sceneInteractor);
CommunalInteractor communalInteractor = mKosmos.getCommunalInteractor();
- FakeKeyguardTransitionRepository keyguardTransitionRepository =
- new FakeKeyguardTransitionRepository();
-
KeyguardTransitionInteractor keyguardTransitionInteractor =
- new KeyguardTransitionInteractor(
- mTestScope.getBackgroundScope(),
- keyguardTransitionRepository,
- () -> keyguardInteractor,
- () -> mFromLockscreenTransitionInteractor,
- () -> mFromPrimaryBouncerTransitionInteractor);
+ mKosmos.getKeyguardTransitionInteractor();
- mFromLockscreenTransitionInteractor = new FromLockscreenTransitionInteractor(
- keyguardTransitionRepository,
- keyguardTransitionInteractor,
- mTestScope.getBackgroundScope(),
- mKosmos.getTestDispatcher(),
- mKosmos.getTestDispatcher(),
- keyguardInteractor,
- featureFlags,
- shadeRepository,
- powerInteractor,
- new GlanceableHubTransitions(
- mTestScope,
- mKosmos.getTestDispatcher(),
- keyguardTransitionInteractor,
- keyguardTransitionRepository,
- communalInteractor
- ),
- () ->
- new InWindowLauncherUnlockAnimationInteractor(
- new InWindowLauncherUnlockAnimationRepository(),
- mTestScope.getBackgroundScope(),
- keyguardTransitionInteractor,
- () -> new FakeKeyguardSurfaceBehindRepository(),
- mock(ActivityManagerWrapper.class)
- )
- );
-
- mFromPrimaryBouncerTransitionInteractor = new FromPrimaryBouncerTransitionInteractor(
- keyguardTransitionRepository,
- keyguardTransitionInteractor,
- mTestScope.getBackgroundScope(),
- mKosmos.getTestDispatcher(),
- mKosmos.getTestDispatcher(),
- keyguardInteractor,
- communalInteractor,
- featureFlags,
- mKeyguardSecurityModel,
- mSelectedUserInteractor,
- powerInteractor);
+ mFromLockscreenTransitionInteractor = mKosmos.getFromLockscreenTransitionInteractor();
+ mFromPrimaryBouncerTransitionInteractor =
+ mKosmos.getFromPrimaryBouncerTransitionInteractor();
DeviceEntryUdfpsInteractor deviceEntryUdfpsInteractor =
mock(DeviceEntryUdfpsInteractor.class);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt
index 22b05be..248ed24 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt
@@ -488,7 +488,8 @@
return
}
- whenever(mGlanceableHubContainerController.isEnabled()).thenReturn(true)
+ whenever(mGlanceableHubContainerController.communalAvailable())
+ .thenReturn(MutableStateFlow(true))
val mockCommunalView = mock(View::class.java)
whenever(mGlanceableHubContainerController.initView(any<Context>()))
@@ -513,7 +514,6 @@
return
}
- whenever(mGlanceableHubContainerController.isEnabled()).thenReturn(false)
whenever(mGlanceableHubContainerController.communalAvailable())
.thenReturn(MutableStateFlow(false))
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerBaseTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerBaseTest.java
index 7bd9d92..f582402 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerBaseTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerBaseTest.java
@@ -35,14 +35,12 @@
import com.android.internal.jank.InteractionJankMonitor;
import com.android.internal.logging.MetricsLogger;
import com.android.internal.logging.UiEventLogger;
-import com.android.keyguard.KeyguardSecurityModel;
import com.android.keyguard.KeyguardStatusView;
import com.android.keyguard.KeyguardUpdateMonitor;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.bouncer.data.repository.FakeKeyguardBouncerRepository;
import com.android.systemui.common.ui.data.repository.FakeConfigurationRepository;
import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractor;
-import com.android.systemui.communal.domain.interactor.CommunalInteractor;
import com.android.systemui.deviceentry.domain.interactor.DeviceEntryFaceAuthInteractor;
import com.android.systemui.deviceentry.domain.interactor.DeviceEntryUdfpsInteractor;
import com.android.systemui.dump.DumpManager;
@@ -51,13 +49,8 @@
import com.android.systemui.fragments.FragmentHostManager;
import com.android.systemui.keyguard.data.repository.FakeCommandQueue;
import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository;
-import com.android.systemui.keyguard.data.repository.FakeKeyguardSurfaceBehindRepository;
-import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository;
-import com.android.systemui.keyguard.data.repository.InWindowLauncherUnlockAnimationRepository;
import com.android.systemui.keyguard.domain.interactor.FromLockscreenTransitionInteractor;
import com.android.systemui.keyguard.domain.interactor.FromPrimaryBouncerTransitionInteractor;
-import com.android.systemui.keyguard.domain.interactor.GlanceableHubTransitions;
-import com.android.systemui.keyguard.domain.interactor.InWindowLauncherUnlockAnimationInteractor;
import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor;
import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor;
import com.android.systemui.kosmos.KosmosJavaAdapter;
@@ -78,7 +71,6 @@
import com.android.systemui.shade.domain.interactor.ShadeInteractorImpl;
import com.android.systemui.shade.domain.interactor.ShadeInteractorLegacyImpl;
import com.android.systemui.shade.transition.ShadeTransitionController;
-import com.android.systemui.shared.system.ActivityManagerWrapper;
import com.android.systemui.statusbar.LockscreenShadeTransitionController;
import com.android.systemui.statusbar.NotificationRemoteInputManager;
import com.android.systemui.statusbar.NotificationShadeDepthController;
@@ -229,58 +221,13 @@
new ConfigurationInteractor(configurationRepository),
mShadeRepository,
() -> sceneInteractor);
- CommunalInteractor communalInteractor = mKosmos.getCommunalInteractor();
-
- FakeKeyguardTransitionRepository keyguardTransitionRepository =
- new FakeKeyguardTransitionRepository();
KeyguardTransitionInteractor keyguardTransitionInteractor =
- new KeyguardTransitionInteractor(
- mTestScope.getBackgroundScope(),
- keyguardTransitionRepository,
- () -> keyguardInteractor,
- () -> mFromLockscreenTransitionInteractor,
- () -> mFromPrimaryBouncerTransitionInteractor);
+ mKosmos.getKeyguardTransitionInteractor();
- mFromLockscreenTransitionInteractor = new FromLockscreenTransitionInteractor(
- keyguardTransitionRepository,
- keyguardTransitionInteractor,
- mTestScope.getBackgroundScope(),
- mKosmos.getTestDispatcher(),
- mKosmos.getTestDispatcher(),
- keyguardInteractor,
- featureFlags,
- mShadeRepository,
- powerInteractor,
- new GlanceableHubTransitions(
- mTestScope,
- mKosmos.getTestDispatcher(),
- keyguardTransitionInteractor,
- keyguardTransitionRepository,
- communalInteractor
- ),
- () ->
- new InWindowLauncherUnlockAnimationInteractor(
- new InWindowLauncherUnlockAnimationRepository(),
- mTestScope.getBackgroundScope(),
- keyguardTransitionInteractor,
- FakeKeyguardSurfaceBehindRepository::new,
- mock(ActivityManagerWrapper.class)
- )
- );
-
- mFromPrimaryBouncerTransitionInteractor = new FromPrimaryBouncerTransitionInteractor(
- keyguardTransitionRepository,
- keyguardTransitionInteractor,
- mTestScope.getBackgroundScope(),
- mKosmos.getTestDispatcher(),
- mKosmos.getTestDispatcher(),
- keyguardInteractor,
- communalInteractor,
- featureFlags,
- mock(KeyguardSecurityModel.class),
- mSelectedUserInteractor,
- powerInteractor);
+ mFromLockscreenTransitionInteractor = mKosmos.getFromLockscreenTransitionInteractor();
+ mFromPrimaryBouncerTransitionInteractor =
+ mKosmos.getFromPrimaryBouncerTransitionInteractor();
ResourcesSplitShadeStateController splitShadeStateController =
new ResourcesSplitShadeStateController();
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shared/notifications/data/repository/NotificationSettingsRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shared/notifications/data/repository/NotificationSettingsRepositoryTest.kt
index 50349be..0dd988d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shared/notifications/data/repository/NotificationSettingsRepositoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shared/notifications/data/repository/NotificationSettingsRepositoryTest.kt
@@ -82,4 +82,22 @@
underTest.setShowNotificationsOnLockscreenEnabled(false)
assertThat(showNotifs).isEqualTo(false)
}
+
+ @Test
+ fun testGetIsNotificationHistoryEnabled() =
+ testScope.runTest {
+ val historyEnabled by collectLastValue(underTest.isNotificationHistoryEnabled)
+
+ secureSettingsRepository.setInt(
+ name = Settings.Secure.NOTIFICATION_HISTORY_ENABLED,
+ value = 1,
+ )
+ assertThat(historyEnabled).isEqualTo(true)
+
+ secureSettingsRepository.setInt(
+ name = Settings.Secure.NOTIFICATION_HISTORY_ENABLED,
+ value = 0,
+ )
+ assertThat(historyEnabled).isEqualTo(false)
+ }
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/StatusBarStateControllerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/StatusBarStateControllerImplTest.kt
index 5450537..fb105e2 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/StatusBarStateControllerImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/StatusBarStateControllerImplTest.kt
@@ -27,20 +27,17 @@
import com.android.systemui.classifier.FalsingCollectorFake
import com.android.systemui.common.ui.data.repository.FakeConfigurationRepository
import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractor
-import com.android.systemui.communal.domain.interactor.communalInteractor
import com.android.systemui.deviceentry.domain.interactor.DeviceEntryUdfpsInteractor
import com.android.systemui.flags.FakeFeatureFlagsClassic
import com.android.systemui.keyguard.data.repository.FakeCommandQueue
import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
-import com.android.systemui.keyguard.data.repository.FakeKeyguardSurfaceBehindRepository
import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
-import com.android.systemui.keyguard.data.repository.InWindowLauncherUnlockAnimationRepository
import com.android.systemui.keyguard.domain.interactor.FromLockscreenTransitionInteractor
import com.android.systemui.keyguard.domain.interactor.FromPrimaryBouncerTransitionInteractor
-import com.android.systemui.keyguard.domain.interactor.GlanceableHubTransitions
-import com.android.systemui.keyguard.domain.interactor.InWindowLauncherUnlockAnimationInteractor
import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
-import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
+import com.android.systemui.keyguard.domain.interactor.fromLockscreenTransitionInteractor
+import com.android.systemui.keyguard.domain.interactor.fromPrimaryBouncerTransitionInteractor
+import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor
import com.android.systemui.kosmos.testDispatcher
import com.android.systemui.kosmos.testScope
import com.android.systemui.plugins.statusbar.StatusBarStateController
@@ -74,8 +71,8 @@
import org.mockito.Mockito
import org.mockito.Mockito.mock
import org.mockito.Mockito.verify
-import org.mockito.Mockito.`when` as whenever
import org.mockito.MockitoAnnotations
+import org.mockito.Mockito.`when` as whenever
@SmallTest
@RunWith(AndroidTestingRunner::class)
@@ -136,57 +133,9 @@
shadeRepository,
{ kosmos.sceneInteractor },
)
- val keyguardTransitionInteractor =
- KeyguardTransitionInteractor(
- testScope.backgroundScope,
- keyguardTransitionRepository,
- { keyguardInteractor },
- { fromLockscreenTransitionInteractor },
- { fromPrimaryBouncerTransitionInteractor }
- )
- val communalInteractor = kosmos.communalInteractor
- fromLockscreenTransitionInteractor =
- FromLockscreenTransitionInteractor(
- keyguardTransitionRepository,
- keyguardTransitionInteractor,
- testScope.backgroundScope,
- testDispatcher,
- testDispatcher,
- keyguardInteractor,
- featureFlags,
- shadeRepository,
- powerInteractor,
- GlanceableHubTransitions(
- testScope,
- testDispatcher,
- keyguardTransitionInteractor,
- keyguardTransitionRepository,
- communalInteractor
- ),
- {
- InWindowLauncherUnlockAnimationInteractor(
- InWindowLauncherUnlockAnimationRepository(),
- testScope,
- keyguardTransitionInteractor,
- { FakeKeyguardSurfaceBehindRepository() },
- mock(),
- )
- }
- )
- fromPrimaryBouncerTransitionInteractor =
- FromPrimaryBouncerTransitionInteractor(
- keyguardTransitionRepository,
- keyguardTransitionInteractor,
- testScope.backgroundScope,
- testDispatcher,
- testDispatcher,
- keyguardInteractor,
- communalInteractor,
- featureFlags,
- mock(),
- mock(),
- powerInteractor
- )
+ val keyguardTransitionInteractor = kosmos.keyguardTransitionInteractor
+ fromLockscreenTransitionInteractor = kosmos.fromLockscreenTransitionInteractor
+ fromPrimaryBouncerTransitionInteractor = kosmos.fromPrimaryBouncerTransitionInteractor
whenever(deviceEntryUdfpsInteractor.isUdfpsSupported).thenReturn(emptyFlow())
shadeInteractor =
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationLaunchAnimatorControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationLaunchAnimatorControllerTest.kt
index cd74410..3315e68 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationLaunchAnimatorControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationLaunchAnimatorControllerTest.kt
@@ -97,7 +97,7 @@
@Test
fun testHunIsRemovedAndCallbackIsInvokedWhenAnimationIsCancelled() {
flagNotificationAsHun()
- controller.onLaunchAnimationCancelled()
+ controller.onTransitionAnimationCancelled()
assertTrue(HeadsUpUtil.isClickedHeadsUpNotification(notification))
assertFalse(notification.entry.isExpandAnimationRunning)
@@ -115,7 +115,7 @@
@Test
fun testHunIsRemovedAndCallbackIsInvokedWhenAnimationEnds() {
flagNotificationAsHun()
- controller.onLaunchAnimationEnd(isExpandingFullyAbove = true)
+ controller.onTransitionAnimationEnd(isExpandingFullyAbove = true)
assertFalse(HeadsUpUtil.isClickedHeadsUpNotification(notification))
assertFalse(notification.entry.isExpandAnimationRunning)
@@ -157,7 +157,7 @@
assertNotSame(GROUP_ALERT_SUMMARY, summary.sbn.notification.groupAlertBehavior)
assertNotSame(GROUP_ALERT_SUMMARY, notification.entry.sbn.notification.groupAlertBehavior)
- controller.onLaunchAnimationEnd(isExpandingFullyAbove = true)
+ controller.onTransitionAnimationEnd(isExpandingFullyAbove = true)
verify(headsUpManager)
.removeNotification(summary.key, true /* releaseImmediately */, false /* animate */)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/SensitiveContentCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/SensitiveContentCoordinatorTest.kt
index 350ed2d..7d99d05 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/SensitiveContentCoordinatorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/SensitiveContentCoordinatorTest.kt
@@ -21,7 +21,7 @@
import android.service.notification.StatusBarNotification
import androidx.test.filters.SmallTest
import com.android.keyguard.KeyguardUpdateMonitor
-import com.android.systemui.Flags.FLAG_SCREENSHARE_NOTIFICATION_HIDING
+import com.android.server.notification.Flags.FLAG_SCREENSHARE_NOTIFICATION_HIDING
import com.android.systemui.SysuiTestCase
import com.android.systemui.plugins.statusbar.StatusBarStateController
import com.android.systemui.statusbar.NotificationLockscreenUserManager
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/footer/ui/view/FooterViewTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/footer/ui/view/FooterViewTest.java
index 57dac3a..cac4a8d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/footer/ui/view/FooterViewTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/footer/ui/view/FooterViewTest.java
@@ -17,10 +17,13 @@
package com.android.systemui.statusbar.notification.footer.ui.view;
import static com.android.systemui.log.LogAssertKt.assertLogsWtf;
+
import static com.google.common.truth.Truth.assertThat;
+
import static junit.framework.Assert.assertFalse;
import static junit.framework.Assert.assertNotNull;
import static junit.framework.Assert.assertTrue;
+
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.clearInvocations;
@@ -95,6 +98,7 @@
}
@Test
+ @DisableFlags(FooterViewRefactor.FLAG_NAME)
public void setHistoryShown() {
mView.showHistory(true);
assertTrue(mView.isHistoryShown());
@@ -103,6 +107,7 @@
}
@Test
+ @DisableFlags(FooterViewRefactor.FLAG_NAME)
public void setHistoryNotShown() {
mView.showHistory(false);
assertFalse(mView.isHistoryShown());
@@ -128,6 +133,62 @@
@Test
@EnableFlags(FooterViewRefactor.FLAG_NAME)
+ public void testSetManageOrHistoryButtonText_resourceOnlyFetchedOnce() {
+ int resId = R.string.manage_notifications_history_text;
+ mView.setManageOrHistoryButtonText(resId);
+ verify(mSpyContext).getString(eq(resId));
+
+ clearInvocations(mSpyContext);
+
+ assertThat(((TextView) mView.findViewById(R.id.manage_text))
+ .getText().toString()).contains("History");
+
+ // Set it a few more times, it shouldn't lead to the resource being fetched again
+ mView.setManageOrHistoryButtonText(resId);
+ mView.setManageOrHistoryButtonText(resId);
+
+ verify(mSpyContext, never()).getString(anyInt());
+ }
+
+ @Test
+ @DisableFlags(FooterViewRefactor.FLAG_NAME)
+ public void testSetManageOrHistoryButtonText_expectsFlagEnabled() {
+ clearInvocations(mSpyContext);
+ int resId = R.string.manage_notifications_history_text;
+ assertLogsWtf(() -> mView.setManageOrHistoryButtonText(resId));
+ verify(mSpyContext, never()).getString(anyInt());
+ }
+
+ @Test
+ @EnableFlags(FooterViewRefactor.FLAG_NAME)
+ public void testSetManageOrHistoryButtonDescription_resourceOnlyFetchedOnce() {
+ int resId = R.string.manage_notifications_history_text;
+ mView.setManageOrHistoryButtonDescription(resId);
+ verify(mSpyContext).getString(eq(resId));
+
+ clearInvocations(mSpyContext);
+
+ assertThat(((TextView) mView.findViewById(R.id.manage_text))
+ .getContentDescription().toString()).contains("History");
+
+ // Set it a few more times, it shouldn't lead to the resource being fetched again
+ mView.setManageOrHistoryButtonDescription(resId);
+ mView.setManageOrHistoryButtonDescription(resId);
+
+ verify(mSpyContext, never()).getString(anyInt());
+ }
+
+ @Test
+ @DisableFlags(FooterViewRefactor.FLAG_NAME)
+ public void testSetManageOrHistoryButtonDescription_expectsFlagEnabled() {
+ clearInvocations(mSpyContext);
+ int resId = R.string.accessibility_clear_all;
+ assertLogsWtf(() -> mView.setManageOrHistoryButtonDescription(resId));
+ verify(mSpyContext, never()).getString(anyInt());
+ }
+
+ @Test
+ @EnableFlags(FooterViewRefactor.FLAG_NAME)
public void testSetClearAllButtonText_resourceOnlyFetchedOnce() {
int resId = R.string.clear_all_notifications_text;
mView.setClearAllButtonText(resId);
@@ -150,7 +211,7 @@
public void testSetClearAllButtonText_expectsFlagEnabled() {
clearInvocations(mSpyContext);
int resId = R.string.clear_all_notifications_text;
- assertLogsWtf(()-> mView.setClearAllButtonText(resId));
+ assertLogsWtf(() -> mView.setClearAllButtonText(resId));
verify(mSpyContext, never()).getString(anyInt());
}
@@ -178,7 +239,7 @@
public void testSetClearAllButtonDescription_expectsFlagEnabled() {
clearInvocations(mSpyContext);
int resId = R.string.accessibility_clear_all;
- assertLogsWtf(()-> mView.setClearAllButtonDescription(resId));
+ assertLogsWtf(() -> mView.setClearAllButtonDescription(resId));
verify(mSpyContext, never()).getString(anyInt());
}
@@ -206,7 +267,7 @@
public void testSetMessageString_expectsFlagEnabled() {
clearInvocations(mSpyContext);
int resId = R.string.unlock_to_see_notif_text;
- assertLogsWtf(()-> mView.setMessageString(resId));
+ assertLogsWtf(() -> mView.setMessageString(resId));
verify(mSpyContext, never()).getString(anyInt());
}
@@ -231,7 +292,7 @@
public void testSetMessageIcon_expectsFlagEnabled() {
clearInvocations(mSpyContext);
int resId = R.drawable.ic_friction_lock_closed;
- assertLogsWtf(()-> mView.setMessageIcon(resId));
+ assertLogsWtf(() -> mView.setMessageIcon(resId));
verify(mSpyContext, never()).getDrawable(anyInt());
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModelTest.kt
index 8ab13f5..620d972 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModelTest.kt
@@ -14,109 +14,61 @@
* limitations under the License.
*/
+@file:OptIn(ExperimentalCoroutinesApi::class)
+
package com.android.systemui.statusbar.notification.footer.ui.viewmodel
import android.platform.test.annotations.EnableFlags
+import android.provider.Settings
import android.testing.AndroidTestingRunner
import androidx.test.filters.SmallTest
-import com.android.systemui.SysUITestComponent
-import com.android.systemui.SysUITestModule
import com.android.systemui.SysuiTestCase
-import com.android.systemui.TestMocksModule
-import com.android.systemui.collectLastValue
-import com.android.systemui.common.ui.data.repository.FakeConfigurationRepository
-import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.flags.FakeFeatureFlagsClassicModule
-import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
-import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.flags.Flags
+import com.android.systemui.flags.fakeFeatureFlagsClassic
+import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository
import com.android.systemui.keyguard.shared.model.StatusBarState
-import com.android.systemui.power.data.repository.FakePowerRepository
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.power.data.repository.powerRepository
import com.android.systemui.power.shared.model.WakeSleepReason
import com.android.systemui.power.shared.model.WakefulnessState
-import com.android.systemui.runCurrent
-import com.android.systemui.runTest
-import com.android.systemui.shade.data.repository.FakeShadeRepository
+import com.android.systemui.res.R
+import com.android.systemui.shade.data.repository.shadeRepository
+import com.android.systemui.shared.settings.data.repository.fakeSecureSettingsRepository
import com.android.systemui.statusbar.notification.collection.render.NotifStats
-import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationListRepository
+import com.android.systemui.statusbar.notification.data.repository.activeNotificationListRepository
import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor
-import com.android.systemui.statusbar.notification.row.ui.viewmodel.ActivatableNotificationViewModelModule
-import com.android.systemui.statusbar.phone.DozeParameters
-import com.android.systemui.user.domain.interactor.HeadlessSystemUserModeModule
-import com.android.systemui.util.mockito.mock
+import com.android.systemui.testKosmos
import com.android.systemui.util.ui.isAnimating
import com.android.systemui.util.ui.value
import com.google.common.truth.Truth.assertThat
-import dagger.BindsInstance
-import dagger.Component
-import java.util.Optional
-import org.junit.Before
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
import org.junit.Test
import org.junit.runner.RunWith
-import org.mockito.MockitoAnnotations
@RunWith(AndroidTestingRunner::class)
@SmallTest
@EnableFlags(FooterViewRefactor.FLAG_NAME)
class FooterViewModelTest : SysuiTestCase() {
- private lateinit var footerViewModel: FooterViewModel
-
- @SysUISingleton
- @Component(
- modules =
- [
- SysUITestModule::class,
- ActivatableNotificationViewModelModule::class,
- FooterViewModelModule::class,
- HeadlessSystemUserModeModule::class,
- ]
- )
- interface TestComponent : SysUITestComponent<Optional<FooterViewModel>> {
- val activeNotificationListRepository: ActiveNotificationListRepository
- val configurationRepository: FakeConfigurationRepository
- val keyguardRepository: FakeKeyguardRepository
- val keyguardTransitionRepository: FakeKeyguardTransitionRepository
- val shadeRepository: FakeShadeRepository
- val powerRepository: FakePowerRepository
-
- @Component.Factory
- interface Factory {
- fun create(
- @BindsInstance test: SysuiTestCase,
- featureFlags: FakeFeatureFlagsClassicModule,
- mocks: TestMocksModule,
- ): TestComponent
+ private val kosmos =
+ testKosmos().apply {
+ fakeFeatureFlagsClassic.apply { set(Flags.FULL_SCREEN_USER_SWITCHER, false) }
}
- }
+ private val testScope = kosmos.testScope
+ private val activeNotificationListRepository = kosmos.activeNotificationListRepository
+ private val fakeKeyguardRepository = kosmos.fakeKeyguardRepository
+ private val shadeRepository = kosmos.shadeRepository
+ private val powerRepository = kosmos.powerRepository
+ private val fakeSecureSettingsRepository = kosmos.fakeSecureSettingsRepository
- private val dozeParameters: DozeParameters = mock()
-
- private val testComponent: TestComponent =
- DaggerFooterViewModelTest_TestComponent.factory()
- .create(
- test = this,
- featureFlags =
- FakeFeatureFlagsClassicModule {
- set(com.android.systemui.flags.Flags.FULL_SCREEN_USER_SWITCHER, true)
- },
- mocks =
- TestMocksModule(
- dozeParameters = dozeParameters,
- )
- )
-
- @Before
- fun setUp() {
- MockitoAnnotations.initMocks(this)
-
- // The underTest in the component is Optional, because that matches the provider we
- // currently have for the footer view model.
- footerViewModel = testComponent.underTest.get()
- }
+ val underTest = kosmos.footerViewModel
@Test
fun testMessageVisible_whenFilteredNotifications() =
- testComponent.runTest {
- val visible by collectLastValue(footerViewModel.message.isVisible)
+ testScope.runTest {
+ val visible by collectLastValue(underTest.message.isVisible)
activeNotificationListRepository.hasFilteredOutSeenNotifications.value = true
@@ -125,8 +77,8 @@
@Test
fun testMessageVisible_whenNoFilteredNotifications() =
- testComponent.runTest {
- val visible by collectLastValue(footerViewModel.message.isVisible)
+ testScope.runTest {
+ val visible by collectLastValue(underTest.message.isVisible)
activeNotificationListRepository.hasFilteredOutSeenNotifications.value = false
@@ -135,8 +87,8 @@
@Test
fun testClearAllButtonVisible_whenHasClearableNotifs() =
- testComponent.runTest {
- val visible by collectLastValue(footerViewModel.clearAllButton.isVisible)
+ testScope.runTest {
+ val visible by collectLastValue(underTest.clearAllButton.isVisible)
activeNotificationListRepository.notifStats.value =
NotifStats(
@@ -153,8 +105,8 @@
@Test
fun testClearAllButtonVisible_whenHasNoClearableNotifs() =
- testComponent.runTest {
- val visible by collectLastValue(footerViewModel.clearAllButton.isVisible)
+ testScope.runTest {
+ val visible by collectLastValue(underTest.clearAllButton.isVisible)
activeNotificationListRepository.notifStats.value =
NotifStats(
@@ -171,12 +123,12 @@
@Test
fun testClearAllButtonAnimating_whenShadeExpandedAndTouchable() =
- testComponent.runTest {
- val visible by collectLastValue(footerViewModel.clearAllButton.isVisible)
+ testScope.runTest {
+ val visible by collectLastValue(underTest.clearAllButton.isVisible)
runCurrent()
// WHEN shade is expanded
- keyguardRepository.setStatusBarState(StatusBarState.SHADE)
+ fakeKeyguardRepository.setStatusBarState(StatusBarState.SHADE)
shadeRepository.setLegacyShadeExpansion(1f)
// AND QS not expanded
shadeRepository.setQsExpansion(0f)
@@ -205,12 +157,12 @@
@Test
fun testClearAllButtonAnimating_whenShadeNotExpanded() =
- testComponent.runTest {
- val visible by collectLastValue(footerViewModel.clearAllButton.isVisible)
+ testScope.runTest {
+ val visible by collectLastValue(underTest.clearAllButton.isVisible)
runCurrent()
// WHEN shade is collapsed
- keyguardRepository.setStatusBarState(StatusBarState.SHADE)
+ fakeKeyguardRepository.setStatusBarState(StatusBarState.SHADE)
shadeRepository.setLegacyShadeExpansion(0f)
// AND QS not expanded
shadeRepository.setQsExpansion(0f)
@@ -236,4 +188,30 @@
// THEN button visibility should not animate
assertThat(visible?.isAnimating).isFalse()
}
+
+ @Test
+ fun testManageButton_whenHistoryDisabled() =
+ testScope.runTest {
+ val buttonLabel by collectLastValue(underTest.manageOrHistoryButton.labelId)
+ runCurrent()
+
+ // WHEN notification history is disabled
+ fakeSecureSettingsRepository.setInt(Settings.Secure.NOTIFICATION_HISTORY_ENABLED, 0)
+
+ // THEN label is "Manage"
+ assertThat(buttonLabel).isEqualTo(R.string.manage_notifications_text)
+ }
+
+ @Test
+ fun testHistoryButton_whenHistoryEnabled() =
+ testScope.runTest {
+ val buttonLabel by collectLastValue(underTest.manageOrHistoryButton.labelId)
+ runCurrent()
+
+ // WHEN notification history is disabled
+ fakeSecureSettingsRepository.setInt(Settings.Secure.NOTIFICATION_HISTORY_ENABLED, 1)
+
+ // THEN label is "History"
+ assertThat(buttonLabel).isEqualTo(R.string.manage_notifications_history_text)
+ }
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderImplTest.kt
index da68d9c..5410864 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderImplTest.kt
@@ -16,6 +16,9 @@
package com.android.systemui.statusbar.notification.interruption
+import android.app.Notification.CATEGORY_EVENT
+import android.app.Notification.CATEGORY_REMINDER
+import android.app.NotificationManager
import android.platform.test.annotations.EnableFlags
import android.testing.AndroidTestingRunner
import androidx.test.filters.SmallTest
@@ -47,6 +50,7 @@
systemClock,
uiEventLogger,
userTracker,
+ avalancheProvider
)
}
@@ -70,6 +74,114 @@
}
}
+ // Avalanche tests are in VisualInterruptionDecisionProviderImplTest
+ // instead of VisualInterruptionDecisionProviderTestBase
+ // because avalanche code is based on the suppression refactor.
+
+ @Test
+ fun testAvalancheFilter_duringAvalanche_allowConversationFromAfterEvent() {
+ avalancheProvider.startTime = whenAgo(10)
+
+ withFilter(AvalancheSuppressor(avalancheProvider, systemClock)) {
+ ensurePeekState()
+ assertShouldHeadsUp(buildEntry {
+ importance = NotificationManager.IMPORTANCE_HIGH
+ isConversation = true
+ isImportantConversation = false
+ whenMs = whenAgo(5)
+ })
+ }
+ }
+
+ @Test
+ fun testAvalancheFilter_duringAvalanche_suppressConversationFromBeforeEvent() {
+ avalancheProvider.startTime = whenAgo(10)
+
+ withFilter(AvalancheSuppressor(avalancheProvider, systemClock)) {
+ ensurePeekState()
+ assertShouldNotHeadsUp(buildEntry {
+ importance = NotificationManager.IMPORTANCE_DEFAULT
+ isConversation = true
+ isImportantConversation = false
+ whenMs = whenAgo(15)
+ })
+ }
+ }
+
+ @Test
+ fun testAvalancheFilter_duringAvalanche_allowHighPriorityConversation() {
+ avalancheProvider.startTime = whenAgo(10)
+
+ withFilter(AvalancheSuppressor(avalancheProvider, systemClock)) {
+ ensurePeekState()
+ assertShouldHeadsUp(buildEntry {
+ importance = NotificationManager.IMPORTANCE_HIGH
+ isImportantConversation = true
+ })
+ }
+ }
+
+ @Test
+ fun testAvalancheFilter_duringAvalanche_allowCall() {
+ avalancheProvider.startTime = whenAgo(10)
+
+ withFilter(AvalancheSuppressor(avalancheProvider, systemClock)) {
+ ensurePeekState()
+ assertShouldHeadsUp(buildEntry {
+ importance = NotificationManager.IMPORTANCE_HIGH
+ isCall = true
+ })
+ }
+ }
+
+ @Test
+ fun testAvalancheFilter_duringAvalanche_allowCategoryReminder() {
+ avalancheProvider.startTime = whenAgo(10)
+
+ withFilter(AvalancheSuppressor(avalancheProvider, systemClock)) {
+ ensurePeekState()
+ assertShouldHeadsUp(buildEntry {
+ importance = NotificationManager.IMPORTANCE_HIGH
+ category = CATEGORY_REMINDER
+ })
+ }
+ }
+
+ @Test
+ fun testAvalancheFilter_duringAvalanche_allowCategoryEvent() {
+ avalancheProvider.startTime = whenAgo(10)
+
+ withFilter(AvalancheSuppressor(avalancheProvider, systemClock)) {
+ ensurePeekState()
+ assertShouldHeadsUp(buildEntry {
+ importance = NotificationManager.IMPORTANCE_HIGH
+ category = CATEGORY_EVENT
+ })
+ }
+ }
+
+ @Test
+ fun testAvalancheFilter_duringAvalanche_allowFsi() {
+ avalancheProvider.startTime = whenAgo(10)
+
+ withFilter(AvalancheSuppressor(avalancheProvider, systemClock)) {
+ assertFsiNotSuppressed()
+ }
+ }
+
+ @Test
+ fun testAvalancheFilter_duringAvalanche_allowColorized() {
+ avalancheProvider.startTime = whenAgo(10)
+
+ withFilter(AvalancheSuppressor(avalancheProvider, systemClock)) {
+ ensurePeekState()
+ assertShouldHeadsUp(buildEntry {
+ importance = NotificationManager.IMPORTANCE_HIGH
+ isColorized = true
+ })
+ }
+ }
+
@Test
fun testPeekCondition_suppressesOnlyPeek() {
withCondition(TestCondition(types = setOf(PEEK)) { true }) {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderTestBase.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderTestBase.kt
index 2ac0cb7..f89b9cd 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderTestBase.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderTestBase.kt
@@ -19,7 +19,10 @@
import android.app.ActivityManager
import android.app.Notification
import android.app.Notification.BubbleMetadata
+import android.app.Notification.EXTRA_COLORIZED
+import android.app.Notification.EXTRA_TEMPLATE
import android.app.Notification.FLAG_BUBBLE
+import android.app.Notification.FLAG_CAN_COLORIZE
import android.app.Notification.FLAG_FOREGROUND_SERVICE
import android.app.Notification.FLAG_FSI_REQUESTED_BUT_DENIED
import android.app.Notification.FLAG_USER_INITIATED_JOB
@@ -50,6 +53,8 @@
import com.android.internal.logging.UiEventLogger.UiEventEnum
import com.android.internal.logging.testing.UiEventLoggerFake
import com.android.systemui.SysuiTestCase
+import com.android.systemui.broadcast.BroadcastDispatcher
+import com.android.systemui.broadcast.FakeBroadcastDispatcher
import com.android.systemui.log.LogBuffer
import com.android.systemui.log.LogcatEchoTracker
import com.android.systemui.log.core.LogLevel
@@ -122,6 +127,7 @@
protected val systemClock = FakeSystemClock()
protected val uiEventLogger = UiEventLoggerFake()
protected val userTracker = FakeUserTracker()
+ protected val avalancheProvider: AvalancheProvider = mock()
protected abstract val provider: VisualInterruptionDecisionProvider
@@ -1097,6 +1103,8 @@
var whenMs: Long? = null
var isGrouped = false
var isGroupSummary = false
+ var isCall = false
+ var category: String? = null
var groupAlertBehavior: Int? = null
var hasBubbleMetadata = false
var hasFsi = false
@@ -1106,10 +1114,12 @@
var isUserInitiatedJob = false
var isBubble = false
var isStickyAndNotDemoted = false
+ var isColorized = false
// Set on NotificationEntryBuilder:
var importance = IMPORTANCE_DEFAULT
var canBubble: Boolean? = null
+ var isImportantConversation = false
// Set on NotificationEntry:
var hasJustLaunchedFsi = false
@@ -1118,6 +1128,7 @@
var packageSuspended = false
var visibilityOverride: Int? = null
var suppressedVisualEffects: Int? = null
+ var isConversation = false
private fun buildBubbleMetadata(): BubbleMetadata {
val builder =
@@ -1158,6 +1169,13 @@
nb.setGroupSummary(true)
}
+ if (isCall) {
+ nb.extras.putString(EXTRA_TEMPLATE, Notification.CallStyle::class.java.name)
+ }
+
+ if (category != null) {
+ nb.setCategory(category)
+ }
groupAlertBehavior?.let { nb.setGroupAlertBehavior(it) }
if (hasBubbleMetadata) {
@@ -1185,6 +1203,10 @@
if (isStickyAndNotDemoted) {
n.flags = n.flags or FLAG_FSI_REQUESTED_BUT_DENIED
}
+ if (isColorized) {
+ n.extras.putBoolean(EXTRA_COLORIZED, true)
+ n.flags = n.flags or FLAG_CAN_COLORIZE
+ }
}
.let { NotificationEntryBuilder().setNotification(it) }
.also { neb ->
@@ -1193,9 +1215,10 @@
neb.setTag(TEST_TAG)
neb.setImportance(importance)
- neb.setChannel(
- NotificationChannel(TEST_CHANNEL_ID, TEST_CHANNEL_NAME, importance)
- )
+ val channel =
+ NotificationChannel(TEST_CHANNEL_ID, TEST_CHANNEL_NAME, importance)
+ channel.isImportantConversation = isImportantConversation
+ neb.setChannel(channel)
canBubble?.let { neb.setCanBubble(it) }
}
@@ -1216,6 +1239,7 @@
}
visibilityOverride?.let { mrb.setVisibilityOverride(it) }
suppressedVisualEffects?.let { mrb.setSuppressedVisualEffects(it) }
+ mrb.setIsConversation(isConversation)
}
.build()
}
@@ -1287,7 +1311,7 @@
}
}
- private fun whenAgo(whenAgeMs: Long) = systemClock.currentTimeMillis() - whenAgeMs
+ protected fun whenAgo(whenAgeMs: Long) = systemClock.currentTimeMillis() - whenAgeMs
}
private const val TEST_CONTENT_TITLE = "Test Content Title"
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderTestUtil.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderTestUtil.kt
index 0356c2c..620ad9c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderTestUtil.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderTestUtil.kt
@@ -19,6 +19,7 @@
import android.os.Handler
import android.os.PowerManager
import com.android.internal.logging.UiEventLogger
+import com.android.systemui.broadcast.BroadcastDispatcher
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.plugins.statusbar.StatusBarStateController
import com.android.systemui.settings.UserTracker
@@ -49,7 +50,8 @@
statusBarStateController: StatusBarStateController,
systemClock: SystemClock,
uiEventLogger: UiEventLogger,
- userTracker: UserTracker
+ userTracker: UserTracker,
+ avalancheProvider: AvalancheProvider
): VisualInterruptionDecisionProvider {
return if (VisualInterruptionRefactor.isEnabled) {
VisualInterruptionDecisionProviderImpl(
@@ -67,7 +69,8 @@
statusBarStateController,
systemClock,
uiEventLogger,
- userTracker
+ userTracker,
+ avalancheProvider
)
} else {
NotificationInterruptStateProviderWrapper(
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java
index dbe63f2..354f3f6 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java
@@ -16,7 +16,7 @@
package com.android.systemui.statusbar.notification.stack;
-import static com.android.systemui.Flags.FLAG_SCREENSHARE_NOTIFICATION_HIDING;
+import static com.android.server.notification.Flags.FLAG_SCREENSHARE_NOTIFICATION_HIDING;
import static com.android.systemui.log.LogBufferHelperKt.logcatLogBuffer;
import static com.android.systemui.statusbar.StatusBarState.KEYGUARD;
import static com.android.systemui.statusbar.StatusBarState.SHADE;
@@ -797,6 +797,7 @@
}
@Test
+ @DisableFlags(FooterViewRefactor.FLAG_NAME)
public void testUpdateFooter_remoteInput() {
ArgumentCaptor<RemoteInputController.Callback> callbackCaptor =
ArgumentCaptor.forClass(RemoteInputController.Callback.class);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java
index 4afcc8c..04f3216 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java
@@ -440,6 +440,7 @@
}
@Test
+ @DisableFlags(FooterViewRefactor.FLAG_NAME)
public void testUpdateFooter_noNotifications() {
setBarStateForTest(StatusBarState.SHADE);
mStackScroller.setCurrentUserSetup(true);
@@ -451,6 +452,7 @@
}
@Test
+ @DisableFlags(FooterViewRefactor.FLAG_NAME)
public void testUpdateFooter_remoteInput() {
setBarStateForTest(StatusBarState.SHADE);
mStackScroller.setCurrentUserSetup(true);
@@ -467,6 +469,7 @@
}
@Test
+ @DisableFlags(FooterViewRefactor.FLAG_NAME)
public void testUpdateFooter_withoutNotifications() {
setBarStateForTest(StatusBarState.SHADE);
mStackScroller.setCurrentUserSetup(true);
@@ -482,6 +485,7 @@
}
@Test
+ @DisableFlags(FooterViewRefactor.FLAG_NAME)
public void testUpdateFooter_oneClearableNotification() {
setBarStateForTest(StatusBarState.SHADE);
mStackScroller.setCurrentUserSetup(true);
@@ -497,6 +501,7 @@
}
@Test
+ @DisableFlags(FooterViewRefactor.FLAG_NAME)
public void testUpdateFooter_withoutHistory() {
setBarStateForTest(StatusBarState.SHADE);
mStackScroller.setCurrentUserSetup(true);
@@ -513,6 +518,7 @@
}
@Test
+ @DisableFlags(FooterViewRefactor.FLAG_NAME)
public void testUpdateFooter_oneClearableNotification_beforeUserSetup() {
setBarStateForTest(StatusBarState.SHADE);
mStackScroller.setCurrentUserSetup(false);
@@ -528,6 +534,7 @@
}
@Test
+ @DisableFlags(FooterViewRefactor.FLAG_NAME)
public void testUpdateFooter_oneNonClearableNotification() {
setBarStateForTest(StatusBarState.SHADE);
mStackScroller.setCurrentUserSetup(true);
@@ -544,9 +551,7 @@
}
@Test
- public void testUpdateFooter_atEnd() {
- mStackScroller.setCurrentUserSetup(true);
-
+ public void testFooterPosition_atEnd() {
// add footer
FooterView view = mock(FooterView.class);
mStackScroller.setFooterView(view);
@@ -559,8 +564,6 @@
// Expecting the footer to be the last child
int expected = mStackScroller.getChildCount() - 1;
-
- // move footer to end
verify(mStackScroller).changeViewPosition(any(FooterView.class), eq(expected));
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/StackStateAnimatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/StackStateAnimatorTest.kt
index 5a57035..dfbe1ac 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/StackStateAnimatorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/StackStateAnimatorTest.kt
@@ -18,8 +18,10 @@
import android.platform.test.annotations.EnableFlags
import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper.RunWithLooper
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
+import com.android.systemui.res.R
import com.android.systemui.statusbar.notification.row.ExpandableView
import com.android.systemui.statusbar.notification.shared.NotificationsImprovedHunAnimation
import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout.AnimationEvent
@@ -28,6 +30,7 @@
import com.android.systemui.util.mockito.argumentCaptor
import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.whenever
+import com.google.common.truth.Truth.assertThat
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@@ -38,9 +41,12 @@
import org.mockito.Mockito.verify
private const val VIEW_HEIGHT = 100
+private const val FULL_SHADE_APPEAR_TRANSLATION = 300
+private const val HEADS_UP_ABOVE_SCREEN = 80
@SmallTest
@RunWith(AndroidTestingRunner::class)
+@RunWithLooper
class StackStateAnimatorTest : SysuiTestCase() {
private lateinit var stackStateAnimator: StackStateAnimator
@@ -51,9 +57,15 @@
private val runnableCaptor: ArgumentCaptor<Runnable> = argumentCaptor()
@Before
fun setUp() {
+ overrideResource(
+ R.dimen.go_to_full_shade_appearing_translation,
+ FULL_SHADE_APPEAR_TRANSLATION
+ )
+ overrideResource(R.dimen.heads_up_appear_y_above_screen, HEADS_UP_ABOVE_SCREEN)
+
whenever(stackScroller.context).thenReturn(context)
whenever(view.viewState).thenReturn(viewState)
- stackStateAnimator = StackStateAnimator(stackScroller)
+ stackStateAnimator = StackStateAnimator(mContext, stackScroller)
}
@Test
@@ -122,4 +134,22 @@
verify(view, description("should be called at the end of the animation"))
.removeFromTransientContainer()
}
+
+ @Test
+ fun initView_updatesResources() {
+ // Given: the resource values are initialized in the SSA
+ assertThat(stackStateAnimator.mGoToFullShadeAppearingTranslation)
+ .isEqualTo(FULL_SHADE_APPEAR_TRANSLATION)
+ assertThat(stackStateAnimator.mHeadsUpAppearStartAboveScreen)
+ .isEqualTo(HEADS_UP_ABOVE_SCREEN)
+
+ // When: initView is called after the resources have changed
+ overrideResource(R.dimen.go_to_full_shade_appearing_translation, 200)
+ overrideResource(R.dimen.heads_up_appear_y_above_screen, 100)
+ stackStateAnimator.initView(mContext)
+
+ // Then: the resource values are updated
+ assertThat(stackStateAnimator.mGoToFullShadeAppearingTranslation).isEqualTo(200)
+ assertThat(stackStateAnimator.mHeadsUpAppearStartAboveScreen).isEqualTo(100)
+ }
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelTest.kt
index 4188c5d..3a7659d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelTest.kt
@@ -23,36 +23,35 @@
import android.provider.Settings
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
-import com.android.systemui.SysUITestComponent
-import com.android.systemui.SysUITestModule
import com.android.systemui.SysuiTestCase
-import com.android.systemui.TestMocksModule
-import com.android.systemui.collectLastValue
-import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.flags.FakeFeatureFlagsClassicModule
-import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.flags.Flags
+import com.android.systemui.flags.fakeFeatureFlagsClassic
+import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository
+import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.keyguard.shared.model.StatusBarState
import com.android.systemui.keyguard.shared.model.TransitionState
import com.android.systemui.keyguard.shared.model.TransitionStep
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.power.data.repository.fakePowerRepository
+import com.android.systemui.power.shared.model.WakefulnessState
import com.android.systemui.res.R
-import com.android.systemui.runCurrent
-import com.android.systemui.runTest
-import com.android.systemui.shade.data.repository.FakeShadeRepository
-import com.android.systemui.statusbar.notification.dagger.NotificationStatsLoggerModule
-import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationListRepository
+import com.android.systemui.shade.data.repository.fakeShadeRepository
+import com.android.systemui.statusbar.data.repository.fakeRemoteInputRepository
+import com.android.systemui.statusbar.notification.data.repository.activeNotificationListRepository
import com.android.systemui.statusbar.notification.data.repository.setActiveNotifs
import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor
-import com.android.systemui.statusbar.notification.footer.ui.viewmodel.FooterViewModelModule
-import com.android.systemui.statusbar.notification.row.ui.viewmodel.ActivatableNotificationViewModelModule
-import com.android.systemui.statusbar.policy.FakeConfigurationController
-import com.android.systemui.statusbar.policy.data.repository.FakeZenModeRepository
-import com.android.systemui.unfold.UnfoldTransitionModule
-import com.android.systemui.user.domain.interactor.HeadlessSystemUserModeModule
+import com.android.systemui.statusbar.policy.data.repository.fakeUserSetupRepository
+import com.android.systemui.statusbar.policy.data.repository.zenModeRepository
+import com.android.systemui.statusbar.policy.fakeConfigurationController
+import com.android.systemui.testKosmos
+import com.android.systemui.util.ui.isAnimating
+import com.android.systemui.util.ui.value
import com.google.common.truth.Truth.assertThat
-import dagger.BindsInstance
-import dagger.Component
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@@ -62,46 +61,23 @@
@RunWith(AndroidJUnit4::class)
@EnableFlags(FooterViewRefactor.FLAG_NAME)
class NotificationListViewModelTest : SysuiTestCase() {
-
- @SysUISingleton
- @Component(
- modules =
- [
- SysUITestModule::class,
- ActivatableNotificationViewModelModule::class,
- FooterViewModelModule::class,
- HeadlessSystemUserModeModule::class,
- UnfoldTransitionModule.Bindings::class,
- NotificationStatsLoggerModule::class,
- ]
- )
- interface TestComponent : SysUITestComponent<NotificationListViewModel> {
- val activeNotificationListRepository: ActiveNotificationListRepository
- val keyguardTransitionRepository: FakeKeyguardTransitionRepository
- val shadeRepository: FakeShadeRepository
- val zenModeRepository: FakeZenModeRepository
- val configurationController: FakeConfigurationController
-
- @Component.Factory
- interface Factory {
- fun create(
- @BindsInstance test: SysuiTestCase,
- featureFlags: FakeFeatureFlagsClassicModule,
- mocks: TestMocksModule,
- ): TestComponent
+ private val kosmos =
+ testKosmos().apply {
+ fakeFeatureFlagsClassic.apply { set(Flags.FULL_SCREEN_USER_SWITCHER, false) }
}
- }
+ private val testScope = kosmos.testScope
- private val testComponent: TestComponent =
- DaggerNotificationListViewModelTest_TestComponent.factory()
- .create(
- test = this,
- featureFlags =
- FakeFeatureFlagsClassicModule {
- set(com.android.systemui.flags.Flags.FULL_SCREEN_USER_SWITCHER, true)
- },
- mocks = TestMocksModule()
- )
+ private val activeNotificationListRepository = kosmos.activeNotificationListRepository
+ private val fakeConfigurationController = kosmos.fakeConfigurationController
+ private val fakeKeyguardRepository = kosmos.fakeKeyguardRepository
+ private val fakeKeyguardTransitionRepository = kosmos.fakeKeyguardTransitionRepository
+ private val fakePowerRepository = kosmos.fakePowerRepository
+ private val fakeRemoteInputRepository = kosmos.fakeRemoteInputRepository
+ private val fakeShadeRepository = kosmos.fakeShadeRepository
+ private val fakeUserSetupRepository = kosmos.fakeUserSetupRepository
+ private val zenModeRepository = kosmos.zenModeRepository
+
+ val underTest = kosmos.notificationListViewModel
@Before
fun setUp() {
@@ -110,11 +86,11 @@
@Test
fun testIsImportantForAccessibility_falseWhenNoNotifs() =
- testComponent.runTest {
+ testScope.runTest {
val important by collectLastValue(underTest.isImportantForAccessibility)
// WHEN on lockscreen
- keyguardTransitionRepository.sendTransitionSteps(
+ fakeKeyguardTransitionRepository.sendTransitionSteps(
from = KeyguardState.GONE,
to = KeyguardState.LOCKSCREEN,
testScope,
@@ -129,11 +105,11 @@
@Test
fun testIsImportantForAccessibility_trueWhenNotifs() =
- testComponent.runTest {
+ testScope.runTest {
val important by collectLastValue(underTest.isImportantForAccessibility)
// WHEN on lockscreen
- keyguardTransitionRepository.sendTransitionSteps(
+ fakeKeyguardTransitionRepository.sendTransitionSteps(
from = KeyguardState.GONE,
to = KeyguardState.LOCKSCREEN,
testScope,
@@ -148,11 +124,11 @@
@Test
fun testIsImportantForAccessibility_trueWhenNotKeyguard() =
- testComponent.runTest {
+ testScope.runTest {
val important by collectLastValue(underTest.isImportantForAccessibility)
// WHEN not on lockscreen
- keyguardTransitionRepository.sendTransitionSteps(
+ fakeKeyguardTransitionRepository.sendTransitionSteps(
from = KeyguardState.LOCKSCREEN,
to = KeyguardState.GONE,
testScope,
@@ -167,7 +143,7 @@
@Test
fun testShouldShowEmptyShadeView_trueWhenNoNotifs() =
- testComponent.runTest {
+ testScope.runTest {
val shouldShow by collectLastValue(underTest.shouldShowEmptyShadeView)
// WHEN has no notifs
@@ -180,7 +156,7 @@
@Test
fun testShouldShowEmptyShadeView_falseWhenNotifs() =
- testComponent.runTest {
+ testScope.runTest {
val shouldShow by collectLastValue(underTest.shouldShowEmptyShadeView)
// WHEN has notifs
@@ -193,13 +169,13 @@
@Test
fun testShouldShowEmptyShadeView_falseWhenQsExpandedDefault() =
- testComponent.runTest {
+ testScope.runTest {
val shouldShow by collectLastValue(underTest.shouldShowEmptyShadeView)
// WHEN has no notifs
activeNotificationListRepository.setActiveNotifs(count = 0)
// AND quick settings are expanded
- shadeRepository.legacyQsFullscreen.value = true
+ fakeShadeRepository.legacyQsFullscreen.value = true
runCurrent()
// THEN should not show
@@ -208,16 +184,16 @@
@Test
fun testShouldShowEmptyShadeView_trueWhenQsExpandedInSplitShade() =
- testComponent.runTest {
+ testScope.runTest {
val shouldShow by collectLastValue(underTest.shouldShowEmptyShadeView)
// WHEN has no notifs
activeNotificationListRepository.setActiveNotifs(count = 0)
// AND quick settings are expanded
- shadeRepository.setQsExpansion(1f)
+ fakeShadeRepository.setQsExpansion(1f)
// AND split shade is enabled
overrideResource(R.bool.config_use_split_notification_shade, true)
- configurationController.notifyConfigurationChanged()
+ fakeConfigurationController.notifyConfigurationChanged()
runCurrent()
// THEN should show
@@ -226,13 +202,13 @@
@Test
fun testShouldShowEmptyShadeView_falseWhenTransitioningToAOD() =
- testComponent.runTest {
+ testScope.runTest {
val shouldShow by collectLastValue(underTest.shouldShowEmptyShadeView)
// WHEN has no notifs
activeNotificationListRepository.setActiveNotifs(count = 0)
// AND transitioning to AOD
- keyguardTransitionRepository.sendTransitionStep(
+ fakeKeyguardTransitionRepository.sendTransitionStep(
TransitionStep(
transitionState = TransitionState.STARTED,
from = KeyguardState.LOCKSCREEN,
@@ -248,13 +224,13 @@
@Test
fun testShouldShowEmptyShadeView_falseWhenBouncerShowing() =
- testComponent.runTest {
+ testScope.runTest {
val shouldShow by collectLastValue(underTest.shouldShowEmptyShadeView)
// WHEN has no notifs
activeNotificationListRepository.setActiveNotifs(count = 0)
// AND is on bouncer
- keyguardTransitionRepository.sendTransitionSteps(
+ fakeKeyguardTransitionRepository.sendTransitionSteps(
from = KeyguardState.LOCKSCREEN,
to = KeyguardState.PRIMARY_BOUNCER,
testScope,
@@ -267,7 +243,7 @@
@Test
fun testAreNotificationsHiddenInShade_true() =
- testComponent.runTest {
+ testScope.runTest {
val hidden by collectLastValue(underTest.areNotificationsHiddenInShade)
zenModeRepository.setSuppressedVisualEffects(Policy.SUPPRESSED_EFFECT_NOTIFICATION_LIST)
@@ -279,7 +255,7 @@
@Test
fun testAreNotificationsHiddenInShade_false() =
- testComponent.runTest {
+ testScope.runTest {
val hidden by collectLastValue(underTest.areNotificationsHiddenInShade)
zenModeRepository.setSuppressedVisualEffects(Policy.SUPPRESSED_EFFECT_NOTIFICATION_LIST)
@@ -291,7 +267,7 @@
@Test
fun testHasFilteredOutSeenNotifications_true() =
- testComponent.runTest {
+ testScope.runTest {
val hasFilteredNotifs by collectLastValue(underTest.hasFilteredOutSeenNotifications)
activeNotificationListRepository.hasFilteredOutSeenNotifications.value = true
@@ -302,7 +278,7 @@
@Test
fun testHasFilteredOutSeenNotifications_false() =
- testComponent.runTest {
+ testScope.runTest {
val hasFilteredNotifs by collectLastValue(underTest.hasFilteredOutSeenNotifications)
activeNotificationListRepository.hasFilteredOutSeenNotifications.value = false
@@ -310,4 +286,193 @@
assertThat(hasFilteredNotifs).isFalse()
}
+
+ @Test
+ fun testShouldShowFooterView_trueWhenShade() =
+ testScope.runTest {
+ val shouldShow by collectLastValue(underTest.shouldShowFooterView)
+
+ // WHEN has notifs
+ activeNotificationListRepository.setActiveNotifs(count = 2)
+ // AND shade is open
+ fakeKeyguardRepository.setStatusBarState(StatusBarState.SHADE)
+ fakeShadeRepository.setLegacyShadeExpansion(1f)
+ runCurrent()
+
+ // THEN footer is visible
+ assertThat(shouldShow?.value).isTrue()
+ }
+
+ @Test
+ fun testShouldShowFooterView_trueWhenLockedShade() =
+ testScope.runTest {
+ val shouldShow by collectLastValue(underTest.shouldShowFooterView)
+
+ // WHEN has notifs
+ activeNotificationListRepository.setActiveNotifs(count = 2)
+ // AND shade is open on lockscreen
+ fakeKeyguardRepository.setStatusBarState(StatusBarState.SHADE_LOCKED)
+ fakeShadeRepository.setLegacyShadeExpansion(1f)
+ runCurrent()
+
+ // THEN footer is visible
+ assertThat(shouldShow?.value).isTrue()
+ }
+
+ @Test
+ fun testShouldShowFooterView_falseWhenKeyguard() =
+ testScope.runTest {
+ val shouldShow by collectLastValue(underTest.shouldShowFooterView)
+
+ // WHEN has notifs
+ activeNotificationListRepository.setActiveNotifs(count = 2)
+ // AND is on keyguard
+ fakeKeyguardRepository.setStatusBarState(StatusBarState.KEYGUARD)
+ runCurrent()
+
+ // THEN footer is not visible
+ assertThat(shouldShow?.value).isFalse()
+ }
+
+ @Test
+ fun testShouldShowFooterView_falseWhenUserNotSetUp() =
+ testScope.runTest {
+ val shouldShow by collectLastValue(underTest.shouldShowFooterView)
+
+ // WHEN has notifs
+ activeNotificationListRepository.setActiveNotifs(count = 2)
+ // AND shade is open
+ fakeKeyguardRepository.setStatusBarState(StatusBarState.SHADE)
+ fakeShadeRepository.setLegacyShadeExpansion(1f)
+ // AND user is not set up
+ fakeUserSetupRepository.setUserSetUp(false)
+ runCurrent()
+
+ // THEN footer is not visible
+ assertThat(shouldShow?.value).isFalse()
+ }
+
+ @Test
+ fun testShouldShowFooterView_falseWhenStartingToSleep() =
+ testScope.runTest {
+ val shouldShow by collectLastValue(underTest.shouldShowFooterView)
+
+ // WHEN has notifs
+ activeNotificationListRepository.setActiveNotifs(count = 2)
+ // AND shade is open
+ fakeKeyguardRepository.setStatusBarState(StatusBarState.SHADE)
+ fakeShadeRepository.setLegacyShadeExpansion(1f)
+ // AND device is starting to go to sleep
+ fakePowerRepository.updateWakefulness(WakefulnessState.STARTING_TO_SLEEP)
+ runCurrent()
+
+ // THEN footer is not visible
+ assertThat(shouldShow?.value).isFalse()
+ }
+
+ @Test
+ fun testShouldShowFooterView_falseWhenQsExpandedDefault() =
+ testScope.runTest {
+ val shouldShow by collectLastValue(underTest.shouldShowFooterView)
+
+ // WHEN has notifs
+ activeNotificationListRepository.setActiveNotifs(count = 2)
+ // AND shade is open
+ fakeKeyguardRepository.setStatusBarState(StatusBarState.SHADE)
+ fakeShadeRepository.setLegacyShadeExpansion(1f)
+ // AND quick settings are expanded
+ fakeShadeRepository.setQsExpansion(1f)
+ fakeShadeRepository.legacyQsFullscreen.value = true
+ runCurrent()
+
+ // THEN footer is not visible
+ assertThat(shouldShow?.value).isFalse()
+ }
+
+ @Test
+ fun testShouldShowFooterView_trueWhenQsExpandedSplitShade() =
+ testScope.runTest {
+ val shouldShow by collectLastValue(underTest.shouldShowFooterView)
+
+ // WHEN has notifs
+ activeNotificationListRepository.setActiveNotifs(count = 2)
+ // AND shade is open
+ fakeKeyguardRepository.setStatusBarState(StatusBarState.SHADE)
+ fakeShadeRepository.setLegacyShadeExpansion(1f)
+ // AND quick settings are expanded
+ fakeShadeRepository.setQsExpansion(1f)
+ // AND split shade is enabled
+ overrideResource(R.bool.config_use_split_notification_shade, true)
+ fakeConfigurationController.notifyConfigurationChanged()
+ runCurrent()
+
+ // THEN footer is visible
+ assertThat(shouldShow?.value).isTrue()
+ }
+
+ @Test
+ fun testShouldShowFooterView_falseWhenRemoteInputActive() =
+ testScope.runTest {
+ val shouldShow by collectLastValue(underTest.shouldShowFooterView)
+
+ // WHEN has notifs
+ activeNotificationListRepository.setActiveNotifs(count = 2)
+ // AND shade is open
+ fakeKeyguardRepository.setStatusBarState(StatusBarState.SHADE)
+ fakeShadeRepository.setLegacyShadeExpansion(1f)
+ // AND remote input is active
+ fakeRemoteInputRepository.isRemoteInputActive.value = true
+ runCurrent()
+
+ // THEN footer is not visible
+ assertThat(shouldShow?.value).isFalse()
+ }
+
+ @Test
+ fun testShouldShowFooterView_falseWhenShadeIsClosed() =
+ testScope.runTest {
+ val shouldShow by collectLastValue(underTest.shouldShowFooterView)
+
+ // WHEN has notifs
+ activeNotificationListRepository.setActiveNotifs(count = 2)
+ // AND shade is closed
+ fakeKeyguardRepository.setStatusBarState(StatusBarState.SHADE)
+ fakeShadeRepository.setLegacyShadeExpansion(0f)
+ runCurrent()
+
+ // THEN footer is not visible
+ assertThat(shouldShow?.value).isFalse()
+ }
+
+ @Test
+ fun testShouldShowFooterView_animatesWhenShade() =
+ testScope.runTest {
+ val shouldShow by collectLastValue(underTest.shouldShowFooterView)
+
+ // WHEN has notifs
+ activeNotificationListRepository.setActiveNotifs(count = 2)
+ // AND shade is open and fully expanded
+ fakeKeyguardRepository.setStatusBarState(StatusBarState.SHADE)
+ fakeShadeRepository.setLegacyShadeExpansion(1f)
+ runCurrent()
+
+ // THEN footer visibility animates
+ assertThat(shouldShow?.isAnimating).isTrue()
+ }
+
+ @Test
+ fun testShouldShowFooterView_notAnimatingOnKeyguard() =
+ testScope.runTest {
+ val shouldShow by collectLastValue(underTest.shouldShowFooterView)
+
+ // WHEN has notifs
+ activeNotificationListRepository.setActiveNotifs(count = 2)
+ // AND we are on the keyguard
+ fakeKeyguardRepository.setStatusBarState(StatusBarState.KEYGUARD)
+ fakeShadeRepository.setLegacyShadeExpansion(1f)
+ runCurrent()
+
+ // THEN footer visibility does not animate
+ assertThat(shouldShow?.isAnimating).isFalse()
+ }
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt
index 32c727c..ff882b1 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt
@@ -69,7 +69,10 @@
val kosmos =
testKosmos().apply {
- fakeFeatureFlagsClassic.apply { set(Flags.FULL_SCREEN_USER_SWITCHER, false) }
+ fakeFeatureFlagsClassic.apply {
+ set(Flags.FULL_SCREEN_USER_SWITCHER, false)
+ set(Flags.REFACTOR_KEYGUARD_DISMISS_INTENT, false)
+ }
}
init {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java
index 849a13b..9c60a2b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java
@@ -83,7 +83,7 @@
import com.android.systemui.InitController;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.accessibility.floatingmenu.AccessibilityFloatingMenuController;
-import com.android.systemui.animation.ActivityLaunchAnimator;
+import com.android.systemui.animation.ActivityTransitionAnimator;
import com.android.systemui.assist.AssistManager;
import com.android.systemui.back.domain.interactor.BackActionInteractor;
import com.android.systemui.biometrics.AuthRippleController;
@@ -153,6 +153,7 @@
import com.android.systemui.statusbar.notification.NotificationLaunchAnimatorControllerProvider;
import com.android.systemui.statusbar.notification.NotificationWakeUpCoordinator;
import com.android.systemui.statusbar.notification.init.NotificationsController;
+import com.android.systemui.statusbar.notification.interruption.AvalancheProvider;
import com.android.systemui.statusbar.notification.interruption.KeyguardNotificationVisibilityProvider;
import com.android.systemui.statusbar.notification.interruption.NotificationInterruptLogger;
import com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProviderImpl;
@@ -306,13 +307,14 @@
@Mock private StartingSurface mStartingSurface;
@Mock private OperatorNameViewController mOperatorNameViewController;
@Mock private OperatorNameViewController.Factory mOperatorNameViewControllerFactory;
- @Mock private ActivityLaunchAnimator mActivityLaunchAnimator;
+ @Mock private ActivityTransitionAnimator mActivityTransitionAnimator;
@Mock private DeviceStateManager mDeviceStateManager;
@Mock private WiredChargingRippleController mWiredChargingRippleController;
@Mock private Lazy<CameraLauncher> mCameraLauncherLazy;
@Mock private CameraLauncher mCameraLauncher;
@Mock private AlternateBouncerInteractor mAlternateBouncerInteractor;
@Mock private UserTracker mUserTracker;
+ @Mock private AvalancheProvider mAvalancheProvider;
@Mock private FingerprintManager mFingerprintManager;
@Mock IPowerManager mPowerManagerService;
@Mock ActivityStarter mActivityStarter;
@@ -367,7 +369,8 @@
mStatusBarStateController,
mFakeSystemClock,
mock(UiEventLogger.class),
- mUserTracker);
+ mUserTracker,
+ mAvalancheProvider);
mVisualInterruptionDecisionProvider.start();
mContext.addMockSystemService(TrustManager.class, mock(TrustManager.class));
@@ -540,7 +543,7 @@
new MessageRouterImpl(mMainExecutor),
mWallpaperManager,
Optional.of(mStartingSurface),
- mActivityLaunchAnimator,
+ mActivityTransitionAnimator,
mDeviceStateManager,
mWiredChargingRippleController,
mDreamManager,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java
index 3bde6e3..99c2dc7 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java
@@ -315,6 +315,8 @@
@After
public void tearDown() {
+ // Detaching view stops flow collection and prevents memory leak.
+ ViewUtils.detachView(mScrimBehind);
finishAnimationsImmediately();
Arrays.stream(ScrimState.values()).forEach((scrim) -> {
scrim.setAodFrontScrimAlpha(0f);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java
index cb45315..4015361 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java
@@ -79,6 +79,7 @@
import com.android.systemui.flags.FakeFeatureFlags;
import com.android.systemui.flags.Flags;
import com.android.systemui.keyguard.domain.interactor.KeyguardDismissActionInteractor;
+import com.android.systemui.keyguard.domain.interactor.KeyguardSurfaceBehindInteractor;
import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor;
import com.android.systemui.keyguard.domain.interactor.WindowManagerLockscreenVisibilityInteractor;
import com.android.systemui.navigationbar.NavigationModeController;
@@ -222,7 +223,8 @@
StandardTestDispatcher(null, null),
() -> mock(WindowManagerLockscreenVisibilityInteractor.class),
() -> mock(KeyguardDismissActionInteractor.class),
- mSelectedUserInteractor) {
+ mSelectedUserInteractor,
+ () -> mock(KeyguardSurfaceBehindInteractor.class)) {
@Override
public ViewRootImpl getViewRootImpl() {
return mViewRootImpl;
@@ -730,7 +732,8 @@
StandardTestDispatcher(null, null),
() -> mock(WindowManagerLockscreenVisibilityInteractor.class),
() -> mock(KeyguardDismissActionInteractor.class),
- mSelectedUserInteractor) {
+ mSelectedUserInteractor,
+ () -> mock(KeyguardSurfaceBehindInteractor.class)) {
@Override
public ViewRootImpl getViewRootImpl() {
return mViewRootImpl;
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterTest.java
index 597e2e3..41514ce 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterTest.java
@@ -63,7 +63,7 @@
import com.android.internal.widget.LockPatternUtils;
import com.android.systemui.ActivityIntentHelper;
import com.android.systemui.SysuiTestCase;
-import com.android.systemui.animation.ActivityLaunchAnimator;
+import com.android.systemui.animation.ActivityTransitionAnimator;
import com.android.systemui.assist.AssistManager;
import com.android.systemui.classifier.FalsingCollectorFake;
import com.android.systemui.flags.FakeFeatureFlags;
@@ -159,7 +159,7 @@
@Mock
private StatusBarNotificationActivityStarter mNotificationActivityStarter;
@Mock
- private ActivityLaunchAnimator mActivityLaunchAnimator;
+ private ActivityTransitionAnimator mActivityTransitionAnimator;
@Mock
private InteractionJankMonitor mJankMonitor;
private FakePowerRepository mPowerRepository;
@@ -255,7 +255,7 @@
mock(NotificationPresenter.class),
mock(ShadeViewController.class),
mock(NotificationShadeWindowController.class),
- mActivityLaunchAnimator,
+ mActivityTransitionAnimator,
new ShadeAnimationInteractorLegacyImpl(
new ShadeAnimationRepository(), new FakeShadeRepository()),
notificationAnimationProvider,
@@ -306,7 +306,7 @@
// Then
verify(mShadeController, atLeastOnce()).collapseShade();
- verify(mActivityLaunchAnimator).startPendingIntentWithAnimation(any(),
+ verify(mActivityTransitionAnimator).startPendingIntentWithAnimation(any(),
eq(false) /* animate */, any(), any());
verify(mAssistManager).hideAssist();
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionControllerFlagDisabledTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionControllerFlagDisabledTest.kt
new file mode 100644
index 0000000..98be163
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionControllerFlagDisabledTest.kt
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.policy
+
+import android.app.IActivityManager
+import android.media.projection.MediaProjectionManager
+import android.os.Handler
+import android.platform.test.annotations.DisableFlags
+import android.testing.AndroidTestingRunner
+import androidx.test.filters.SmallTest
+import com.android.server.notification.Flags
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.util.concurrency.FakeExecutor
+import com.android.systemui.util.time.FakeSystemClock
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.verifyZeroInteractions
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+@DisableFlags(Flags.FLAG_SCREENSHARE_NOTIFICATION_HIDING)
+class SensitiveNotificationProtectionControllerFlagDisabledTest : SysuiTestCase() {
+ @Mock private lateinit var handler: Handler
+ @Mock private lateinit var activityManager: IActivityManager
+ @Mock private lateinit var mediaProjectionManager: MediaProjectionManager
+ private lateinit var controller: SensitiveNotificationProtectionControllerImpl
+
+ @Before
+ fun setUp() {
+ MockitoAnnotations.initMocks(this)
+
+ controller =
+ SensitiveNotificationProtectionControllerImpl(
+ mContext,
+ mediaProjectionManager,
+ activityManager,
+ handler,
+ FakeExecutor(FakeSystemClock())
+ )
+ }
+
+ @Test
+ fun init_noRegisterMediaProjectionManagerCallback() {
+ verifyZeroInteractions(mediaProjectionManager)
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionControllerTest.kt
index 9d53e7d..a1aff48 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionControllerTest.kt
@@ -17,88 +17,93 @@
package com.android.systemui.statusbar.policy
import android.app.ActivityOptions
+import android.app.IActivityManager
import android.app.Notification
import android.media.projection.MediaProjectionInfo
import android.media.projection.MediaProjectionManager
-import android.os.Handler
+import android.platform.test.annotations.EnableFlags
import android.service.notification.StatusBarNotification
import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper.RunWithLooper
import androidx.test.filters.SmallTest
-import com.android.systemui.Flags
+import com.android.server.notification.Flags
import com.android.systemui.SysuiTestCase
import com.android.systemui.statusbar.notification.collection.NotificationEntry
+import com.android.systemui.util.concurrency.FakeExecutor
+import com.android.systemui.util.concurrency.mockExecutorHandler
+import com.android.systemui.util.mockito.whenever
+import com.android.systemui.util.mockito.withArgCaptor
+import com.android.systemui.util.time.FakeSystemClock
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
-import org.mockito.ArgumentCaptor
import org.mockito.ArgumentMatchers.any
-import org.mockito.Captor
import org.mockito.Mock
import org.mockito.Mockito.mock
-import org.mockito.Mockito.reset
import org.mockito.Mockito.times
import org.mockito.Mockito.verify
import org.mockito.Mockito.verifyNoMoreInteractions
import org.mockito.Mockito.verifyZeroInteractions
-import org.mockito.Mockito.`when`
import org.mockito.MockitoAnnotations
@SmallTest
@RunWith(AndroidTestingRunner::class)
+@RunWithLooper
+@EnableFlags(Flags.FLAG_SCREENSHARE_NOTIFICATION_HIDING)
class SensitiveNotificationProtectionControllerTest : SysuiTestCase() {
- @Mock private lateinit var handler: Handler
-
+ @Mock private lateinit var activityManager: IActivityManager
@Mock private lateinit var mediaProjectionManager: MediaProjectionManager
-
@Mock private lateinit var mediaProjectionInfo: MediaProjectionInfo
-
@Mock private lateinit var listener1: Runnable
@Mock private lateinit var listener2: Runnable
@Mock private lateinit var listener3: Runnable
- @Captor
- private lateinit var mediaProjectionCallbackCaptor:
- ArgumentCaptor<MediaProjectionManager.Callback>
-
+ private lateinit var mediaProjectionCallback: MediaProjectionManager.Callback
private lateinit var controller: SensitiveNotificationProtectionControllerImpl
@Before
fun setUp() {
+ allowTestableLooperAsMainThread() // for updating exempt packages and notifying listeners
MockitoAnnotations.initMocks(this)
- mSetFlagsRule.enableFlags(Flags.FLAG_SCREENSHARE_NOTIFICATION_HIDING)
setShareFullScreen()
+ whenever(activityManager.bugreportWhitelistedPackages)
+ .thenReturn(listOf(BUGREPORT_PACKAGE_NAME))
- controller = SensitiveNotificationProtectionControllerImpl(mediaProjectionManager, handler)
+ val executor = FakeExecutor(FakeSystemClock())
+
+ controller =
+ SensitiveNotificationProtectionControllerImpl(
+ mContext,
+ mediaProjectionManager,
+ activityManager,
+ mockExecutorHandler(executor),
+ executor
+ )
+
+ // Process exemption processing
+ executor.runAllReady()
// Obtain useful MediaProjectionCallback
- verify(mediaProjectionManager).addCallback(mediaProjectionCallbackCaptor.capture(), any())
+ mediaProjectionCallback = withArgCaptor {
+ verify(mediaProjectionManager).addCallback(capture(), any())
+ }
}
@Test
- fun init_flagEnabled_registerMediaProjectionManagerCallback() {
- assertNotNull(mediaProjectionCallbackCaptor.value)
- }
-
- @Test
- fun init_flagDisabled_noRegisterMediaProjectionManagerCallback() {
- mSetFlagsRule.disableFlags(Flags.FLAG_SCREENSHARE_NOTIFICATION_HIDING)
- reset(mediaProjectionManager)
-
- controller = SensitiveNotificationProtectionControllerImpl(mediaProjectionManager, handler)
-
- verifyZeroInteractions(mediaProjectionManager)
+ fun init_registerMediaProjectionManagerCallback() {
+ assertNotNull(mediaProjectionCallback)
}
@Test
fun registerSensitiveStateListener_singleListener() {
controller.registerSensitiveStateListener(listener1)
- mediaProjectionCallbackCaptor.value.onStart(mediaProjectionInfo)
- mediaProjectionCallbackCaptor.value.onStop(mediaProjectionInfo)
+ mediaProjectionCallback.onStart(mediaProjectionInfo)
+ mediaProjectionCallback.onStop(mediaProjectionInfo)
verify(listener1, times(2)).run()
}
@@ -108,8 +113,8 @@
controller.registerSensitiveStateListener(listener1)
controller.registerSensitiveStateListener(listener2)
- mediaProjectionCallbackCaptor.value.onStart(mediaProjectionInfo)
- mediaProjectionCallbackCaptor.value.onStop(mediaProjectionInfo)
+ mediaProjectionCallback.onStart(mediaProjectionInfo)
+ mediaProjectionCallback.onStop(mediaProjectionInfo)
verify(listener1, times(2)).run()
verify(listener2, times(2)).run()
@@ -117,12 +122,12 @@
@Test
fun registerSensitiveStateListener_afterProjectionActive() {
- mediaProjectionCallbackCaptor.value.onStart(mediaProjectionInfo)
+ mediaProjectionCallback.onStart(mediaProjectionInfo)
controller.registerSensitiveStateListener(listener1)
verifyZeroInteractions(listener1)
- mediaProjectionCallbackCaptor.value.onStop(mediaProjectionInfo)
+ mediaProjectionCallback.onStop(mediaProjectionInfo)
verify(listener1).run()
}
@@ -131,15 +136,15 @@
fun unregisterSensitiveStateListener_singleListener() {
controller.registerSensitiveStateListener(listener1)
- mediaProjectionCallbackCaptor.value.onStart(mediaProjectionInfo)
- mediaProjectionCallbackCaptor.value.onStop(mediaProjectionInfo)
+ mediaProjectionCallback.onStart(mediaProjectionInfo)
+ mediaProjectionCallback.onStop(mediaProjectionInfo)
verify(listener1, times(2)).run()
controller.unregisterSensitiveStateListener(listener1)
- mediaProjectionCallbackCaptor.value.onStart(mediaProjectionInfo)
- mediaProjectionCallbackCaptor.value.onStop(mediaProjectionInfo)
+ mediaProjectionCallback.onStart(mediaProjectionInfo)
+ mediaProjectionCallback.onStop(mediaProjectionInfo)
verifyNoMoreInteractions(listener1)
}
@@ -150,8 +155,8 @@
controller.registerSensitiveStateListener(listener2)
controller.registerSensitiveStateListener(listener3)
- mediaProjectionCallbackCaptor.value.onStart(mediaProjectionInfo)
- mediaProjectionCallbackCaptor.value.onStop(mediaProjectionInfo)
+ mediaProjectionCallback.onStart(mediaProjectionInfo)
+ mediaProjectionCallback.onStop(mediaProjectionInfo)
verify(listener1, times(2)).run()
verify(listener2, times(2)).run()
@@ -160,8 +165,8 @@
controller.unregisterSensitiveStateListener(listener1)
controller.unregisterSensitiveStateListener(listener2)
- mediaProjectionCallbackCaptor.value.onStart(mediaProjectionInfo)
- mediaProjectionCallbackCaptor.value.onStop(mediaProjectionInfo)
+ mediaProjectionCallback.onStart(mediaProjectionInfo)
+ mediaProjectionCallback.onStop(mediaProjectionInfo)
verifyNoMoreInteractions(listener1)
verifyNoMoreInteractions(listener2)
@@ -175,24 +180,24 @@
@Test
fun isSensitiveStateActive_projectionActive_true() {
- mediaProjectionCallbackCaptor.value.onStart(mediaProjectionInfo)
+ mediaProjectionCallback.onStart(mediaProjectionInfo)
assertTrue(controller.isSensitiveStateActive)
}
@Test
fun isSensitiveStateActive_projectionInactiveAfterActive_false() {
- mediaProjectionCallbackCaptor.value.onStart(mediaProjectionInfo)
- mediaProjectionCallbackCaptor.value.onStop(mediaProjectionInfo)
+ mediaProjectionCallback.onStart(mediaProjectionInfo)
+ mediaProjectionCallback.onStop(mediaProjectionInfo)
assertFalse(controller.isSensitiveStateActive)
}
@Test
fun isSensitiveStateActive_projectionActiveAfterInactive_true() {
- mediaProjectionCallbackCaptor.value.onStart(mediaProjectionInfo)
- mediaProjectionCallbackCaptor.value.onStop(mediaProjectionInfo)
- mediaProjectionCallbackCaptor.value.onStart(mediaProjectionInfo)
+ mediaProjectionCallback.onStart(mediaProjectionInfo)
+ mediaProjectionCallback.onStop(mediaProjectionInfo)
+ mediaProjectionCallback.onStart(mediaProjectionInfo)
assertTrue(controller.isSensitiveStateActive)
}
@@ -200,7 +205,25 @@
@Test
fun isSensitiveStateActive_projectionActive_singleActivity_false() {
setShareSingleApp()
- mediaProjectionCallbackCaptor.value.onStart(mediaProjectionInfo)
+ mediaProjectionCallback.onStart(mediaProjectionInfo)
+
+ assertFalse(controller.isSensitiveStateActive)
+ }
+
+ @Test
+ fun isSensitiveStateActive_projectionActive_sysuiExempt_false() {
+ // SystemUi context packge name is exempt, but in test scenarios its
+ // com.android.systemui.tests so use that instead of hardcoding
+ whenever(mediaProjectionInfo.packageName).thenReturn(mContext.packageName)
+ mediaProjectionCallback.onStart(mediaProjectionInfo)
+
+ assertFalse(controller.isSensitiveStateActive)
+ }
+
+ @Test
+ fun isSensitiveStateActive_projectionActive_bugReportHandlerExempt_false() {
+ whenever(mediaProjectionInfo.packageName).thenReturn(BUGREPORT_PACKAGE_NAME)
+ mediaProjectionCallback.onStart(mediaProjectionInfo)
assertFalse(controller.isSensitiveStateActive)
}
@@ -215,7 +238,7 @@
@Test
fun shouldProtectNotification_projectionActive_singleActivity_false() {
setShareSingleApp()
- mediaProjectionCallbackCaptor.value.onStart(mediaProjectionInfo)
+ mediaProjectionCallback.onStart(mediaProjectionInfo)
val notificationEntry = setupNotificationEntry(TEST_PACKAGE_NAME)
@@ -224,7 +247,7 @@
@Test
fun shouldProtectNotification_projectionActive_fgsNotificationFromProjectionApp_false() {
- mediaProjectionCallbackCaptor.value.onStart(mediaProjectionInfo)
+ mediaProjectionCallback.onStart(mediaProjectionInfo)
val notificationEntry = setupFgsNotificationEntry(TEST_PROJECTION_PACKAGE_NAME)
@@ -233,7 +256,7 @@
@Test
fun shouldProtectNotification_projectionActive_fgsNotificationNotFromProjectionApp_true() {
- mediaProjectionCallbackCaptor.value.onStart(mediaProjectionInfo)
+ mediaProjectionCallback.onStart(mediaProjectionInfo)
val notificationEntry = setupFgsNotificationEntry(TEST_PACKAGE_NAME)
@@ -242,21 +265,43 @@
@Test
fun shouldProtectNotification_projectionActive_notFgsNotification_true() {
- mediaProjectionCallbackCaptor.value.onStart(mediaProjectionInfo)
+ mediaProjectionCallback.onStart(mediaProjectionInfo)
val notificationEntry = setupNotificationEntry(TEST_PROJECTION_PACKAGE_NAME)
assertTrue(controller.shouldProtectNotification(notificationEntry))
}
+ @Test
+ fun shouldProtectNotification_projectionActive_sysuiExempt_false() {
+ // SystemUi context packge name is exempt, but in test scenarios its
+ // com.android.systemui.tests so use that instead of hardcoding
+ whenever(mediaProjectionInfo.packageName).thenReturn(mContext.packageName)
+ mediaProjectionCallback.onStart(mediaProjectionInfo)
+
+ val notificationEntry = setupNotificationEntry(TEST_PACKAGE_NAME, false)
+
+ assertFalse(controller.shouldProtectNotification(notificationEntry))
+ }
+
+ @Test
+ fun shouldProtectNotification_projectionActive_bugReportHandlerExempt_false() {
+ whenever(mediaProjectionInfo.packageName).thenReturn(BUGREPORT_PACKAGE_NAME)
+ mediaProjectionCallback.onStart(mediaProjectionInfo)
+
+ val notificationEntry = setupNotificationEntry(TEST_PACKAGE_NAME, false)
+
+ assertFalse(controller.shouldProtectNotification(notificationEntry))
+ }
+
private fun setShareFullScreen() {
- `when`(mediaProjectionInfo.packageName).thenReturn(TEST_PROJECTION_PACKAGE_NAME)
- `when`(mediaProjectionInfo.launchCookie).thenReturn(null)
+ whenever(mediaProjectionInfo.packageName).thenReturn(TEST_PROJECTION_PACKAGE_NAME)
+ whenever(mediaProjectionInfo.launchCookie).thenReturn(null)
}
private fun setShareSingleApp() {
- `when`(mediaProjectionInfo.packageName).thenReturn(TEST_PROJECTION_PACKAGE_NAME)
- `when`(mediaProjectionInfo.launchCookie).thenReturn(ActivityOptions.LaunchCookie())
+ whenever(mediaProjectionInfo.packageName).thenReturn(TEST_PROJECTION_PACKAGE_NAME)
+ whenever(mediaProjectionInfo.launchCookie).thenReturn(ActivityOptions.LaunchCookie())
}
private fun setupNotificationEntry(
@@ -266,10 +311,10 @@
val notificationEntry = mock(NotificationEntry::class.java)
val sbn = mock(StatusBarNotification::class.java)
val notification = mock(Notification::class.java)
- `when`(notificationEntry.sbn).thenReturn(sbn)
- `when`(sbn.packageName).thenReturn(packageName)
- `when`(sbn.notification).thenReturn(notification)
- `when`(notification.isFgsOrUij).thenReturn(isFgs)
+ whenever(notificationEntry.sbn).thenReturn(sbn)
+ whenever(sbn.packageName).thenReturn(packageName)
+ whenever(sbn.notification).thenReturn(notification)
+ whenever(notification.isFgsOrUij).thenReturn(isFgs)
return notificationEntry
}
@@ -282,5 +327,6 @@
private const val TEST_PROJECTION_PACKAGE_NAME =
"com.android.systemui.statusbar.policy.projectionpackage"
private const val TEST_PACKAGE_NAME = "com.android.systemui.statusbar.policy.testpackage"
+ private const val BUGREPORT_PACKAGE_NAME = "com.android.test.bugreporthandler"
}
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/theme/ThemeOverlayApplierTest.java b/packages/SystemUI/tests/src/com/android/systemui/theme/ThemeOverlayApplierTest.java
index 83439f0..8f4cbaf 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/theme/ThemeOverlayApplierTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/theme/ThemeOverlayApplierTest.java
@@ -104,9 +104,9 @@
@Before
public void setup() throws Exception {
MockitoAnnotations.initMocks(this);
- mManager = new ThemeOverlayApplier(mOverlayManager,
- MoreExecutors.directExecutor(),
- LAUNCHER_PACKAGE, THEMEPICKER_PACKAGE, mDumpManager) {
+ mManager = new ThemeOverlayApplier(mOverlayManager, MoreExecutors.directExecutor(),
+ LAUNCHER_PACKAGE, THEMEPICKER_PACKAGE, mDumpManager,
+ MoreExecutors.directExecutor()) {
@Override
protected OverlayManagerTransaction.Builder getTransactionBuilder() {
return mTransactionBuilder;
@@ -179,7 +179,7 @@
@Test
public void allCategoriesSpecified_allEnabledExclusively() {
mManager.applyCurrentUserOverlays(ALL_CATEGORIES_MAP, null, TEST_USER.getIdentifier(),
- TEST_USER_HANDLES);
+ TEST_USER_HANDLES, null);
verify(mOverlayManager).commit(any());
for (OverlayIdentifier overlayPackage : ALL_CATEGORIES_MAP.values()) {
@@ -191,7 +191,7 @@
@Test
public void allCategoriesSpecified_sysuiCategoriesAlsoAppliedToSysuiUser() {
mManager.applyCurrentUserOverlays(ALL_CATEGORIES_MAP, null, TEST_USER.getIdentifier(),
- TEST_USER_HANDLES);
+ TEST_USER_HANDLES, null);
for (Map.Entry<String, OverlayIdentifier> entry : ALL_CATEGORIES_MAP.entrySet()) {
if (SYSTEM_USER_CATEGORIES.contains(entry.getKey())) {
@@ -208,7 +208,7 @@
public void allCategoriesSpecified_enabledForAllUserHandles() {
Set<UserHandle> userHandles = Sets.newHashSet(TEST_USER_HANDLES);
mManager.applyCurrentUserOverlays(ALL_CATEGORIES_MAP, null, TEST_USER.getIdentifier(),
- userHandles);
+ userHandles, null);
for (OverlayIdentifier overlayPackage : ALL_CATEGORIES_MAP.values()) {
verify(mTransactionBuilder).setEnabled(eq(overlayPackage), eq(true),
@@ -225,7 +225,7 @@
Set<UserHandle> userHandles = Sets.newHashSet(TEST_USER_HANDLES);
mManager.applyCurrentUserOverlays(ALL_CATEGORIES_MAP, null, TEST_USER.getIdentifier(),
- userHandles);
+ userHandles, null);
for (OverlayIdentifier overlayPackage : ALL_CATEGORIES_MAP.values()) {
verify(mTransactionBuilder, never()).setEnabled(eq(overlayPackage), eq(true),
@@ -239,7 +239,7 @@
mock(FabricatedOverlay.class)
};
mManager.applyCurrentUserOverlays(ALL_CATEGORIES_MAP, pendingCreation,
- TEST_USER.getIdentifier(), TEST_USER_HANDLES);
+ TEST_USER.getIdentifier(), TEST_USER_HANDLES, null);
for (FabricatedOverlay overlay : pendingCreation) {
verify(mTransactionBuilder).registerFabricatedOverlay(eq(overlay));
@@ -253,7 +253,7 @@
categoryToPackage.remove(OVERLAY_CATEGORY_ICON_ANDROID);
mManager.applyCurrentUserOverlays(categoryToPackage, null, TEST_USER.getIdentifier(),
- TEST_USER_HANDLES);
+ TEST_USER_HANDLES, null);
for (OverlayIdentifier overlayPackage : categoryToPackage.values()) {
verify(mTransactionBuilder).setEnabled(eq(overlayPackage), eq(true),
@@ -270,7 +270,7 @@
@Test
public void zeroCategoriesSpecified_allDisabled() {
mManager.applyCurrentUserOverlays(Maps.newArrayMap(), null, TEST_USER.getIdentifier(),
- TEST_USER_HANDLES);
+ TEST_USER_HANDLES, null);
for (String category : THEME_CATEGORIES) {
verify(mTransactionBuilder).setEnabled(
@@ -285,7 +285,7 @@
categoryToPackage.put("blah.category", new OverlayIdentifier("com.example.blah.category"));
mManager.applyCurrentUserOverlays(categoryToPackage, null, TEST_USER.getIdentifier(),
- TEST_USER_HANDLES);
+ TEST_USER_HANDLES, null);
verify(mTransactionBuilder, never()).setEnabled(
eq(new OverlayIdentifier("com.example.blah.category")), eq(false),
diff --git a/packages/SystemUI/tests/src/com/android/systemui/theme/ThemeOverlayControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/theme/ThemeOverlayControllerTest.java
index b58a41c..c02583a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/theme/ThemeOverlayControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/theme/ThemeOverlayControllerTest.java
@@ -37,6 +37,7 @@
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
+import android.app.ActivityManager;
import android.app.UiModeManager;
import android.app.WallpaperColors;
import android.app.WallpaperManager;
@@ -129,6 +130,8 @@
private WakefulnessLifecycle mWakefulnessLifecycle;
@Mock
private UiModeManager mUiModeManager;
+ @Mock
+ private ActivityManager mActivityManager;
@Captor
private ArgumentCaptor<BroadcastReceiver> mBroadcastReceiver;
@Captor
@@ -164,7 +167,7 @@
mBroadcastDispatcher, mBgHandler, mMainExecutor, mBgExecutor, mThemeOverlayApplier,
mSecureSettings, mWallpaperManager, mUserManager, mDeviceProvisionedController,
mUserTracker, mDumpManager, mFeatureFlags, mResources, mWakefulnessLifecycle,
- mJavaAdapter, mKeyguardTransitionInteractor, mUiModeManager) {
+ mJavaAdapter, mKeyguardTransitionInteractor, mUiModeManager, mActivityManager) {
@VisibleForTesting
protected boolean isNightMode() {
return false;
@@ -224,7 +227,7 @@
ArgumentCaptor.forClass(Map.class);
verify(mThemeOverlayApplier)
- .applyCurrentUserOverlays(themeOverlays.capture(), any(), anyInt(), any());
+ .applyCurrentUserOverlays(themeOverlays.capture(), any(), anyInt(), any(), any());
// Assert that we received the colors that we were expecting
assertThat(themeOverlays.getValue().get(OVERLAY_CATEGORY_SYSTEM_PALETTE))
@@ -249,7 +252,7 @@
mBroadcastReceiver.getValue().onReceive(null, intent);
mColorsListener.getValue().onColorsChanged(new WallpaperColors(Color.valueOf(Color.BLACK),
null, null), WallpaperManager.FLAG_SYSTEM, USER_SYSTEM);
- verify(mThemeOverlayApplier).applyCurrentUserOverlays(any(), any(), anyInt(), any());
+ verify(mThemeOverlayApplier).applyCurrentUserOverlays(any(), any(), anyInt(), any(), any());
}
@Test
@@ -263,7 +266,7 @@
ArgumentCaptor.forClass(Map.class);
verify(mThemeOverlayApplier)
- .applyCurrentUserOverlays(themeOverlays.capture(), any(), anyInt(), any());
+ .applyCurrentUserOverlays(themeOverlays.capture(), any(), anyInt(), any(), any());
// Should not change theme after changing wallpapers, if intent doesn't have
// WallpaperManager.EXTRA_FROM_FOREGROUND_APP set to true.
@@ -272,7 +275,7 @@
mColorsListener.getValue().onColorsChanged(new WallpaperColors(Color.valueOf(Color.BLACK),
null, null), WallpaperManager.FLAG_SYSTEM, USER_SYSTEM);
verify(mThemeOverlayApplier, never())
- .applyCurrentUserOverlays(any(), any(), anyInt(), any());
+ .applyCurrentUserOverlays(any(), any(), anyInt(), any(), any());
}
@Test
@@ -294,7 +297,7 @@
ArgumentCaptor.forClass(Map.class);
verify(mThemeOverlayApplier)
- .applyCurrentUserOverlays(themeOverlays.capture(), any(), anyInt(), any());
+ .applyCurrentUserOverlays(themeOverlays.capture(), any(), anyInt(), any(), any());
// Assert that we received the colors that we were expecting
assertThat(themeOverlays.getValue().get(OVERLAY_CATEGORY_SYSTEM_PALETTE))
@@ -333,7 +336,7 @@
.isFalse();
verify(mThemeOverlayApplier)
- .applyCurrentUserOverlays(any(), any(), anyInt(), any());
+ .applyCurrentUserOverlays(any(), any(), anyInt(), any(), any());
}
@Test
@@ -367,8 +370,7 @@
assertThat(updatedSetting.getValue().contains(
"android.theme.customization.color_both\":\"0")).isTrue();
- verify(mThemeOverlayApplier)
- .applyCurrentUserOverlays(any(), any(), anyInt(), any());
+ verify(mThemeOverlayApplier).applyCurrentUserOverlays(any(), any(), anyInt(), any(), any());
}
@Test
@@ -423,7 +425,7 @@
assertThat(updatedSetting.getValue().contains(
"android.theme.customization.color_both\":\"1")).isTrue();
verify(mThemeOverlayApplier)
- .applyCurrentUserOverlays(any(), any(), anyInt(), any());
+ .applyCurrentUserOverlays(any(), any(), anyInt(), any(), any());
}
@Test
@@ -492,7 +494,7 @@
"android.theme.customization.color_both\":\"1")).isTrue();
verify(mThemeOverlayApplier)
- .applyCurrentUserOverlays(any(), any(), anyInt(), any());
+ .applyCurrentUserOverlays(any(), any(), anyInt(), any(), any());
}
@Test
@@ -523,7 +525,7 @@
assertThat(updatedSetting.getValue().contains("android.theme.customization.color_index"))
.isFalse();
verify(mThemeOverlayApplier)
- .applyCurrentUserOverlays(any(), any(), anyInt(), any());
+ .applyCurrentUserOverlays(any(), any(), anyInt(), any(), any());
}
@Test
@@ -554,7 +556,7 @@
assertThat(updatedSetting.getValue().contains("android.theme.customization.color_index"))
.isFalse();
verify(mThemeOverlayApplier)
- .applyCurrentUserOverlays(any(), any(), anyInt(), any());
+ .applyCurrentUserOverlays(any(), any(), anyInt(), any(), any());
}
@Test
@@ -587,7 +589,7 @@
anyInt());
verify(mThemeOverlayApplier)
- .applyCurrentUserOverlays(any(), any(), anyInt(), any());
+ .applyCurrentUserOverlays(any(), any(), anyInt(), any(), any());
}
@Test
@@ -620,7 +622,7 @@
eq(Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES), updatedSetting.capture());
// Apply overlay by existing theme from secure setting
- verify(mThemeOverlayApplier).applyCurrentUserOverlays(any(), any(), anyInt(), any());
+ verify(mThemeOverlayApplier).applyCurrentUserOverlays(any(), any(), anyInt(), any(), any());
}
@Test
@@ -653,7 +655,7 @@
verify(mThemeOverlayApplier, never())
- .applyCurrentUserOverlays(any(), any(), anyInt(), any());
+ .applyCurrentUserOverlays(any(), any(), anyInt(), any(), any());
}
@Test
@@ -675,7 +677,7 @@
ArgumentCaptor<Map<String, OverlayIdentifier>> themeOverlays =
ArgumentCaptor.forClass(Map.class);
verify(mThemeOverlayApplier)
- .applyCurrentUserOverlays(themeOverlays.capture(), any(), anyInt(), any());
+ .applyCurrentUserOverlays(themeOverlays.capture(), any(), anyInt(), any(), any());
// Assert that we received secondary user colors
assertThat(themeOverlays.getValue().get(OVERLAY_CATEGORY_SYSTEM_PALETTE))
@@ -689,7 +691,7 @@
mBroadcastReceiver.getValue().onReceive(null,
new Intent(Intent.ACTION_PROFILE_ADDED)
.putExtra(Intent.EXTRA_USER, MANAGED_USER_HANDLE));
- verify(mThemeOverlayApplier).applyCurrentUserOverlays(any(), any(), anyInt(), any());
+ verify(mThemeOverlayApplier).applyCurrentUserOverlays(any(), any(), anyInt(), any(), any());
}
@Test
@@ -700,7 +702,7 @@
new Intent(Intent.ACTION_PROFILE_ADDED)
.putExtra(Intent.EXTRA_USER, MANAGED_USER_HANDLE));
verify(mThemeOverlayApplier)
- .applyCurrentUserOverlays(any(), any(), anyInt(), any());
+ .applyCurrentUserOverlays(any(), any(), anyInt(), any(), any());
}
@Test
@@ -711,7 +713,7 @@
new Intent(Intent.ACTION_PROFILE_ADDED)
.putExtra(Intent.EXTRA_USER, MANAGED_USER_HANDLE));
verify(mThemeOverlayApplier, never())
- .applyCurrentUserOverlays(any(), any(), anyInt(), any());
+ .applyCurrentUserOverlays(any(), any(), anyInt(), any(), any());
}
@Test
@@ -723,7 +725,7 @@
(new Intent(Intent.ACTION_PROFILE_ADDED))
.putExtra(Intent.EXTRA_USER, PRIVATE_USER_HANDLE));
verify(mThemeOverlayApplier, never())
- .applyCurrentUserOverlays(any(), any(), anyInt(), any());
+ .applyCurrentUserOverlays(any(), any(), anyInt(), any(), any());
}
@@ -737,7 +739,7 @@
mColorsListener.getValue().onColorsChanged(mainColors, WallpaperManager.FLAG_SYSTEM,
USER_SYSTEM);
- verify(mThemeOverlayApplier).applyCurrentUserOverlays(any(), any(), anyInt(), any());
+ verify(mThemeOverlayApplier).applyCurrentUserOverlays(any(), any(), anyInt(), any(), any());
// Regression test: null events should not reset the internal state and allow colors to be
// applied again.
@@ -747,11 +749,11 @@
mBroadcastReceiver.getValue().onReceive(null, intent);
mColorsListener.getValue().onColorsChanged(null, WallpaperManager.FLAG_SYSTEM, USER_SYSTEM);
verify(mThemeOverlayApplier, never()).applyCurrentUserOverlays(any(), any(), anyInt(),
- any());
+ any(), any());
mColorsListener.getValue().onColorsChanged(new WallpaperColors(Color.valueOf(Color.GREEN),
null, null), WallpaperManager.FLAG_SYSTEM, USER_SYSTEM);
verify(mThemeOverlayApplier, never()).applyCurrentUserOverlays(any(), any(), anyInt(),
- any());
+ any(), any());
}
@Test
@@ -770,7 +772,7 @@
mBroadcastDispatcher, mBgHandler, executor, executor, mThemeOverlayApplier,
mSecureSettings, mWallpaperManager, mUserManager, mDeviceProvisionedController,
mUserTracker, mDumpManager, mFeatureFlags, mResources, mWakefulnessLifecycle,
- mJavaAdapter, mKeyguardTransitionInteractor, mUiModeManager) {
+ mJavaAdapter, mKeyguardTransitionInteractor, mUiModeManager, mActivityManager) {
@VisibleForTesting
protected boolean isNightMode() {
return false;
@@ -791,7 +793,7 @@
verify(mDeviceProvisionedController).addCallback(mDeviceProvisionedListener.capture());
// Colors were applied during controller initialization.
- verify(mThemeOverlayApplier).applyCurrentUserOverlays(any(), any(), anyInt(), any());
+ verify(mThemeOverlayApplier).applyCurrentUserOverlays(any(), any(), anyInt(), any(), any());
clearInvocations(mThemeOverlayApplier);
}
@@ -810,7 +812,7 @@
mBroadcastDispatcher, mBgHandler, executor, executor, mThemeOverlayApplier,
mSecureSettings, mWallpaperManager, mUserManager, mDeviceProvisionedController,
mUserTracker, mDumpManager, mFeatureFlags, mResources, mWakefulnessLifecycle,
- mJavaAdapter, mKeyguardTransitionInteractor, mUiModeManager) {
+ mJavaAdapter, mKeyguardTransitionInteractor, mUiModeManager, mActivityManager) {
@VisibleForTesting
protected boolean isNightMode() {
return false;
@@ -831,7 +833,7 @@
verify(mDeviceProvisionedController).addCallback(mDeviceProvisionedListener.capture());
// Colors were applied during controller initialization.
- verify(mThemeOverlayApplier).applyCurrentUserOverlays(any(), any(), anyInt(), any());
+ verify(mThemeOverlayApplier).applyCurrentUserOverlays(any(), any(), anyInt(), any(), any());
clearInvocations(mThemeOverlayApplier);
WallpaperColors mainColors = new WallpaperColors(Color.valueOf(Color.RED),
@@ -853,12 +855,12 @@
// Defers event because we already have initial colors.
verify(mThemeOverlayApplier, never())
- .applyCurrentUserOverlays(any(), any(), anyInt(), any());
+ .applyCurrentUserOverlays(any(), any(), anyInt(), any(), any());
// Then event happens after setup phase is over.
when(mDeviceProvisionedController.isCurrentUserSetup()).thenReturn(true);
mDeviceProvisionedListener.getValue().onUserSetupChanged();
- verify(mThemeOverlayApplier).applyCurrentUserOverlays(any(), any(), anyInt(), any());
+ verify(mThemeOverlayApplier).applyCurrentUserOverlays(any(), any(), anyInt(), any(), any());
}
@Test
@@ -881,11 +883,11 @@
mColorsListener.getValue().onColorsChanged(mainColors, WallpaperManager.FLAG_SYSTEM,
USER_SYSTEM);
verify(mThemeOverlayApplier, never())
- .applyCurrentUserOverlays(any(), any(), anyInt(), any());
+ .applyCurrentUserOverlays(any(), any(), anyInt(), any(), any());
mWakefulnessLifecycle.dispatchFinishedGoingToSleep();
verify(mThemeOverlayApplier, never())
- .applyCurrentUserOverlays(any(), any(), anyInt(), any());
+ .applyCurrentUserOverlays(any(), any(), anyInt(), any(), any());
}
@Test
@@ -907,10 +909,10 @@
mColorsListener.getValue().onColorsChanged(mainColors, WallpaperManager.FLAG_SYSTEM,
USER_SYSTEM);
verify(mThemeOverlayApplier, never())
- .applyCurrentUserOverlays(any(), any(), anyInt(), any());
+ .applyCurrentUserOverlays(any(), any(), anyInt(), any(), any());
mWakefulnessLifecycleObserver.getValue().onFinishedGoingToSleep();
- verify(mThemeOverlayApplier).applyCurrentUserOverlays(any(), any(), anyInt(), any());
+ verify(mThemeOverlayApplier).applyCurrentUserOverlays(any(), any(), anyInt(), any(), any());
}
@Test
@@ -930,7 +932,7 @@
ArgumentCaptor.forClass(Map.class);
verify(mThemeOverlayApplier)
- .applyCurrentUserOverlays(themeOverlays.capture(), any(), anyInt(), any());
+ .applyCurrentUserOverlays(themeOverlays.capture(), any(), anyInt(), any(), any());
// Assert that we received the colors that we were expecting
assertThat(themeOverlays.getValue().get(OVERLAY_CATEGORY_SYSTEM_PALETTE))
@@ -949,19 +951,19 @@
mColorsListener.getValue().onColorsChanged(startingColors, WallpaperManager.FLAG_SYSTEM,
USER_SYSTEM);
verify(mThemeOverlayApplier)
- .applyCurrentUserOverlays(any(), any(), anyInt(), any());
+ .applyCurrentUserOverlays(any(), any(), anyInt(), any(), any());
clearInvocations(mThemeOverlayApplier);
// Set to the same colors.
mColorsListener.getValue().onColorsChanged(sameColors, WallpaperManager.FLAG_SYSTEM,
USER_SYSTEM);
verify(mThemeOverlayApplier, never())
- .applyCurrentUserOverlays(any(), any(), anyInt(), any());
+ .applyCurrentUserOverlays(any(), any(), anyInt(), any(), any());
// Verify that no change resulted.
mWakefulnessLifecycleObserver.getValue().onFinishedGoingToSleep();
verify(mThemeOverlayApplier, never()).applyCurrentUserOverlays(any(), any(), anyInt(),
- any());
+ any(), any());
}
@Test
@@ -975,7 +977,7 @@
ArgumentCaptor.forClass(FabricatedOverlay[].class);
verify(mThemeOverlayApplier)
- .applyCurrentUserOverlays(any(), themeOverlays.capture(), anyInt(), any());
+ .applyCurrentUserOverlays(any(), themeOverlays.capture(), anyInt(), any(), any());
FabricatedOverlay[] overlays = themeOverlays.getValue();
FabricatedOverlay accents = overlays[0];
diff --git a/packages/SystemUI/tests/src/com/android/systemui/tracing/TraceUtilsTest.kt b/packages/SystemUI/tests/src/com/android/systemui/tracing/TraceUtilsTest.kt
index ba34ce6..bda339f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/tracing/TraceUtilsTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/tracing/TraceUtilsTest.kt
@@ -12,7 +12,7 @@
* permissions and limitations under the License.
*/
-package com.android.app.tracing
+package com.android.systemui.tracing
import android.os.Handler
import android.os.Looper
@@ -20,11 +20,13 @@
import android.testing.AndroidTestingRunner
import android.util.Log
import androidx.test.filters.SmallTest
+import com.android.app.tracing.TraceUtils.traceRunnable
+import com.android.app.tracing.namedRunnable
+import com.android.app.tracing.traceSection
import com.android.systemui.SysuiTestCase
import org.junit.After
import org.junit.Assert.assertThrows
import org.junit.Before
-import org.junit.Ignore
import org.junit.Test
import org.junit.runner.RunWith
@@ -68,7 +70,6 @@
}
@Test
- @Ignore("b/267482189 - Enable once androidx.tracing >= 1.2.0-beta04")
fun testLongTraceSection_doesNotThrow_whenUsingAndroidX() {
androidx.tracing.Trace.beginSection(SECTION_NAME_THATS_TOO_LONG)
}
@@ -84,17 +85,13 @@
fun testLongTraceSection_doesNotThrow_whenUsedAsTraceNameSupplier() {
Handler(Looper.getMainLooper())
.runWithScissors(
- TraceUtils.namedRunnable(SECTION_NAME_THATS_TOO_LONG) {
- Log.v(TAG, "TraceUtils.namedRunnable() block.")
- },
+ namedRunnable(SECTION_NAME_THATS_TOO_LONG) { Log.v(TAG, "namedRunnable() block.") },
TEST_FAIL_TIMEOUT
)
}
@Test
fun testLongTraceSection_doesNotThrow_whenUsingTraceRunnable() {
- TraceUtils.traceRunnable(SECTION_NAME_THATS_TOO_LONG) {
- Log.v(TAG, "TraceUtils.traceRunnable() block.")
- }
+ traceRunnable(SECTION_NAME_THATS_TOO_LONG) { Log.v(TAG, "traceRunnable() block.") }.run()
}
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserSwitcherInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserSwitcherInteractorTest.kt
index b6a033a..1b43851 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserSwitcherInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserSwitcherInteractorTest.kt
@@ -46,6 +46,7 @@
import com.android.systemui.kosmos.testDispatcher
import com.android.systemui.kosmos.testScope
import com.android.systemui.plugins.ActivityStarter
+import com.android.systemui.process.processWrapper
import com.android.systemui.qs.user.UserSwitchDialogController
import com.android.systemui.res.R
import com.android.systemui.statusbar.policy.DeviceProvisionedController
@@ -1147,6 +1148,7 @@
uiEventLogger = uiEventLogger,
featureFlags = kosmos.fakeFeatureFlagsClassic,
userRestrictionChecker = mock(),
+ processWrapper = kosmos.processWrapper,
)
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/StatusBarUserChipViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/StatusBarUserChipViewModelTest.kt
index 21d4549..661837b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/StatusBarUserChipViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/StatusBarUserChipViewModelTest.kt
@@ -34,6 +34,7 @@
import com.android.systemui.flags.Flags
import com.android.systemui.keyguard.domain.interactor.KeyguardInteractorFactory
import com.android.systemui.plugins.ActivityStarter
+import com.android.systemui.process.ProcessWrapperFake
import com.android.systemui.statusbar.policy.DeviceProvisionedController
import com.android.systemui.telephony.data.repository.FakeTelephonyRepository
import com.android.systemui.telephony.domain.interactor.TelephonyInteractor
@@ -264,6 +265,7 @@
guestUserInteractor = guestUserInteractor,
uiEventLogger = uiEventLogger,
userRestrictionChecker = mock(),
+ processWrapper = ProcessWrapperFake()
)
)
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModelTest.kt
index d0804be..5661e20 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModelTest.kt
@@ -34,6 +34,7 @@
import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
import com.android.systemui.keyguard.domain.interactor.KeyguardInteractorFactory
import com.android.systemui.plugins.ActivityStarter
+import com.android.systemui.process.ProcessWrapperFake
import com.android.systemui.statusbar.policy.DeviceProvisionedController
import com.android.systemui.telephony.data.repository.FakeTelephonyRepository
import com.android.systemui.telephony.domain.interactor.TelephonyInteractor
@@ -176,6 +177,7 @@
guestUserInteractor = guestUserInteractor,
uiEventLogger = uiEventLogger,
userRestrictionChecker = mock(),
+ processWrapper = ProcessWrapperFake()
),
guestUserInteractor = guestUserInteractor,
)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/util/TestUtils.kt b/packages/SystemUI/tests/src/com/android/systemui/util/TestUtils.kt
new file mode 100644
index 0000000..27cd4b6
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/util/TestUtils.kt
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.util
+
+import junit.framework.Assert
+
+/**
+ * Assert that a list's values match the corresponding predicates.
+ *
+ * Useful to test animations or more complex objects where you only care about some of an object's
+ * properties.
+ */
+fun <T> List<T>.assertValuesMatch(vararg matchers: (T) -> Boolean) {
+ if (size != matchers.size) {
+ Assert.fail("Expected size ${matchers.size}, but was size $size:\n$this")
+ }
+
+ for (i in indices) {
+ if (!matchers[i].invoke(this[i])) {
+ Assert.fail("Assertion failed. Element #$i did not match:\n${this[i]}")
+ }
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/util/settings/repository/UserAwareSecureSettingsRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/util/settings/repository/UserAwareSecureSettingsRepositoryTest.kt
new file mode 100644
index 0000000..913759f
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/util/settings/repository/UserAwareSecureSettingsRepositoryTest.kt
@@ -0,0 +1,108 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.util.settings.repository
+
+import android.content.pm.UserInfo
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.coroutines.collectValues
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.user.data.repository.fakeUserRepository
+import com.android.systemui.util.settings.FakeSettings
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(JUnit4::class)
+class UserAwareSecureSettingsRepositoryTest : SysuiTestCase() {
+
+ private val dispatcher = StandardTestDispatcher()
+ private val testScope = TestScope(dispatcher)
+ private val secureSettings = FakeSettings()
+ private val userRepository = Kosmos().fakeUserRepository
+ private lateinit var repository: UserAwareSecureSettingsRepository
+
+ @Before
+ fun setup() {
+ repository = UserAwareSecureSettingsRepositoryImpl(
+ secureSettings,
+ userRepository,
+ dispatcher,
+ )
+ userRepository.setUserInfos(USER_INFOS)
+ setSettingValueForUser(enabled = true, userInfo = SETTING_ENABLED_USER)
+ setSettingValueForUser(enabled = false, userInfo = SETTING_DISABLED_USER)
+ }
+
+ @Test
+ fun settingEnabledEmitsValueForCurrentUser() {
+ testScope.runTest {
+ userRepository.setSelectedUserInfo(SETTING_ENABLED_USER)
+
+ val enabled by collectLastValue(repository.boolSettingForActiveUser(SETTING_NAME))
+
+ assertThat(enabled).isTrue()
+ }
+ }
+
+ @Test
+ fun settingEnabledEmitsNewValueWhenSettingChanges() {
+ testScope.runTest {
+ userRepository.setSelectedUserInfo(SETTING_ENABLED_USER)
+ val enabled by collectValues(repository.boolSettingForActiveUser(SETTING_NAME))
+ runCurrent()
+
+ setSettingValueForUser(enabled = false, userInfo = SETTING_ENABLED_USER)
+
+ assertThat(enabled).containsExactly(true, false).inOrder()
+ }
+ }
+
+ @Test
+ fun settingEnabledEmitsValueForNewUserWhenUserChanges() {
+ testScope.runTest {
+ userRepository.setSelectedUserInfo(SETTING_ENABLED_USER)
+ val enabled by collectLastValue(repository.boolSettingForActiveUser(SETTING_NAME))
+ runCurrent()
+
+ userRepository.setSelectedUserInfo(SETTING_DISABLED_USER)
+
+ assertThat(enabled).isFalse()
+ }
+ }
+
+ private fun setSettingValueForUser(enabled: Boolean, userInfo: UserInfo) {
+ secureSettings.putBoolForUser(SETTING_NAME, enabled, userInfo.id)
+ }
+
+ private companion object {
+ const val SETTING_NAME = "SETTING_NAME"
+ val SETTING_ENABLED_USER = UserInfo(/* id= */ 0, "user1", /* flags= */ 0)
+ val SETTING_DISABLED_USER = UserInfo(/* id= */ 1, "user2", /* flags= */ 0)
+ val USER_INFOS = listOf(SETTING_ENABLED_USER, SETTING_DISABLED_USER)
+ }
+}
\ No newline at end of file
diff --git a/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java
index 7a8dce8..8a33778 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java
@@ -64,7 +64,6 @@
import com.android.internal.jank.InteractionJankMonitor;
import com.android.internal.logging.testing.UiEventLoggerFake;
-import com.android.keyguard.TestScopeProvider;
import com.android.systemui.Prefs;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.animation.AnimatorTestRule;
@@ -102,8 +101,6 @@
import java.util.Arrays;
import java.util.function.Predicate;
-import kotlinx.coroutines.Dispatchers;
-
@SmallTest
@RunWith(AndroidTestingRunner.class)
@TestableLooper.RunWithLooper(setAsMainLooper = true)
@@ -208,8 +205,6 @@
mDumpManager,
mLazySecureSettings,
mVibratorHelper,
- Dispatchers.getUnconfined(),
- TestScopeProvider.getTestScope(),
new FakeSystemClock());
mDialog.init(0, null);
State state = createShellState();
diff --git a/packages/SystemUI/tests/src/com/android/systemui/wallet/controller/QuickAccessWalletControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/wallet/controller/QuickAccessWalletControllerTest.java
index 8263174..fccb936 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/wallet/controller/QuickAccessWalletControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/wallet/controller/QuickAccessWalletControllerTest.java
@@ -37,10 +37,10 @@
import androidx.test.filters.SmallTest;
-import com.android.systemui.res.R;
import com.android.systemui.SysuiTestCase;
-import com.android.systemui.animation.ActivityLaunchAnimator;
+import com.android.systemui.animation.ActivityTransitionAnimator;
import com.android.systemui.plugins.ActivityStarter;
+import com.android.systemui.res.R;
import com.android.systemui.util.settings.SecureSettings;
import com.android.systemui.util.time.FakeSystemClock;
@@ -68,7 +68,7 @@
@Mock
private ActivityStarter mActivityStarter;
@Mock
- private ActivityLaunchAnimator.Controller mAnimationController;
+ private ActivityTransitionAnimator.Controller mAnimationController;
@Captor
private ArgumentCaptor<GetWalletCardsRequest> mRequestCaptor;
@Captor
@@ -219,7 +219,7 @@
public void getQuickAccessUiIntent_hasCards_noPendingIntent_startsWalletActivity() {
mController.startQuickAccessUiIntent(mActivityStarter, mAnimationController, true);
verify(mActivityStarter).startActivity(mIntentCaptor.capture(), eq(true),
- any(ActivityLaunchAnimator.Controller.class), eq(true));
+ any(ActivityTransitionAnimator.Controller.class), eq(true));
Intent intent = mIntentCaptor.getValue();
assertEquals(intent.getAction(), Intent.ACTION_VIEW);
assertEquals(
@@ -231,7 +231,7 @@
public void getQuickAccessUiIntent_noCards_noPendingIntent_startsWalletActivity() {
mController.startQuickAccessUiIntent(mActivityStarter, mAnimationController, false);
verify(mActivityStarter).postStartActivityDismissingKeyguard(mIntentCaptor.capture(), eq(0),
- any(ActivityLaunchAnimator.Controller.class));
+ any(ActivityTransitionAnimator.Controller.class));
Intent intent = mIntentCaptor.getValue();
assertEquals(intent.getAction(), Intent.ACTION_VIEW);
assertEquals(
@@ -254,7 +254,7 @@
}).when(mQuickAccessWalletClient).getWalletPendingIntent(any(), any());
mController.startQuickAccessUiIntent(mActivityStarter, mAnimationController, true);
verify(mActivityStarter).postStartActivityDismissingKeyguard(mPendingIntentCaptor.capture(),
- any(ActivityLaunchAnimator.Controller.class));
+ any(ActivityTransitionAnimator.Controller.class));
PendingIntent pendingIntent = mPendingIntentCaptor.getValue();
Intent intent = pendingIntent.getIntent();
assertEquals(intent.getAction(), Intent.ACTION_VIEW);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
index 9ea4142..d45a9a9 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
@@ -91,7 +91,6 @@
import com.android.internal.colorextraction.ColorExtractor;
import com.android.internal.logging.UiEventLogger;
import com.android.internal.statusbar.IStatusBarService;
-import com.android.keyguard.KeyguardSecurityModel;
import com.android.launcher3.icons.BubbleIconFactory;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.biometrics.AuthController;
@@ -99,7 +98,6 @@
import com.android.systemui.colorextraction.SysuiColorExtractor;
import com.android.systemui.common.ui.data.repository.FakeConfigurationRepository;
import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractor;
-import com.android.systemui.communal.domain.interactor.CommunalInteractor;
import com.android.systemui.deviceentry.domain.interactor.DeviceEntryUdfpsInteractor;
import com.android.systemui.dump.DumpManager;
import com.android.systemui.flags.FakeFeatureFlags;
@@ -107,13 +105,8 @@
import com.android.systemui.keyguard.KeyguardViewMediator;
import com.android.systemui.keyguard.data.repository.FakeCommandQueue;
import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository;
-import com.android.systemui.keyguard.data.repository.FakeKeyguardSurfaceBehindRepository;
-import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository;
-import com.android.systemui.keyguard.data.repository.InWindowLauncherUnlockAnimationRepository;
import com.android.systemui.keyguard.domain.interactor.FromLockscreenTransitionInteractor;
import com.android.systemui.keyguard.domain.interactor.FromPrimaryBouncerTransitionInteractor;
-import com.android.systemui.keyguard.domain.interactor.GlanceableHubTransitions;
-import com.android.systemui.keyguard.domain.interactor.InWindowLauncherUnlockAnimationInteractor;
import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor;
import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor;
import com.android.systemui.kosmos.KosmosJavaAdapter;
@@ -137,7 +130,6 @@
import com.android.systemui.shade.domain.interactor.ShadeInteractor;
import com.android.systemui.shade.domain.interactor.ShadeInteractorImpl;
import com.android.systemui.shade.domain.interactor.ShadeInteractorLegacyImpl;
-import com.android.systemui.shared.system.ActivityManagerWrapper;
import com.android.systemui.shared.system.QuickStepContract;
import com.android.systemui.statusbar.NotificationEntryHelper;
import com.android.systemui.statusbar.NotificationLockscreenUserManager;
@@ -151,6 +143,7 @@
import com.android.systemui.statusbar.notification.collection.notifcollection.CommonNotifCollection;
import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener;
import com.android.systemui.statusbar.notification.collection.render.NotificationVisibilityProvider;
+import com.android.systemui.statusbar.notification.interruption.AvalancheProvider;
import com.android.systemui.statusbar.notification.interruption.KeyguardNotificationVisibilityProvider;
import com.android.systemui.statusbar.notification.interruption.NotificationInterruptLogger;
import com.android.systemui.statusbar.notification.interruption.VisualInterruptionDecisionLogger;
@@ -432,57 +425,12 @@
shadeRepository,
() -> sceneInteractor);
- FakeKeyguardTransitionRepository keyguardTransitionRepository =
- new FakeKeyguardTransitionRepository();
-
KeyguardTransitionInteractor keyguardTransitionInteractor =
- new KeyguardTransitionInteractor(
- mTestScope.getBackgroundScope(),
- keyguardTransitionRepository,
- () -> keyguardInteractor,
- () -> mFromLockscreenTransitionInteractor,
- () -> mFromPrimaryBouncerTransitionInteractor);
- CommunalInteractor communalInteractor = mKosmos.getCommunalInteractor();
+ mKosmos.getKeyguardTransitionInteractor();
- mFromLockscreenTransitionInteractor = new FromLockscreenTransitionInteractor(
- keyguardTransitionRepository,
- keyguardTransitionInteractor,
- mTestScope.getBackgroundScope(),
- mKosmos.getTestDispatcher(),
- mKosmos.getTestDispatcher(),
- keyguardInteractor,
- featureFlags,
- shadeRepository,
- powerInteractor,
- new GlanceableHubTransitions(
- mTestScope,
- mKosmos.getTestDispatcher(),
- keyguardTransitionInteractor,
- keyguardTransitionRepository,
- communalInteractor
- ),
- () ->
- new InWindowLauncherUnlockAnimationInteractor(
- new InWindowLauncherUnlockAnimationRepository(),
- mTestScope.getBackgroundScope(),
- keyguardTransitionInteractor,
- FakeKeyguardSurfaceBehindRepository::new,
- mock(ActivityManagerWrapper.class)
- )
- );
-
- mFromPrimaryBouncerTransitionInteractor = new FromPrimaryBouncerTransitionInteractor(
- keyguardTransitionRepository,
- keyguardTransitionInteractor,
- mTestScope.getBackgroundScope(),
- mKosmos.getTestDispatcher(),
- mKosmos.getTestDispatcher(),
- keyguardInteractor,
- communalInteractor,
- featureFlags,
- mock(KeyguardSecurityModel.class),
- mSelectedUserInteractor,
- powerInteractor);
+ mFromLockscreenTransitionInteractor = mKosmos.getFromLockscreenTransitionInteractor();
+ mFromPrimaryBouncerTransitionInteractor =
+ mKosmos.getFromPrimaryBouncerTransitionInteractor();
ResourcesSplitShadeStateController splitShadeStateController =
new ResourcesSplitShadeStateController();
@@ -587,7 +535,9 @@
mock(StatusBarStateController.class),
mock(SystemClock.class),
mock(UiEventLogger.class),
- mock(UserTracker.class));
+ mock(UserTracker.class),
+ mock(AvalancheProvider.class)
+ );
interruptionDecisionProvider.start();
mShellTaskOrganizer = new ShellTaskOrganizer(mock(ShellInit.class),
@@ -616,7 +566,7 @@
mPositioner,
mock(DisplayController.class),
mOneHandedOptional,
- Optional.of(mock(DragAndDropController.class)),
+ mock(DragAndDropController.class),
syncExecutor,
mock(Handler.class),
mTaskViewTransitions,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/wmshell/TestableBubbleController.java b/packages/SystemUI/tests/src/com/android/systemui/wmshell/TestableBubbleController.java
index 9ad234e1..4a5ebd0 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/TestableBubbleController.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/TestableBubbleController.java
@@ -71,7 +71,7 @@
BubblePositioner positioner,
DisplayController displayController,
Optional<OneHandedController> oneHandedOptional,
- Optional<DragAndDropController> dragAndDropController,
+ DragAndDropController dragAndDropController,
ShellExecutor shellMainExecutor,
Handler shellMainHandler,
TaskViewTransitions taskViewTransitions,
diff --git a/packages/SystemUI/tests/utils/src/android/service/dream/DreamManagerKosmos.kt b/packages/SystemUI/tests/utils/src/android/service/dream/DreamManagerKosmos.kt
index fb51f0f..37e3a590 100644
--- a/packages/SystemUI/tests/utils/src/android/service/dream/DreamManagerKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/android/service/dream/DreamManagerKosmos.kt
@@ -16,8 +16,10 @@
package android.service.dream
+import android.app.DreamManager
import android.service.dreams.IDreamManager
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.util.mockito.mock
-var Kosmos.dreamManager by Kosmos.Fixture { mock<IDreamManager>() }
+var Kosmos.dreamManagerInterface by Kosmos.Fixture { mock<IDreamManager>() }
+val Kosmos.dreamManager by Kosmos.Fixture { mock<DreamManager>() }
diff --git a/packages/SystemUI/tests/utils/src/com/android/internal/logging/UiEventLoggerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/internal/logging/UiEventLoggerKosmos.kt
index 9059da2..20b9e84 100644
--- a/packages/SystemUI/tests/utils/src/com/android/internal/logging/UiEventLoggerKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/internal/logging/UiEventLoggerKosmos.kt
@@ -20,4 +20,4 @@
import com.android.systemui.kosmos.Kosmos
var Kosmos.uiEventLogger: UiEventLogger by Kosmos.Fixture { uiEventLoggerFake }
-val Kosmos.uiEventLoggerFake by Kosmos.Fixture { UiEventLoggerFake() }
+var Kosmos.uiEventLoggerFake by Kosmos.Fixture { UiEventLoggerFake() }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/animation/ActivityLaunchAnimatorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/keyguard/logging/ScrimLoggerKosmos.kt
similarity index 74%
copy from packages/SystemUI/tests/utils/src/com/android/systemui/animation/ActivityLaunchAnimatorKosmos.kt
copy to packages/SystemUI/tests/utils/src/com/android/keyguard/logging/ScrimLoggerKosmos.kt
index 128f58b..901bdcc 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/animation/ActivityLaunchAnimatorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/keyguard/logging/ScrimLoggerKosmos.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2023 The Android Open Source Project
+ * Copyright (C) 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,8 +14,9 @@
* limitations under the License.
*/
-package com.android.systemui.animation
+package com.android.keyguard.logging
import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.util.mockito.mock
-val Kosmos.activityLaunchAnimator by Kosmos.Fixture { ActivityLaunchAnimator() }
+val Kosmos.scrimLogger by Kosmos.Fixture { mock<ScrimLogger>() }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/animation/ActivityLaunchAnimatorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/animation/ActivityTransitionAnimatorKosmos.kt
similarity index 88%
rename from packages/SystemUI/tests/utils/src/com/android/systemui/animation/ActivityLaunchAnimatorKosmos.kt
rename to packages/SystemUI/tests/utils/src/com/android/systemui/animation/ActivityTransitionAnimatorKosmos.kt
index 128f58b..66c9afb 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/animation/ActivityLaunchAnimatorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/animation/ActivityTransitionAnimatorKosmos.kt
@@ -18,4 +18,4 @@
import com.android.systemui.kosmos.Kosmos
-val Kosmos.activityLaunchAnimator by Kosmos.Fixture { ActivityLaunchAnimator() }
+val Kosmos.activityTransitionAnimator by Kosmos.Fixture { ActivityTransitionAnimator() }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/animation/FakeDialogLaunchAnimator.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/animation/FakeDialogLaunchAnimator.kt
index f723a9e5..5b84a41 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/animation/FakeDialogLaunchAnimator.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/animation/FakeDialogLaunchAnimator.kt
@@ -36,7 +36,7 @@
object : AnimationFeatureFlags {
override val isPredictiveBackQsDialogAnim = isPredictiveBackQsDialogAnim
},
- launchAnimator = fakeLaunchAnimator(),
+ transitionAnimator = fakeTransitionAnimator(),
isForTesting = true,
)
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/animation/FakeLaunchAnimator.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/animation/FakeTransitionAnimator.kt
similarity index 79%
rename from packages/SystemUI/tests/utils/src/com/android/systemui/animation/FakeLaunchAnimator.kt
rename to packages/SystemUI/tests/utils/src/com/android/systemui/animation/FakeTransitionAnimator.kt
index 0983041..bc7ec3f 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/animation/FakeLaunchAnimator.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/animation/FakeTransitionAnimator.kt
@@ -16,19 +16,19 @@
import com.android.app.animation.Interpolators
-/** A [LaunchAnimator] to be used in tests. */
-fun fakeLaunchAnimator(): LaunchAnimator {
- return LaunchAnimator(TEST_TIMINGS, TEST_INTERPOLATORS)
+/** A [TransitionAnimator] to be used in tests. */
+fun fakeTransitionAnimator(): TransitionAnimator {
+ return TransitionAnimator(TEST_TIMINGS, TEST_INTERPOLATORS)
}
/**
- * A [LaunchAnimator.Timings] to be used in tests.
+ * A [TransitionAnimator.Timings] to be used in tests.
*
* Note that all timings except the total duration are non-zero to avoid divide-by-zero exceptions
* when computing the progress of a sub-animation (the contents fade in/out).
*/
private val TEST_TIMINGS =
- LaunchAnimator.Timings(
+ TransitionAnimator.Timings(
totalDuration = 0L,
contentBeforeFadeOutDelay = 1L,
contentBeforeFadeOutDuration = 1L,
@@ -36,9 +36,9 @@
contentAfterFadeInDuration = 1L
)
-/** A [LaunchAnimator.Interpolators] to be used in tests. */
+/** A [TransitionAnimator.Interpolators] to be used in tests. */
private val TEST_INTERPOLATORS =
- LaunchAnimator.Interpolators(
+ TransitionAnimator.Interpolators(
positionInterpolator = Interpolators.STANDARD,
positionXInterpolator = Interpolators.STANDARD,
contentBeforeFadeOutInterpolator = Interpolators.STANDARD,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/BiometricStatusRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/BiometricStatusRepositoryKosmos.kt
index 961022f..a4f28f3 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/BiometricStatusRepositoryKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/BiometricStatusRepositoryKosmos.kt
@@ -20,4 +20,4 @@
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.Kosmos.Fixture
-val Kosmos.biometricStatusRepository by Fixture { FakeBiometricStatusRepository() }
+var Kosmos.biometricStatusRepository by Fixture { FakeBiometricStatusRepository() }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FakeBiometricStatusRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FakeBiometricStatusRepository.kt
index 1c8bd3b..e9b7a69 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FakeBiometricStatusRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FakeBiometricStatusRepository.kt
@@ -18,9 +18,12 @@
package com.android.systemui.biometrics.data.repository
import com.android.systemui.biometrics.shared.model.AuthenticationReason
+import com.android.systemui.keyguard.shared.model.FingerprintAuthenticationStatus
+import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.filterNotNull
class FakeBiometricStatusRepository : BiometricStatusRepository {
private val _fingerprintAuthenticationReason =
@@ -28,7 +31,16 @@
override val fingerprintAuthenticationReason: StateFlow<AuthenticationReason> =
_fingerprintAuthenticationReason.asStateFlow()
+ private val _fingerprintAcquiredStatus =
+ MutableStateFlow<FingerprintAuthenticationStatus?>(null)
+ override val fingerprintAcquiredStatus: Flow<FingerprintAuthenticationStatus> =
+ _fingerprintAcquiredStatus.asStateFlow().filterNotNull()
+
fun setFingerprintAuthenticationReason(reason: AuthenticationReason) {
_fingerprintAuthenticationReason.value = reason
}
+
+ fun setFingerprintAcquiredStatus(status: FingerprintAuthenticationStatus) {
+ _fingerprintAcquiredStatus.value = status
+ }
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/common/data/repository/FakePackageChangeRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/common/data/repository/FakePackageChangeRepository.kt
index 60f0448..3a61bf6 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/common/data/repository/FakePackageChangeRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/common/data/repository/FakePackageChangeRepository.kt
@@ -17,11 +17,12 @@
package com.android.systemui.common.data.repository
import android.os.UserHandle
-import com.android.systemui.common.data.shared.model.PackageChangeModel
+import com.android.systemui.common.shared.model.PackageChangeModel
+import com.android.systemui.util.time.SystemClock
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.filter
-class FakePackageChangeRepository : PackageChangeRepository {
+class FakePackageChangeRepository(private val systemClock: SystemClock) : PackageChangeRepository {
private var _packageChanged = MutableSharedFlow<PackageChangeModel>()
@@ -33,4 +34,60 @@
suspend fun notifyChange(model: PackageChangeModel) {
_packageChanged.emit(model)
}
+
+ suspend fun notifyInstall(packageName: String, user: UserHandle) {
+ notifyChange(
+ PackageChangeModel.Installed(
+ packageName = packageName,
+ packageUid =
+ UserHandle.getUid(
+ /* userId = */ user.identifier,
+ /* appId = */ packageName.hashCode(),
+ ),
+ timeMillis = systemClock.currentTimeMillis(),
+ )
+ )
+ }
+
+ suspend fun notifyUpdateStarted(packageName: String, user: UserHandle) {
+ notifyChange(
+ PackageChangeModel.UpdateStarted(
+ packageName = packageName,
+ packageUid =
+ UserHandle.getUid(
+ /* userId = */ user.identifier,
+ /* appId = */ packageName.hashCode(),
+ ),
+ timeMillis = systemClock.currentTimeMillis(),
+ )
+ )
+ }
+
+ suspend fun notifyUpdateFinished(packageName: String, user: UserHandle) {
+ notifyChange(
+ PackageChangeModel.UpdateFinished(
+ packageName = packageName,
+ packageUid =
+ UserHandle.getUid(
+ /* userId = */ user.identifier,
+ /* appId = */ packageName.hashCode(),
+ ),
+ timeMillis = systemClock.currentTimeMillis(),
+ )
+ )
+ }
+
+ suspend fun notifyUninstall(packageName: String, user: UserHandle) {
+ notifyChange(
+ PackageChangeModel.Uninstalled(
+ packageName = packageName,
+ packageUid =
+ UserHandle.getUid(
+ /* userId = */ user.identifier,
+ /* appId = */ packageName.hashCode(),
+ ),
+ timeMillis = systemClock.currentTimeMillis(),
+ )
+ )
+ }
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/common/data/repository/PackageChangeRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/common/data/repository/PackageChangeRepositoryKosmos.kt
index adc05e0..57caa52 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/common/data/repository/PackageChangeRepositoryKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/common/data/repository/PackageChangeRepositoryKosmos.kt
@@ -17,7 +17,9 @@
package com.android.systemui.common.data.repository
import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.util.time.fakeSystemClock
var Kosmos.packageChangeRepository: PackageChangeRepository by
Kosmos.Fixture { fakePackageChangeRepository }
-val Kosmos.fakePackageChangeRepository by Kosmos.Fixture { FakePackageChangeRepository() }
+val Kosmos.fakePackageChangeRepository by
+ Kosmos.Fixture { FakePackageChangeRepository(fakeSystemClock) }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/animation/ActivityLaunchAnimatorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/common/domain/interactor/PackageChangeInteractorKosmos.kt
similarity index 60%
copy from packages/SystemUI/tests/utils/src/com/android/systemui/animation/ActivityLaunchAnimatorKosmos.kt
copy to packages/SystemUI/tests/utils/src/com/android/systemui/common/domain/interactor/PackageChangeInteractorKosmos.kt
index 128f58b..e56d9d9 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/animation/ActivityLaunchAnimatorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/common/domain/interactor/PackageChangeInteractorKosmos.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2023 The Android Open Source Project
+ * Copyright (C) 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,8 +14,11 @@
* limitations under the License.
*/
-package com.android.systemui.animation
+package com.android.systemui.common.domain.interactor
+import com.android.systemui.common.data.repository.packageChangeRepository
import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.user.domain.interactor.selectedUserInteractor
-val Kosmos.activityLaunchAnimator by Kosmos.Fixture { ActivityLaunchAnimator() }
+var Kosmos.packageChangeInteractor by
+ Kosmos.Fixture { PackageChangeInteractor(packageChangeRepository, selectedUserInteractor) }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/CommunalSettingsRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/CommunalSettingsRepositoryKosmos.kt
new file mode 100644
index 0000000..5485f79
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/CommunalSettingsRepositoryKosmos.kt
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2024 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.communal.data.repository
+
+import android.app.admin.devicePolicyManager
+import com.android.systemui.broadcast.broadcastDispatcher
+import com.android.systemui.flags.featureFlagsClassic
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.testDispatcher
+import com.android.systemui.util.settings.fakeSettings
+
+val Kosmos.communalSettingsRepository: CommunalSettingsRepository by
+ Kosmos.Fixture {
+ CommunalSettingsRepositoryImpl(
+ bgDispatcher = testDispatcher,
+ featureFlagsClassic = featureFlagsClassic,
+ secureSettings = fakeSettings,
+ broadcastDispatcher = broadcastDispatcher,
+ devicePolicyManager = devicePolicyManager,
+ )
+ }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalMediaRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalMediaRepository.kt
index 3ea3ccf..1884a32 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalMediaRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalMediaRepository.kt
@@ -28,7 +28,7 @@
fun mediaActive(timestamp: Long = 0L) {
_mediaModel.value =
CommunalMediaModel(
- hasAnyMediaOrRecommendation = true,
+ hasActiveMediaOrRecommendation = true,
createdTimestampMillis = timestamp,
)
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalRepository.kt
index cccd908..ae7d877 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalRepository.kt
@@ -16,7 +16,6 @@
@OptIn(ExperimentalCoroutinesApi::class)
class FakeCommunalRepository(
applicationScope: CoroutineScope,
- override var isCommunalEnabled: Boolean = true,
override val desiredScene: MutableStateFlow<CommunalSceneKey> =
MutableStateFlow(CommunalSceneKey.DEFAULT),
) : CommunalRepository {
@@ -40,21 +39,10 @@
_transitionState.value = transitionState
}
- fun setIsCommunalEnabled(value: Boolean) {
- isCommunalEnabled = value
- }
-
private val _isCommunalHubShowing: MutableStateFlow<Boolean> = MutableStateFlow(false)
override val isCommunalHubShowing: Flow<Boolean> = _isCommunalHubShowing
fun setIsCommunalHubShowing(isCommunalHubShowing: Boolean) {
_isCommunalHubShowing.value = isCommunalHubShowing
}
-
- private val _communalEnabledState: MutableStateFlow<Boolean> = MutableStateFlow(false)
- override val communalEnabledState: StateFlow<Boolean> = _communalEnabledState
-
- fun setCommunalEnabledState(enabled: Boolean) {
- _communalEnabledState.value = enabled
- }
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalInteractorKosmos.kt
index c47f020..f7e9a11 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalInteractorKosmos.kt
@@ -27,7 +27,6 @@
import com.android.systemui.kosmos.applicationCoroutineScope
import com.android.systemui.log.logcatLogBuffer
import com.android.systemui.smartspace.data.repository.smartspaceRepository
-import com.android.systemui.user.data.repository.userRepository
import com.android.systemui.util.mockito.mock
val Kosmos.communalInteractor by Fixture {
@@ -38,12 +37,12 @@
mediaRepository = communalMediaRepository,
communalPrefsRepository = communalPrefsRepository,
smartspaceRepository = smartspaceRepository,
- userRepository = userRepository,
appWidgetHost = mock(),
keyguardInteractor = keyguardInteractor,
editWidgetsActivityStarter = editWidgetsActivityStarter,
logBuffer = logcatLogBuffer("CommunalInteractor"),
tableLogBuffer = mock(),
+ communalSettingsInteractor = communalSettingsInteractor,
)
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalSettingsInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalSettingsInteractorKosmos.kt
new file mode 100644
index 0000000..b4773f6
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalSettingsInteractorKosmos.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2024 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.communal.domain.interactor
+
+import com.android.systemui.communal.data.repository.communalSettingsRepository
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.Kosmos.Fixture
+import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.user.domain.interactor.selectedUserInteractor
+import com.android.systemui.util.mockito.mock
+
+val Kosmos.communalSettingsInteractor by Fixture {
+ CommunalSettingsInteractor(
+ bgScope = applicationCoroutineScope,
+ repository = communalSettingsRepository,
+ userInteractor = selectedUserInteractor,
+ tableLogBuffer = mock(),
+ )
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalTutorialInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalTutorialInteractorKosmos.kt
index 9776b43..00fdced 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalTutorialInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalTutorialInteractorKosmos.kt
@@ -31,6 +31,7 @@
keyguardInteractor = keyguardInteractor,
communalRepository = communalRepository,
communalInteractor = communalInteractor,
+ communalSettingsInteractor = communalSettingsInteractor,
tableLogBuffer = mock(),
)
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/controls/panels/AuthorizedPanelsRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/controls/panels/AuthorizedPanelsRepositoryKosmos.kt
new file mode 100644
index 0000000..109e113
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/controls/panels/AuthorizedPanelsRepositoryKosmos.kt
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.controls.panels
+
+import android.content.applicationContext
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.settings.fakeUserFileManager
+import com.android.systemui.settings.fakeUserTracker
+
+var Kosmos.authorizedPanelsRepository: AuthorizedPanelsRepository by
+ Kosmos.Fixture {
+ AuthorizedPanelsRepositoryImpl(applicationContext, fakeUserFileManager, fakeUserTracker)
+ }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/controls/panels/FakeSelectedComponentRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/controls/panels/FakeSelectedComponentRepository.kt
deleted file mode 100644
index a231212..0000000
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/controls/panels/FakeSelectedComponentRepository.kt
+++ /dev/null
@@ -1,74 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.controls.panels
-
-import android.os.UserHandle
-import com.android.systemui.kosmos.Kosmos
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.MutableStateFlow
-
-class FakeSelectedComponentRepository : SelectedComponentRepository {
- private var shouldAddDefaultPanel: Boolean = true
- private val _selectedComponentFlows =
- mutableMapOf<UserHandle, MutableStateFlow<SelectedComponentRepository.SelectedComponent?>>()
- private var currentUserHandle: UserHandle = UserHandle.of(0)
-
- override fun selectedComponentFlow(
- userHandle: UserHandle
- ): Flow<SelectedComponentRepository.SelectedComponent?> {
- // Return an existing flow for the user or create a new one
- return _selectedComponentFlows.getOrPut(getUserHandle(userHandle)) {
- MutableStateFlow(null)
- }
- }
-
- override fun getSelectedComponent(
- userHandle: UserHandle
- ): SelectedComponentRepository.SelectedComponent? {
- return _selectedComponentFlows[getUserHandle(userHandle)]?.value
- }
-
- override fun setSelectedComponent(
- selectedComponent: SelectedComponentRepository.SelectedComponent
- ) {
- val flow = _selectedComponentFlows.getOrPut(currentUserHandle) { MutableStateFlow(null) }
- flow.value = selectedComponent
- }
-
- override fun removeSelectedComponent() {
- _selectedComponentFlows[currentUserHandle]?.value = null
- }
- override fun shouldAddDefaultComponent(): Boolean = shouldAddDefaultPanel
-
- override fun setShouldAddDefaultComponent(shouldAdd: Boolean) {
- shouldAddDefaultPanel = shouldAdd
- }
-
- fun setCurrentUserHandle(userHandle: UserHandle) {
- currentUserHandle = userHandle
- }
- private fun getUserHandle(userHandle: UserHandle): UserHandle {
- return if (userHandle == UserHandle.CURRENT) {
- currentUserHandle
- } else {
- userHandle
- }
- }
-}
-
-val Kosmos.selectedComponentRepository by
- Kosmos.Fixture<FakeSelectedComponentRepository> { FakeSelectedComponentRepository() }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/controls/panels/SelectedComponentRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/controls/panels/SelectedComponentRepositoryKosmos.kt
new file mode 100644
index 0000000..ee5b7e5
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/controls/panels/SelectedComponentRepositoryKosmos.kt
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.controls.panels
+
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.testDispatcher
+import com.android.systemui.settings.fakeUserFileManager
+import com.android.systemui.settings.fakeUserTracker
+
+var Kosmos.selectedComponentRepository: SelectedComponentRepository by
+ Kosmos.Fixture {
+ SelectedComponentRepositoryImpl(fakeUserFileManager, fakeUserTracker, testDispatcher)
+ }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/data/repository/FaceWakeUpTriggersConfigKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/data/repository/FaceWakeUpTriggersConfigKosmos.kt
index 21cff0d..3b3e23e 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/data/repository/FaceWakeUpTriggersConfigKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/data/repository/FaceWakeUpTriggersConfigKosmos.kt
@@ -18,5 +18,7 @@
import com.android.systemui.kosmos.Kosmos
+var Kosmos.fakeFaceWakeUpTriggersConfig by Kosmos.Fixture { FakeFaceWakeUpTriggersConfig() }
+
var Kosmos.faceWakeUpTriggersConfig: FaceWakeUpTriggersConfig by
- Kosmos.Fixture { FakeFaceWakeUpTriggersConfig() }
+ Kosmos.Fixture { fakeFaceWakeUpTriggersConfig }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFaceAuthInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFaceAuthInteractorKosmos.kt
index 5575b05..a8fc27a 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFaceAuthInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFaceAuthInteractorKosmos.kt
@@ -27,7 +27,6 @@
import com.android.systemui.deviceentry.data.repository.faceWakeUpTriggersConfig
import com.android.systemui.keyguard.data.repository.biometricSettingsRepository
import com.android.systemui.keyguard.data.repository.deviceEntryFaceAuthRepository
-import com.android.systemui.keyguard.data.repository.deviceEntryFingerprintAuthRepository
import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.applicationCoroutineScope
@@ -51,7 +50,7 @@
keyguardTransitionInteractor = keyguardTransitionInteractor,
faceAuthenticationLogger = faceAuthLogger,
keyguardUpdateMonitor = keyguardUpdateMonitor,
- deviceEntryFingerprintAuthRepository = deviceEntryFingerprintAuthRepository,
+ deviceEntryFingerprintAuthInteractor = deviceEntryFingerprintAuthInteractor,
userRepository = userRepository,
facePropertyRepository = facePropertyRepository,
faceWakeUpTriggersConfig = faceWakeUpTriggersConfig,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/flags/FeatureFlagsClassicKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/flags/FeatureFlagsClassicKosmos.kt
index 7b36a29..365d97f 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/flags/FeatureFlagsClassicKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/flags/FeatureFlagsClassicKosmos.kt
@@ -35,6 +35,7 @@
FakeFeatureFlagsClassic().apply {
set(Flags.FULL_SCREEN_USER_SWITCHER, false)
set(Flags.NSSL_DEBUG_LINES, false)
+ set(Flags.LOCK_SCREEN_LONG_PRESS_ENABLED, false)
}
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/animation/ActivityLaunchAnimatorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/LightRevealScrimRepositoryKosmos.kt
similarity index 61%
copy from packages/SystemUI/tests/utils/src/com/android/systemui/animation/ActivityLaunchAnimatorKosmos.kt
copy to packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/LightRevealScrimRepositoryKosmos.kt
index 128f58b..046946e 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/animation/ActivityLaunchAnimatorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/LightRevealScrimRepositoryKosmos.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2023 The Android Open Source Project
+ * Copyright (C) 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,8 +14,11 @@
* limitations under the License.
*/
-package com.android.systemui.animation
+package com.android.systemui.keyguard.data
+import com.android.systemui.keyguard.data.repository.FakeLightRevealScrimRepository
import com.android.systemui.kosmos.Kosmos
-val Kosmos.activityLaunchAnimator by Kosmos.Fixture { ActivityLaunchAnimator() }
+val Kosmos.lightRevealScrimRepository by Kosmos.Fixture { fakeLightRevealScrimRepository }
+
+var Kosmos.fakeLightRevealScrimRepository by Kosmos.Fixture { FakeLightRevealScrimRepository() }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/animation/ActivityLaunchAnimatorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeCommandQueueKosmos.kt
similarity index 77%
copy from packages/SystemUI/tests/utils/src/com/android/systemui/animation/ActivityLaunchAnimatorKosmos.kt
copy to packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeCommandQueueKosmos.kt
index 128f58b..d0a0e1d 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/animation/ActivityLaunchAnimatorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeCommandQueueKosmos.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2023 The Android Open Source Project
+ * Copyright (C) 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,8 +14,8 @@
* limitations under the License.
*/
-package com.android.systemui.animation
+package com.android.systemui.keyguard.data.repository
import com.android.systemui.kosmos.Kosmos
-val Kosmos.activityLaunchAnimator by Kosmos.Fixture { ActivityLaunchAnimator() }
+val Kosmos.fakeCommandQueue by Kosmos.Fixture { FakeCommandQueue() }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardClockRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardClockRepository.kt
index 85a233fd..534f773 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardClockRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardClockRepository.kt
@@ -20,6 +20,7 @@
import com.android.keyguard.KeyguardClockSwitch.ClockSize
import com.android.keyguard.KeyguardClockSwitch.LARGE
import com.android.systemui.keyguard.shared.model.SettingsClockSize
+import com.android.systemui.plugins.clocks.ClockController
import com.android.systemui.plugins.clocks.ClockId
import com.android.systemui.shared.clocks.DEFAULT_CLOCK_ID
import com.android.systemui.util.mockito.mock
@@ -29,6 +30,7 @@
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
+import org.mockito.Mockito
class FakeKeyguardClockRepository @Inject constructor() : KeyguardClockRepository {
private val _clockSize = MutableStateFlow(LARGE)
@@ -42,6 +44,9 @@
private val _currentClock = MutableStateFlow(null)
override val currentClock = _currentClock
+
+ private val _previewClock = MutableStateFlow(Mockito.mock(ClockController::class.java))
+ override val previewClock: StateFlow<ClockController> = _previewClock
override val clockEventController: ClockEventController
get() = mock()
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepositoryKosmos.kt
index 008f79a..408157b 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepositoryKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepositoryKosmos.kt
@@ -20,4 +20,4 @@
var Kosmos.keyguardTransitionRepository: KeyguardTransitionRepository by
Kosmos.Fixture { fakeKeyguardTransitionRepository }
-val Kosmos.fakeKeyguardTransitionRepository by Kosmos.Fixture { FakeKeyguardTransitionRepository() }
+var Kosmos.fakeKeyguardTransitionRepository by Kosmos.Fixture { FakeKeyguardTransitionRepository() }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractorKosmos.kt
new file mode 100644
index 0000000..da5cd67
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractorKosmos.kt
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyguard.domain.interactor
+
+import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.testDispatcher
+import com.android.systemui.kosmos.testScope
+
+val Kosmos.fromAodTransitionInteractor by
+ Kosmos.Fixture {
+ FromAodTransitionInteractor(
+ transitionRepository = fakeKeyguardTransitionRepository,
+ transitionInteractor = keyguardTransitionInteractor,
+ scope = testScope,
+ bgDispatcher = testDispatcher,
+ mainDispatcher = testDispatcher,
+ keyguardInteractor = keyguardInteractor,
+ )
+ }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractorKosmos.kt
new file mode 100644
index 0000000..25fc67a
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractorKosmos.kt
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyguard.domain.interactor
+
+import com.android.systemui.communal.domain.interactor.communalInteractor
+import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.kosmos.testDispatcher
+import com.android.systemui.power.domain.interactor.powerInteractor
+
+val Kosmos.fromGoneTransitionInteractor by
+ Kosmos.Fixture {
+ FromGoneTransitionInteractor(
+ transitionRepository = fakeKeyguardTransitionRepository,
+ transitionInteractor = keyguardTransitionInteractor,
+ scope = applicationCoroutineScope,
+ bgDispatcher = testDispatcher,
+ mainDispatcher = testDispatcher,
+ keyguardInteractor = keyguardInteractor,
+ powerInteractor = powerInteractor,
+ communalInteractor = communalInteractor,
+ )
+ }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractorKosmos.kt
index 3b38342..3b52676 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractorKosmos.kt
@@ -23,9 +23,8 @@
import com.android.systemui.kosmos.testDispatcher
import com.android.systemui.power.domain.interactor.powerInteractor
import com.android.systemui.shade.data.repository.shadeRepository
-import dagger.Lazy
-val Kosmos.fromLockscreenTransitionInteractor by
+var Kosmos.fromLockscreenTransitionInteractor by
Kosmos.Fixture {
FromLockscreenTransitionInteractor(
transitionRepository = keyguardTransitionRepository,
@@ -38,7 +37,6 @@
shadeRepository = shadeRepository,
powerInteractor = powerInteractor,
glanceableHubTransitions = glanceableHubTransitions,
- inWindowLauncherUnlockAnimationInteractor =
- Lazy { inWindowLauncherUnlockAnimationInteractor },
+ swipeToDismissInteractor = swipeToDismissInteractor,
)
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromPrimaryBouncerTransitionInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromPrimaryBouncerTransitionInteractorKosmos.kt
index 719686e..6b76449 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromPrimaryBouncerTransitionInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromPrimaryBouncerTransitionInteractorKosmos.kt
@@ -26,7 +26,7 @@
import com.android.systemui.power.domain.interactor.powerInteractor
import com.android.systemui.user.domain.interactor.selectedUserInteractor
-val Kosmos.fromPrimaryBouncerTransitionInteractor by
+var Kosmos.fromPrimaryBouncerTransitionInteractor by
Kosmos.Fixture {
FromPrimaryBouncerTransitionInteractor(
transitionRepository = keyguardTransitionRepository,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardLongPressInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardLongPressInteractorKosmos.kt
index 638a6a3..c06f833 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardLongPressInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardLongPressInteractorKosmos.kt
@@ -18,12 +18,12 @@
import android.content.applicationContext
import android.view.accessibility.accessibilityManagerWrapper
+import com.android.internal.logging.uiEventLogger
import com.android.systemui.broadcast.broadcastDispatcher
import com.android.systemui.flags.featureFlagsClassic
import com.android.systemui.keyguard.data.repository.keyguardRepository
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.applicationCoroutineScope
-import com.android.systemui.qs.uiEventLogger
val Kosmos.keyguardLongPressInteractor by
Kosmos.Fixture {
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardSurfaceBehindInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardSurfaceBehindInteractorKosmos.kt
new file mode 100644
index 0000000..c9c17d9
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardSurfaceBehindInteractorKosmos.kt
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyguard.domain.interactor
+
+import android.content.applicationContext
+import com.android.systemui.keyguard.data.repository.keyguardSurfaceBehindRepository
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.statusbar.notification.domain.interactor.notificationLaunchAnimationInteractor
+
+var Kosmos.keyguardSurfaceBehindInteractor by
+ Kosmos.Fixture {
+ KeyguardSurfaceBehindInteractor(
+ repository = keyguardSurfaceBehindRepository,
+ context = applicationContext,
+ transitionInteractor = keyguardTransitionInteractor,
+ inWindowLauncherUnlockAnimationInteractor = {
+ inWindowLauncherUnlockAnimationInteractor
+ },
+ swipeToDismissInteractor = swipeToDismissInteractor,
+ notificationLaunchInteractor = notificationLaunchAnimationInteractor,
+ )
+ }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorFactory.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorFactory.kt
deleted file mode 100644
index 5cf656c..0000000
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorFactory.kt
+++ /dev/null
@@ -1,69 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-package com.android.systemui.keyguard.domain.interactor
-
-import com.android.systemui.flags.FakeFeatureFlags
-import com.android.systemui.flags.FakeFeatureFlagsClassic
-import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
-import com.android.systemui.util.mockito.mock
-import dagger.Lazy
-import kotlinx.coroutines.CoroutineScope
-
-/**
- * Helper to create a new KeyguardTransitionInteractor in a way that doesn't require modifying 20+
- * tests whenever we add a constructor param.
- */
-object KeyguardTransitionInteractorFactory {
- @JvmOverloads
- @JvmStatic
- fun create(
- scope: CoroutineScope,
- repository: FakeKeyguardTransitionRepository = FakeKeyguardTransitionRepository(),
- featureFlags: FakeFeatureFlags = FakeFeatureFlagsClassic(),
- keyguardInteractor: KeyguardInteractor =
- KeyguardInteractorFactory.create(featureFlags = featureFlags).keyguardInteractor,
- fromLockscreenTransitionInteractor: Lazy<FromLockscreenTransitionInteractor> = Lazy {
- mock<FromLockscreenTransitionInteractor>()
- },
- fromPrimaryBouncerTransitionInteractor: Lazy<FromPrimaryBouncerTransitionInteractor> =
- Lazy {
- mock<FromPrimaryBouncerTransitionInteractor>()
- },
- ): WithDependencies {
- return WithDependencies(
- repository = repository,
- keyguardInteractor = keyguardInteractor,
- fromLockscreenTransitionInteractor = fromLockscreenTransitionInteractor,
- fromPrimaryBouncerTransitionInteractor = fromPrimaryBouncerTransitionInteractor,
- KeyguardTransitionInteractor(
- scope = scope,
- repository = repository,
- keyguardInteractor = { keyguardInteractor },
- fromLockscreenTransitionInteractor = fromLockscreenTransitionInteractor,
- fromPrimaryBouncerTransitionInteractor = fromPrimaryBouncerTransitionInteractor,
- )
- )
- }
-
- data class WithDependencies(
- val repository: FakeKeyguardTransitionRepository,
- val keyguardInteractor: KeyguardInteractor,
- val fromLockscreenTransitionInteractor: Lazy<FromLockscreenTransitionInteractor>,
- val fromPrimaryBouncerTransitionInteractor: Lazy<FromPrimaryBouncerTransitionInteractor>,
- val keyguardTransitionInteractor: KeyguardTransitionInteractor,
- )
-}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorKosmos.kt
index e4d115e..0c38fd9 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorKosmos.kt
@@ -30,5 +30,6 @@
fromLockscreenTransitionInteractor = Lazy { fromLockscreenTransitionInteractor },
fromPrimaryBouncerTransitionInteractor =
Lazy { fromPrimaryBouncerTransitionInteractor },
+ fromAodTransitionInteractor = Lazy { fromAodTransitionInteractor },
)
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/LightRevealScrimInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/LightRevealScrimInteractorKosmos.kt
new file mode 100644
index 0000000..58e0a3b
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/LightRevealScrimInteractorKosmos.kt
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyguard.domain.interactor
+
+import com.android.keyguard.logging.scrimLogger
+import com.android.systemui.keyguard.data.lightRevealScrimRepository
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.power.domain.interactor.powerInteractor
+
+val Kosmos.lightRevealScrimInteractor by
+ Kosmos.Fixture {
+ LightRevealScrimInteractor(
+ keyguardTransitionInteractor,
+ lightRevealScrimRepository,
+ applicationCoroutineScope,
+ scrimLogger,
+ powerInteractor,
+ )
+ }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/SwipeToDismissInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/SwipeToDismissInteractorKosmos.kt
new file mode 100644
index 0000000..25aad04
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/SwipeToDismissInteractorKosmos.kt
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyguard.domain.interactor
+
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.shade.data.repository.shadeRepository
+
+val Kosmos.swipeToDismissInteractor by
+ Kosmos.Fixture {
+ SwipeToDismissInteractor(
+ backgroundScope = applicationCoroutineScope,
+ shadeRepository = shadeRepository,
+ transitionInteractor = keyguardTransitionInteractor,
+ keyguardInteractor = keyguardInteractor,
+ )
+ }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/WindowManagerLockscreenVisibilityInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/WindowManagerLockscreenVisibilityInteractorKosmos.kt
new file mode 100644
index 0000000..d84988d
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/WindowManagerLockscreenVisibilityInteractorKosmos.kt
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyguard.domain.interactor
+
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.statusbar.notification.domain.interactor.notificationLaunchAnimationInteractor
+
+val Kosmos.windowManagerLockscreenVisibilityInteractor by
+ Kosmos.Fixture {
+ WindowManagerLockscreenVisibilityInteractor(
+ keyguardInteractor = keyguardInteractor,
+ transitionInteractor = keyguardTransitionInteractor,
+ surfaceBehindInteractor = keyguardSurfaceBehindInteractor,
+ fromLockscreenInteractor = fromLockscreenTransitionInteractor,
+ fromBouncerInteractor = fromPrimaryBouncerTransitionInteractor,
+ notificationLaunchAnimationInteractor = notificationLaunchAnimationInteractor,
+ )
+ }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/AodBurnInViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/AodBurnInViewModelKosmos.kt
index a8f45b0..6f168d4 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/AodBurnInViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/AodBurnInViewModelKosmos.kt
@@ -33,6 +33,7 @@
keyguardInteractor = keyguardInteractor,
keyguardTransitionInteractor = keyguardTransitionInteractor,
goneToAodTransitionViewModel = goneToAodTransitionViewModel,
+ aodToLockscreenTransitionViewModel = aodToLockscreenTransitionViewModel,
occludedToLockscreenTransitionViewModel = occludedToLockscreenTransitionViewModel,
keyguardClockViewModel = keyguardClockViewModel,
)
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelKosmos.kt
index d376f12..24bb9c5 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelKosmos.kt
@@ -38,6 +38,9 @@
keyguardTransitionInteractor = keyguardTransitionInteractor,
notificationsKeyguardInteractor = notificationsKeyguardInteractor,
aodToLockscreenTransitionViewModel = aodToLockscreenTransitionViewModel,
+ lockscreenToGoneTransitionViewModel = lockscreenToGoneTransitionViewModel,
+ alternateBouncerToGoneTransitionViewModel = alternateBouncerToGoneTransitionViewModel,
+ primaryBouncerToGoneTransitionViewModel = primaryBouncerToGoneTransitionViewModel,
screenOffAnimationController = screenOffAnimationController,
aodBurnInViewModel = aodBurnInViewModel,
aodAlphaViewModel = aodAlphaViewModel,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/KosmosJavaAdapter.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/KosmosJavaAdapter.kt
index b9a3d38..10305f7 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/KosmosJavaAdapter.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/KosmosJavaAdapter.kt
@@ -33,6 +33,8 @@
import com.android.systemui.jank.interactionJankMonitor
import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository
import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
+import com.android.systemui.keyguard.domain.interactor.fromLockscreenTransitionInteractor
+import com.android.systemui.keyguard.domain.interactor.fromPrimaryBouncerTransitionInteractor
import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor
import com.android.systemui.model.sceneContainerPlugin
import com.android.systemui.plugins.statusbar.statusBarStateController
@@ -84,6 +86,10 @@
val sceneContainerPlugin by lazy { kosmos.sceneContainerPlugin }
val deviceProvisioningInteractor by lazy { kosmos.deviceProvisioningInteractor }
val fakeDeviceProvisioningRepository by lazy { kosmos.fakeDeviceProvisioningRepository }
+ val fromLockscreenTransitionInteractor by lazy { kosmos.fromLockscreenTransitionInteractor }
+ val fromPrimaryBouncerTransitionInteractor by lazy {
+ kosmos.fromPrimaryBouncerTransitionInteractor
+ }
init {
kosmos.applicationContext = testCase.context
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/plugins/statusbar/StatusBarStateControllerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/plugins/statusbar/StatusBarStateControllerKosmos.kt
index 73b7c50..be559ef 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/plugins/statusbar/StatusBarStateControllerKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/plugins/statusbar/StatusBarStateControllerKosmos.kt
@@ -16,9 +16,9 @@
package com.android.systemui.plugins.statusbar
+import com.android.internal.logging.uiEventLogger
import com.android.systemui.jank.interactionJankMonitor
import com.android.systemui.kosmos.Kosmos
-import com.android.systemui.qs.uiEventLogger
import com.android.systemui.shade.domain.interactor.shadeInteractor
import com.android.systemui.statusbar.StatusBarStateControllerImpl
import com.android.systemui.util.mockito.mock
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/process/ProcessWrapperFake.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/process/ProcessWrapperFake.kt
index 9841778..dee3644 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/process/ProcessWrapperFake.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/process/ProcessWrapperFake.kt
@@ -16,11 +16,17 @@
package com.android.systemui.process
+import android.os.UserHandle
+
class ProcessWrapperFake : ProcessWrapper() {
var systemUser: Boolean = false
+ var userHandle: UserHandle = UserHandle.getUserHandleForUid(0)
+
override fun isSystemUser(): Boolean {
return systemUser
}
+
+ override fun myUserHandle() = userHandle
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/QuickSettingsKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/QuickSettingsKosmos.kt
index 6332c1a..23d657d 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/QuickSettingsKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/QuickSettingsKosmos.kt
@@ -16,7 +16,7 @@
package com.android.systemui.qs
-import com.android.internal.logging.testing.UiEventLoggerFake
+import com.android.internal.logging.uiEventLoggerFake
import com.android.systemui.InstanceIdSequenceFake
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.plugins.qs.QSFactory
@@ -24,9 +24,8 @@
val Kosmos.instanceIdSequenceFake: InstanceIdSequenceFake by
Kosmos.Fixture { InstanceIdSequenceFake(0) }
-val Kosmos.uiEventLogger: UiEventLoggerFake by Kosmos.Fixture { UiEventLoggerFake() }
val Kosmos.qsEventLogger: QsEventLoggerFake by
- Kosmos.Fixture { QsEventLoggerFake(uiEventLogger, instanceIdSequenceFake) }
+ Kosmos.Fixture { QsEventLoggerFake(uiEventLoggerFake, instanceIdSequenceFake) }
var Kosmos.newQSTileFactory by Kosmos.Fixture<NewQSTileFactory>()
var Kosmos.qsTileFactory by Kosmos.Fixture<QSFactory>()
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/ui/adapter/FakeQSSceneAdapter.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/ui/adapter/FakeQSSceneAdapter.kt
index e67df9d..8e430db 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/ui/adapter/FakeQSSceneAdapter.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/ui/adapter/FakeQSSceneAdapter.kt
@@ -26,6 +26,8 @@
class FakeQSSceneAdapter(
private val inflateDelegate: suspend (Context) -> View,
+ override val qqsHeight: Int = 0,
+ override val qsHeight: Int = 0,
) : QSSceneAdapter {
private val _customizing = MutableStateFlow(false)
override val isCustomizing: StateFlow<Boolean> = _customizing.asStateFlow()
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/settings/FakeUserFileManager.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/settings/FakeUserFileManager.kt
new file mode 100644
index 0000000..207c3f7
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/settings/FakeUserFileManager.kt
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.settings
+
+import android.content.SharedPreferences
+import com.android.systemui.util.FakeSharedPreferences
+import java.io.File
+
+class FakeUserFileManager : UserFileManager {
+ private val sharedPreferences = mutableMapOf<SharedPrefKey, FakeSharedPreferences>()
+
+ override fun getFile(fileName: String, userId: Int): File {
+ throw UnsupportedOperationException("getFile not implemented in fake")
+ }
+
+ override fun getSharedPreferences(fileName: String, mode: Int, userId: Int): SharedPreferences {
+ val key = SharedPrefKey(fileName, mode, userId)
+ return sharedPreferences.getOrPut(key) { FakeSharedPreferences() }
+ }
+
+ private data class SharedPrefKey(val fileName: String, val mode: Int, val userId: Int)
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/animation/ActivityLaunchAnimatorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/settings/UserFileManagerKosmos.kt
similarity index 70%
copy from packages/SystemUI/tests/utils/src/com/android/systemui/animation/ActivityLaunchAnimatorKosmos.kt
copy to packages/SystemUI/tests/utils/src/com/android/systemui/settings/UserFileManagerKosmos.kt
index 128f58b..4d7a40a 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/animation/ActivityLaunchAnimatorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/settings/UserFileManagerKosmos.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2023 The Android Open Source Project
+ * Copyright (C) 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,8 +14,9 @@
* limitations under the License.
*/
-package com.android.systemui.animation
+package com.android.systemui.settings
import com.android.systemui.kosmos.Kosmos
-val Kosmos.activityLaunchAnimator by Kosmos.Fixture { ActivityLaunchAnimator() }
+val Kosmos.fakeUserFileManager by Kosmos.Fixture { FakeUserFileManager() }
+var Kosmos.userFileManager: UserFileManager by Kosmos.Fixture { fakeUserFileManager }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ShadeControllerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ShadeControllerKosmos.kt
index e13fa52..82e0b8e 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ShadeControllerKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ShadeControllerKosmos.kt
@@ -24,6 +24,7 @@
import com.android.systemui.deviceentry.domain.interactor.deviceEntryInteractor
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.kosmos.testDispatcher
import com.android.systemui.log.LogBuffer
import com.android.systemui.plugins.statusbar.statusBarStateController
import com.android.systemui.scene.domain.interactor.sceneInteractor
@@ -44,6 +45,7 @@
val Kosmos.shadeControllerSceneImpl by
Kosmos.Fixture {
ShadeControllerSceneImpl(
+ mainDispatcher = testDispatcher,
scope = applicationCoroutineScope,
shadeInteractor = shadeInteractor,
sceneInteractor = sceneInteractor,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/data/repository/FakeShadeRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/data/repository/FakeShadeRepository.kt
index f7005ab..4aab822 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/data/repository/FakeShadeRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/data/repository/FakeShadeRepository.kt
@@ -22,6 +22,7 @@
import dagger.Module
import javax.inject.Inject
import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
/** Fake implementation of [ShadeRepository] */
@SysUISingleton
@@ -32,6 +33,9 @@
private val _udfpsTransitionToFullShadeProgress = MutableStateFlow(0f)
override val udfpsTransitionToFullShadeProgress = _udfpsTransitionToFullShadeProgress
+ private val _currentFling: MutableStateFlow<FlingInfo?> = MutableStateFlow(null)
+ override val currentFling: StateFlow<FlingInfo?> = _currentFling
+
private val _lockscreenShadeExpansion = MutableStateFlow(0f)
override val lockscreenShadeExpansion = _lockscreenShadeExpansion
@@ -115,6 +119,10 @@
_udfpsTransitionToFullShadeProgress.value = progress
}
+ override fun setCurrentFling(info: FlingInfo?) {
+ _currentFling.value = info
+ }
+
override fun setLockscreenShadeExpansion(lockscreenShadeExpansion: Float) {
_lockscreenShadeExpansion.value = lockscreenShadeExpansion
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeBackActionInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeBackActionInteractorKosmos.kt
new file mode 100644
index 0000000..5dc0333
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeBackActionInteractorKosmos.kt
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.shade.domain.interactor
+
+import com.android.systemui.deviceentry.domain.interactor.deviceEntryInteractor
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.scene.domain.interactor.sceneInteractor
+
+val Kosmos.shadeBackActionInteractor by
+ Kosmos.Fixture {
+ ShadeBackActionInteractorImpl(
+ shadeInteractor = shadeInteractor,
+ sceneInteractor = sceneInteractor,
+ deviceEntryInteractor = deviceEntryInteractor,
+ )
+ }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/animation/ActivityLaunchAnimatorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/data/repository/NotificationLaunchAnimationRepositoryKosmos.kt
similarity index 71%
copy from packages/SystemUI/tests/utils/src/com/android/systemui/animation/ActivityLaunchAnimatorKosmos.kt
copy to packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/data/repository/NotificationLaunchAnimationRepositoryKosmos.kt
index 128f58b..5638cfc 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/animation/ActivityLaunchAnimatorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/data/repository/NotificationLaunchAnimationRepositoryKosmos.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2023 The Android Open Source Project
+ * Copyright (C) 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,8 +14,9 @@
* limitations under the License.
*/
-package com.android.systemui.animation
+package com.android.systemui.statusbar.notification.data.repository
import com.android.systemui.kosmos.Kosmos
-val Kosmos.activityLaunchAnimator by Kosmos.Fixture { ActivityLaunchAnimator() }
+val Kosmos.notificationLaunchAnimationRepository by
+ Kosmos.Fixture { NotificationLaunchAnimationRepository() }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/domain/interactor/NotificationLaunchAnimationInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/domain/interactor/NotificationLaunchAnimationInteractorKosmos.kt
new file mode 100644
index 0000000..0d84bab
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/domain/interactor/NotificationLaunchAnimationInteractorKosmos.kt
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2024 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.notification.domain.interactor
+
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.statusbar.notification.data.repository.notificationLaunchAnimationRepository
+
+val Kosmos.notificationLaunchAnimationInteractor by
+ Kosmos.Fixture {
+ NotificationLaunchAnimationInteractor(
+ repository = notificationLaunchAnimationRepository,
+ )
+ }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModelKosmos.kt
index ff22ca0..01cac4c 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModelKosmos.kt
@@ -19,12 +19,14 @@
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.Kosmos.Fixture
import com.android.systemui.shade.domain.interactor.shadeInteractor
+import com.android.systemui.shared.notifications.domain.interactor.notificationSettingsInteractor
import com.android.systemui.statusbar.notification.domain.interactor.activeNotificationsInteractor
import com.android.systemui.statusbar.notification.domain.interactor.seenNotificationsInteractor
val Kosmos.footerViewModel by Fixture {
FooterViewModel(
activeNotificationsInteractor = activeNotificationsInteractor,
+ notificationSettingsInteractor = notificationSettingsInteractor,
seenNotificationsInteractor = seenNotificationsInteractor,
shadeInteractor = shadeInteractor,
)
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinderKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinderKosmos.kt
index 748d04d..489598c 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinderKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinderKosmos.kt
@@ -23,6 +23,7 @@
import com.android.systemui.kosmos.Kosmos.Fixture
import com.android.systemui.kosmos.testDispatcher
import com.android.systemui.statusbar.notification.icon.ui.viewbinder.notificationIconContainerShelfViewBinder
+import com.android.systemui.statusbar.notification.notificationActivityStarter
import com.android.systemui.statusbar.notification.stack.ui.view.notificationStatsLogger
import com.android.systemui.statusbar.notification.stack.displaySwitchNotificationsHiderTracker
import com.android.systemui.statusbar.notification.stack.ui.viewmodel.notificationListViewModel
@@ -36,9 +37,10 @@
configuration = configurationState,
falsingManager = falsingManager,
iconAreaController = notificationIconAreaController,
+ loggerOptional = Optional.of(notificationStatsLogger),
metricsLogger = metricsLogger,
hiderTracker = displaySwitchNotificationsHiderTracker,
nicBinder = notificationIconContainerShelfViewBinder,
- loggerOptional = Optional.of(notificationStatsLogger),
+ notificationActivityStarter = { notificationActivityStarter },
)
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelKosmos.kt
index 998e579..37b2b76 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelKosmos.kt
@@ -16,10 +16,14 @@
package com.android.systemui.statusbar.notification.stack.ui.viewmodel
+import com.android.systemui.keyguard.domain.interactor.keyguardInteractor
import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.Kosmos.Fixture
+import com.android.systemui.power.domain.interactor.powerInteractor
+import com.android.systemui.shade.domain.interactor.shadeAnimationInteractor
import com.android.systemui.shade.domain.interactor.shadeInteractor
+import com.android.systemui.statusbar.domain.interactor.remoteInputInteractor
import com.android.systemui.statusbar.notification.domain.interactor.activeNotificationsInteractor
import com.android.systemui.statusbar.notification.domain.interactor.seenNotificationsInteractor
import com.android.systemui.statusbar.notification.footer.ui.viewmodel.footerViewModel
@@ -30,14 +34,18 @@
val Kosmos.notificationListViewModel by Fixture {
NotificationListViewModel(
- shelf = notificationShelfViewModel,
- hideListViewModel = hideListViewModel,
- footer = Optional.of(footerViewModel),
- logger = Optional.of(notificationListLoggerViewModel),
- activeNotificationsInteractor = activeNotificationsInteractor,
- keyguardTransitionInteractor = keyguardTransitionInteractor,
- seenNotificationsInteractor = seenNotificationsInteractor,
- shadeInteractor = shadeInteractor,
- zenModeInteractor = zenModeInteractor,
+ notificationShelfViewModel,
+ hideListViewModel,
+ Optional.of(footerViewModel),
+ Optional.of(notificationListLoggerViewModel),
+ activeNotificationsInteractor,
+ keyguardInteractor,
+ keyguardTransitionInteractor,
+ powerInteractor,
+ remoteInputInteractor,
+ seenNotificationsInteractor,
+ shadeInteractor,
+ userSetupInteractor,
+ zenModeInteractor,
)
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModelKosmos.kt
index d7e948e..29faa58 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModelKosmos.kt
@@ -17,6 +17,7 @@
package com.android.systemui.statusbar.notification.stack.ui.viewmodel
import com.android.systemui.flags.featureFlagsClassic
+import com.android.systemui.keyguard.domain.interactor.keyguardInteractor
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.Kosmos.Fixture
import com.android.systemui.scene.shared.flag.sceneContainerFlags
@@ -29,5 +30,6 @@
shadeInteractor = shadeInteractor,
flags = sceneContainerFlags,
featureFlags = featureFlagsClassic,
+ keyguardInteractor = keyguardInteractor,
)
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelKosmos.kt
index 549a775..30d4105 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelKosmos.kt
@@ -19,13 +19,16 @@
import com.android.systemui.communal.domain.interactor.communalInteractor
import com.android.systemui.keyguard.domain.interactor.keyguardInteractor
import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor
+import com.android.systemui.keyguard.ui.viewmodel.alternateBouncerToGoneTransitionViewModel
import com.android.systemui.keyguard.ui.viewmodel.aodBurnInViewModel
import com.android.systemui.keyguard.ui.viewmodel.dreamingToLockscreenTransitionViewModel
import com.android.systemui.keyguard.ui.viewmodel.glanceableHubToLockscreenTransitionViewModel
import com.android.systemui.keyguard.ui.viewmodel.lockscreenToDreamingTransitionViewModel
import com.android.systemui.keyguard.ui.viewmodel.lockscreenToGlanceableHubTransitionViewModel
+import com.android.systemui.keyguard.ui.viewmodel.lockscreenToGoneTransitionViewModel
import com.android.systemui.keyguard.ui.viewmodel.lockscreenToOccludedTransitionViewModel
import com.android.systemui.keyguard.ui.viewmodel.occludedToLockscreenTransitionViewModel
+import com.android.systemui.keyguard.ui.viewmodel.primaryBouncerToGoneTransitionViewModel
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.Kosmos.Fixture
import com.android.systemui.kosmos.applicationCoroutineScope
@@ -41,6 +44,9 @@
shadeInteractor = shadeInteractor,
communalInteractor = communalInteractor,
occludedToLockscreenTransitionViewModel = occludedToLockscreenTransitionViewModel,
+ lockscreenToGoneTransitionViewModel = lockscreenToGoneTransitionViewModel,
+ alternateBouncerToGoneTransitionViewModel = alternateBouncerToGoneTransitionViewModel,
+ primaryBouncerToGoneTransitionViewModel = primaryBouncerToGoneTransitionViewModel,
lockscreenToOccludedTransitionViewModel = lockscreenToOccludedTransitionViewModel,
dreamingToLockscreenTransitionViewModel = dreamingToLockscreenTransitionViewModel,
lockscreenToDreamingTransitionViewModel = lockscreenToDreamingTransitionViewModel,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterKosmos.kt
index 6ddc9df..c83710a 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterKosmos.kt
@@ -19,11 +19,11 @@
import android.app.keyguardManager
import android.content.applicationContext
import android.os.fakeExecutorHandler
-import android.service.dream.dreamManager
+import android.service.dream.dreamManagerInterface
import com.android.internal.logging.metricsLogger
import com.android.internal.widget.lockPatternUtils
import com.android.systemui.activityIntentHelper
-import com.android.systemui.animation.activityLaunchAnimator
+import com.android.systemui.animation.activityTransitionAnimator
import com.android.systemui.assist.assistManager
import com.android.systemui.concurrency.fakeExecutor
import com.android.systemui.kosmos.Kosmos
@@ -62,7 +62,7 @@
notificationClickNotifier,
statusBarKeyguardViewManager,
keyguardManager,
- dreamManager,
+ dreamManagerInterface,
Optional.of(bubblesManager),
{ assistManager },
notificationRemoteInputManager,
@@ -78,7 +78,7 @@
notificationPresenter,
shadeViewController,
notificationShadeWindowController,
- activityLaunchAnimator,
+ activityTransitionAnimator,
shadeAnimationInteractor,
notificationLaunchAnimatorControllerProvider,
launchFullScreenIntentProvider,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/ConfigurationControllerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/ConfigurationControllerKosmos.kt
index 33ed7e6..d4e9bfb 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/ConfigurationControllerKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/ConfigurationControllerKosmos.kt
@@ -17,8 +17,8 @@
package com.android.systemui.statusbar.policy
import com.android.systemui.kosmos.Kosmos
-import com.android.systemui.util.mockito.mock
-val Kosmos.configurationController by Kosmos.Fixture { mock<ConfigurationController>() }
+var Kosmos.configurationController: ConfigurationController by
+ Kosmos.Fixture { fakeConfigurationController }
val Kosmos.fakeConfigurationController: FakeConfigurationController by
Kosmos.Fixture { FakeConfigurationController() }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/data/repository/FakeDeviceProvisioningRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/data/repository/FakeDeviceProvisioningRepository.kt
index 3002299..fc6a800 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/data/repository/FakeDeviceProvisioningRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/data/repository/FakeDeviceProvisioningRepository.kt
@@ -24,7 +24,7 @@
@SysUISingleton
class FakeDeviceProvisioningRepository @Inject constructor() : DeviceProvisioningRepository {
- private val _isDeviceProvisioned = MutableStateFlow(false)
+ private val _isDeviceProvisioned = MutableStateFlow(true)
override val isDeviceProvisioned: Flow<Boolean> = _isDeviceProvisioned
private val _isFactoryResetProtectionActive = MutableStateFlow(false)
override val isFactoryResetProtectionActive: Flow<Boolean> = _isFactoryResetProtectionActive
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/user/domain/interactor/UserSwitcherInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/user/domain/interactor/UserSwitcherInteractorKosmos.kt
index 4e2dc7a..1504df4 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/user/domain/interactor/UserSwitcherInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/user/domain/interactor/UserSwitcherInteractorKosmos.kt
@@ -28,6 +28,7 @@
import com.android.systemui.kosmos.applicationCoroutineScope
import com.android.systemui.kosmos.testDispatcher
import com.android.systemui.plugins.activityStarter
+import com.android.systemui.process.processWrapper
import com.android.systemui.telephony.domain.interactor.telephonyInteractor
import com.android.systemui.user.data.repository.userRepository
import com.android.systemui.utils.userRestrictionChecker
@@ -53,5 +54,6 @@
guestUserInteractor = guestUserInteractor,
uiEventLogger = uiEventLogger,
userRestrictionChecker = userRestrictionChecker,
+ processWrapper = processWrapper,
)
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/animation/ActivityLaunchAnimatorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/util/settings/FakeSettingsKosmos.kt
similarity index 73%
copy from packages/SystemUI/tests/utils/src/com/android/systemui/animation/ActivityLaunchAnimatorKosmos.kt
copy to packages/SystemUI/tests/utils/src/com/android/systemui/util/settings/FakeSettingsKosmos.kt
index 128f58b..bcb5848 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/animation/ActivityLaunchAnimatorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/util/settings/FakeSettingsKosmos.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2023 The Android Open Source Project
+ * Copyright (C) 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,8 +14,9 @@
* limitations under the License.
*/
-package com.android.systemui.animation
+package com.android.systemui.util.settings
import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.Kosmos.Fixture
-val Kosmos.activityLaunchAnimator by Kosmos.Fixture { ActivityLaunchAnimator() }
+val Kosmos.fakeSettings: FakeSettings by Fixture { FakeSettings() }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/util/settings/FakeUserAwareSecureSettingsRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/util/settings/FakeUserAwareSecureSettingsRepository.kt
new file mode 100644
index 0000000..5054e29
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/util/settings/FakeUserAwareSecureSettingsRepository.kt
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.util.settings
+
+import com.android.systemui.util.settings.repository.UserAwareSecureSettingsRepository
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.map
+
+class FakeUserAwareSecureSettingsRepository : UserAwareSecureSettingsRepository {
+
+ private val settings = MutableStateFlow<Map<String, Boolean>>(mutableMapOf())
+
+ override fun boolSettingForActiveUser(name: String, defaultValue: Boolean): Flow<Boolean> {
+ return settings.map { it.getOrDefault(name, defaultValue) }
+ }
+
+ fun setBoolSettingForActiveUser(name: String, value: Boolean) {
+ settings.value = settings.value.toMutableMap().apply { this[name] = value }
+ }
+}
\ No newline at end of file
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/animation/ActivityLaunchAnimatorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/util/settings/UserAwareSecureSettingsRepositoryKosmos.kt
similarity index 74%
copy from packages/SystemUI/tests/utils/src/com/android/systemui/animation/ActivityLaunchAnimatorKosmos.kt
copy to packages/SystemUI/tests/utils/src/com/android/systemui/util/settings/UserAwareSecureSettingsRepositoryKosmos.kt
index 128f58b..94b2bdf 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/animation/ActivityLaunchAnimatorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/util/settings/UserAwareSecureSettingsRepositoryKosmos.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2023 The Android Open Source Project
+ * Copyright (C) 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,8 +14,9 @@
* limitations under the License.
*/
-package com.android.systemui.animation
+package com.android.systemui.util.settings
import com.android.systemui.kosmos.Kosmos
-val Kosmos.activityLaunchAnimator by Kosmos.Fixture { ActivityLaunchAnimator() }
+val Kosmos.userAwareSecureSettingsRepository by
+ Kosmos.Fixture { FakeUserAwareSecureSettingsRepository() }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeFlashlightController.java b/packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeFlashlightController.java
index f68baf5..592c6f4 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeFlashlightController.java
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeFlashlightController.java
@@ -30,11 +30,11 @@
private final List<FlashlightListener> callbacks = new ArrayList<>();
@VisibleForTesting
- public boolean isAvailable;
+ public boolean isAvailable = true;
@VisibleForTesting
- public boolean isEnabled;
+ public boolean isEnabled = false;
@VisibleForTesting
- public boolean hasFlashlight;
+ public boolean hasFlashlight = true;
public FakeFlashlightController(LeakCheck test) {
super(test, "flashlight");
@@ -52,16 +52,26 @@
callbacks.forEach(FlashlightListener::onFlashlightError);
}
+ /**
+ * Used to decide if tile should be shown or gone
+ * @return available/unavailable
+ */
@Override
public boolean hasFlashlight() {
return hasFlashlight;
}
+ /**
+ * @param newState active/inactive
+ */
@Override
public void setFlashlight(boolean newState) {
callbacks.forEach(flashlightListener -> flashlightListener.onFlashlightChanged(newState));
}
+ /**
+ * @return temporary availability
+ */
@Override
public boolean isAvailable() {
return isAvailable;
@@ -76,6 +86,9 @@
public void addCallback(FlashlightListener listener) {
super.addCallback(listener);
callbacks.add(listener);
+
+ listener.onFlashlightAvailabilityChanged(isAvailable());
+ listener.onFlashlightChanged(isEnabled());
}
@Override
diff --git a/packages/SystemUI/unfold/Android.bp b/packages/SystemUI/unfold/Android.bp
index 81fd8ce..32b7020 100644
--- a/packages/SystemUI/unfold/Android.bp
+++ b/packages/SystemUI/unfold/Android.bp
@@ -13,6 +13,7 @@
// limitations under the License.
package {
+ default_team: "trendy_team_system_ui_please_use_a_more_specific_subteam_if_possible_",
// See: http://go/android-license-faq
// A large-scale-change added 'default_applicable_licenses' to import
// all of the 'license_kinds' from "frameworks_base_packages_SystemUI_license"
diff --git a/packages/services/CameraExtensionsProxy/src/com/android/cameraextensions/CameraExtensionsProxyService.java b/packages/services/CameraExtensionsProxy/src/com/android/cameraextensions/CameraExtensionsProxyService.java
index 155c523..c134a4c 100644
--- a/packages/services/CameraExtensionsProxy/src/com/android/cameraextensions/CameraExtensionsProxyService.java
+++ b/packages/services/CameraExtensionsProxy/src/com/android/cameraextensions/CameraExtensionsProxyService.java
@@ -120,6 +120,7 @@
import java.io.IOException;
import java.util.ArrayList;
+import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -142,11 +143,13 @@
// Support for various latency improvements
private static final String LATENCY_VERSION_PREFIX = "1.4";
private static final String EFV_VERSION_PREFIX = "1.5";
+ private static final String GET_VERSION_PREFIX = "1.5";
private static final String[] ADVANCED_VERSION_PREFIXES = {EFV_VERSION_PREFIX,
- LATENCY_VERSION_PREFIX, ADVANCED_VERSION_PREFIX, RESULTS_VERSION_PREFIX };
+ LATENCY_VERSION_PREFIX, ADVANCED_VERSION_PREFIX, RESULTS_VERSION_PREFIX,
+ GET_VERSION_PREFIX};
private static final String[] SUPPORTED_VERSION_PREFIXES = {EFV_VERSION_PREFIX,
LATENCY_VERSION_PREFIX, RESULTS_VERSION_PREFIX, ADVANCED_VERSION_PREFIX, "1.1",
- NON_INIT_VERSION_PREFIX};
+ NON_INIT_VERSION_PREFIX, GET_VERSION_PREFIX};
private static final boolean EXTENSIONS_PRESENT = checkForExtensions();
private static final String EXTENSIONS_VERSION = EXTENSIONS_PRESENT ?
(new ExtensionVersionImpl()).checkApiVersion(LATEST_VERSION) : null;
@@ -156,6 +159,8 @@
(EXTENSIONS_VERSION.startsWith(EFV_VERSION_PREFIX)));
private static final boolean EFV_SUPPORTED = EXTENSIONS_PRESENT &&
(EXTENSIONS_VERSION.startsWith(EFV_VERSION_PREFIX));
+ private static final boolean GET_API_SUPPORTED = EXTENSIONS_PRESENT
+ && (EXTENSIONS_VERSION.startsWith(GET_VERSION_PREFIX));
private static final boolean ADVANCED_API_SUPPORTED = checkForAdvancedAPI();
private static final boolean INIT_API_SUPPORTED = EXTENSIONS_PRESENT &&
(!EXTENSIONS_VERSION.startsWith(NON_INIT_VERSION_PREFIX));
@@ -777,6 +782,12 @@
public boolean isPostviewAvailable() {
return false;
}
+
+ @Override
+ public List<Pair<CameraCharacteristics.Key, Object>>
+ getAvailableCharacteristicsKeyValues() {
+ return Collections.emptyList();
+ }
};
}
}
@@ -1186,6 +1197,35 @@
return false;
}
+
+ @Override
+ public CameraMetadataNative getAvailableCharacteristicsKeyValues(String cameraId) {
+ if (GET_API_SUPPORTED) {
+ List<Pair<CameraCharacteristics.Key, Object>> entries =
+ mAdvancedExtender.getAvailableCharacteristicsKeyValues();
+
+ if ((entries != null) && !entries.isEmpty()) {
+ CameraMetadataNative ret = new CameraMetadataNative();
+ long vendorId = mMetadataVendorIdMap.containsKey(cameraId)
+ ? mMetadataVendorIdMap.get(cameraId) : Long.MAX_VALUE;
+ ret.setVendorId(vendorId);
+ int[] characteristicsKeyTags = new int[entries.size()];
+ int i = 0;
+ for (Pair<CameraCharacteristics.Key, Object> entry : entries) {
+ int tag = CameraMetadataNative.getTag(entry.first.getName(), vendorId);
+ characteristicsKeyTags[i++] = tag;
+ ret.set(entry.first, entry.second);
+ }
+ ret.set(CameraCharacteristics.REQUEST_AVAILABLE_CHARACTERISTICS_KEYS,
+ characteristicsKeyTags);
+
+ return ret;
+ }
+ }
+
+ return null;
+ }
+
}
private class CaptureCallbackStub implements SessionProcessorImpl.CaptureCallback {
diff --git a/ravenwood/bulk_enable.py b/ravenwood/bulk_enable.py
index 36d398c..aafaaff 100644
--- a/ravenwood/bulk_enable.py
+++ b/ravenwood/bulk_enable.py
@@ -21,7 +21,7 @@
classes that have partial success.
Typical usage:
-$ ENABLE_PROBE_IGNORED=1 atest MyTestsRavenwood
+$ RAVENWOOD_RUN_DISABLED_TESTS=1 atest MyTestsRavenwood
$ cd /path/to/tests/root
$ python bulk_enable.py /path/to/atest/output/host_log.txt
"""
@@ -34,6 +34,8 @@
re_result = re.compile("I/ModuleListener.+?null-device-0 (.+?)#(.+?) ([A-Z_]+)(.*)$")
+DRY_RUN = "-n" in sys.argv
+
ANNOTATION = "@android.platform.test.annotations.EnabledOnRavenwood"
SED_ARG = "s/^((public )?class )/%s\\n\\1/g" % (ANNOTATION)
@@ -46,7 +48,7 @@
stats_class = collections.defaultdict(lambda: collections.defaultdict(int))
stats_method = collections.defaultdict()
-with open(sys.argv[1]) as f:
+with open(sys.argv[-1]) as f:
for line in f.readlines():
result = re_result.search(line)
if result:
@@ -67,7 +69,7 @@
clazz_match = re.compile("%s\.(kt|java)" % (clazz.split(".")[-1]))
for root, dirs, files in os.walk("."):
for f in files:
- if clazz_match.match(f):
+ if clazz_match.match(f) and not DRY_RUN:
path = os.path.join(root, f)
subprocess.run(["sed", "-i", "-E", SED_ARG, path])
diff --git a/ravenwood/coretest/Android.bp b/ravenwood/coretest/Android.bp
new file mode 100644
index 0000000..9b7f8f7
--- /dev/null
+++ b/ravenwood/coretest/Android.bp
@@ -0,0 +1,23 @@
+package {
+ // See: http://go/android-license-faq
+ // A large-scale-change added 'default_applicable_licenses' to import
+ // all of the 'license_kinds' from "frameworks_base_license"
+ // to get the below license kinds:
+ // SPDX-license-identifier-Apache-2.0
+ default_applicable_licenses: ["frameworks_base_license"],
+}
+
+android_ravenwood_test {
+ name: "RavenwoodCoreTest",
+
+ static_libs: [
+ "androidx.annotation_annotation",
+ "androidx.test.rules",
+ ],
+
+ srcs: [
+ "test/**/*.java",
+ ],
+ sdk_version: "test_current",
+ auto_gen_config: true,
+}
diff --git a/ravenwood/coretest/test/com/android/platform/test/ravenwood/coretest/RavenwoodTestRunnerValidationTest.java b/ravenwood/coretest/test/com/android/platform/test/ravenwood/coretest/RavenwoodTestRunnerValidationTest.java
new file mode 100644
index 0000000..e58c282
--- /dev/null
+++ b/ravenwood/coretest/test/com/android/platform/test/ravenwood/coretest/RavenwoodTestRunnerValidationTest.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2024 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.platform.test.ravenwood.coretest;
+
+import android.platform.test.ravenwood.RavenwoodRule;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Assume;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.rules.RuleChain;
+import org.junit.runner.RunWith;
+
+/**
+ * Test for the test runner validator in RavenwoodRule.
+ */
+@RunWith(AndroidJUnit4.class)
+public class RavenwoodTestRunnerValidationTest {
+ // Note the following rules don't have a @Rule, because they need to be applied in a specific
+ // order. So we use a RuleChain instead.
+ private ExpectedException mThrown = ExpectedException.none();
+ private final RavenwoodRule mRavenwood = new RavenwoodRule();
+
+ @Rule
+ public final RuleChain chain = RuleChain.outerRule(mThrown).around(mRavenwood);
+
+ public RavenwoodTestRunnerValidationTest() {
+ Assume.assumeTrue(mRavenwood._ravenwood_private$isOptionalValidationEnabled());
+ // Because RavenwoodRule will throw this error before executing the test method,
+ // we can't do it in the test method itself.
+ // So instead, we initialize it here.
+ mThrown.expectMessage("Switch to androidx.test.ext.junit.runners.AndroidJUnit4");
+ }
+
+ @Test
+ public void testValidateTestRunner() {
+ }
+}
diff --git a/ravenwood/fix_test_runner.py b/ravenwood/fix_test_runner.py
new file mode 100755
index 0000000..99b7a1f
--- /dev/null
+++ b/ravenwood/fix_test_runner.py
@@ -0,0 +1,68 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2024 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.
+
+"""
+Tool switch the deprecated jetpack test runner to the correct one.
+
+Typical usage:
+$ RAVENWOOD_OPTIONAL_VALIDATION=1 atest MyTestsRavenwood # Prepend RAVENWOOD_RUN_DISABLED_TESTS=1 as needed
+$ cd /path/to/tests/root
+$ python bulk_enable.py /path/to/atest/output/host_log.txt
+"""
+
+import collections
+import os
+import re
+import subprocess
+import sys
+
+re_result = re.compile("I/ModuleListener.+?null-device-0 (.+?)#(.+?) ([A-Z_]+)(.*)$")
+
+OLD_RUNNER = "androidx.test.runner.AndroidJUnit4"
+NEW_RUNNER = "androidx.test.ext.junit.runners.AndroidJUnit4"
+SED_ARG = r"s/%s/%s/g" % (OLD_RUNNER, NEW_RUNNER)
+
+target = collections.defaultdict()
+
+with open(sys.argv[1]) as f:
+ for line in f.readlines():
+ result = re_result.search(line)
+ if result:
+ clazz, method, state, msg = result.groups()
+ if NEW_RUNNER in msg:
+ target[clazz] = 1
+
+if len(target) == 0:
+ print("No tests need updating.")
+ sys.exit(0)
+
+num_fixed = 0
+for clazz in target.keys():
+ print("Fixing test runner", clazz)
+ clazz_match = re.compile("%s\.(kt|java)" % (clazz.split(".")[-1]))
+ found = False
+ for root, dirs, files in os.walk("."):
+ for f in files:
+ if clazz_match.match(f):
+ found = True
+ num_fixed += 1
+ path = os.path.join(root, f)
+ subprocess.run(["sed", "-i", "-E", SED_ARG, path])
+ if not found:
+ print(f" Warining: tests {clazz} not found")
+
+
+print("Tests fixed", num_fixed)
diff --git a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuleImpl.java b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuleImpl.java
index 3670459..7b5932b 100644
--- a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuleImpl.java
+++ b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuleImpl.java
@@ -16,7 +16,9 @@
package android.platform.test.ravenwood;
+import android.app.ActivityManager;
import android.app.Instrumentation;
+import android.os.Build;
import android.os.Bundle;
import android.os.HandlerThread;
import android.os.Looper;
@@ -26,14 +28,19 @@
import com.android.internal.os.RuntimeInit;
+import org.junit.Assert;
import org.junit.runner.Description;
+import org.junit.runner.RunWith;
+import org.junit.runners.model.Statement;
import java.io.PrintStream;
import java.util.Map;
+import java.util.Objects;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
public class RavenwoodRuleImpl {
private static final String MAIN_THREAD_NAME = "RavenwoodMain";
@@ -50,11 +57,34 @@
private static ScheduledFuture<?> sPendingTimeout;
+ /**
+ * When enabled, attempt to detect uncaught exceptions from background threads.
+ */
+ private static final boolean ENABLE_UNCAUGHT_EXCEPTION_DETECTION = false;
+
+ /**
+ * When set, an unhandled exception was discovered (typically on a background thread), and we
+ * capture it here to ensure it's reported as a test failure.
+ */
+ private static final AtomicReference<Throwable> sPendingUncaughtException =
+ new AtomicReference<>();
+
+ private static final Thread.UncaughtExceptionHandler sUncaughtExceptionHandler =
+ (thread, throwable) -> {
+ // Remember the first exception we discover
+ sPendingUncaughtException.compareAndSet(null, throwable);
+ };
+
public static boolean isOnRavenwood() {
return true;
}
public static void init(RavenwoodRule rule) {
+ if (ENABLE_UNCAUGHT_EXCEPTION_DETECTION) {
+ maybeThrowPendingUncaughtException(false);
+ Thread.setDefaultUncaughtExceptionHandler(sUncaughtExceptionHandler);
+ }
+
RuntimeInit.redirectLogStreams();
android.os.Process.init$ravenwood(rule.mUid, rule.mPid);
@@ -64,6 +94,8 @@
rule.mSystemProperties.getKeyReadablePredicate(),
rule.mSystemProperties.getKeyWritablePredicate());
+ ActivityManager.init$ravenwood(rule.mCurrentUser);
+
com.android.server.LocalServices.removeAllServicesForTest();
if (rule.mProvideMainThread) {
@@ -78,6 +110,10 @@
sPendingTimeout = sTimeoutExecutor.schedule(RavenwoodRuleImpl::dumpStacks,
TIMEOUT_MILLIS, TimeUnit.MILLISECONDS);
}
+
+ // Touch some references early to ensure they're <clinit>'ed
+ Objects.requireNonNull(Build.IS_USERDEBUG);
+ Objects.requireNonNull(Build.VERSION.SDK);
}
public static void reset(RavenwoodRule rule) {
@@ -94,9 +130,15 @@
com.android.server.LocalServices.removeAllServicesForTest();
+ ActivityManager.reset$ravenwood();
+
android.os.SystemProperties.reset$ravenwood();
android.os.Binder.reset$ravenwood();
android.os.Process.reset$ravenwood();
+
+ if (ENABLE_UNCAUGHT_EXCEPTION_DETECTION) {
+ maybeThrowPendingUncaughtException(true);
+ }
}
public static void logTestRunner(String label, Description description) {
@@ -120,4 +162,48 @@
}
out.println("-----END ALL THREAD STACKS-----");
}
+
+ /**
+ * If there's a pending uncaught exception, consume and throw it now. Typically used to
+ * report an exception on a background thread as a failure for the currently running test.
+ */
+ private static void maybeThrowPendingUncaughtException(boolean duringReset) {
+ final Throwable pending = sPendingUncaughtException.getAndSet(null);
+ if (pending != null) {
+ if (duringReset) {
+ throw new IllegalStateException(
+ "Found an uncaught exception during this test", pending);
+ } else {
+ throw new IllegalStateException(
+ "Found an uncaught exception before this test started", pending);
+ }
+ }
+ }
+
+ public static void validate(Statement base, Description description,
+ boolean enableOptionalValidation) {
+ validateTestRunner(base, description, enableOptionalValidation);
+ }
+
+ private static void validateTestRunner(Statement base, Description description,
+ boolean shouldFail) {
+ final var testClass = description.getTestClass();
+ final var runWith = testClass.getAnnotation(RunWith.class);
+ if (runWith == null) {
+ return;
+ }
+
+ // Due to build dependencies, we can't directly refer to androidx classes here,
+ // so just check the class name instead.
+ if (runWith.value().getCanonicalName().equals("androidx.test.runner.AndroidJUnit4")) {
+ var message = "Test " + testClass.getCanonicalName() + " uses deprecated"
+ + " test runner androidx.test.runner.AndroidJUnit4."
+ + " Switch to androidx.test.ext.junit.runners.AndroidJUnit4.";
+ if (shouldFail) {
+ Assert.fail(message);
+ } else {
+ System.err.println("Warning: " + message);
+ }
+ }
+ }
}
diff --git a/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodRule.java b/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodRule.java
index 0285b38..764573d 100644
--- a/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodRule.java
+++ b/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodRule.java
@@ -16,6 +16,10 @@
package android.platform.test.ravenwood;
+import static android.os.Process.FIRST_APPLICATION_UID;
+import static android.os.Process.SYSTEM_UID;
+import static android.os.UserHandle.USER_SYSTEM;
+
import static org.junit.Assert.fail;
import android.platform.test.annotations.DisabledOnRavenwood;
@@ -85,6 +89,12 @@
private static final boolean ENABLE_REALLY_DISABLE_PATTERN =
!REALLY_DISABLE_PATTERN.pattern().isEmpty();
+ /**
+ * If true, enable optional validation on running tests.
+ */
+ private static final boolean ENABLE_OPTIONAL_VALIDATION = "1".equals(
+ System.getenv("RAVENWOOD_OPTIONAL_VALIDATION"));
+
static {
if (ENABLE_PROBE_IGNORED) {
System.out.println("$RAVENWOOD_RUN_DISABLED_TESTS enabled: force running all tests");
@@ -94,12 +104,12 @@
}
}
- private static final int SYSTEM_UID = 1000;
private static final int NOBODY_UID = 9999;
- private static final int FIRST_APPLICATION_UID = 10000;
private static final AtomicInteger sNextPid = new AtomicInteger(100);
+ int mCurrentUser = USER_SYSTEM;
+
/**
* Unless the test author requests differently, run as "nobody", and give each collection of
* tests its own unique PID.
@@ -271,6 +281,12 @@
}
}
+ private void commonPrologue(Statement base, Description description) {
+ RavenwoodRuleImpl.logTestRunner("started", description);
+ RavenwoodRuleImpl.validate(base, description, ENABLE_OPTIONAL_VALIDATION);
+ RavenwoodRuleImpl.init(RavenwoodRule.this);
+ }
+
/**
* Run the given {@link Statement} with no special treatment.
*/
@@ -280,8 +296,7 @@
public void evaluate() throws Throwable {
Assume.assumeTrue(shouldEnableOnRavenwood(description));
- RavenwoodRuleImpl.logTestRunner("started", description);
- RavenwoodRuleImpl.init(RavenwoodRule.this);
+ commonPrologue(base, description);
try {
base.evaluate();
RavenwoodRuleImpl.logTestRunner("finished", description);
@@ -306,8 +321,7 @@
public void evaluate() throws Throwable {
Assume.assumeFalse(shouldStillIgnoreInProbeIgnoreMode(description));
- RavenwoodRuleImpl.logTestRunner("started", description);
- RavenwoodRuleImpl.init(RavenwoodRule.this);
+ commonPrologue(base, description);
try {
base.evaluate();
} catch (Throwable t) {
@@ -327,4 +341,11 @@
}
};
}
+
+ /**
+ * Do not use it outside ravenwood core classes.
+ */
+ public boolean _ravenwood_private$isOptionalValidationEnabled() {
+ return ENABLE_OPTIONAL_VALIDATION;
+ }
}
diff --git a/ravenwood/junit-stub-src/android/platform/test/ravenwood/RavenwoodRuleImpl.java b/ravenwood/junit-stub-src/android/platform/test/ravenwood/RavenwoodRuleImpl.java
index 7d172f2..e951351b 100644
--- a/ravenwood/junit-stub-src/android/platform/test/ravenwood/RavenwoodRuleImpl.java
+++ b/ravenwood/junit-stub-src/android/platform/test/ravenwood/RavenwoodRuleImpl.java
@@ -17,6 +17,7 @@
package android.platform.test.ravenwood;
import org.junit.runner.Description;
+import org.junit.runners.model.Statement;
public class RavenwoodRuleImpl {
public static boolean isOnRavenwood() {
@@ -34,4 +35,8 @@
public static void logTestRunner(String label, Description description) {
// No-op when running on a real device
}
+
+ public static void validate(Statement base, Description description,
+ boolean enableOptionalValidation) {
+ }
}
diff --git a/ravenwood/minimum-test/Android.bp b/ravenwood/minimum-test/Android.bp
index bf3583c..e4ed3d5 100644
--- a/ravenwood/minimum-test/Android.bp
+++ b/ravenwood/minimum-test/Android.bp
@@ -13,6 +13,7 @@
static_libs: [
"androidx.annotation_annotation",
+ "androidx.test.ext.junit",
"androidx.test.rules",
],
diff --git a/ravenwood/minimum-test/test/com/android/ravenwood/RavenwoodMinimumTest.java b/ravenwood/minimum-test/test/com/android/ravenwood/RavenwoodMinimumTest.java
index 7abfecf..03cfad5 100644
--- a/ravenwood/minimum-test/test/com/android/ravenwood/RavenwoodMinimumTest.java
+++ b/ravenwood/minimum-test/test/com/android/ravenwood/RavenwoodMinimumTest.java
@@ -18,7 +18,7 @@
import android.platform.test.annotations.IgnoreUnderRavenwood;
import android.platform.test.ravenwood.RavenwoodRule;
-import androidx.test.runner.AndroidJUnit4;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.Assert;
import org.junit.Rule;
diff --git a/ravenwood/ravenwood-annotation-allowed-classes.txt b/ravenwood/ravenwood-annotation-allowed-classes.txt
index e49b64e..a5ecd20 100644
--- a/ravenwood/ravenwood-annotation-allowed-classes.txt
+++ b/ravenwood/ravenwood-annotation-allowed-classes.txt
@@ -82,6 +82,8 @@
android.os.UidBatteryConsumer$Builder
android.os.UserHandle
android.os.UserManager
+android.os.VibrationAttributes
+android.os.VibrationAttributes$Builder
android.os.WorkSource
android.content.ClipData
@@ -144,6 +146,7 @@
android.content.ContentProvider
+android.app.ActivityManager
android.app.Instrumentation
android.metrics.LogMaker
diff --git a/ravenwood/test-authors.md b/ravenwood/test-authors.md
index 5df827f..9179a62 100644
--- a/ravenwood/test-authors.md
+++ b/ravenwood/test-authors.md
@@ -101,7 +101,7 @@
@RunWith(AndroidJUnit4.class)
public class MyCodeTest {
@Rule
- public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+ public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(SetFlagsRule.DefaultInitValueType.NULL_DEFAULT);
@Test
public void testEnabled() {
diff --git a/services/accessibility/accessibility.aconfig b/services/accessibility/accessibility.aconfig
index 44682e2..f902439 100644
--- a/services/accessibility/accessibility.aconfig
+++ b/services/accessibility/accessibility.aconfig
@@ -52,6 +52,13 @@
}
flag {
+ name: "fullscreen_fling_gesture"
+ namespace: "accessibility"
+ description: "When true, adds a fling gesture animation for fullscreen magnification"
+ bug: "319175022"
+}
+
+flag {
name: "pinch_zoom_zero_min_span"
namespace: "accessibility"
description: "Whether to set min span of ScaleGestureDetector to zero."
diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
index 44144f8..c58cb72 100644
--- a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
+++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
@@ -31,7 +31,10 @@
import static android.companion.virtual.VirtualDeviceManager.EXTRA_VIRTUAL_DEVICE_ID;
import static android.content.Context.DEVICE_ID_DEFAULT;
import static android.provider.Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_NAVBAR_ENABLED;
+import static android.view.accessibility.AccessibilityManager.ACCESSIBILITY_BUTTON;
+import static android.view.accessibility.AccessibilityManager.ACCESSIBILITY_SHORTCUT_KEY;
import static android.view.accessibility.AccessibilityManager.FlashNotificationReason;
+import static android.view.accessibility.AccessibilityManager.ShortcutType;
import static com.android.internal.accessibility.AccessibilityShortcutController.ACCESSIBILITY_HEARING_AIDS_COMPONENT_NAME;
import static com.android.internal.accessibility.AccessibilityShortcutController.MAGNIFICATION_COMPONENT_NAME;
@@ -140,7 +143,6 @@
import com.android.internal.accessibility.AccessibilityShortcutController;
import com.android.internal.accessibility.AccessibilityShortcutController.FrameworkFeatureInfo;
import com.android.internal.accessibility.AccessibilityShortcutController.LaunchableFrameworkFeatureInfo;
-import com.android.internal.accessibility.common.ShortcutConstants;
import com.android.internal.accessibility.dialog.AccessibilityButtonChooserActivity;
import com.android.internal.accessibility.dialog.AccessibilityShortcutChooserActivity;
import com.android.internal.accessibility.util.AccessibilityUtils;
@@ -1720,7 +1722,7 @@
}
mMainHandler.sendMessage(obtainMessage(
AccessibilityManagerService::performAccessibilityShortcutInternal, this,
- displayId, ShortcutConstants.UserShortcutType.SOFTWARE, targetName));
+ displayId, ACCESSIBILITY_BUTTON, targetName));
}
/**
@@ -2199,12 +2201,11 @@
}
private void showAccessibilityTargetsSelection(int displayId,
- @ShortcutConstants.UserShortcutType int shortcutType) {
+ @ShortcutType int shortcutType) {
final Intent intent = new Intent(AccessibilityManager.ACTION_CHOOSE_ACCESSIBILITY_BUTTON);
- final String chooserClassName =
- (shortcutType == ShortcutConstants.UserShortcutType.HARDWARE)
- ? AccessibilityShortcutChooserActivity.class.getName()
- : AccessibilityButtonChooserActivity.class.getName();
+ final String chooserClassName = (shortcutType == ACCESSIBILITY_SHORTCUT_KEY)
+ ? AccessibilityShortcutChooserActivity.class.getName()
+ : AccessibilityButtonChooserActivity.class.getName();
intent.setClassName(CHOOSER_PACKAGE_NAME, chooserClassName);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
final Bundle bundle = ActivityOptions.makeBasic().setLaunchDisplayId(displayId).toBundle();
@@ -3275,7 +3276,7 @@
}
final Set<String> currentTargets =
- userState.getShortcutTargetsLocked(ShortcutConstants.UserShortcutType.HARDWARE);
+ userState.getShortcutTargetsLocked(ACCESSIBILITY_SHORTCUT_KEY);
if (targetsFromSetting.equals(currentTargets)) {
return false;
}
@@ -3291,7 +3292,7 @@
userState.mUserId, str -> str, targetsFromSetting);
final Set<String> currentTargets =
- userState.getShortcutTargetsLocked(ShortcutConstants.UserShortcutType.SOFTWARE);
+ userState.getShortcutTargetsLocked(ACCESSIBILITY_BUTTON);
if (targetsFromSetting.equals(currentTargets)) {
return false;
}
@@ -3346,7 +3347,7 @@
*/
private void updateAccessibilityShortcutKeyTargetsLocked(AccessibilityUserState userState) {
final Set<String> currentTargets =
- userState.getShortcutTargetsLocked(ShortcutConstants.UserShortcutType.HARDWARE);
+ userState.getShortcutTargetsLocked(ACCESSIBILITY_SHORTCUT_KEY);
final int lastSize = currentTargets.size();
if (lastSize == 0) {
return;
@@ -3531,7 +3532,7 @@
}
final Set<String> currentTargets =
- userState.getShortcutTargetsLocked(ShortcutConstants.UserShortcutType.SOFTWARE);
+ userState.getShortcutTargetsLocked(ACCESSIBILITY_BUTTON);
final int lastSize = currentTargets.size();
if (lastSize == 0) {
return;
@@ -3571,7 +3572,7 @@
return;
}
final Set<String> buttonTargets =
- userState.getShortcutTargetsLocked(ShortcutConstants.UserShortcutType.SOFTWARE);
+ userState.getShortcutTargetsLocked(ACCESSIBILITY_BUTTON);
int lastSize = buttonTargets.size();
buttonTargets.removeIf(name -> {
if (packageName != null && name != null && !name.contains(packageName)) {
@@ -3608,7 +3609,7 @@
lastSize = buttonTargets.size();
final Set<String> shortcutKeyTargets =
- userState.getShortcutTargetsLocked(ShortcutConstants.UserShortcutType.HARDWARE);
+ userState.getShortcutTargetsLocked(ACCESSIBILITY_SHORTCUT_KEY);
userState.mEnabledServices.forEach(componentName -> {
if (packageName != null && componentName != null
&& !packageName.equals(componentName.getPackageName())) {
@@ -3665,18 +3666,16 @@
return;
}
final ComponentName serviceName = service.getComponentName();
- if (userState.removeShortcutTargetLocked(
- ShortcutConstants.UserShortcutType.HARDWARE, serviceName)) {
+ if (userState.removeShortcutTargetLocked(ACCESSIBILITY_SHORTCUT_KEY, serviceName)) {
final Set<String> currentTargets = userState.getShortcutTargetsLocked(
- ShortcutConstants.UserShortcutType.HARDWARE);
+ ACCESSIBILITY_SHORTCUT_KEY);
persistColonDelimitedSetToSettingLocked(
Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_SERVICE,
userState.mUserId, currentTargets, str -> str);
}
- if (userState.removeShortcutTargetLocked(
- ShortcutConstants.UserShortcutType.SOFTWARE, serviceName)) {
+ if (userState.removeShortcutTargetLocked(ACCESSIBILITY_BUTTON, serviceName)) {
final Set<String> currentTargets = userState.getShortcutTargetsLocked(
- ShortcutConstants.UserShortcutType.SOFTWARE);
+ ACCESSIBILITY_BUTTON);
persistColonDelimitedSetToSettingLocked(Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS,
userState.mUserId, currentTargets, str -> str);
}
@@ -3752,7 +3751,7 @@
}
mMainHandler.sendMessage(obtainMessage(
AccessibilityManagerService::performAccessibilityShortcutInternal, this,
- Display.DEFAULT_DISPLAY, ShortcutConstants.UserShortcutType.HARDWARE, targetName));
+ Display.DEFAULT_DISPLAY, ACCESSIBILITY_SHORTCUT_KEY, targetName));
}
/**
@@ -3765,7 +3764,7 @@
* specified target.
*/
private void performAccessibilityShortcutInternal(int displayId,
- @ShortcutConstants.UserShortcutType int shortcutType, @Nullable String targetName) {
+ @ShortcutType int shortcutType, @Nullable String targetName) {
final List<String> shortcutTargets = getAccessibilityShortcutTargetsInternal(shortcutType);
if (shortcutTargets.isEmpty()) {
Slog.d(LOG_TAG, "No target to perform shortcut, shortcutType=" + shortcutType);
@@ -3816,7 +3815,7 @@
}
private boolean performAccessibilityFrameworkFeature(int displayId,
- ComponentName assignedTarget, @ShortcutConstants.UserShortcutType int shortcutType) {
+ ComponentName assignedTarget, @ShortcutType int shortcutType) {
final Map<ComponentName, FrameworkFeatureInfo> frameworkFeatureMap =
AccessibilityShortcutController.getFrameworkShortcutFeaturesMap();
if (!frameworkFeatureMap.containsKey(assignedTarget)) {
@@ -3865,12 +3864,12 @@
/**
* Perform accessibility service shortcut action.
*
- * 1) For {@link ShortcutConstants.UserShortcutType.SOFTWARE} type and services targeting sdk
+ * 1) For {@link AccessibilityManager#ACCESSIBILITY_BUTTON} type and services targeting sdk
* version <= Q: callbacks to accessibility service if service is bounded and requests
* accessibility button.
- * 2) For {@link ShortcutConstants.UserShortcutType.HARDWARE} type and service targeting sdk
+ * 2) For {@link AccessibilityManager#ACCESSIBILITY_SHORTCUT_KEY} type and service targeting sdk
* version <= Q: turns on / off the accessibility service.
- * 3) For {@link ShortcutConstants.UserShortcutType.HARDWARE} type and service targeting sdk
+ * 3) For {@link AccessibilityManager#ACCESSIBILITY_SHORTCUT_KEY} type and service targeting sdk
* version > Q and request accessibility button: turn on the accessibility service if it's
* not in the enabled state.
* (It'll happen when a service is disabled and assigned to shortcut then upgraded.)
@@ -3881,7 +3880,7 @@
* button.
*/
private boolean performAccessibilityShortcutTargetService(int displayId,
- @ShortcutConstants.UserShortcutType int shortcutType, ComponentName assignedTarget) {
+ @ShortcutType int shortcutType, ComponentName assignedTarget) {
synchronized (mLock) {
final AccessibilityUserState userState = getCurrentUserStateLocked();
final AccessibilityServiceInfo installedServiceInfo =
@@ -3899,8 +3898,7 @@
final boolean requestA11yButton = (installedServiceInfo.flags
& FLAG_REQUEST_ACCESSIBILITY_BUTTON) != 0;
// Turns on / off the accessibility service
- if ((targetSdk <= Build.VERSION_CODES.Q
- && shortcutType == ShortcutConstants.UserShortcutType.HARDWARE)
+ if ((targetSdk <= Build.VERSION_CODES.Q && shortcutType == ACCESSIBILITY_SHORTCUT_KEY)
|| (targetSdk > Build.VERSION_CODES.Q && !requestA11yButton)) {
if (serviceConnection == null) {
logAccessibilityShortcutActivated(mContext, assignedTarget, shortcutType,
@@ -3914,8 +3912,7 @@
}
return true;
}
- if (shortcutType == ShortcutConstants.UserShortcutType.HARDWARE
- && targetSdk > Build.VERSION_CODES.Q
+ if (shortcutType == ACCESSIBILITY_SHORTCUT_KEY && targetSdk > Build.VERSION_CODES.Q
&& requestA11yButton) {
if (!userState.getEnabledServicesLocked().contains(assignedTarget)) {
enableAccessibilityServiceLocked(assignedTarget, mCurrentUserId);
@@ -3945,8 +3942,7 @@
}
@Override
- public List<String> getAccessibilityShortcutTargets(
- @ShortcutConstants.UserShortcutType int shortcutType) {
+ public List<String> getAccessibilityShortcutTargets(@ShortcutType int shortcutType) {
if (mTraceManager.isA11yTracingEnabledForTypes(FLAGS_ACCESSIBILITY_MANAGER)) {
mTraceManager.logTrace(LOG_TAG + ".getAccessibilityShortcutTargets",
FLAGS_ACCESSIBILITY_MANAGER, "shortcutType=" + shortcutType);
@@ -3960,13 +3956,12 @@
return getAccessibilityShortcutTargetsInternal(shortcutType);
}
- private List<String> getAccessibilityShortcutTargetsInternal(
- @ShortcutConstants.UserShortcutType int shortcutType) {
+ private List<String> getAccessibilityShortcutTargetsInternal(@ShortcutType int shortcutType) {
synchronized (mLock) {
final AccessibilityUserState userState = getCurrentUserStateLocked();
final ArrayList<String> shortcutTargets = new ArrayList<>(
userState.getShortcutTargetsLocked(shortcutType));
- if (shortcutType != ShortcutConstants.UserShortcutType.SOFTWARE) {
+ if (shortcutType != ACCESSIBILITY_BUTTON) {
return shortcutTargets;
}
// Adds legacy a11y services requesting a11y button into the list.
@@ -4429,7 +4424,7 @@
}
}
// Warning is not required if the service is already assigned to a shortcut.
- for (int shortcutType : ShortcutConstants.USER_SHORTCUT_TYPES) {
+ for (int shortcutType : AccessibilityManager.SHORTCUT_TYPES) {
if (getAccessibilityShortcutTargets(shortcutType).contains(
componentName.flattenToString())) {
return false;
@@ -4609,8 +4604,8 @@
public void onShellCommand(FileDescriptor in, FileDescriptor out,
FileDescriptor err, String[] args, ShellCallback callback,
ResultReceiver resultReceiver) {
- new AccessibilityShellCommand(this, mSystemActionPerformer)
- .exec(this, in, out, err, args, callback, resultReceiver);
+ new AccessibilityShellCommand(this, mSystemActionPerformer).exec(this, in, out, err, args,
+ callback, resultReceiver);
}
private final class InteractionBridge {
diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityUserState.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityUserState.java
index 41165b6..68ee780 100644
--- a/services/accessibility/java/com/android/server/accessibility/AccessibilityUserState.java
+++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityUserState.java
@@ -24,6 +24,9 @@
import static android.accessibilityservice.AccessibilityService.SHOW_MODE_MASK;
import static android.provider.Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE_FULLSCREEN;
import static android.provider.Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE_NONE;
+import static android.view.accessibility.AccessibilityManager.ACCESSIBILITY_BUTTON;
+import static android.view.accessibility.AccessibilityManager.ACCESSIBILITY_SHORTCUT_KEY;
+import static android.view.accessibility.AccessibilityManager.ShortcutType;
import static com.android.internal.accessibility.AccessibilityShortcutController.MAGNIFICATION_CONTROLLER_NAME;
@@ -48,7 +51,6 @@
import com.android.internal.R;
import com.android.internal.accessibility.AccessibilityShortcutController;
-import com.android.internal.accessibility.common.ShortcutConstants;
import java.io.FileDescriptor;
import java.io.PrintWriter;
@@ -755,21 +757,13 @@
* @param shortcutType The shortcut type.
* @return The array set of the strings
*/
- public ArraySet<String> getShortcutTargetsLocked(
- @ShortcutConstants.UserShortcutType int shortcutType) {
- if (shortcutType == ShortcutConstants.UserShortcutType.HARDWARE) {
+ public ArraySet<String> getShortcutTargetsLocked(@ShortcutType int shortcutType) {
+ if (shortcutType == ACCESSIBILITY_SHORTCUT_KEY) {
return mAccessibilityShortcutKeyTargets;
- } else if (shortcutType == ShortcutConstants.UserShortcutType.SOFTWARE) {
+ } else if (shortcutType == ACCESSIBILITY_BUTTON) {
return mAccessibilityButtonTargets;
- } else if ((shortcutType == ShortcutConstants.UserShortcutType.TRIPLETAP
- && isMagnificationSingleFingerTripleTapEnabledLocked()) || (
- shortcutType == ShortcutConstants.UserShortcutType.TWO_FINGERS_TRIPLE_TAP
- && isMagnificationTwoFingerTripleTapEnabledLocked())) {
- ArraySet<String> targets = new ArraySet<>();
- targets.add(MAGNIFICATION_CONTROLLER_NAME);
- return targets;
}
- return new ArraySet<>();
+ return null;
}
/**
@@ -808,22 +802,12 @@
/**
* Removes given shortcut target in the list.
*
- * @param shortcutType one of {@link ShortcutConstants.UserShortcutType.HARDWARE} or
- * {@link ShortcutConstants.UserShortcutType.SOFTWARE}. Other types are not
- * implemented.
- * @param target The component name of the shortcut target.
+ * @param shortcutType The shortcut type.
+ * @param target The component name of the shortcut target.
* @return true if the shortcut target is removed.
*/
- public boolean removeShortcutTargetLocked(@ShortcutConstants.UserShortcutType int shortcutType,
+ public boolean removeShortcutTargetLocked(@ShortcutType int shortcutType,
ComponentName target) {
- if (shortcutType == ShortcutConstants.UserShortcutType.TRIPLETAP
- || shortcutType == ShortcutConstants.UserShortcutType.TWO_FINGERS_TRIPLE_TAP) {
- throw new UnsupportedOperationException(
- "removeShortcutTargetLocked only support shortcut type: "
- + "software and hardware for now"
- );
- }
-
return getShortcutTargetsLocked(shortcutType).removeIf(name -> {
ComponentName componentName;
if (name == null
diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityWindowManager.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityWindowManager.java
index 78f07e4..26c1bc9 100644
--- a/services/accessibility/java/com/android/server/accessibility/AccessibilityWindowManager.java
+++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityWindowManager.java
@@ -818,7 +818,7 @@
}
// Don't need to add the embedded hierarchy windows into the accessibility windows list.
- if (mHostEmbeddedMap.size() > 0 && isEmbeddedHierarchyWindowsLocked(windowId)) {
+ if (isEmbeddedHierarchyWindowsLocked(windowId)) {
return null;
}
final AccessibilityWindowInfo reportedWindow = AccessibilityWindowInfo.obtain();
@@ -866,21 +866,6 @@
return reportedWindow;
}
- private boolean isEmbeddedHierarchyWindowsLocked(int windowId) {
- final IBinder leashToken = mWindowIdMap.get(windowId);
- if (leashToken == null) {
- return false;
- }
-
- for (int i = 0; i < mHostEmbeddedMap.size(); i++) {
- if (mHostEmbeddedMap.keyAt(i).equals(leashToken)) {
- return true;
- }
- }
-
- return false;
- }
-
private int getTypeForWindowManagerWindowType(int windowType) {
switch (windowType) {
case WindowManager.LayoutParams.TYPE_APPLICATION:
@@ -943,17 +928,11 @@
* Dumps all {@link AccessibilityWindowInfo}s here.
*/
void dumpLocked(FileDescriptor fd, final PrintWriter pw, String[] args) {
- pw.append("Global Info [ ");
- pw.println("Top focused display Id = " + mTopFocusedDisplayId);
- pw.println(" Active Window Id = " + mActiveWindowId);
- pw.println(" Top Focused Window Id = " + mTopFocusedWindowId);
- pw.println(" Accessibility Focused Window Id = " + mAccessibilityFocusedWindowId
- + " ]");
if (mIsProxy) {
pw.println("Proxy accessibility focused window = "
+ mProxyDisplayAccessibilityFocusedWindow);
+ pw.println();
}
- pw.println();
if (mWindows != null) {
final int windowCount = mWindows.size();
for (int j = 0; j < windowCount; j++) {
@@ -1490,7 +1469,7 @@
* @return The windowId of the parent window, or self if no parent exists
*/
public int resolveParentWindowIdLocked(int windowId) {
- final IBinder token = getTokenLocked(windowId);
+ final IBinder token = getLeashTokenLocked(windowId);
if (token == null) {
return windowId;
}
@@ -2095,7 +2074,7 @@
* @param windowId The windowID.
* @return The token, or {@code NULL} if this windowID doesn't exist
*/
- IBinder getTokenLocked(int windowId) {
+ IBinder getLeashTokenLocked(int windowId) {
return mWindowIdMap.get(windowId);
}
@@ -2124,6 +2103,23 @@
}
/**
+ * Checks if the window is embedded into another window so that the window should be excluded
+ * from the exposed accessibility windows, and the node tree should be embedded in the host.
+ */
+ boolean isEmbeddedHierarchyWindowsLocked(int windowId) {
+ if (mHostEmbeddedMap.size() == 0) {
+ return false;
+ }
+
+ final IBinder leashToken = getLeashTokenLocked(windowId);
+ if (leashToken == null) {
+ return false;
+ }
+
+ return mHostEmbeddedMap.containsKey(leashToken);
+ }
+
+ /**
* Checks if the window belongs to a proxy display and if so clears the focused window id.
* @param focusClearedWindowId the cleared window id.
* @return true if an observer is proxy-ed and has cleared its focused window id.
@@ -2199,6 +2195,13 @@
* Dumps all {@link AccessibilityWindowInfo}s here.
*/
public void dump(FileDescriptor fd, final PrintWriter pw, String[] args) {
+ pw.append("Global Info [ ");
+ pw.println("Top focused display Id = " + mTopFocusedDisplayId);
+ pw.println(" Active Window Id = " + mActiveWindowId);
+ pw.println(" Top Focused Window Id = " + mTopFocusedWindowId);
+ pw.println(" Accessibility Focused Window Id = " + mAccessibilityFocusedWindowId
+ + " ]");
+ pw.println();
final int count = mDisplayWindowsObservers.size();
for (int i = 0; i < count; i++) {
final DisplayWindowsObserver observer = mDisplayWindowsObservers.valueAt(i);
diff --git a/services/accessibility/java/com/android/server/accessibility/magnification/FullScreenMagnificationController.java b/services/accessibility/java/com/android/server/accessibility/magnification/FullScreenMagnificationController.java
index 805f6e3..351760b 100644
--- a/services/accessibility/java/com/android/server/accessibility/magnification/FullScreenMagnificationController.java
+++ b/services/accessibility/java/com/android/server/accessibility/magnification/FullScreenMagnificationController.java
@@ -25,7 +25,9 @@
import android.accessibilityservice.MagnificationConfig;
import android.animation.Animator;
+import android.animation.TimeAnimator;
import android.animation.ValueAnimator;
+import android.annotation.MainThread;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.BroadcastReceiver;
@@ -51,6 +53,7 @@
import android.view.WindowManager;
import android.view.accessibility.MagnificationAnimationCallback;
import android.view.animation.DecelerateInterpolator;
+import android.widget.Scroller;
import com.android.internal.R;
import com.android.internal.accessibility.common.MagnificationConstants;
@@ -60,6 +63,7 @@
import com.android.server.LocalServices;
import com.android.server.accessibility.AccessibilityManagerService;
import com.android.server.accessibility.AccessibilityTraceManager;
+import com.android.server.accessibility.Flags;
import com.android.server.wm.WindowManagerInternal;
import java.util.ArrayList;
@@ -86,6 +90,8 @@
private static final boolean DEBUG_SET_MAGNIFICATION_SPEC = false;
private final Object mLock;
+ private final Supplier<Scroller> mScrollerSupplier;
+ private final Supplier<TimeAnimator> mTimeAnimatorSupplier;
private final ControllerContext mControllerCtx;
@@ -149,7 +155,13 @@
DisplayMagnification(int displayId) {
mDisplayId = displayId;
- mSpecAnimationBridge = new SpecAnimationBridge(mControllerCtx, mLock, mDisplayId);
+ mSpecAnimationBridge =
+ new SpecAnimationBridge(
+ mControllerCtx,
+ mLock,
+ mDisplayId,
+ mScrollerSupplier,
+ mTimeAnimatorSupplier);
}
/**
@@ -406,6 +418,52 @@
}
}
+ void startFlingAnimation(
+ float xPixelsPerSecond,
+ float yPixelsPerSecond,
+ MagnificationAnimationCallback animationCallback
+ ) {
+ if (DEBUG) {
+ Slog.i(LOG_TAG,
+ "startFlingAnimation(spec = " + xPixelsPerSecond + ", animationCallback = "
+ + animationCallback + ")");
+ }
+ if (Thread.currentThread().getId() == mMainThreadId) {
+ mSpecAnimationBridge.startFlingAnimation(
+ xPixelsPerSecond,
+ yPixelsPerSecond,
+ getMinOffsetXLocked(),
+ getMaxOffsetXLocked(),
+ getMinOffsetYLocked(),
+ getMaxOffsetYLocked(),
+ animationCallback);
+ } else {
+ final Message m =
+ PooledLambda.obtainMessage(
+ SpecAnimationBridge::startFlingAnimation,
+ mSpecAnimationBridge,
+ xPixelsPerSecond,
+ yPixelsPerSecond,
+ getMinOffsetXLocked(),
+ getMaxOffsetXLocked(),
+ getMinOffsetYLocked(),
+ getMaxOffsetYLocked(),
+ animationCallback);
+ mControllerCtx.getHandler().sendMessage(m);
+ }
+ }
+
+ void cancelFlingAnimation() {
+ if (DEBUG) {
+ Slog.i(LOG_TAG, "cancelFlingAnimation()");
+ }
+ if (Thread.currentThread().getId() == mMainThreadId) {
+ mSpecAnimationBridge.cancelFlingAnimation();
+ } else {
+ mControllerCtx.getHandler().post(mSpecAnimationBridge::cancelFlingAnimation);
+ }
+ }
+
/**
* Get the ID of the last service that changed the magnification spec.
*
@@ -759,6 +817,63 @@
sendSpecToAnimation(mCurrentMagnificationSpec, null);
}
+ @GuardedBy("mLock")
+ void startFling(float xPixelsPerSecond, float yPixelsPerSecond, int id) {
+ if (!mRegistered) {
+ return;
+ }
+ if (!isActivated()) {
+ return;
+ }
+
+ if (id != INVALID_SERVICE_ID) {
+ mIdOfLastServiceToMagnify = id;
+ }
+
+ startFlingAnimation(
+ xPixelsPerSecond,
+ yPixelsPerSecond,
+ new MagnificationAnimationCallback() {
+ @Override
+ public void onResult(boolean success) {
+ // never called
+ }
+
+ @Override
+ public void onResult(boolean success, MagnificationSpec lastSpecSent) {
+ if (DEBUG) {
+ Slog.i(
+ LOG_TAG,
+ "startFlingAnimation finished( "
+ + success
+ + " = "
+ + lastSpecSent.offsetX
+ + ", "
+ + lastSpecSent.offsetY
+ + ")");
+ }
+ synchronized (mLock) {
+ mCurrentMagnificationSpec.setTo(lastSpecSent);
+ onMagnificationChangedLocked();
+ }
+ }
+ });
+ }
+
+
+ @GuardedBy("mLock")
+ void cancelFling(int id) {
+ if (!mRegistered) {
+ return;
+ }
+
+ if (id != INVALID_SERVICE_ID) {
+ mIdOfLastServiceToMagnify = id;
+ }
+
+ cancelFlingAnimation();
+ }
+
boolean updateCurrentSpecWithOffsetsLocked(float nonNormOffsetX, float nonNormOffsetY) {
if (DEBUG) {
Slog.i(LOG_TAG,
@@ -838,7 +953,9 @@
magnificationInfoChangedCallback,
scaleProvider,
/* thumbnailSupplier= */ null,
- backgroundExecutor);
+ backgroundExecutor,
+ () -> new Scroller(context),
+ TimeAnimator::new);
}
/** Constructor for tests */
@@ -849,9 +966,13 @@
@NonNull MagnificationInfoChangedCallback magnificationInfoChangedCallback,
@NonNull MagnificationScaleProvider scaleProvider,
Supplier<MagnificationThumbnail> thumbnailSupplier,
- @NonNull Executor backgroundExecutor) {
+ @NonNull Executor backgroundExecutor,
+ Supplier<Scroller> scrollerSupplier,
+ Supplier<TimeAnimator> timeAnimatorSupplier) {
mControllerCtx = ctx;
mLock = lock;
+ mScrollerSupplier = scrollerSupplier;
+ mTimeAnimatorSupplier = timeAnimatorSupplier;
mMainThreadId = mControllerCtx.getContext().getMainLooper().getThread().getId();
mScreenStateObserver = new ScreenStateObserver(mControllerCtx.getContext(), this);
addInfoChangedCallback(magnificationInfoChangedCallback);
@@ -1437,6 +1558,42 @@
}
/**
+ * Call after a pan ends, if the velocity has passed the threshold, to start a fling animation.
+ *
+ * @param displayId The logical display id.
+ * @param xPixelsPerSecond the velocity of the last pan gesture in the X direction, in current
+ * screen pixels per second.
+ * @param yPixelsPerSecond the velocity of the last pan gesture in the Y direction, in current
+ * screen pixels per second.
+ * @param id the ID of the service requesting the change
+ */
+ public void startFling(int displayId, float xPixelsPerSecond, float yPixelsPerSecond, int id) {
+ synchronized (mLock) {
+ final DisplayMagnification display = mDisplays.get(displayId);
+ if (display == null) {
+ return;
+ }
+ display.startFling(xPixelsPerSecond, yPixelsPerSecond, id);
+ }
+ }
+
+ /**
+ * Call to cancel the fling animation if it is running. Call this on any ACTION_DOWN event.
+ *
+ * @param displayId The logical display id.
+ * @param id the ID of the service requesting the change
+ */
+ public void cancelFling(int displayId, int id) {
+ synchronized (mLock) {
+ final DisplayMagnification display = mDisplays.get(displayId);
+ if (display == null) {
+ return;
+ }
+ display.cancelFling(id);
+ }
+ }
+
+ /**
* Get the ID of the last service that changed the magnification spec.
*
* @param displayId The logical display id.
@@ -1698,7 +1855,15 @@
@GuardedBy("mLock")
private boolean mEnabled = false;
- private SpecAnimationBridge(ControllerContext ctx, Object lock, int displayId) {
+ private final Scroller mScroller;
+ private final TimeAnimator mScrollAnimator;
+
+ private SpecAnimationBridge(
+ ControllerContext ctx,
+ Object lock,
+ int displayId,
+ Supplier<Scroller> scrollerSupplier,
+ Supplier<TimeAnimator> timeAnimatorSupplier) {
mControllerCtx = ctx;
mLock = lock;
mDisplayId = displayId;
@@ -1709,6 +1874,37 @@
mValueAnimator.setFloatValues(0.0f, 1.0f);
mValueAnimator.addUpdateListener(this);
mValueAnimator.addListener(this);
+
+ if (Flags.fullscreenFlingGesture()) {
+ mScroller = scrollerSupplier.get();
+ mScrollAnimator = timeAnimatorSupplier.get();
+ mScrollAnimator.addListener(this);
+ mScrollAnimator.setTimeListener(
+ (animation, totalTime, deltaTime) -> {
+ synchronized (mLock) {
+ if (DEBUG) {
+ Slog.v(
+ LOG_TAG,
+ "onScrollAnimationUpdate: "
+ + mEnabled + " : " + totalTime);
+ }
+
+ if (mEnabled) {
+ if (!mScroller.computeScrollOffset()) {
+ animation.end();
+ return;
+ }
+
+ mEndMagnificationSpec.offsetX = mScroller.getCurrX();
+ mEndMagnificationSpec.offsetY = mScroller.getCurrY();
+ setMagnificationSpecLocked(mEndMagnificationSpec);
+ }
+ }
+ });
+ } else {
+ mScroller = null;
+ mScrollAnimator = null;
+ }
}
/**
@@ -1735,16 +1931,20 @@
}
}
- void updateSentSpecMainThread(MagnificationSpec spec,
- MagnificationAnimationCallback animationCallback) {
- if (mValueAnimator.isRunning()) {
- mValueAnimator.cancel();
- }
+ @MainThread
+ void updateSentSpecMainThread(
+ MagnificationSpec spec, MagnificationAnimationCallback animationCallback) {
+ cancelAnimations();
mAnimationCallback = animationCallback;
// If the current and sent specs don't match, update the sent spec.
synchronized (mLock) {
final boolean changed = !mSentMagnificationSpec.equals(spec);
+ if (DEBUG_SET_MAGNIFICATION_SPEC) {
+ Slog.d(
+ LOG_TAG,
+ "updateSentSpecMainThread: " + mEnabled + " : changed: " + changed);
+ }
if (changed) {
if (mAnimationCallback != null) {
animateMagnificationSpecLocked(spec);
@@ -1757,12 +1957,13 @@
}
}
+ @MainThread
private void sendEndCallbackMainThread(boolean success) {
if (mAnimationCallback != null) {
if (DEBUG) {
Slog.d(LOG_TAG, "sendEndCallbackMainThread: " + success);
}
- mAnimationCallback.onResult(success);
+ mAnimationCallback.onResult(success, mSentMagnificationSpec);
mAnimationCallback = null;
}
}
@@ -1830,6 +2031,77 @@
public void onAnimationRepeat(Animator animation) {
}
+
+ /**
+ * Call after a pan ends, if the velocity has passed the threshold, to start a fling
+ * animation.
+ */
+ @MainThread
+ public void startFlingAnimation(
+ float xPixelsPerSecond,
+ float yPixelsPerSecond,
+ float minX,
+ float maxX,
+ float minY,
+ float maxY,
+ MagnificationAnimationCallback animationCallback
+ ) {
+ if (!Flags.fullscreenFlingGesture()) {
+ return;
+ }
+ cancelAnimations();
+
+ mAnimationCallback = animationCallback;
+
+ // We use this as a temp object to send updates every animation frame, so make sure it
+ // matches the current spec before we start.
+ mEndMagnificationSpec.setTo(mSentMagnificationSpec);
+
+ if (DEBUG) {
+ Slog.d(LOG_TAG, "startFlingAnimation: "
+ + "offsetX " + mSentMagnificationSpec.offsetX
+ + "offsetY " + mSentMagnificationSpec.offsetY
+ + "xPixelsPerSecond " + xPixelsPerSecond
+ + "yPixelsPerSecond " + yPixelsPerSecond
+ + "minX " + minX
+ + "maxX " + maxX
+ + "minY " + minY
+ + "maxY " + maxY
+ );
+ }
+
+ mScroller.fling(
+ (int) mSentMagnificationSpec.offsetX,
+ (int) mSentMagnificationSpec.offsetY,
+ (int) xPixelsPerSecond,
+ (int) yPixelsPerSecond,
+ (int) minX,
+ (int) maxX,
+ (int) minY,
+ (int) maxY);
+
+ mScrollAnimator.start();
+ }
+
+ @MainThread
+ void cancelAnimations() {
+ if (mValueAnimator.isRunning()) {
+ mValueAnimator.cancel();
+ }
+
+ cancelFlingAnimation();
+ }
+
+ @MainThread
+ void cancelFlingAnimation() {
+ if (!Flags.fullscreenFlingGesture()) {
+ return;
+ }
+ if (mScrollAnimator.isRunning()) {
+ mScrollAnimator.cancel();
+ }
+ mScroller.forceFinished(true);
+ }
}
private static class ScreenStateObserver extends BroadcastReceiver {
diff --git a/services/accessibility/java/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandler.java b/services/accessibility/java/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandler.java
index baae1d93..f4ea754 100644
--- a/services/accessibility/java/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandler.java
+++ b/services/accessibility/java/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandler.java
@@ -62,6 +62,7 @@
import android.view.MotionEvent.PointerProperties;
import android.view.ScaleGestureDetector;
import android.view.ScaleGestureDetector.OnScaleGestureListener;
+import android.view.VelocityTracker;
import android.view.ViewConfiguration;
import com.android.internal.R;
@@ -174,6 +175,10 @@
private final boolean mIsWatch;
+ @Nullable private VelocityTracker mVelocityTracker;
+ private final int mMinimumVelocity;
+ private final int mMaximumVelocity;
+
public FullScreenMagnificationGestureHandler(@UiContext Context context,
FullScreenMagnificationController fullScreenMagnificationController,
AccessibilityTraceManager trace,
@@ -184,15 +189,25 @@
@NonNull WindowMagnificationPromptController promptController,
int displayId,
FullScreenMagnificationVibrationHelper fullScreenMagnificationVibrationHelper) {
- this(context, fullScreenMagnificationController, trace, callback,
- detectSingleFingerTripleTap, detectTwoFingerTripleTap,
- detectShortcutTrigger, promptController, displayId,
- fullScreenMagnificationVibrationHelper, /* magnificationLogger= */ null);
+ this(
+ context,
+ fullScreenMagnificationController,
+ trace,
+ callback,
+ detectSingleFingerTripleTap,
+ detectTwoFingerTripleTap,
+ detectShortcutTrigger,
+ promptController,
+ displayId,
+ fullScreenMagnificationVibrationHelper,
+ /* magnificationLogger= */ null,
+ ViewConfiguration.get(context));
}
/** Constructor for tests. */
@VisibleForTesting
- FullScreenMagnificationGestureHandler(@UiContext Context context,
+ FullScreenMagnificationGestureHandler(
+ @UiContext Context context,
FullScreenMagnificationController fullScreenMagnificationController,
AccessibilityTraceManager trace,
Callback callback,
@@ -202,7 +217,8 @@
@NonNull WindowMagnificationPromptController promptController,
int displayId,
FullScreenMagnificationVibrationHelper fullScreenMagnificationVibrationHelper,
- MagnificationLogger magnificationLogger) {
+ MagnificationLogger magnificationLogger,
+ ViewConfiguration viewConfiguration) {
super(displayId, detectSingleFingerTripleTap, detectTwoFingerTripleTap,
detectShortcutTrigger, trace, callback);
if (DEBUG_ALL) {
@@ -212,6 +228,15 @@
+ ", detectTwoFingerTripleTap = " + detectTwoFingerTripleTap
+ ", detectShortcutTrigger = " + detectShortcutTrigger + ")");
}
+
+ if (Flags.fullscreenFlingGesture()) {
+ mMinimumVelocity = viewConfiguration.getScaledMinimumFlingVelocity();
+ mMaximumVelocity = viewConfiguration.getScaledMaximumFlingVelocity();
+ } else {
+ mMinimumVelocity = 0;
+ mMaximumVelocity = 0;
+ }
+
mFullScreenMagnificationController = fullScreenMagnificationController;
mMagnificationInfoChangedCallback =
new FullScreenMagnificationController.MagnificationInfoChangedCallback() {
@@ -299,6 +324,10 @@
@Override
void onMotionEventInternal(MotionEvent event, MotionEvent rawEvent, int policyFlags) {
+ if (event.getActionMasked() == ACTION_DOWN) {
+ cancelFling();
+ }
+
handleEventWith(mCurrentState, event, rawEvent, policyFlags);
}
@@ -501,6 +530,7 @@
}
persistScaleAndTransitionTo(mViewportDraggingState);
} else if (action == ACTION_UP || action == ACTION_CANCEL) {
+ onPanningFinished(event);
// if feature flag is enabled, currently only true on watches
if (mIsSinglePanningEnabled) {
mOverscrollHandler.setScaleAndCenterToEdgeIfNeeded();
@@ -578,6 +608,7 @@
Slog.i(mLogTag, "Panned content by scrollX: " + distanceX
+ " scrollY: " + distanceY);
}
+ onPan(second);
mFullScreenMagnificationController.offsetMagnifiedRegion(mDisplayId, distanceX,
distanceY, AccessibilityManagerService.MAGNIFICATION_GESTURE_HANDLER_ID);
if (mIsSinglePanningEnabled) {
@@ -973,7 +1004,7 @@
&& overscrollState(event, mFirstPointerDownLocation)
== OVERSCROLL_VERTICAL_EDGE) {
transitionToDelegatingStateAndClear();
- }
+ } // TODO(b/319537921): should there be an else here?
//Primary pointer is swiping, so transit to PanningScalingState
transitToPanningScalingStateAndClear();
} else if (mIsSinglePanningEnabled
@@ -982,7 +1013,7 @@
if (overscrollState(event, mFirstPointerDownLocation)
== OVERSCROLL_VERTICAL_EDGE) {
transitionToDelegatingStateAndClear();
- }
+ } // TODO(b/319537921): should there be an else here?
transitToSinglePanningStateAndClear();
} else if (!mIsTwoFingerCountReached) {
// If it is a two-finger gesture, do not transition to the
@@ -1742,6 +1773,71 @@
}
}
+ /** Call during MOVE events for a panning gesture. */
+ private void onPan(MotionEvent event) {
+ if (!Flags.fullscreenFlingGesture()) {
+ return;
+ }
+
+ if (mVelocityTracker == null) {
+ mVelocityTracker = VelocityTracker.obtain();
+ }
+ mVelocityTracker.addMovement(event);
+ }
+
+ /**
+ * Call during UP events for a panning gesture, so we can detect a fling and play a physics-
+ * based fling animation.
+ */
+ private void onPanningFinished(MotionEvent event) {
+ if (!Flags.fullscreenFlingGesture()) {
+ return;
+ }
+
+ if (mVelocityTracker == null) {
+ Log.e(mLogTag, "onPanningFinished: mVelocityTracker is null");
+ return;
+ }
+ mVelocityTracker.addMovement(event);
+ mVelocityTracker.computeCurrentVelocity(/* units= */ 1000, mMaximumVelocity);
+
+ float xPixelsPerSecond = mVelocityTracker.getXVelocity();
+ float yPixelsPerSecond = mVelocityTracker.getYVelocity();
+
+ mVelocityTracker.recycle();
+ mVelocityTracker = null;
+
+ if (DEBUG_PANNING_SCALING) {
+ Slog.v(
+ mLogTag,
+ "onPanningFinished: pixelsPerSecond: "
+ + xPixelsPerSecond
+ + ", "
+ + yPixelsPerSecond
+ + " mMinimumVelocity: "
+ + mMinimumVelocity);
+ }
+
+ if ((Math.abs(yPixelsPerSecond) > mMinimumVelocity)
+ || (Math.abs(xPixelsPerSecond) > mMinimumVelocity)) {
+ mFullScreenMagnificationController.startFling(
+ mDisplayId,
+ xPixelsPerSecond,
+ yPixelsPerSecond,
+ AccessibilityManagerService.MAGNIFICATION_GESTURE_HANDLER_ID);
+ }
+ }
+
+ private void cancelFling() {
+ if (!Flags.fullscreenFlingGesture()) {
+ return;
+ }
+
+ mFullScreenMagnificationController.cancelFling(
+ mDisplayId,
+ AccessibilityManagerService.MAGNIFICATION_GESTURE_HANDLER_ID);
+ }
+
final class SinglePanningState extends SimpleOnGestureListener implements State {
@@ -1756,6 +1852,8 @@
int action = event.getActionMasked();
switch (action) {
case ACTION_UP:
+ onPanningFinished(event);
+ // fall-through!
case ACTION_CANCEL:
mOverscrollHandler.setScaleAndCenterToEdgeIfNeeded();
mOverscrollHandler.clearEdgeState();
@@ -1770,6 +1868,7 @@
if (mCurrentState != mSinglePanningState) {
return true;
}
+ onPan(second);
mFullScreenMagnificationController.offsetMagnifiedRegion(
mDisplayId,
distanceX,
diff --git a/services/autofill/java/com/android/server/autofill/AutofillManagerServiceImpl.java b/services/autofill/java/com/android/server/autofill/AutofillManagerServiceImpl.java
index fd8ab96..e1291e5 100644
--- a/services/autofill/java/com/android/server/autofill/AutofillManagerServiceImpl.java
+++ b/services/autofill/java/com/android/server/autofill/AutofillManagerServiceImpl.java
@@ -451,7 +451,7 @@
final Session.SaveResult saveResult = session.showSaveLocked();
- session.logContextCommitted(saveResult.getNoSaveUiReason(), commitReason);
+ session.logContextCommittedLocked(saveResult.getNoSaveUiReason(), commitReason);
if (saveResult.isLogSaveShown()) {
session.logSaveUiShown();
diff --git a/services/autofill/java/com/android/server/autofill/PresentationStatsEventLogger.java b/services/autofill/java/com/android/server/autofill/PresentationStatsEventLogger.java
index cf414d1..3645c40 100644
--- a/services/autofill/java/com/android/server/autofill/PresentationStatsEventLogger.java
+++ b/services/autofill/java/com/android/server/autofill/PresentationStatsEventLogger.java
@@ -254,6 +254,14 @@
mEventInternal.ifPresent(event -> event.mIsCredentialRequest = isCredentialRequest);
}
+ /**
+ * Set webview_requested_credential
+ */
+ public void maybeSetWebviewRequestedCredential(boolean webviewRequestedCredential) {
+ mEventInternal.ifPresent(event ->
+ event.mWebviewRequestedCredential = webviewRequestedCredential);
+ }
+
public void maybeSetNoPresentationEventReason(@NotShownReason int reason) {
mEventInternal.ifPresent(event -> {
if (event.mCountShown == 0) {
@@ -578,7 +586,8 @@
+ " mDetectionPreference=" + event.mDetectionPreference
+ " mFieldClassificationRequestId=" + event.mFieldClassificationRequestId
+ " mAppPackageUid=" + mCallingAppUid
- + " mIsCredentialRequest=" + event.mIsCredentialRequest);
+ + " mIsCredentialRequest=" + event.mIsCredentialRequest
+ + " mWebviewRequestedCredential=" + event.mWebviewRequestedCredential);
}
// TODO(b/234185326): Distinguish empty responses from other no presentation reasons.
@@ -618,7 +627,8 @@
event.mDetectionPreference,
event.mFieldClassificationRequestId,
mCallingAppUid,
- event.mIsCredentialRequest);
+ event.mIsCredentialRequest,
+ event.mWebviewRequestedCredential);
mEventInternal = Optional.empty();
}
@@ -653,6 +663,7 @@
@DetectionPreference int mDetectionPreference = DETECTION_PREFER_UNKNOWN;
int mFieldClassificationRequestId = -1;
boolean mIsCredentialRequest = false;
+ boolean mWebviewRequestedCredential = false;
PresentationStatsEventInternal() {}
}
diff --git a/services/autofill/java/com/android/server/autofill/Session.java b/services/autofill/java/com/android/server/autofill/Session.java
index 049feee..b89e0d8 100644
--- a/services/autofill/java/com/android/server/autofill/Session.java
+++ b/services/autofill/java/com/android/server/autofill/Session.java
@@ -16,8 +16,11 @@
package com.android.server.autofill;
+import static android.credentials.Constants.FAILURE_CREDMAN_SELECTOR;
+import static android.credentials.Constants.SUCCESS_CREDMAN_SELECTOR;
import static android.service.autofill.AutofillFieldClassificationService.EXTRA_SCORES;
import static android.service.autofill.AutofillService.EXTRA_FILL_RESPONSE;
+import static android.service.autofill.AutofillService.WEBVIEW_REQUESTED_CREDENTIAL_KEY;
import static android.service.autofill.Dataset.PICK_REASON_NO_PCC;
import static android.service.autofill.Dataset.PICK_REASON_PCC_DETECTION_ONLY;
import static android.service.autofill.Dataset.PICK_REASON_PCC_DETECTION_PREFERRED_WITH_PROVIDER;
@@ -112,6 +115,8 @@
import android.content.IntentFilter;
import android.content.IntentSender;
import android.content.pm.ServiceInfo;
+import android.credentials.GetCredentialException;
+import android.credentials.GetCredentialResponse;
import android.graphics.Bitmap;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
@@ -122,10 +127,12 @@
import android.os.Handler;
import android.os.IBinder;
import android.os.IBinder.DeathRecipient;
+import android.os.Parcel;
import android.os.Parcelable;
import android.os.Process;
import android.os.RemoteCallback;
import android.os.RemoteException;
+import android.os.ResultReceiver;
import android.os.SystemClock;
import android.service.assist.classification.FieldClassificationRequest;
import android.service.assist.classification.FieldClassificationResponse;
@@ -152,6 +159,7 @@
import android.service.autofill.SaveRequest;
import android.service.autofill.UserData;
import android.service.autofill.ValueFinder;
+import android.service.credentials.CredentialProviderService;
import android.text.TextUtils;
import android.util.ArrayMap;
import android.util.ArraySet;
@@ -2506,7 +2514,7 @@
+ id + " destroyed");
return;
}
- fillInIntent = createAuthFillInIntentLocked(requestId, extras, /* authExtras= */ null);
+ fillInIntent = createAuthFillInIntentLocked(requestId, extras);
if (fillInIntent == null) {
forceRemoveFromServiceLocked();
return;
@@ -2807,6 +2815,7 @@
mSessionFlags.mExpiredResponse = false;
final Parcelable result = data.getParcelable(AutofillManager.EXTRA_AUTHENTICATION_RESULT);
+
final Bundle newClientState = data.getBundle(AutofillManager.EXTRA_CLIENT_STATE);
if (sDebug) {
Slog.d(TAG, "setAuthenticationResultLocked(): result=" + result
@@ -2817,6 +2826,12 @@
mPresentationStatsEventLogger.maybeSetAuthenticationResult(
AUTHENTICATION_RESULT_SUCCESS);
replaceResponseLocked(authenticatedResponse, (FillResponse) result, newClientState);
+ } else if (result instanceof GetCredentialResponse) {
+ Slog.d(TAG, "Received GetCredentialResponse from authentication flow");
+ Dataset dataset = getDatasetFromCredentialResponse((GetCredentialResponse) result);
+ if (dataset != null) {
+ autoFill(requestId, datasetIdx, dataset, false, UI_TYPE_UNKNOWN);
+ }
} else if (result instanceof Dataset) {
if (datasetIdx != AutofillManager.AUTHENTICATION_ID_DATASET_ID_UNDEFINED) {
logAuthenticationStatusLocked(requestId,
@@ -2853,6 +2868,17 @@
}
}
+ private Dataset getDatasetFromCredentialResponse(GetCredentialResponse result) {
+ if (result == null) {
+ return null;
+ }
+ Bundle bundle = result.getCredential().getData();
+ if (bundle == null) {
+ return null;
+ }
+ return bundle.getParcelable(AutofillManager.EXTRA_AUTHENTICATION_RESULT, Dataset.class);
+ }
+
Dataset getEffectiveDatasetForAuthentication(Dataset authenticatedDataset) {
FillResponse response = new FillResponse.Builder().addDataset(authenticatedDataset).build();
response = getEffectiveFillResponse(response);
@@ -3060,6 +3086,10 @@
* when necessary.
*/
public void logContextCommitted() {
+ if (sVerbose) {
+ Slog.v(TAG, "logContextCommitted (" + id + "): commit_reason:" + COMMIT_REASON_UNKNOWN
+ + " no_save_reason:" + Event.NO_SAVE_UI_REASON_NONE);
+ }
mHandler.sendMessage(obtainMessage(Session::handleLogContextCommitted, this,
Event.NO_SAVE_UI_REASON_NONE,
COMMIT_REASON_UNKNOWN));
@@ -3068,16 +3098,26 @@
/**
* Generates a {@link android.service.autofill.FillEventHistory.Event#TYPE_CONTEXT_COMMITTED}
- * when necessary.
+ * when necessary. Note that it could be called before save UI is shown and the session is
+ * committed.
*
* @param saveDialogNotShowReason The reason why a save dialog was not shown.
* @param commitReason The reason why context is committed.
*/
- public void logContextCommitted(@NoSaveReason int saveDialogNotShowReason,
+
+ @GuardedBy("mLock")
+ public void logContextCommittedLocked(@NoSaveReason int saveDialogNotShowReason,
@AutofillCommitReason int commitReason) {
+ if (sVerbose) {
+ Slog.v(TAG, "logContextCommittedLocked (" + id + "): commit_reason:" + commitReason
+ + " no_save_reason:" + saveDialogNotShowReason);
+ }
mHandler.sendMessage(obtainMessage(Session::handleLogContextCommitted, this,
saveDialogNotShowReason, commitReason));
- logAllEvents(commitReason);
+
+ mSessionCommittedEventLogger.maybeSetCommitReason(commitReason);
+ mSessionCommittedEventLogger.maybeSetRequestCount(mRequestCount);
+ mSaveEventLogger.maybeSetSaveUiNotShownReason(NO_SAVE_REASON_NONE);
}
private void handleLogContextCommitted(@NoSaveReason int saveDialogNotShowReason,
@@ -3133,6 +3173,10 @@
@Nullable ArrayList<FieldClassification> detectedFieldClassifications,
@NoSaveReason int saveDialogNotShowReason,
@AutofillCommitReason int commitReason) {
+ if (sVerbose) {
+ Slog.v(TAG, "logContextCommittedLocked (" + id + "): commit_reason:" + commitReason
+ + " no_save_reason:" + saveDialogNotShowReason);
+ }
final FillResponse lastResponse = getLastResponseLocked("logContextCommited(%s)");
if (lastResponse == null) return;
@@ -3309,7 +3353,9 @@
changedFieldIds, changedDatasetIds, manuallyFilledFieldIds,
manuallyFilledDatasetIds, detectedFieldIds, detectedFieldClassifications,
mComponentName, mCompatMode, saveDialogNotShowReason);
- logAllEvents(commitReason);
+ mSessionCommittedEventLogger.maybeSetCommitReason(commitReason);
+ mSessionCommittedEventLogger.maybeSetRequestCount(mRequestCount);
+ mSaveEventLogger.maybeSetSaveUiNotShownReason(saveDialogNotShowReason);
}
/**
@@ -3750,11 +3796,6 @@
}
}
- if (sDebug) {
- Slog.d(TAG, "Good news, everyone! All checks passed, show save UI for "
- + id + "!");
- }
-
final IAutoFillManagerClient client = getClient();
mPendingSaveUi = new PendingUi(new Binder(), id, client);
@@ -3786,6 +3827,10 @@
}
}
mSessionFlags.mShowingSaveUi = true;
+ if (sDebug) {
+ Slog.d(TAG, "Good news, everyone! All checks passed, show save UI for "
+ + id + "!");
+ }
return new SaveResult(/* logSaveShown= */ true, /* removeSession= */ false,
Event.NO_SAVE_UI_REASON_NONE);
}
@@ -4689,6 +4734,11 @@
}
+ if (isCredmanIntegrationActive(response)) {
+ Slog.d(TAG, "Attempting to add Credential Manager callback to pinned entries");
+ addCredentialManagerCallback(response);
+ }
+
if (response.supportsInlineSuggestions()) {
synchronized (mLock) {
if (requestShowInlineSuggestionsLocked(response, filterText)) {
@@ -4748,6 +4798,11 @@
}
}
+ private boolean isCredmanIntegrationActive(FillResponse response) {
+ return Flags.autofillCredmanIntegration()
+ && (response.getFlags() & FillResponse.FLAG_CREDENTIAL_MANAGER_RESPONSE) != 0;
+ }
+
@GuardedBy("mLock")
private void updateFillDialogTriggerIdsLocked() {
final FillResponse response = getLastResponseLocked(null);
@@ -4963,6 +5018,69 @@
return mInlineSessionController.setInlineFillUiLocked(inlineFillUi);
}
+ private void addCredentialManagerCallback(FillResponse response) {
+ if (response.getDatasets() == null) {
+ return;
+ }
+ for (Dataset dataset: response.getDatasets()) {
+ if (isPinnedDataset(dataset)) {
+ Slog.d(TAG, "Adding Credential Manager callback to a pinned entry");
+ addCredentialManagerCallbackForDataset(dataset, response.getRequestId());
+ }
+ }
+ }
+
+ private void addCredentialManagerCallbackForDataset(Dataset dataset, int requestId) {
+ final ResultReceiver resultReceiver = new ResultReceiver(mHandler) {
+ @Override
+ protected void onReceiveResult(int resultCode, Bundle resultData) {
+ if (resultCode == SUCCESS_CREDMAN_SELECTOR) {
+ Slog.d(TAG, "onReceiveResult from Credential Manager bottom sheet");
+ GetCredentialResponse getCredentialResponse =
+ resultData.getParcelable(
+ CredentialProviderService.EXTRA_GET_CREDENTIAL_RESPONSE,
+ GetCredentialResponse.class);
+ Dataset datasetFromCredential = getDatasetFromCredentialResponse(
+ getCredentialResponse);
+ if (datasetFromCredential != null) {
+ autoFill(requestId, /*datasetIndex=*/-1,
+ datasetFromCredential, false,
+ UI_TYPE_CREDMAN_BOTTOM_SHEET);
+ }
+ } else if (resultCode == FAILURE_CREDMAN_SELECTOR) {
+ GetCredentialException exception = resultData.getParcelable(
+ CredentialProviderService.EXTRA_GET_CREDENTIAL_EXCEPTION,
+ GetCredentialException.class);
+ Slog.d(TAG, "Credman bottom sheet from pinned "
+ + "entry failed with: + " + exception.getType() + " , "
+ + exception.getMessage());
+ } else {
+ Slog.d(TAG, "Unknown resultCode from credential "
+ + "manager bottom sheet: " + resultCode);
+ }
+ }
+ };
+ ResultReceiver ipcFriendlyResultReceiver =
+ toIpcFriendlyResultReceiver(resultReceiver);
+
+ Intent metadataIntent = dataset.getCredentialFillInIntent();
+ metadataIntent.putExtra(
+ android.credentials.selection.Constants.EXTRA_FINAL_RESPONSE_RECEIVER,
+ ipcFriendlyResultReceiver);
+ dataset.setCredentialFillInIntent(metadataIntent);
+ }
+
+ private ResultReceiver toIpcFriendlyResultReceiver(ResultReceiver resultReceiver) {
+ final Parcel parcel = Parcel.obtain();
+ resultReceiver.writeToParcel(parcel, 0);
+ parcel.setDataPosition(0);
+
+ final ResultReceiver ipcFriendly = ResultReceiver.CREATOR.createFromParcel(parcel);
+ parcel.recycle();
+
+ return ipcFriendly;
+ }
+
boolean isDestroyed() {
synchronized (mLock) {
return mDestroyed;
@@ -5516,8 +5634,11 @@
mResponses.put(requestId, newResponse);
mClientState = newClientState != null ? newClientState : newResponse.getClientState();
+ boolean webviewRequestedCredman = newClientState != null && newClientState.getBoolean(
+ WEBVIEW_REQUESTED_CREDENTIAL_KEY, false);
List<Dataset> datasetList = newResponse.getDatasets();
+ mPresentationStatsEventLogger.maybeSetWebviewRequestedCredential(webviewRequestedCredman);
mPresentationStatsEventLogger.maybeSetFieldClassificationRequestId(sIdCounterForPcc.get());
mPresentationStatsEventLogger.maybeSetAvailableCount(datasetList, mCurrentViewId);
mFillResponseEventLogger.maybeSetDatasetsCountAfterPotentialPccFiltering(datasetList);
@@ -5665,8 +5786,14 @@
// does not matter the value of isPrimary because null response won't be overridden.
setViewStatesLocked(null, dataset, ViewState.STATE_WAITING_DATASET_AUTH,
/* clearResponse= */ false, /* isPrimary= */ true);
- final Intent fillInIntent = createAuthFillInIntentLocked(requestId, mClientState,
- dataset.getAuthenticationExtras());
+ final Intent fillInIntent;
+ if (dataset.getCredentialFillInIntent() != null && Flags.autofillCredmanIntegration()) {
+ Slog.d(TAG, "Setting credential fill intent");
+ fillInIntent = dataset.getCredentialFillInIntent();
+ } else {
+ fillInIntent = createAuthFillInIntentLocked(requestId, mClientState);
+ }
+
if (fillInIntent == null) {
forceRemoveFromServiceLocked();
return;
@@ -5682,8 +5809,7 @@
// TODO: this should never be null, but we got at least one occurrence, probably due to a race.
@GuardedBy("mLock")
@Nullable
- private Intent createAuthFillInIntentLocked(int requestId, Bundle extras,
- @Nullable Bundle authExtras) {
+ private Intent createAuthFillInIntentLocked(int requestId, Bundle extras) {
final Intent fillInIntent = new Intent();
final FillContext context = getFillContextByRequestIdLocked(requestId);
@@ -5700,9 +5826,6 @@
}
fillInIntent.putExtra(AutofillManager.EXTRA_ASSIST_STRUCTURE, context.getStructure());
fillInIntent.putExtra(AutofillManager.EXTRA_CLIENT_STATE, extras);
- if (authExtras != null) {
- fillInIntent.putExtra(AutofillManager.EXTRA_AUTH_STATE, authExtras);
- }
return fillInIntent;
}
@@ -6282,6 +6405,9 @@
@GuardedBy("mLock")
private void logAllEvents(@AutofillCommitReason int val) {
+ if (sVerbose) {
+ Slog.v(TAG, "logAllEvents(" + id + "): commitReason: " + val);
+ }
mSessionCommittedEventLogger.maybeSetCommitReason(val);
mSessionCommittedEventLogger.maybeSetRequestCount(mRequestCount);
mSessionCommittedEventLogger.maybeSetSessionDurationMillis(
@@ -6307,6 +6433,9 @@
@GuardedBy("mLock")
RemoteFillService destroyLocked() {
// Log unlogged events.
+ if (sVerbose) {
+ Slog.v(TAG, "destroyLocked for session: " + id);
+ }
logAllEvents(COMMIT_REASON_SESSION_DESTROYED);
if (mDestroyed) {
diff --git a/services/autofill/java/com/android/server/autofill/ui/BottomSheetButtonBarLayout.java b/services/autofill/java/com/android/server/autofill/ui/BottomSheetButtonBarLayout.java
new file mode 100644
index 0000000..69c7b29
--- /dev/null
+++ b/services/autofill/java/com/android/server/autofill/ui/BottomSheetButtonBarLayout.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2023 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.autofill.ui;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.Gravity;
+import android.view.View;
+import android.widget.LinearLayout;
+
+import com.android.internal.R;
+import com.android.internal.widget.ButtonBarLayout;
+
+/** An extension of {@link ButtonBarLayout} for use in Autofill bottom sheets. */
+public class BottomSheetButtonBarLayout extends ButtonBarLayout {
+
+ public BottomSheetButtonBarLayout(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+
+ final View spacer = findViewById(R.id.autofill_button_bar_spacer);
+ if (spacer == null) {
+ return;
+ }
+ if (isStacked()) {
+ spacer.getLayoutParams().width = 0;
+ spacer.getLayoutParams().height =
+ getResources().getDimensionPixelSize(R.dimen.autofill_button_bar_spacer_height);
+ setGravity(Gravity.CENTER_VERTICAL | Gravity.END);
+ } else {
+ spacer.getLayoutParams().width =
+ getResources().getDimensionPixelSize(R.dimen.autofill_button_bar_spacer_width);
+ spacer.getLayoutParams().height = 0;
+ setGravity(Gravity.CENTER_VERTICAL);
+ }
+ }
+
+ private boolean isStacked() {
+ return getOrientation() == LinearLayout.VERTICAL;
+ }
+}
diff --git a/services/autofill/java/com/android/server/autofill/ui/FillUi.java b/services/autofill/java/com/android/server/autofill/ui/FillUi.java
index b2716ec..d580f3a 100644
--- a/services/autofill/java/com/android/server/autofill/ui/FillUi.java
+++ b/services/autofill/java/com/android/server/autofill/ui/FillUi.java
@@ -15,6 +15,7 @@
*/
package com.android.server.autofill.ui;
+import static android.service.autofill.FillRequest.FLAG_VIEW_REQUESTS_CREDMAN_SERVICE;
import static com.android.server.autofill.Helper.paramsToString;
import static com.android.server.autofill.Helper.sDebug;
import static com.android.server.autofill.Helper.sFullScreenMode;
@@ -31,6 +32,7 @@
import android.service.autofill.Dataset;
import android.service.autofill.Dataset.DatasetFieldFilter;
import android.service.autofill.FillResponse;
+import android.service.autofill.Flags;
import android.text.TextUtils;
import android.util.PluralsMessageFormatter;
import android.util.Slog;
@@ -79,6 +81,7 @@
com.android.internal.R.style.Theme_DeviceDefault_Light_Autofill;
private static final int THEME_ID_DARK =
com.android.internal.R.style.Theme_DeviceDefault_Autofill;
+ private static final int AUTOFILL_CREDMAN_MAX_VISIBLE_DATASETS = 5;
private static final TypedValue sTempTypedValue = new TypedValue();
@@ -211,7 +214,11 @@
if (sVerbose) {
Slog.v(TAG, "overriding maximum visible datasets to " + mVisibleDatasetsMaxCount);
}
- } else {
+ } else if (Flags.autofillCredmanIntegration() && (
+ (response.getFlags() & FLAG_VIEW_REQUESTS_CREDMAN_SERVICE) != 0)) {
+ mVisibleDatasetsMaxCount = AUTOFILL_CREDMAN_MAX_VISIBLE_DATASETS;
+ }
+ else {
mVisibleDatasetsMaxCount = mContext.getResources()
.getInteger(com.android.internal.R.integer.autofill_max_visible_datasets);
}
diff --git a/services/backup/flags.aconfig b/services/backup/flags.aconfig
index 6a63b3a..71f2b9e 100644
--- a/services/backup/flags.aconfig
+++ b/services/backup/flags.aconfig
@@ -32,4 +32,13 @@
description: "Enables clearing the pipe buffer after restoring a single file to a BackupAgent."
bug: "320633449"
is_fixed_read_only: true
+}
+
+flag {
+ name: "enable_increase_datatypes_for_agent_logging"
+ namespace: "onboarding"
+ description: "Increase the number of a supported datatypes that an agent can define for its "
+ "logger."
+ bug: "296844513"
+ is_fixed_read_only: true
}
\ No newline at end of file
diff --git a/services/companion/java/com/android/server/companion/CompanionApplicationController.java b/services/companion/java/com/android/server/companion/CompanionApplicationController.java
index 586aa8a..af0777c 100644
--- a/services/companion/java/com/android/server/companion/CompanionApplicationController.java
+++ b/services/companion/java/com/android/server/companion/CompanionApplicationController.java
@@ -16,6 +16,8 @@
package com.android.server.companion;
+import static android.companion.AssociationRequest.DEVICE_PROFILE_AUTOMOTIVE_PROJECTION;
+
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.SuppressLint;
@@ -25,8 +27,10 @@
import android.companion.DevicePresenceEvent;
import android.content.ComponentName;
import android.content.Context;
+import android.hardware.power.Mode;
import android.os.Handler;
import android.os.ParcelUuid;
+import android.os.PowerManagerInternal;
import android.util.Log;
import android.util.Slog;
import android.util.SparseArray;
@@ -79,6 +83,8 @@
private final @NonNull CompanionDevicePresenceMonitor mDevicePresenceMonitor;
private final @NonNull CompanionServicesRegister mCompanionServicesRegister;
+ private final PowerManagerInternal mPowerManagerInternal;
+
@GuardedBy("mBoundCompanionApplications")
private final @NonNull AndroidPackageMap<List<CompanionDeviceServiceConnector>>
mBoundCompanionApplications;
@@ -87,11 +93,13 @@
CompanionApplicationController(Context context, AssociationStore associationStore,
ObservableUuidStore observableUuidStore,
- CompanionDevicePresenceMonitor companionDevicePresenceMonitor) {
+ CompanionDevicePresenceMonitor companionDevicePresenceMonitor,
+ PowerManagerInternal powerManagerInternal) {
mContext = context;
mAssociationStore = associationStore;
mObservableUuidStore = observableUuidStore;
mDevicePresenceMonitor = companionDevicePresenceMonitor;
+ mPowerManagerInternal = powerManagerInternal;
mCompanionServicesRegister = new CompanionServicesRegister();
mBoundCompanionApplications = new AndroidPackageMap<>();
mScheduledForRebindingCompanionApplications = new AndroidPackageMap<>();
@@ -364,9 +372,21 @@
boolean isPrimary = serviceConnector.isPrimary();
Slog.i(TAG, "onBinderDied() u" + userId + "/" + packageName + " isPrimary: " + isPrimary);
- // First: Only mark not BOUND for primary service.
- synchronized (mBoundCompanionApplications) {
- if (serviceConnector.isPrimary()) {
+ // First, disable hint mode for Auto profile and mark not BOUND for primary service ONLY.
+ if (isPrimary) {
+ final List<AssociationInfo> associations =
+ mAssociationStore.getAssociationsForPackage(userId, packageName);
+
+ for (AssociationInfo association : associations) {
+ final String deviceProfile = association.getDeviceProfile();
+ if (DEVICE_PROFILE_AUTOMOTIVE_PROJECTION.equals(deviceProfile)) {
+ Slog.i(TAG, "Disable hint mode for device profile: " + deviceProfile);
+ mPowerManagerInternal.setPowerMode(Mode.AUTOMOTIVE_PROJECTION, false);
+ break;
+ }
+ }
+
+ synchronized (mBoundCompanionApplications) {
mBoundCompanionApplications.removePackage(userId, packageName);
}
}
diff --git a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java
index 5019428..b43f1a9 100644
--- a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java
+++ b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java
@@ -20,7 +20,6 @@
import static android.Manifest.permission.ASSOCIATE_COMPANION_DEVICES;
import static android.Manifest.permission.DELIVER_COMPANION_MESSAGES;
import static android.Manifest.permission.MANAGE_COMPANION_DEVICES;
-import static android.Manifest.permission.REQUEST_COMPANION_PROFILE_AUTOMOTIVE_PROJECTION;
import static android.Manifest.permission.REQUEST_OBSERVE_COMPANION_DEVICE_PRESENCE;
import static android.Manifest.permission.USE_COMPANION_TRANSPORTS;
import static android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_VISIBLE;
@@ -270,7 +269,8 @@
mAssociationStore, mObservableUuidStore, mDevicePresenceCallback);
mCompanionAppController = new CompanionApplicationController(
- context, mAssociationStore, mObservableUuidStore, mDevicePresenceMonitor);
+ context, mAssociationStore, mObservableUuidStore, mDevicePresenceMonitor,
+ mPowerManagerInternal);
mTransportManager = new CompanionTransportManager(context, mAssociationStore);
mSystemDataTransferProcessor = new SystemDataTransferProcessor(this,
mPackageManagerInternal, mAssociationStore,
@@ -1128,7 +1128,9 @@
mDevicePresenceMonitor.onSelfManagedDeviceConnected(associationId);
- if (association.getDeviceProfile() == REQUEST_COMPANION_PROFILE_AUTOMOTIVE_PROJECTION) {
+ final String deviceProfile = association.getDeviceProfile();
+ if (DEVICE_PROFILE_AUTOMOTIVE_PROJECTION.equals(deviceProfile)) {
+ Slog.i(TAG, "Enable hint mode for device device profile: " + deviceProfile);
mPowerManagerInternal.setPowerMode(Mode.AUTOMOTIVE_PROJECTION, true);
}
}
@@ -1146,7 +1148,9 @@
mDevicePresenceMonitor.onSelfManagedDeviceDisconnected(associationId);
- if (association.getDeviceProfile() == REQUEST_COMPANION_PROFILE_AUTOMOTIVE_PROJECTION) {
+ final String deviceProfile = association.getDeviceProfile();
+ if (DEVICE_PROFILE_AUTOMOTIVE_PROJECTION.equals(deviceProfile)) {
+ Slog.i(TAG, "Disable hint mode for device profile: " + deviceProfile);
mPowerManagerInternal.setPowerMode(Mode.AUTOMOTIVE_PROJECTION, false);
}
}
@@ -1301,6 +1305,8 @@
mAssociationStore.dump(out);
mDevicePresenceMonitor.dump(out);
mCompanionAppController.dump(out);
+ mTransportManager.dump(out);
+ mSystemDataTransferRequestStore.dump(out);
}
}
diff --git a/services/companion/java/com/android/server/companion/datatransfer/SystemDataTransferRequestStore.java b/services/companion/java/com/android/server/companion/datatransfer/SystemDataTransferRequestStore.java
index 51c5fd6..c4c80f9 100644
--- a/services/companion/java/com/android/server/companion/datatransfer/SystemDataTransferRequestStore.java
+++ b/services/companion/java/com/android/server/companion/datatransfer/SystemDataTransferRequestStore.java
@@ -48,6 +48,7 @@
import java.io.ByteArrayInputStream;
import java.io.FileInputStream;
import java.io.IOException;
+import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
@@ -303,6 +304,32 @@
}
}
+
+
+ /**
+ * Dumps current system data transfer request states.
+ */
+ public void dump(@NonNull PrintWriter out) {
+ synchronized (mLock) {
+ out.append("System Data Transfer Requests (Cached): ");
+ if (mCachedPerUser.size() == 0) {
+ out.append("<empty>\n");
+ } else {
+ out.append("\n");
+ for (int i = 0; i < mCachedPerUser.size(); i++) {
+ final int userId = mCachedPerUser.keyAt(i);
+ for (SystemDataTransferRequest request : mCachedPerUser.get(userId)) {
+ out.append(" u")
+ .append(String.valueOf(userId))
+ .append(" -> ")
+ .append(request.toString())
+ .append('\n');
+ }
+ }
+ }
+ }
+ }
+
private void writeRequestsToXml(@NonNull TypedXmlSerializer serializer,
@Nullable Collection<SystemDataTransferRequest> requests) throws IOException {
serializer.startTag(null, XML_TAG_REQUESTS);
diff --git a/services/companion/java/com/android/server/companion/transport/CompanionTransportManager.java b/services/companion/java/com/android/server/companion/transport/CompanionTransportManager.java
index 3e45626..3861f99 100644
--- a/services/companion/java/com/android/server/companion/transport/CompanionTransportManager.java
+++ b/services/companion/java/com/android/server/companion/transport/CompanionTransportManager.java
@@ -36,6 +36,7 @@
import java.io.FileDescriptor;
import java.io.IOException;
+import java.io.PrintWriter;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
@@ -225,6 +226,25 @@
}
/**
+ * Dumps current list of active transports.
+ */
+ public void dump(@NonNull PrintWriter out) {
+ synchronized (mTransports) {
+ out.append("System Data Transports: ");
+ if (mTransports.size() == 0) {
+ out.append("<empty>\n");
+ } else {
+ out.append("\n");
+ for (int i = 0; i < mTransports.size(); i++) {
+ final int associationId = mTransports.keyAt(i);
+ final Transport transport = mTransports.get(associationId);
+ out.append(" ").append(transport.toString()).append('\n');
+ }
+ }
+ }
+ }
+
+ /**
* @hide
*/
public void enableSecureTransport(boolean enabled) {
diff --git a/services/companion/java/com/android/server/companion/transport/RawTransport.java b/services/companion/java/com/android/server/companion/transport/RawTransport.java
index ca169aac..05703ce 100644
--- a/services/companion/java/com/android/server/companion/transport/RawTransport.java
+++ b/services/companion/java/com/android/server/companion/transport/RawTransport.java
@@ -94,6 +94,13 @@
}
}
+ @Override
+ public String toString() {
+ return "RawTransport{"
+ + "mAssociationId=" + mAssociationId
+ + '}';
+ }
+
private void receiveMessage() throws IOException {
synchronized (mRemoteIn) {
final byte[] headerBytes = new byte[HEADER_LENGTH];
diff --git a/services/companion/java/com/android/server/companion/transport/SecureTransport.java b/services/companion/java/com/android/server/companion/transport/SecureTransport.java
index 6e906eb..1e95e65 100644
--- a/services/companion/java/com/android/server/companion/transport/SecureTransport.java
+++ b/services/companion/java/com/android/server/companion/transport/SecureTransport.java
@@ -152,4 +152,12 @@
close();
}
}
+
+ @Override
+ public String toString() {
+ return "SecureTransport{"
+ + "mAssociationId=" + mAssociationId
+ + ", mSecureChannel=" + mSecureChannel
+ + '}';
+ }
}
diff --git a/services/companion/java/com/android/server/companion/virtual/InputController.java b/services/companion/java/com/android/server/companion/virtual/InputController.java
index 8962bf0..1b49f18e 100644
--- a/services/companion/java/com/android/server/companion/virtual/InputController.java
+++ b/services/companion/java/com/android/server/companion/virtual/InputController.java
@@ -692,32 +692,37 @@
private int mInputDeviceId = IInputConstants.INVALID_INPUT_DEVICE_ID;
- WaitForDevice(String deviceName, int vendorId, int productId) {
+ WaitForDevice(String deviceName, int vendorId, int productId, int associatedDisplayId) {
mListener = new InputManager.InputDeviceListener() {
@Override
public void onInputDeviceAdded(int deviceId) {
- final InputDevice device = InputManagerGlobal.getInstance().getInputDevice(
- deviceId);
- Objects.requireNonNull(device, "Newly added input device was null.");
- if (!device.getName().equals(deviceName)) {
- return;
- }
- final InputDeviceIdentifier id = device.getIdentifier();
- if (id.getVendorId() != vendorId || id.getProductId() != productId) {
- return;
- }
- mInputDeviceId = deviceId;
- mDeviceAddedLatch.countDown();
+ onInputDeviceChanged(deviceId);
}
@Override
public void onInputDeviceRemoved(int deviceId) {
-
}
@Override
public void onInputDeviceChanged(int deviceId) {
+ if (isMatchingDevice(deviceId)) {
+ mInputDeviceId = deviceId;
+ mDeviceAddedLatch.countDown();
+ }
+ }
+ private boolean isMatchingDevice(int deviceId) {
+ final InputDevice device = InputManagerGlobal.getInstance().getInputDevice(
+ deviceId);
+ Objects.requireNonNull(device, "Newly added input device was null.");
+ if (!device.getName().equals(deviceName)) {
+ return false;
+ }
+ final InputDeviceIdentifier id = device.getIdentifier();
+ if (id.getVendorId() != vendorId || id.getProductId() != productId) {
+ return false;
+ }
+ return device.getAssociatedDisplayId() == associatedDisplayId;
}
};
InputManagerGlobal.getInstance().registerInputDeviceListener(mListener, mHandler);
@@ -799,7 +804,7 @@
final int inputDeviceId;
setUniqueIdAssociation(displayId, phys);
- try (WaitForDevice waiter = new WaitForDevice(deviceName, vendorId, productId)) {
+ try (WaitForDevice waiter = new WaitForDevice(deviceName, vendorId, productId, displayId)) {
ptr = deviceOpener.get();
// See INVALID_PTR in libs/input/VirtualInputDevice.cpp.
if (ptr == 0) {
diff --git a/services/contextualsearch/OWNERS b/services/contextualsearch/OWNERS
new file mode 100644
index 0000000..0c2612c
--- /dev/null
+++ b/services/contextualsearch/OWNERS
@@ -0,0 +1 @@
+include /core/java/android/service/contextualsearch/OWNERS
diff --git a/services/core/java/com/android/server/BatteryService.java b/services/core/java/com/android/server/BatteryService.java
index 9d9e7c9..7979936 100644
--- a/services/core/java/com/android/server/BatteryService.java
+++ b/services/core/java/com/android/server/BatteryService.java
@@ -16,8 +16,8 @@
package com.android.server;
-import static android.os.Flags.stateOfHealthPublic;
import static android.os.Flags.batteryServiceSupportCurrentAdbCommand;
+import static android.os.Flags.stateOfHealthPublic;
import static com.android.internal.logging.nano.MetricsProto.MetricsEvent;
import static com.android.server.health.Utils.copyV1Battery;
@@ -81,6 +81,7 @@
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.NoSuchElementException;
+import java.util.concurrent.CopyOnWriteArraySet;
/**
* <p>BatteryService monitors the charging status, and charge level of the device
@@ -157,6 +158,12 @@
private int mLastChargeCounter;
private int mLastBatteryCycleCount;
private int mLastCharingState;
+ /**
+ * The last seen charging policy. This requires the
+ * {@link android.Manifest.permission#BATTERY_STATS} permission and should therefore not be
+ * included in the ACTION_BATTERY_CHANGED intent extras.
+ */
+ private int mLastChargingPolicy;
private int mSequence = 1;
@@ -197,6 +204,9 @@
private ArrayDeque<Bundle> mBatteryLevelsEventQueue;
private long mLastBatteryLevelChangedSentMs;
+ private final CopyOnWriteArraySet<BatteryManagerInternal.ChargingPolicyChangeListener>
+ mChargingPolicyChangeListeners = new CopyOnWriteArraySet<>();
+
private Bundle mBatteryChangedOptions = BroadcastOptions.makeBasic()
.setDeliveryGroupPolicy(BroadcastOptions.DELIVERY_GROUP_POLICY_MOST_RECENT)
.setDeferralPolicy(BroadcastOptions.DEFERRAL_POLICY_UNTIL_ACTIVE)
@@ -527,6 +537,11 @@
shutdownIfNoPowerLocked();
shutdownIfOverTempLocked();
+ if (force || mHealthInfo.chargingPolicy != mLastChargingPolicy) {
+ mLastChargingPolicy = mHealthInfo.chargingPolicy;
+ mHandler.post(this::notifyChargingPolicyChanged);
+ }
+
if (force
|| (mHealthInfo.batteryStatus != mLastBatteryStatus
|| mHealthInfo.batteryHealth != mLastBatteryHealth
@@ -827,6 +842,17 @@
mLastBatteryLevelChangedSentMs = SystemClock.elapsedRealtime();
}
+ private void notifyChargingPolicyChanged() {
+ final int newPolicy;
+ synchronized (mLock) {
+ newPolicy = mLastChargingPolicy;
+ }
+ for (BatteryManagerInternal.ChargingPolicyChangeListener listener
+ : mChargingPolicyChangeListeners) {
+ listener.onChargingPolicyChanged(newPolicy);
+ }
+ }
+
// TODO: Current code doesn't work since "--unplugged" flag in BSS was purposefully removed.
private void logBatteryStatsLocked() {
IBinder batteryInfoService = ServiceManager.getService(BatteryStats.SERVICE_NAME);
@@ -1220,6 +1246,8 @@
pw.println(" voltage: " + mHealthInfo.batteryVoltageMillivolts);
pw.println(" temperature: " + mHealthInfo.batteryTemperatureTenthsCelsius);
pw.println(" technology: " + mHealthInfo.batteryTechnology);
+ pw.println(" Charging state: " + mHealthInfo.chargingState);
+ pw.println(" Charging policy: " + mHealthInfo.chargingPolicy);
} else {
Shell shell = new Shell();
shell.exec(mBinderService, null, fd, null, args, null, new ResultReceiver(null));
@@ -1452,6 +1480,19 @@
}
@Override
+ public void registerChargingPolicyChangeListener(
+ BatteryManagerInternal.ChargingPolicyChangeListener listener) {
+ mChargingPolicyChangeListeners.add(listener);
+ }
+
+ @Override
+ public int getChargingPolicy() {
+ synchronized (mLock) {
+ return mLastChargingPolicy;
+ }
+ }
+
+ @Override
public int getInvalidCharger() {
synchronized (mLock) {
return mInvalidCharger;
diff --git a/services/core/java/com/android/server/DockObserver.java b/services/core/java/com/android/server/DockObserver.java
index 9554e63..fb527c1 100644
--- a/services/core/java/com/android/server/DockObserver.java
+++ b/services/core/java/com/android/server/DockObserver.java
@@ -20,8 +20,9 @@
import android.content.Context;
import android.content.Intent;
import android.database.ContentObserver;
-import android.media.AudioAttributes;
+import android.media.AudioManager;
import android.media.Ringtone;
+import android.media.RingtoneManager;
import android.net.Uri;
import android.os.Binder;
import android.os.Handler;
@@ -305,16 +306,11 @@
if (soundPath != null) {
final Uri soundUri = Uri.parse("file://" + soundPath);
if (soundUri != null) {
- AudioAttributes audioAttributes = new AudioAttributes.Builder()
- .setUsage(AudioAttributes.USAGE_ASSISTANCE_SONIFICATION)
- .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
- .build();
- final Ringtone sfx = new Ringtone.Builder(getContext(),
- Ringtone.MEDIA_SOUND, audioAttributes)
- .setUri(soundUri)
- .setPreferBuiltinDevice()
- .build();
+ final Ringtone sfx = RingtoneManager.getRingtone(
+ getContext(), soundUri);
if (sfx != null) {
+ sfx.setStreamType(AudioManager.STREAM_SYSTEM);
+ sfx.preferBuiltinDevice(true);
sfx.play();
}
}
diff --git a/services/core/java/com/android/server/StorageManagerService.java b/services/core/java/com/android/server/StorageManagerService.java
index ea1b0f5..e7fae24 100644
--- a/services/core/java/com/android/server/StorageManagerService.java
+++ b/services/core/java/com/android/server/StorageManagerService.java
@@ -48,6 +48,7 @@
import static org.xmlpull.v1.XmlPullParser.END_DOCUMENT;
import static org.xmlpull.v1.XmlPullParser.START_TAG;
+import android.annotation.EnforcePermission;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.UserIdInt;
@@ -3597,6 +3598,13 @@
return mInternalStorageSize;
}
+ @EnforcePermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE)
+ @Override
+ public int getInternalStorageRemainingLifetime() throws RemoteException {
+ super.getInternalStorageRemainingLifetime_enforcePermission();
+ return mVold.getStorageRemainingLifetime();
+ }
+
/**
* Enforces that the caller is the {@link ExternalStorageService}
*
diff --git a/services/core/java/com/android/server/SystemConfig.java b/services/core/java/com/android/server/SystemConfig.java
index a493d7a..a341b4a 100644
--- a/services/core/java/com/android/server/SystemConfig.java
+++ b/services/core/java/com/android/server/SystemConfig.java
@@ -24,6 +24,8 @@
import android.content.ComponentName;
import android.content.pm.FeatureInfo;
import android.content.pm.PackageManager;
+import android.content.pm.Signature;
+import android.content.pm.SignedPackage;
import android.os.Build;
import android.os.CarrierAssociatedAppEntry;
import android.os.Environment;
@@ -350,6 +352,16 @@
// updated to avoid cached/potentially tampered results.
private final Set<String> mPreinstallPackagesWithStrictSignatureCheck = new ArraySet<>();
+ // A set of packages that should be considered "trusted packages" by ECM (Enhanced
+ // Confirmation Mode). "Trusted packages" are exempt from ECM (i.e., they will never be
+ // considered "restricted").
+ private final ArraySet<SignedPackage> mEnhancedConfirmationTrustedPackages = new ArraySet<>();
+
+ // A set of packages that should be considered "trusted installers" by ECM (Enhanced
+ // Confirmation Mode). "Trusted installers", and all apps installed by a trusted installer, are
+ // exempt from ECM (i.e., they will never be considered "restricted").
+ private final ArraySet<SignedPackage> mEnhancedConfirmationTrustedInstallers = new ArraySet<>();
+
/**
* Map of system pre-defined, uniquely named actors; keys are namespace,
* value maps actor name to package name.
@@ -560,6 +572,14 @@
return mPreinstallPackagesWithStrictSignatureCheck;
}
+ public ArraySet<SignedPackage> getEnhancedConfirmationTrustedPackages() {
+ return mEnhancedConfirmationTrustedPackages;
+ }
+
+ public ArraySet<SignedPackage> getEnhancedConfirmationTrustedInstallers() {
+ return mEnhancedConfirmationTrustedInstallers;
+ }
+
/**
* Only use for testing. Do NOT use in production code.
* @param readPermissions false to create an empty SystemConfig; true to read the permissions.
@@ -1558,6 +1578,26 @@
}
}
} break;
+ case "enhanced-confirmation-trusted-package": {
+ if (android.permission.flags.Flags.enhancedConfirmationModeApisEnabled()) {
+ SignedPackage signedPackage = parseEnhancedConfirmationTrustedPackage(
+ parser, permFile, name);
+ if (signedPackage != null) {
+ mEnhancedConfirmationTrustedPackages.add(signedPackage);
+ }
+ break;
+ }
+ } // fall through if flag is not enabled
+ case "enhanced-confirmation-trusted-installer": {
+ if (android.permission.flags.Flags.enhancedConfirmationModeApisEnabled()) {
+ SignedPackage signedPackage = parseEnhancedConfirmationTrustedPackage(
+ parser, permFile, name);
+ if (signedPackage != null) {
+ mEnhancedConfirmationTrustedInstallers.add(signedPackage);
+ }
+ break;
+ }
+ } // fall through if flag is not enabled
default: {
Slog.w(TAG, "Tag " + name + " is unknown in "
+ permFile + " at " + parser.getPositionDescription());
@@ -1619,6 +1659,33 @@
}
}
+ private @Nullable SignedPackage parseEnhancedConfirmationTrustedPackage(XmlPullParser parser,
+ File permFile, String elementName) {
+ String pkgName = parser.getAttributeValue(null, "package");
+ if (TextUtils.isEmpty(pkgName)) {
+ Slog.w(TAG, "<" + elementName + "> without package " + permFile + " at "
+ + parser.getPositionDescription());
+ return null;
+ }
+
+ String certificateDigestStr = parser.getAttributeValue(null, "sha256-cert-digest");
+ if (TextUtils.isEmpty(certificateDigestStr)) {
+ Slog.w(TAG, "<" + elementName + "> without sha256-cert-digest in " + permFile
+ + " at " + parser.getPositionDescription());
+ return null;
+ }
+ byte[] certificateDigest = null;
+ try {
+ certificateDigest = new Signature(certificateDigestStr.replace(":", "")).toByteArray();
+ } catch (IllegalArgumentException e) {
+ Slog.w(TAG, "<" + elementName + "> with invalid sha256-cert-digest in "
+ + permFile + " at " + parser.getPositionDescription());
+ return null;
+ }
+
+ return new SignedPackage(pkgName, certificateDigest);
+ }
+
// This method only enables a new Android feature added in U and will not have impact on app
// compatibility
@SuppressWarnings("AndroidFrameworkCompatChange")
diff --git a/services/core/java/com/android/server/adb/AdbDebuggingManager.java b/services/core/java/com/android/server/adb/AdbDebuggingManager.java
index 627a62e..34c3d7e 100644
--- a/services/core/java/com/android/server/adb/AdbDebuggingManager.java
+++ b/services/core/java/com/android/server/adb/AdbDebuggingManager.java
@@ -246,16 +246,6 @@
@Override
public void run() {
- if (mGuid.isEmpty()) {
- Slog.e(TAG, "adbwifi guid was not set");
- return;
- }
- mPort = native_pairing_start(mGuid, mPairingCode);
- if (mPort <= 0 || mPort > 65535) {
- Slog.e(TAG, "Unable to start pairing server");
- return;
- }
-
// Register the mdns service
NsdServiceInfo serviceInfo = new NsdServiceInfo();
serviceInfo.setServiceName(mServiceName);
@@ -288,6 +278,28 @@
mHandler.sendMessage(message);
}
+ @Override
+ public void start() {
+ /*
+ * If a user is fast enough to click cancel, native_pairing_cancel can be invoked
+ * while native_pairing_start is running which run the destruction of the object
+ * while it is being constructed. Here we start the pairing server on foreground
+ * Thread so native_pairing_cancel can never be called concurrently. Then we let
+ * the pairing server run on a background Thread.
+ */
+ if (mGuid.isEmpty()) {
+ Slog.e(TAG, "adbwifi guid was not set");
+ return;
+ }
+ mPort = native_pairing_start(mGuid, mPairingCode);
+ if (mPort <= 0) {
+ Slog.e(TAG, "Unable to start pairing server");
+ return;
+ }
+
+ super.start();
+ }
+
public void cancelPairing() {
native_pairing_cancel();
}
diff --git a/services/core/java/com/android/server/am/ActiveServices.java b/services/core/java/com/android/server/am/ActiveServices.java
index adc0255..cd45b03 100644
--- a/services/core/java/com/android/server/am/ActiveServices.java
+++ b/services/core/java/com/android/server/am/ActiveServices.java
@@ -581,7 +581,8 @@
if (DEBUG_FOREGROUND_SERVICE) {
Slog.i(TAG, " Stopping fg for service " + r);
}
- setServiceForegroundInnerLocked(r, 0, null, 0, 0);
+ setServiceForegroundInnerLocked(r, 0, null, 0, 0,
+ 0);
}
}
@@ -989,7 +990,7 @@
if (fgRequired) {
logFgsBackgroundStart(r);
- if (!r.isFgsAllowedStart() && isBgFgsRestrictionEnabled(r)) {
+ if (!r.isFgsAllowedStart() && isBgFgsRestrictionEnabled(r, callingUid)) {
String msg = "startForegroundService() not allowed due to "
+ "mAllowStartForeground false: service "
+ r.shortInstanceName;
@@ -1787,11 +1788,13 @@
public void setServiceForegroundLocked(ComponentName className, IBinder token,
int id, Notification notification, int flags, int foregroundServiceType) {
final int userId = UserHandle.getCallingUserId();
+ final int callingUid = mAm.mInjector.getCallingUid();
final long origId = mAm.mInjector.clearCallingIdentity();
try {
ServiceRecord r = findServiceLocked(className, token, userId);
if (r != null) {
- setServiceForegroundInnerLocked(r, id, notification, flags, foregroundServiceType);
+ setServiceForegroundInnerLocked(r, id, notification, flags, foregroundServiceType,
+ callingUid);
}
} finally {
mAm.mInjector.restoreCallingIdentity(origId);
@@ -2106,7 +2109,8 @@
*/
@GuardedBy("mAm")
private void setServiceForegroundInnerLocked(final ServiceRecord r, int id,
- Notification notification, int flags, int foregroundServiceType) {
+ Notification notification, int flags, int foregroundServiceType,
+ int callingUidIfStart) {
if (id != 0) {
if (notification == null) {
throw new IllegalArgumentException("null notification");
@@ -2234,7 +2238,8 @@
}
// Whether FGS-BG-start restriction is enabled for this service.
- final boolean isBgFgsRestrictionEnabledForService = isBgFgsRestrictionEnabled(r);
+ final boolean isBgFgsRestrictionEnabledForService = isBgFgsRestrictionEnabled(r,
+ callingUidIfStart);
// Whether to extend the SHORT_SERVICE time out.
boolean extendShortServiceTimeout = false;
@@ -8486,14 +8491,43 @@
NOTE_FOREGROUND_SERVICE_BG_LAUNCH, n.build(), UserHandle.ALL);
}
- private boolean isBgFgsRestrictionEnabled(ServiceRecord r) {
- return mAm.mConstants.mFlagFgsStartRestrictionEnabled
- // Checking service's targetSdkVersion.
- && CompatChanges.isChangeEnabled(FGS_BG_START_RESTRICTION_CHANGE_ID, r.appInfo.uid)
- && (!mAm.mConstants.mFgsStartRestrictionCheckCallerTargetSdk
- // Checking callingUid's targetSdkVersion.
- || CompatChanges.isChangeEnabled(
- FGS_BG_START_RESTRICTION_CHANGE_ID, r.mRecentCallingUid));
+ private boolean isBgFgsRestrictionEnabled(ServiceRecord r, int actualCallingUid) {
+ // mFlagFgsStartRestrictionEnabled controls whether to enable the BG FGS restrictions:
+ // - If true (default), BG-FGS restrictions are enabled if the service targets >= S.
+ // - If false, BG-FGS restrictions are disabled for all apps.
+ if (!mAm.mConstants.mFlagFgsStartRestrictionEnabled) {
+ return false;
+ }
+
+ // If the service target below S, then don't enable the restrictions.
+ if (!CompatChanges.isChangeEnabled(FGS_BG_START_RESTRICTION_CHANGE_ID, r.appInfo.uid)) {
+ return false;
+ }
+
+ // mFgsStartRestrictionCheckCallerTargetSdk controls whether we take the caller's target
+ // SDK level into account or not:
+ // - If true (default), BG-FGS restrictions only happens if the caller _also_ targets >= S.
+ // - If false, BG-FGS restrictions do _not_ use the caller SDK levels.
+ if (!mAm.mConstants.mFgsStartRestrictionCheckCallerTargetSdk) {
+ return true; // In this case, we only check the service's target SDK level.
+ }
+ final int callingUid;
+ if (Flags.newFgsRestrictionLogic()) {
+ // We always consider SYSTEM_UID to target S+, so just enable the restrictions.
+ if (actualCallingUid == Process.SYSTEM_UID) {
+ return true;
+ }
+ callingUid = actualCallingUid;
+ } else {
+ // Legacy logic used mRecentCallingUid.
+ callingUid = r.mRecentCallingUid;
+ }
+ if (!CompatChanges.isChangeEnabled(FGS_BG_START_RESTRICTION_CHANGE_ID, callingUid)) {
+ return false; // If the caller targets < S, then we still disable the restrictions.
+ }
+
+ // Both the service and the caller target S+, so enable the check.
+ return true;
}
private void logFgsBackgroundStart(ServiceRecord r) {
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index a5531ae..2750344 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -187,6 +187,7 @@
import static com.android.server.wm.ActivityTaskManagerService.DUMP_VISIBLE_ACTIVITIES;
import static com.android.server.wm.ActivityTaskManagerService.RELAUNCH_REASON_NONE;
import static com.android.server.wm.ActivityTaskManagerService.relaunchReasonToString;
+import static com.android.systemui.shared.Flags.enableHomeDelay;
import android.Manifest;
import android.Manifest.permission;
@@ -521,6 +522,7 @@
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executor;
import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.BiFunction;
import java.util.function.Consumer;
@@ -921,6 +923,15 @@
@GuardedBy("this")
final ComponentAliasResolver mComponentAliasResolver;
+ private static final long HOME_LAUNCH_TIMEOUT_MS = 15000;
+ private final AtomicBoolean mHasHomeDelay = new AtomicBoolean(false);
+
+ /**
+ * Tracks all users with computed color resources by ThemeOverlaycvontroller
+ */
+ @GuardedBy("this")
+ private final Set<Integer> mThemeOverlayReadiness = new HashSet<>();
+
/**
* Tracks association information for a particular package along with debuggability.
* <p> Associations for a package A are allowed to package B if B is part of the
@@ -2332,6 +2343,7 @@
mService.startBroadcastObservers();
} else if (phase == PHASE_THIRD_PARTY_APPS_CAN_START) {
mService.mPackageWatchdog.onPackagesReady();
+ mService.setHomeTimeout();
}
}
@@ -5304,6 +5316,59 @@
}
}
+ /**
+ * Starts Home if there is no completion signal from ThemeOverlayController
+ */
+ private void setHomeTimeout() {
+ if (enableHomeDelay() && mHasHomeDelay.compareAndSet(false, true)) {
+ mHandler.postDelayed(() -> {
+ if (!getThemeOverlayReadiness()) {
+ Slog.d(TAG,
+ "ThemeHomeDelay: ThemeOverlayController not responding, launching "
+ + "Home after "
+ + HOME_LAUNCH_TIMEOUT_MS + "ms");
+ setThemeOverlayReady(true);
+ }
+ }, HOME_LAUNCH_TIMEOUT_MS);
+ }
+ }
+
+ /**
+ * Used by ThemeOverlayController to notify all listeners for
+ * color palette readiness.
+ * @hide
+ */
+ @Override
+ public void setThemeOverlayReady(boolean readiness) {
+ enforceCallingPermission(Manifest.permission.SET_THEME_OVERLAY_CONTROLLER_READY,
+ "setThemeOverlayReady");
+
+ int currentUserId = mUserController.getCurrentUserId();
+
+ boolean updateReadiness;
+ synchronized (mThemeOverlayReadiness) {
+ updateReadiness = readiness ? mThemeOverlayReadiness.add(currentUserId)
+ : mThemeOverlayReadiness.remove(currentUserId);
+ }
+
+ if (updateReadiness && readiness && enableHomeDelay()) {
+ mAtmInternal.startHomeOnAllDisplays(currentUserId, "setThemeOverlayReady");
+ }
+ }
+
+ /**
+ * Returns current state of ThemeOverlayController color
+ * palette readiness.
+ *
+ * @hide
+ */
+ public boolean getThemeOverlayReadiness() {
+ int uid = mUserController.getCurrentUserId();
+ synchronized (mThemeOverlayReadiness) {
+ return mThemeOverlayReadiness.contains(uid);
+ }
+ }
+
final void ensureBootCompleted() {
boolean booting;
boolean enableScreen;
@@ -18033,6 +18098,10 @@
mAtmInternal.onUserStopped(userId);
// Clean up various services by removing the user
mBatteryStatsService.onUserRemoved(userId);
+
+ synchronized (mThemeOverlayReadiness) {
+ mThemeOverlayReadiness.remove(userId);
+ }
}
@Override
@@ -19391,6 +19460,11 @@
return ActivityManagerService.this.clearApplicationUserData(packageName, keepState,
isRestore, observer, userId);
}
+
+ @Override
+ public boolean getThemeOverlayReadiness() {
+ return ActivityManagerService.this.getThemeOverlayReadiness();
+ }
}
long inputDispatchingTimedOut(int pid, final boolean aboveSystem, TimeoutRecord timeoutRecord) {
diff --git a/services/core/java/com/android/server/am/AssistDataRequester.java b/services/core/java/com/android/server/am/AssistDataRequester.java
index 98129ed..856a15f 100644
--- a/services/core/java/com/android/server/am/AssistDataRequester.java
+++ b/services/core/java/com/android/server/am/AssistDataRequester.java
@@ -222,7 +222,7 @@
// Ensure that the current activity supports assist data
boolean isAssistDataAllowed = false;
try {
- isAssistDataAllowed = mActivityTaskManager.isAssistDataAllowedOnCurrentActivity();
+ isAssistDataAllowed = mActivityTaskManager.isAssistDataAllowed();
} catch (RemoteException e) {
// Should never happen
}
diff --git a/services/core/java/com/android/server/am/BroadcastProcessQueue.java b/services/core/java/com/android/server/am/BroadcastProcessQueue.java
index c37e619..d1c8c30 100644
--- a/services/core/java/com/android/server/am/BroadcastProcessQueue.java
+++ b/services/core/java/com/android/server/am/BroadcastProcessQueue.java
@@ -152,6 +152,11 @@
private int mActiveIndex;
/**
+ * True if the broadcast actively being dispatched to this process was re-enqueued previously.
+ */
+ private boolean mActiveReEnqueued;
+
+ /**
* Count of {@link #mActive} broadcasts that have been dispatched since this
* queue was last idle.
*/
@@ -312,6 +317,7 @@
final SomeArgs broadcastArgs = SomeArgs.obtain();
broadcastArgs.arg1 = record;
broadcastArgs.argi1 = recordIndex;
+ broadcastArgs.argi2 = 1;
getQueueForBroadcast(record).addFirst(broadcastArgs);
onBroadcastEnqueued(record, recordIndex);
}
@@ -609,6 +615,7 @@
final SomeArgs next = removeNextBroadcast();
mActive = (BroadcastRecord) next.arg1;
mActiveIndex = next.argi1;
+ mActiveReEnqueued = (next.argi2 == 1);
mActiveCountSinceIdle++;
mActiveAssumedDeliveryCountSinceIdle +=
(mActive.isAssumedDelivered(mActiveIndex) ? 1 : 0);
@@ -624,12 +631,21 @@
public void makeActiveIdle() {
mActive = null;
mActiveIndex = 0;
+ mActiveReEnqueued = false;
mActiveCountSinceIdle = 0;
mActiveAssumedDeliveryCountSinceIdle = 0;
mActiveViaColdStart = false;
invalidateRunnableAt();
}
+ public boolean wasActiveBroadcastReEnqueued() {
+ // If the flag is not enabled, treat as if the broadcast was never re-enqueued.
+ if (!Flags.avoidRepeatedBcastReEnqueues()) {
+ return false;
+ }
+ return mActiveReEnqueued;
+ }
+
/**
* Update summary statistics when the given record has been enqueued.
*/
@@ -1476,6 +1492,9 @@
if (runningOomAdjusted) {
pw.print("runningOomAdjusted:"); pw.println(runningOomAdjusted);
}
+ if (mActiveReEnqueued) {
+ pw.print("activeReEnqueued:"); pw.println(mActiveReEnqueued);
+ }
}
@NeverCompile
diff --git a/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java b/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java
index db0f03f..98263df 100644
--- a/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java
+++ b/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java
@@ -542,8 +542,8 @@
updateOomAdj |= queue.runningOomAdjusted;
try {
completed = scheduleReceiverWarmLocked(queue);
- } catch (BroadcastDeliveryFailedException e) {
- reEnqueueActiveBroadcast(queue);
+ } catch (BroadcastRetryException e) {
+ finishOrReEnqueueActiveBroadcast(queue);
completed = true;
}
} else {
@@ -586,7 +586,12 @@
private void clearInvalidPendingColdStart() {
logw("Clearing invalid pending cold start: " + mRunningColdStart);
- mRunningColdStart.reEnqueueActiveBroadcast();
+ if (mRunningColdStart.wasActiveBroadcastReEnqueued()) {
+ finishReceiverActiveLocked(mRunningColdStart, BroadcastRecord.DELIVERY_FAILURE,
+ "invalid start with re-enqueued broadcast");
+ } else {
+ mRunningColdStart.reEnqueueActiveBroadcast();
+ }
demoteFromRunningLocked(mRunningColdStart);
clearRunningColdStart();
enqueueUpdateRunningList();
@@ -613,19 +618,26 @@
}
}
- private void reEnqueueActiveBroadcast(@NonNull BroadcastProcessQueue queue) {
+ private void finishOrReEnqueueActiveBroadcast(@NonNull BroadcastProcessQueue queue) {
checkState(queue.isActive(), "isActive");
- final BroadcastRecord record = queue.getActive();
- final int index = queue.getActiveIndex();
- setDeliveryState(queue, queue.app, record, index, record.receivers.get(index),
- BroadcastRecord.DELIVERY_PENDING, "reEnqueueActiveBroadcast");
- queue.reEnqueueActiveBroadcast();
+ if (queue.wasActiveBroadcastReEnqueued()) {
+ // If the broadcast was already re-enqueued previously, finish it to avoid repeated
+ // delivery attempts
+ finishReceiverActiveLocked(queue, BroadcastRecord.DELIVERY_FAILURE,
+ "re-enqueued broadcast delivery failed");
+ } else {
+ final BroadcastRecord record = queue.getActive();
+ final int index = queue.getActiveIndex();
+ setDeliveryState(queue, queue.app, record, index, record.receivers.get(index),
+ BroadcastRecord.DELIVERY_PENDING, "reEnqueueActiveBroadcast");
+ queue.reEnqueueActiveBroadcast();
+ }
}
@Override
public boolean onApplicationAttachedLocked(@NonNull ProcessRecord app)
- throws BroadcastDeliveryFailedException {
+ throws BroadcastRetryException {
if (DEBUG_BROADCAST) {
logv("Process " + app + " is attached");
}
@@ -653,8 +665,8 @@
if (scheduleReceiverWarmLocked(queue)) {
demoteFromRunningLocked(queue);
}
- } catch (BroadcastDeliveryFailedException e) {
- reEnqueueActiveBroadcast(queue);
+ } catch (BroadcastRetryException e) {
+ finishOrReEnqueueActiveBroadcast(queue);
demoteFromRunningLocked(queue);
throw e;
}
@@ -983,7 +995,7 @@
@CheckResult
@GuardedBy("mService")
private boolean scheduleReceiverWarmLocked(@NonNull BroadcastProcessQueue queue)
- throws BroadcastDeliveryFailedException {
+ throws BroadcastRetryException {
checkState(queue.isActive(), "isActive");
final int cookie = traceBegin("scheduleReceiverWarmLocked");
@@ -1065,7 +1077,7 @@
*/
@CheckResult
private boolean dispatchReceivers(@NonNull BroadcastProcessQueue queue,
- @NonNull BroadcastRecord r, int index) throws BroadcastDeliveryFailedException {
+ @NonNull BroadcastRecord r, int index) throws BroadcastRetryException {
final ProcessRecord app = queue.app;
final Object receiver = r.receivers.get(index);
@@ -1157,7 +1169,7 @@
// to try redelivering the broadcast to this receiver.
if (receiver instanceof ResolveInfo) {
cancelDeliveryTimeoutLocked(queue);
- throw new BroadcastDeliveryFailedException(e);
+ throw new BroadcastRetryException(e);
}
finishReceiverActiveLocked(queue, BroadcastRecord.DELIVERY_FAILURE,
"remote app");
@@ -1316,8 +1328,8 @@
demoteFromRunningLocked(queue);
return true;
}
- } catch (BroadcastDeliveryFailedException e) {
- reEnqueueActiveBroadcast(queue);
+ } catch (BroadcastRetryException e) {
+ finishOrReEnqueueActiveBroadcast(queue);
demoteFromRunningLocked(queue);
return true;
}
diff --git a/services/core/java/com/android/server/am/BroadcastRetryException.java b/services/core/java/com/android/server/am/BroadcastRetryException.java
new file mode 100644
index 0000000..8bd6664
--- /dev/null
+++ b/services/core/java/com/android/server/am/BroadcastRetryException.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2024 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.am;
+
+/**
+ * Exception to represent that broadcast delivery failed and we should try redelivering it.
+ */
+public class BroadcastRetryException extends BroadcastDeliveryFailedException {
+ public BroadcastRetryException(String name) {
+ super(name);
+ }
+
+ public BroadcastRetryException(Exception cause) {
+ super(cause);
+ }
+}
diff --git a/services/core/java/com/android/server/am/EventLogTags.logtags b/services/core/java/com/android/server/am/EventLogTags.logtags
index 2aed847..0f75ad48 100644
--- a/services/core/java/com/android/server/am/EventLogTags.logtags
+++ b/services/core/java/com/android/server/am/EventLogTags.logtags
@@ -31,7 +31,7 @@
30017 am_low_memory (Num Processes|1|1)
# Kill a process to reclaim memory.
-30023 am_kill (User|1|5),(PID|1|5),(Process Name|3),(OomAdj|1|5),(Reason|3)
+30023 am_kill (User|1|5),(PID|1|5),(Process Name|3),(OomAdj|1|5),(Reason|3),(Rss|2|2)
# Discard an undelivered serialized broadcast (timeout/ANR/crash)
30024 am_broadcast_discard_filter (User|1|5),(Broadcast|1|5),(Action|3),(Receiver Number|1|1),(BroadcastFilter|1|5)
30025 am_broadcast_discard_app (User|1|5),(Broadcast|1|5),(Action|3),(Receiver Number|1|1),(App|3)
diff --git a/services/core/java/com/android/server/am/OomAdjuster.java b/services/core/java/com/android/server/am/OomAdjuster.java
index 862542e..7d82f0c 100644
--- a/services/core/java/com/android/server/am/OomAdjuster.java
+++ b/services/core/java/com/android/server/am/OomAdjuster.java
@@ -1499,8 +1499,12 @@
&& !uidRec.isCurAllowListed()) {
// UID is now in the background (and not on the temp allowlist). Was it
// previously in the foreground (or on the temp allowlist)?
+ // Or, it wasn't in the foreground / allowlist, but its last background
+ // timestamp is also 0, this means it's never been in the
+ // foreground / allowlist since it's born at all.
if (!ActivityManager.isProcStateBackground(uidRec.getSetProcState())
- || uidRec.isSetAllowListed()) {
+ || uidRec.isSetAllowListed()
+ || uidRec.getLastBackgroundTime() == 0) {
uidRec.setLastBackgroundTime(nowElapsed);
if (mService.mDeterministicUidIdle
|| !mService.mHandler.hasMessages(IDLE_UIDS_MSG)) {
@@ -1526,6 +1530,7 @@
uidRec.setIdle(false);
}
uidRec.setLastBackgroundTime(0);
+ uidRec.setLastIdleTime(0);
}
final boolean wasCached = uidRec.getSetProcState()
> ActivityManager.PROCESS_STATE_RECEIVER;
@@ -3700,12 +3705,14 @@
for (int i = N - 1; i >= 0; i--) {
final UidRecord uidRec = mActiveUids.valueAt(i);
final long bgTime = uidRec.getLastBackgroundTime();
- if (bgTime > 0 && !uidRec.isIdle()) {
+ final long idleTime = uidRec.getLastIdleTime();
+ if (bgTime > 0 && (!uidRec.isIdle() || idleTime == 0)) {
if (bgTime <= maxBgTime) {
EventLogTags.writeAmUidIdle(uidRec.getUid());
synchronized (mProcLock) {
uidRec.setIdle(true);
uidRec.setSetIdle(true);
+ uidRec.setLastIdleTime(nowElapsed);
}
mService.doStopUidLocked(uidRec.getUid(), uidRec);
} else {
diff --git a/services/core/java/com/android/server/am/PhantomProcessRecord.java b/services/core/java/com/android/server/am/PhantomProcessRecord.java
index 1a692df..ac96bdc 100644
--- a/services/core/java/com/android/server/am/PhantomProcessRecord.java
+++ b/services/core/java/com/android/server/am/PhantomProcessRecord.java
@@ -105,6 +105,11 @@
}
}
+ public long getRss(int pid) {
+ long[] rss = Process.getRss(pid);
+ return (rss != null && rss.length > 0) ? rss[0] : 0;
+ }
+
@GuardedBy("mLock")
void killLocked(String reason, boolean noisy) {
if (!mKilled) {
@@ -115,7 +120,7 @@
}
if (mPid > 0) {
EventLog.writeEvent(EventLogTags.AM_KILL, UserHandle.getUserId(mUid),
- mPid, mProcessName, mAdj, reason);
+ mPid, mProcessName, mAdj, reason, getRss(mPid));
if (!Process.supportsPidFd()) {
onProcDied(false);
} else {
diff --git a/services/core/java/com/android/server/am/ProcessList.java b/services/core/java/com/android/server/am/ProcessList.java
index 10cd6e5..3adea7a 100644
--- a/services/core/java/com/android/server/am/ProcessList.java
+++ b/services/core/java/com/android/server/am/ProcessList.java
@@ -1950,7 +1950,8 @@
mService.mNativeDebuggingApp = null;
}
- if (app.info.isEmbeddedDexUsed()) {
+ if (app.info.isEmbeddedDexUsed()
+ || (app.processInfo != null && app.processInfo.useEmbeddedDex)) {
runtimeFlags |= Zygote.ONLY_USE_SYSTEM_OAT_FILES;
}
@@ -2498,7 +2499,7 @@
app.info.dataDir, null, app.info.packageName,
/*zygotePolicyFlags=*/ ZYGOTE_POLICY_FLAG_EMPTY, isTopApp,
app.getDisabledCompatChanges(), pkgDataInfoMap, allowlistedAppDataInfoMap,
- false, false, bindOverrideSysprops,
+ false, false, false,
new String[]{PROC_START_SEQ_IDENT + app.getStartSeq()});
} else {
regularZygote = true;
@@ -4985,19 +4986,7 @@
}
void dispatchProcessStarted(ProcessRecord app, int pid) {
- int i = mProcessObservers.beginBroadcast();
- while (i > 0) {
- i--;
- final IProcessObserver observer = mProcessObservers.getBroadcastItem(i);
- if (observer != null) {
- try {
- observer.onProcessStarted(pid, app.uid, app.info.uid,
- app.info.packageName, app.processName);
- } catch (RemoteException e) {
- }
- }
- }
- mProcessObservers.finishBroadcast();
+ // TODO(b/323959187) Add the implementation.
}
void dispatchProcessDied(int pid, int uid) {
diff --git a/services/core/java/com/android/server/am/ProcessRecord.java b/services/core/java/com/android/server/am/ProcessRecord.java
index de6f034..d23d9fb 100644
--- a/services/core/java/com/android/server/am/ProcessRecord.java
+++ b/services/core/java/com/android/server/am/ProcessRecord.java
@@ -1217,6 +1217,11 @@
}
}
+ public long getRss(int pid) {
+ long[] rss = Process.getRss(pid);
+ return (rss != null && rss.length > 0) ? rss[0] : 0;
+ }
+
@GuardedBy("mService")
void killLocked(String reason, @Reason int reasonCode, boolean noisy) {
killLocked(reason, reasonCode, ApplicationExitInfo.SUBREASON_UNKNOWN, noisy, true);
@@ -1260,7 +1265,7 @@
if (mPid > 0) {
mService.mProcessList.noteAppKill(this, reasonCode, subReason, description);
EventLog.writeEvent(EventLogTags.AM_KILL,
- userId, mPid, processName, mState.getSetAdj(), reason);
+ userId, mPid, processName, mState.getSetAdj(), reason, getRss(mPid));
Process.killProcessQuiet(mPid);
killProcessGroupIfNecessaryLocked(asyncKPG);
} else {
diff --git a/services/core/java/com/android/server/am/TEST_MAPPING b/services/core/java/com/android/server/am/TEST_MAPPING
index e90910a..bd3c8e0 100644
--- a/services/core/java/com/android/server/am/TEST_MAPPING
+++ b/services/core/java/com/android/server/am/TEST_MAPPING
@@ -155,6 +155,16 @@
{ "exclude-annotation": "androidx.test.filters.FlakyTest" },
{ "exclude-annotation": "org.junit.Ignore" }
]
+ },
+ {
+ "file_patterns": ["Broadcast.*"],
+ "name": "CtsContentTestCases",
+ "options": [
+ { "include-filter": "android.content.cts.BroadcastReceiverTest" },
+ { "exclude-annotation": "androidx.test.filters.LargeTest" },
+ { "exclude-annotation": "androidx.test.filters.FlakyTest" },
+ { "exclude-annotation": "org.junit.Ignore" }
+ ]
}
]
}
diff --git a/services/core/java/com/android/server/am/UidRecord.java b/services/core/java/com/android/server/am/UidRecord.java
index 4329afc..45fd470 100644
--- a/services/core/java/com/android/server/am/UidRecord.java
+++ b/services/core/java/com/android/server/am/UidRecord.java
@@ -66,6 +66,9 @@
private long mLastBackgroundTime;
@CompositeRWLock({"mService", "mProcLock"})
+ private long mLastIdleTime;
+
+ @CompositeRWLock({"mService", "mProcLock"})
private boolean mEphemeral;
@CompositeRWLock({"mService", "mProcLock"})
@@ -255,6 +258,16 @@
}
@GuardedBy(anyOf = {"mService", "mProcLock"})
+ long getLastIdleTime() {
+ return mLastIdleTime;
+ }
+
+ @GuardedBy({"mService", "mProcLock"})
+ void setLastIdleTime(long lastActiveTime) {
+ mLastIdleTime = lastActiveTime;
+ }
+
+ @GuardedBy(anyOf = {"mService", "mProcLock"})
boolean isEphemeral() {
return mEphemeral;
}
diff --git a/services/core/java/com/android/server/am/UserController.java b/services/core/java/com/android/server/am/UserController.java
index 2b81dbc..96c6be8 100644
--- a/services/core/java/com/android/server/am/UserController.java
+++ b/services/core/java/com/android/server/am/UserController.java
@@ -993,17 +993,20 @@
/**
* Stops a single User. This can also trigger locking user data out depending on device's
* config ({@code mDelayUserDataLocking}) and arguments.
- * User will be unlocked when
- * - {@code mDelayUserDataLocking} is not set.
- * - {@code mDelayUserDataLocking} is set and {@code keyEvictedCallback} is non-null.
+ *
+ * In the default configuration for most device and users, users will be locked when stopping.
+ * User will remain unlocked only if all the following are true
+ * <li> {@link #canDelayDataLockingForUser(int)} (based on mDelayUserDataLocking) is true
+ * <li> the parameter {@code allowDelayedLocking} is true
+ * <li> {@code keyEvictedCallback} is null
* -
*
* @param userId User Id to stop and lock the data.
* @param allowDelayedLocking When set, do not lock user after stopping. Locking can happen
* later when number of unlocked users reaches
* {@code mMaxRunnngUsers}. Note that this is respected only when
- * {@code mDelayUserDataLocking} is set and {@keyEvictedCallback} is
- * null. Otherwise the user will be locked.
+ * delayed locking is enabled for this user and {@keyEvictedCallback}
+ * is null. Otherwise the user nonetheless will be locked.
* @param stopUserCallback Callback to notify that user has stopped.
* @param keyEvictedCallback Callback to notify that user has been unlocked.
*/
diff --git a/services/core/java/com/android/server/am/flags.aconfig b/services/core/java/com/android/server/am/flags.aconfig
index 31d9cc9..16dbe18 100644
--- a/services/core/java/com/android/server/am/flags.aconfig
+++ b/services/core/java/com/android/server/am/flags.aconfig
@@ -42,3 +42,14 @@
description: "Optimize the service bindings by different policies like skipping oom adjuster"
bug: "318717054"
}
+
+flag {
+ namespace: "backstage_power"
+ name: "avoid_repeated_bcast_re_enqueues"
+ description: "Avoid re-enqueueing a broadcast repeatedly"
+ bug: "319225224"
+ is_fixed_read_only: true
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
diff --git a/services/core/java/com/android/server/biometrics/BiometricHandlerProvider.java b/services/core/java/com/android/server/biometrics/BiometricHandlerProvider.java
new file mode 100644
index 0000000..a923daa
--- /dev/null
+++ b/services/core/java/com/android/server/biometrics/BiometricHandlerProvider.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2024 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.biometrics;
+
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Looper;
+
+/**
+ * This class provides the handler to process biometric operations.
+ */
+public class BiometricHandlerProvider {
+ private static final BiometricHandlerProvider sBiometricHandlerProvider =
+ new BiometricHandlerProvider();
+
+ private final Handler mBiometricsCallbackHandler;
+ private final Handler mFingerprintHandler;
+ private final Handler mFaceHandler;
+
+ /**
+ * @return an instance of {@link BiometricHandlerProvider} which contains the three
+ * threads needed for running biometric operations
+ */
+ public static BiometricHandlerProvider getInstance() {
+ return sBiometricHandlerProvider;
+ }
+
+ private BiometricHandlerProvider() {
+ mBiometricsCallbackHandler = getNewHandler("BiometricsCallbackHandler");
+ mFingerprintHandler = getNewHandler("FingerprintHandler");
+ mFaceHandler = getNewHandler("FaceHandler");
+ }
+
+ /**
+ * @return the handler to process all biometric callback operations
+ */
+ public synchronized Handler getBiometricCallbackHandler() {
+ return mBiometricsCallbackHandler;
+ }
+
+ /**
+ * @return the handler to process all face related biometric operations
+ */
+ public synchronized Handler getFaceHandler() {
+ return mFaceHandler;
+ }
+
+ /**
+ * @return the handler to process all fingerprint related biometric operations
+ */
+ public synchronized Handler getFingerprintHandler() {
+ return mFingerprintHandler;
+ }
+
+ private Handler getNewHandler(String tag) {
+ if (Flags.deHidl()) {
+ HandlerThread handlerThread = new HandlerThread(tag);
+ handlerThread.start();
+ return new Handler(handlerThread.getLooper());
+ }
+ return new Handler(Looper.getMainLooper());
+ }
+}
diff --git a/services/core/java/com/android/server/biometrics/BiometricService.java b/services/core/java/com/android/server/biometrics/BiometricService.java
index 91a68ea..fc948da 100644
--- a/services/core/java/com/android/server/biometrics/BiometricService.java
+++ b/services/core/java/com/android/server/biometrics/BiometricService.java
@@ -62,7 +62,6 @@
import android.os.DeadObjectException;
import android.os.Handler;
import android.os.IBinder;
-import android.os.Looper;
import android.os.RemoteException;
import android.os.ServiceManager;
import android.os.ServiceSpecificException;
@@ -140,7 +139,7 @@
// The current authentication session, null if idle/done.
@VisibleForTesting
AuthSession mAuthSession;
- private final Handler mHandler = new Handler(Looper.getMainLooper());
+ private final Handler mHandler;
private final BiometricCameraManager mBiometricCameraManager;
@@ -1113,14 +1112,16 @@
* @param context The system server context.
*/
public BiometricService(Context context) {
- this(context, new Injector());
+ this(context, new Injector(), BiometricHandlerProvider.getInstance());
}
@VisibleForTesting
- BiometricService(Context context, Injector injector) {
+ BiometricService(Context context, Injector injector,
+ BiometricHandlerProvider biometricHandlerProvider) {
super(context);
mInjector = injector;
+ mHandler = biometricHandlerProvider.getBiometricCallbackHandler();
mDevicePolicyManager = mInjector.getDevicePolicyManager(context);
mImpl = new BiometricServiceWrapper();
mEnabledOnKeyguardCallbacks = new ArrayList<>();
diff --git a/services/core/java/com/android/server/biometrics/sensors/AcquisitionClient.java b/services/core/java/com/android/server/biometrics/sensors/AcquisitionClient.java
index fbd32a6..d061e2d 100644
--- a/services/core/java/com/android/server/biometrics/sensors/AcquisitionClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/AcquisitionClient.java
@@ -31,6 +31,7 @@
import com.android.server.biometrics.log.BiometricContext;
import com.android.server.biometrics.log.BiometricLogger;
+import com.android.server.biometrics.sensors.fingerprint.aidl.AidlSession;
import java.util.function.Supplier;
@@ -202,6 +203,16 @@
}
}
+ // TODO(b/317414324): Deprecate setIgnoreDisplayTouches
+ protected final void resetIgnoreDisplayTouches() {
+ final AidlSession session = (AidlSession) getFreshDaemon();
+ try {
+ session.getSession().setIgnoreDisplayTouches(false);
+ } catch (RemoteException e) {
+ Slog.e(TAG, "Remote exception when resetting setIgnoreDisplayTouches");
+ }
+ }
+
@Override
public boolean isInterruptable() {
return true;
diff --git a/services/core/java/com/android/server/biometrics/sensors/AuthenticationStateListeners.java b/services/core/java/com/android/server/biometrics/sensors/AuthenticationStateListeners.java
index 1ae4d64..1dc882e5 100644
--- a/services/core/java/com/android/server/biometrics/sensors/AuthenticationStateListeners.java
+++ b/services/core/java/com/android/server/biometrics/sensors/AuthenticationStateListeners.java
@@ -18,6 +18,8 @@
import android.annotation.NonNull;
import android.hardware.biometrics.AuthenticationStateListener;
+import android.hardware.biometrics.BiometricFingerprintConstants;
+import android.hardware.biometrics.BiometricSourceType;
import android.os.IBinder;
import android.os.RemoteException;
import android.util.Slog;
@@ -115,7 +117,7 @@
* @param userId The user Id for the requested authentication
*/
public void onAuthenticationFailed(int requestReason, int userId) {
- for (AuthenticationStateListener listener: mAuthenticationStateListeners) {
+ for (AuthenticationStateListener listener : mAuthenticationStateListeners) {
try {
listener.onAuthenticationFailed(requestReason, userId);
} catch (RemoteException e) {
@@ -125,6 +127,27 @@
}
}
+ /**
+ * Defines behavior in response to biometric being acquired.
+ * @param biometricSourceType identifies [BiometricSourceType] biometric was acquired for
+ * @param requestReason reason from [BiometricRequestConstants.RequestReason] for authentication
+ * @param acquiredInfo [BiometricFingerprintConstants.FingerprintAcquired] int corresponding to
+ * a known acquired message.
+ */
+ public void onAuthenticationAcquired(
+ BiometricSourceType biometricSourceType, int requestReason,
+ @BiometricFingerprintConstants.FingerprintAcquired int acquiredInfo
+ ) {
+ for (AuthenticationStateListener listener: mAuthenticationStateListeners) {
+ try {
+ listener.onAuthenticationAcquired(biometricSourceType, requestReason, acquiredInfo);
+ } catch (RemoteException e) {
+ Slog.e(TAG, "Remote exception in notifying listener that authentication "
+ + "stopped", e);
+ }
+ }
+ }
+
@Override
public void binderDied() {
// Do nothing, handled below
diff --git a/services/core/java/com/android/server/biometrics/sensors/LockoutResetDispatcher.java b/services/core/java/com/android/server/biometrics/sensors/LockoutResetDispatcher.java
index 92218b1..199db8c 100644
--- a/services/core/java/com/android/server/biometrics/sensors/LockoutResetDispatcher.java
+++ b/services/core/java/com/android/server/biometrics/sensors/LockoutResetDispatcher.java
@@ -27,9 +27,8 @@
import com.android.internal.annotations.VisibleForTesting;
-import java.util.ArrayList;
import java.util.Iterator;
-import java.util.List;
+import java.util.concurrent.ConcurrentLinkedQueue;
/**
* Allows clients (such as keyguard) to register for notifications on when biometric lockout
@@ -42,7 +41,7 @@
private final Context mContext;
@VisibleForTesting
- final List<ClientCallback> mClientCallbacks = new ArrayList<>();
+ final ConcurrentLinkedQueue<ClientCallback> mClientCallbacks = new ConcurrentLinkedQueue<>();
private static class ClientCallback {
private static final long WAKELOCK_TIMEOUT_MS = 2000;
diff --git a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceProvider.java b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceProvider.java
index d01c268..f469f62 100644
--- a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceProvider.java
+++ b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceProvider.java
@@ -39,7 +39,6 @@
import android.hardware.face.IFaceServiceReceiver;
import android.os.Binder;
import android.os.Handler;
-import android.os.HandlerThread;
import android.os.IBinder;
import android.os.Looper;
import android.os.RemoteException;
@@ -53,6 +52,7 @@
import com.android.internal.annotations.VisibleForTesting;
import com.android.server.biometrics.AuthenticationStatsBroadcastReceiver;
import com.android.server.biometrics.AuthenticationStatsCollector;
+import com.android.server.biometrics.BiometricHandlerProvider;
import com.android.server.biometrics.Flags;
import com.android.server.biometrics.Utils;
import com.android.server.biometrics.log.BiometricContext;
@@ -124,6 +124,8 @@
private final BiometricContext mBiometricContext;
@NonNull
private final AuthSessionCoordinator mAuthSessionCoordinator;
+ @NonNull
+ private final BiometricHandlerProvider mBiometricHandlerProvider;
@Nullable
private AuthenticationStatsCollector mAuthenticationStatsCollector;
@Nullable
@@ -166,8 +168,9 @@
@NonNull BiometricContext biometricContext,
boolean resetLockoutRequiresChallenge) {
this(context, biometricStateCallback, authenticationStateListeners, props, halInstanceName,
- lockoutResetDispatcher, biometricContext, null /* daemon */, getHandler(),
- resetLockoutRequiresChallenge, false /* testHalEnabled */);
+ lockoutResetDispatcher, biometricContext, null /* daemon */,
+ BiometricHandlerProvider.getInstance(), resetLockoutRequiresChallenge,
+ false /* testHalEnabled */);
}
@VisibleForTesting FaceProvider(@NonNull Context context,
@@ -178,7 +181,7 @@
@NonNull LockoutResetDispatcher lockoutResetDispatcher,
@NonNull BiometricContext biometricContext,
@Nullable IFace daemon,
- @NonNull Handler handler,
+ @NonNull BiometricHandlerProvider biometricHandlerProvider,
boolean resetLockoutRequiresChallenge,
boolean testHalEnabled) {
mContext = context;
@@ -187,7 +190,7 @@
mHalInstanceName = halInstanceName;
mFaceSensors = new SensorList<>(ActivityManager.getService());
if (Flags.deHidl()) {
- mHandler = handler;
+ mHandler = biometricHandlerProvider.getFaceHandler();
} else {
mHandler = new Handler(Looper.getMainLooper());
}
@@ -199,18 +202,12 @@
mAuthSessionCoordinator = mBiometricContext.getAuthSessionCoordinator();
mDaemon = daemon;
mTestHalEnabled = testHalEnabled;
+ mBiometricHandlerProvider = biometricHandlerProvider;
initAuthenticationBroadcastReceiver();
initSensors(resetLockoutRequiresChallenge, props);
}
- @NonNull
- private static Handler getHandler() {
- HandlerThread handlerThread = new HandlerThread(TAG);
- handlerThread.start();
- return new Handler(handlerThread.getLooper());
- }
-
private void initAuthenticationBroadcastReceiver() {
new AuthenticationStatsBroadcastReceiver(
mContext,
@@ -622,15 +619,29 @@
@Override
public void onClientStarted(
BaseClientMonitor clientMonitor) {
- mAuthSessionCoordinator.authStartedFor(userId, sensorId, requestId);
+ if (Flags.deHidl()) {
+ mBiometricHandlerProvider.getBiometricCallbackHandler().post(() ->
+ mAuthSessionCoordinator.authStartedFor(userId, sensorId,
+ requestId));
+ } else {
+ mAuthSessionCoordinator.authStartedFor(userId, sensorId, requestId);
+ }
}
@Override
public void onClientFinished(
BaseClientMonitor clientMonitor,
boolean success) {
- mAuthSessionCoordinator.authEndedFor(userId, Utils.getCurrentStrength(sensorId),
- sensorId, requestId, client.wasAuthSuccessful());
+ if (Flags.deHidl()) {
+ mBiometricHandlerProvider.getBiometricCallbackHandler().post(() ->
+ mAuthSessionCoordinator.authEndedFor(userId,
+ Utils.getCurrentStrength(sensorId), sensorId, requestId,
+ client.wasAuthSuccessful()));
+ } else {
+ mAuthSessionCoordinator.authEndedFor(userId,
+ Utils.getCurrentStrength(sensorId),
+ sensorId, requestId, client.wasAuthSuccessful());
+ }
}
});
});
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClient.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClient.java
index e0fd44b..93d1b6e 100644
--- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClient.java
@@ -29,6 +29,7 @@
import android.hardware.biometrics.BiometricFingerprintConstants;
import android.hardware.biometrics.BiometricFingerprintConstants.FingerprintAcquired;
import android.hardware.biometrics.BiometricManager.Authenticators;
+import android.hardware.biometrics.BiometricSourceType;
import android.hardware.biometrics.common.ICancellationSignal;
import android.hardware.biometrics.common.OperationState;
import android.hardware.biometrics.fingerprint.PointerContext;
@@ -102,6 +103,7 @@
private Runnable mAuthSuccessRunnable;
private final Clock mClock;
+
public FingerprintAuthenticationClient(
@NonNull Context context,
@NonNull Supplier<AidlSession> lazyDaemon,
@@ -230,6 +232,7 @@
handleLockout(authenticated);
if (authenticated) {
mState = STATE_STOPPED;
+ resetIgnoreDisplayTouches();
mSensorOverlays.hide(getSensorId());
if (sidefpsControllerRefactor()) {
mAuthenticationStateListeners.onAuthenticationStopped();
@@ -266,6 +269,7 @@
// Send the error, but do not invoke the FinishCallback yet. Since lockout is not
// controlled by the HAL, the framework must stop the sensor before finishing the
// client.
+ resetIgnoreDisplayTouches();
mSensorOverlays.hide(getSensorId());
if (sidefpsControllerRefactor()) {
mAuthenticationStateListeners.onAuthenticationStopped();
@@ -280,6 +284,8 @@
public void onAcquired(@FingerprintAcquired int acquiredInfo, int vendorCode) {
// For UDFPS, notify SysUI with acquiredInfo, so that the illumination can be turned off
// for most ACQUIRED messages. See BiometricFingerprintConstants#FingerprintAcquired
+ mAuthenticationStateListeners.onAuthenticationAcquired(
+ BiometricSourceType.FINGERPRINT, getRequestReason(), acquiredInfo);
mSensorOverlays.ifUdfps(controller -> controller.onAcquired(getSensorId(), acquiredInfo));
super.onAcquired(acquiredInfo, vendorCode);
PerformanceTracker pt = PerformanceTracker.getInstanceForSensorId(getSensorId());
@@ -294,6 +300,7 @@
BiometricNotificationUtils.showBadCalibrationNotification(getContext());
}
+ resetIgnoreDisplayTouches();
mSensorOverlays.hide(getSensorId());
if (sidefpsControllerRefactor()) {
mAuthenticationStateListeners.onAuthenticationStopped();
@@ -302,6 +309,7 @@
@Override
protected void startHalOperation() {
+ resetIgnoreDisplayTouches();
mSensorOverlays.show(getSensorId(), getRequestReason(), this);
if (sidefpsControllerRefactor()) {
mAuthenticationStateListeners.onAuthenticationStarted(getRequestReason());
@@ -415,6 +423,7 @@
@Override
protected void stopHalOperation() {
+ resetIgnoreDisplayTouches();
mSensorOverlays.hide(getSensorId());
if (sidefpsControllerRefactor()) {
mAuthenticationStateListeners.onAuthenticationStopped();
@@ -514,6 +523,7 @@
Slog.e(TAG, "Remote exception", e);
}
+ resetIgnoreDisplayTouches();
mSensorOverlays.hide(getSensorId());
if (sidefpsControllerRefactor()) {
mAuthenticationStateListeners.onAuthenticationStopped();
@@ -544,6 +554,7 @@
Slog.e(TAG, "Remote exception", e);
}
+ resetIgnoreDisplayTouches();
mSensorOverlays.hide(getSensorId());
if (sidefpsControllerRefactor()) {
mAuthenticationStateListeners.onAuthenticationStopped();
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintDetectClient.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintDetectClient.java
index cb220b9e..8d2b46f 100644
--- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintDetectClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintDetectClient.java
@@ -87,6 +87,7 @@
@Override
protected void stopHalOperation() {
+ resetIgnoreDisplayTouches();
mSensorOverlays.hide(getSensorId());
unsubscribeBiometricContext();
@@ -102,6 +103,7 @@
@Override
protected void startHalOperation() {
+ resetIgnoreDisplayTouches();
mSensorOverlays.show(getSensorId(), BiometricRequestConstants.REASON_AUTH_KEYGUARD,
this);
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintEnrollClient.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintEnrollClient.java
index 225bd59..79975e5 100644
--- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintEnrollClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintEnrollClient.java
@@ -144,6 +144,7 @@
controller -> controller.onEnrollmentProgress(getSensorId(), remaining));
if (remaining == 0) {
+ resetIgnoreDisplayTouches();
mSensorOverlays.hide(getSensorId());
if (sidefpsControllerRefactor()) {
mAuthenticationStateListeners.onAuthenticationStopped();
@@ -178,6 +179,7 @@
@Override
public void onError(int errorCode, int vendorCode) {
super.onError(errorCode, vendorCode);
+ resetIgnoreDisplayTouches();
mSensorOverlays.hide(getSensorId());
if (sidefpsControllerRefactor()) {
mAuthenticationStateListeners.onAuthenticationStopped();
@@ -192,6 +194,7 @@
@Override
protected void startHalOperation() {
+ resetIgnoreDisplayTouches();
mSensorOverlays.show(getSensorId(), getRequestReasonFromEnrollReason(mEnrollReason),
this);
if (sidefpsControllerRefactor()) {
@@ -273,6 +276,7 @@
@Override
protected void stopHalOperation() {
+ resetIgnoreDisplayTouches();
mSensorOverlays.hide(getSensorId());
if (sidefpsControllerRefactor()) {
mAuthenticationStateListeners.onAuthenticationStopped();
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintProvider.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintProvider.java
index c0388d1..fd938ed 100644
--- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintProvider.java
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintProvider.java
@@ -46,7 +46,6 @@
import android.hardware.fingerprint.IUdfpsOverlayController;
import android.os.Binder;
import android.os.Handler;
-import android.os.HandlerThread;
import android.os.IBinder;
import android.os.Looper;
import android.os.RemoteException;
@@ -60,6 +59,7 @@
import com.android.internal.annotations.VisibleForTesting;
import com.android.server.biometrics.AuthenticationStatsBroadcastReceiver;
import com.android.server.biometrics.AuthenticationStatsCollector;
+import com.android.server.biometrics.BiometricHandlerProvider;
import com.android.server.biometrics.Flags;
import com.android.server.biometrics.Utils;
import com.android.server.biometrics.log.BiometricContext;
@@ -129,11 +129,12 @@
// for requests that do not use biometric prompt
@NonNull private final AtomicLong mRequestCounter = new AtomicLong(0);
@NonNull private final BiometricContext mBiometricContext;
+ @NonNull private final BiometricHandlerProvider mBiometricHandlerProvider;
@Nullable private IFingerprint mDaemon;
@Nullable private IUdfpsOverlayController mUdfpsOverlayController;
// TODO(b/288175061): remove with Flags.FLAG_SIDEFPS_CONTROLLER_REFACTOR
@Nullable private ISidefpsController mSidefpsController;
- private AuthSessionCoordinator mAuthSessionCoordinator;
+ private final AuthSessionCoordinator mAuthSessionCoordinator;
@Nullable private AuthenticationStatsCollector mAuthenticationStatsCollector;
private final class BiometricTaskStackListener extends TaskStackListener {
@@ -175,8 +176,8 @@
boolean resetLockoutRequiresHardwareAuthToken) {
this(context, biometricStateCallback, authenticationStateListeners, props, halInstanceName,
lockoutResetDispatcher, gestureAvailabilityDispatcher, biometricContext,
- null /* daemon */, getHandler(), resetLockoutRequiresHardwareAuthToken,
- false /* testHalEnabled */);
+ null /* daemon */, BiometricHandlerProvider.getInstance(),
+ resetLockoutRequiresHardwareAuthToken, false /* testHalEnabled */);
}
@VisibleForTesting FingerprintProvider(@NonNull Context context,
@@ -187,7 +188,7 @@
@NonNull GestureAvailabilityDispatcher gestureAvailabilityDispatcher,
@NonNull BiometricContext biometricContext,
@Nullable IFingerprint daemon,
- @NonNull Handler handler,
+ @NonNull BiometricHandlerProvider biometricHandlerProvider,
boolean resetLockoutRequiresHardwareAuthToken,
boolean testHalEnabled) {
mContext = context;
@@ -196,7 +197,7 @@
mHalInstanceName = halInstanceName;
mFingerprintSensors = new SensorList<>(ActivityManager.getService());
if (Flags.deHidl()) {
- mHandler = handler;
+ mHandler = biometricHandlerProvider.getFingerprintHandler();
} else {
mHandler = new Handler(Looper.getMainLooper());
}
@@ -207,18 +208,12 @@
mAuthSessionCoordinator = mBiometricContext.getAuthSessionCoordinator();
mDaemon = daemon;
mTestHalEnabled = testHalEnabled;
+ mBiometricHandlerProvider = biometricHandlerProvider;
initAuthenticationBroadcastReceiver();
initSensors(resetLockoutRequiresHardwareAuthToken, props, gestureAvailabilityDispatcher);
}
- @NonNull
- private static Handler getHandler() {
- HandlerThread handlerThread = new HandlerThread(TAG);
- handlerThread.start();
- return new Handler(handlerThread.getLooper());
- }
-
private void initAuthenticationBroadcastReceiver() {
new AuthenticationStatsBroadcastReceiver(
mContext,
@@ -620,7 +615,13 @@
@Override
public void onClientStarted(@NonNull BaseClientMonitor clientMonitor) {
mBiometricStateCallback.onClientStarted(clientMonitor);
- mAuthSessionCoordinator.authStartedFor(userId, sensorId, requestId);
+ if (Flags.deHidl()) {
+ mBiometricHandlerProvider.getBiometricCallbackHandler().post(() ->
+ mAuthSessionCoordinator.authStartedFor(userId, sensorId,
+ requestId));
+ } else {
+ mAuthSessionCoordinator.authStartedFor(userId, sensorId, requestId);
+ }
}
@Override
@@ -632,8 +633,15 @@
public void onClientFinished(@NonNull BaseClientMonitor clientMonitor,
boolean success) {
mBiometricStateCallback.onClientFinished(clientMonitor, success);
- mAuthSessionCoordinator.authEndedFor(userId, Utils.getCurrentStrength(sensorId),
- sensorId, requestId, success);
+ if (Flags.deHidl()) {
+ mBiometricHandlerProvider.getBiometricCallbackHandler().post(() ->
+ mAuthSessionCoordinator.authEndedFor(userId,
+ Utils.getCurrentStrength(sensorId), sensorId, requestId,
+ success));
+ } else {
+ mAuthSessionCoordinator.authEndedFor(userId,
+ Utils.getCurrentStrength(sensorId), sensorId, requestId, success);
+ }
}
});
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintAuthenticationClient.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintAuthenticationClient.java
index 60c532c..b6311af 100644
--- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintAuthenticationClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintAuthenticationClient.java
@@ -28,6 +28,7 @@
import android.hardware.biometrics.BiometricConstants;
import android.hardware.biometrics.BiometricFingerprintConstants;
import android.hardware.biometrics.BiometricManager.Authenticators;
+import android.hardware.biometrics.BiometricSourceType;
import android.hardware.biometrics.fingerprint.PointerContext;
import android.hardware.biometrics.fingerprint.V2_1.IBiometricsFingerprint;
import android.hardware.fingerprint.FingerprintAuthenticateOptions;
@@ -201,6 +202,8 @@
@Override
public void onAcquired(int acquiredInfo, int vendorCode) {
+ mAuthenticationStateListeners.onAuthenticationAcquired(
+ BiometricSourceType.FINGERPRINT, getRequestReason(), acquiredInfo);
super.onAcquired(acquiredInfo, vendorCode);
@LockoutTracker.LockoutMode final int lockoutMode =
diff --git a/services/core/java/com/android/server/broadcastradio/OWNERS b/services/core/java/com/android/server/broadcastradio/OWNERS
index d2bdd64..51a85e4 100644
--- a/services/core/java/com/android/server/broadcastradio/OWNERS
+++ b/services/core/java/com/android/server/broadcastradio/OWNERS
@@ -1,3 +1,3 @@
xuweilin@google.com
oscarazu@google.com
-keunyoung@google.com
+ericjeong@google.com
diff --git a/services/core/java/com/android/server/camera/CameraServiceProxy.java b/services/core/java/com/android/server/camera/CameraServiceProxy.java
index 458fd82..05e681e 100644
--- a/services/core/java/com/android/server/camera/CameraServiceProxy.java
+++ b/services/core/java/com/android/server/camera/CameraServiceProxy.java
@@ -1020,6 +1020,10 @@
}
}
+ private boolean isAutomotive() {
+ return mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE);
+ }
+
private Set<Integer> getEnabledUserHandles(int currentUserHandle) {
int[] userProfiles = mUserManager.getEnabledProfileIds(currentUserHandle);
Set<Integer> handles = new ArraySet<>(userProfiles.length);
@@ -1030,8 +1034,8 @@
if (Flags.cameraHsumPermission()) {
// If the device is running in headless system user mode then allow
- // User 0 to access camera.
- if (UserManager.isHeadlessSystemUserMode()) {
+ // User 0 to access camera only for automotive form factor.
+ if (UserManager.isHeadlessSystemUserMode() && isAutomotive()) {
handles.add(UserHandle.USER_SYSTEM);
}
}
diff --git a/services/core/java/com/android/server/devicestate/DeviceStateManagerService.java b/services/core/java/com/android/server/devicestate/DeviceStateManagerService.java
index 77cb08b..cb15abc 100644
--- a/services/core/java/com/android/server/devicestate/DeviceStateManagerService.java
+++ b/services/core/java/com/android/server/devicestate/DeviceStateManagerService.java
@@ -914,6 +914,9 @@
}
mOverrideRequestController.dumpInternal(pw);
+ pw.println();
+
+ mDeviceStatePolicy.dump(pw, /* args= */ null);
}
}
diff --git a/services/core/java/com/android/server/devicestate/DeviceStatePolicy.java b/services/core/java/com/android/server/devicestate/DeviceStatePolicy.java
index 5c4e2f3..65e5085 100644
--- a/services/core/java/com/android/server/devicestate/DeviceStatePolicy.java
+++ b/services/core/java/com/android/server/devicestate/DeviceStatePolicy.java
@@ -20,6 +20,7 @@
import android.content.Context;
import android.content.res.Resources;
import android.text.TextUtils;
+import android.util.Dumpable;
import com.android.server.policy.DeviceStatePolicyImpl;
@@ -29,7 +30,7 @@
*
* @see DeviceStateManagerService
*/
-public abstract class DeviceStatePolicy {
+public abstract class DeviceStatePolicy implements Dumpable {
protected final Context mContext;
protected DeviceStatePolicy(@NonNull Context context) {
diff --git a/services/core/java/com/android/server/devicestate/DeviceStateProvider.java b/services/core/java/com/android/server/devicestate/DeviceStateProvider.java
index 50ab3f8..d5945f4 100644
--- a/services/core/java/com/android/server/devicestate/DeviceStateProvider.java
+++ b/services/core/java/com/android/server/devicestate/DeviceStateProvider.java
@@ -21,6 +21,7 @@
import android.annotation.IntDef;
import android.annotation.IntRange;
+import android.util.Dumpable;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -31,7 +32,7 @@
*
* @see DeviceStatePolicy
*/
-public interface DeviceStateProvider {
+public interface DeviceStateProvider extends Dumpable {
int SUPPORTED_DEVICE_STATES_CHANGED_DEFAULT = 0;
/**
diff --git a/services/core/java/com/android/server/display/DisplayManagerService.java b/services/core/java/com/android/server/display/DisplayManagerService.java
index e930627..7ebc311 100644
--- a/services/core/java/com/android/server/display/DisplayManagerService.java
+++ b/services/core/java/com/android/server/display/DisplayManagerService.java
@@ -695,14 +695,14 @@
logicalDisplay.getPrimaryDisplayDeviceLocked().getUniqueId(),
userSerial);
dpc.setBrightnessConfiguration(config, /* shouldResetShortTermModel= */ true);
- // change the brightness value according to the selected user.
- final DisplayDevice device = logicalDisplay.getPrimaryDisplayDeviceLocked();
- if (device != null) {
- dpc.setBrightness(
- mPersistentDataStore.getBrightness(device, userSerial), userSerial);
- }
}
- dpc.onSwitchUser(newUserId);
+ final DisplayDevice device = logicalDisplay.getPrimaryDisplayDeviceLocked();
+ float newBrightness = device == null ? PowerManager.BRIGHTNESS_INVALID_FLOAT
+ : mPersistentDataStore.getBrightness(device, userSerial);
+ if (Float.isNaN(newBrightness)) {
+ newBrightness = logicalDisplay.getDisplayInfoLocked().brightnessDefault;
+ }
+ dpc.onSwitchUser(newUserId, userSerial, newBrightness);
});
handleSettingsChange();
}
diff --git a/services/core/java/com/android/server/display/DisplayPowerController.java b/services/core/java/com/android/server/display/DisplayPowerController.java
index 087cacf..1ca3923 100644
--- a/services/core/java/com/android/server/display/DisplayPowerController.java
+++ b/services/core/java/com/android/server/display/DisplayPowerController.java
@@ -685,17 +685,27 @@
}
@Override
- public void onSwitchUser(@UserIdInt int newUserId) {
- Message msg = mHandler.obtainMessage(MSG_SWITCH_USER, newUserId);
- mHandler.sendMessage(msg);
+ public void onSwitchUser(@UserIdInt int newUserId, int userSerial, float newBrightness) {
+ Message msg = mHandler.obtainMessage(MSG_SWITCH_USER, newUserId, userSerial, newBrightness);
+ mHandler.sendMessageAtTime(msg, mClock.uptimeMillis());
}
- private void handleOnSwitchUser(@UserIdInt int newUserId) {
- handleSettingsChange(true /* userSwitch */);
+ private void handleOnSwitchUser(@UserIdInt int newUserId, int userSerial, float newBrightness) {
+ Slog.i(mTag, "Switching user newUserId=" + newUserId + " userSerial=" + userSerial
+ + " newBrightness=" + newBrightness);
handleBrightnessModeChange();
if (mBrightnessTracker != null) {
mBrightnessTracker.onSwitchUser(newUserId);
}
+ setBrightness(newBrightness, userSerial);
+
+ // Don't treat user switches as user initiated change.
+ mDisplayBrightnessController.setAndNotifyCurrentScreenBrightness(newBrightness);
+
+ if (mAutomaticBrightnessController != null) {
+ mAutomaticBrightnessController.resetShortTermModel();
+ }
+ sendUpdatePowerState();
}
@Nullable
@@ -2394,20 +2404,11 @@
MetricsLogger.action(log);
}
- private void handleSettingsChange(boolean userSwitch) {
+ private void handleSettingsChange() {
mDisplayBrightnessController
.setPendingScreenBrightness(mDisplayBrightnessController
.getScreenBrightnessSetting());
- mAutomaticBrightnessStrategy.updatePendingAutoBrightnessAdjustments(userSwitch);
- if (userSwitch) {
- // Don't treat user switches as user initiated change.
- mDisplayBrightnessController
- .setAndNotifyCurrentScreenBrightness(mDisplayBrightnessController
- .getPendingScreenBrightness());
- if (mAutomaticBrightnessController != null) {
- mAutomaticBrightnessController.resetShortTermModel();
- }
- }
+ mAutomaticBrightnessStrategy.updatePendingAutoBrightnessAdjustments();
sendUpdatePowerState();
}
@@ -2416,11 +2417,8 @@
mContext.getContentResolver(),
Settings.System.SCREEN_BRIGHTNESS_MODE,
Settings.System.SCREEN_BRIGHTNESS_MODE_MANUAL, UserHandle.USER_CURRENT);
- mHandler.postAtTime(() -> {
- mAutomaticBrightnessStrategy.setUseAutoBrightness(screenBrightnessModeSetting
- == Settings.System.SCREEN_BRIGHTNESS_MODE_AUTOMATIC);
- updatePowerState();
- }, mClock.uptimeMillis());
+ mAutomaticBrightnessStrategy.setUseAutoBrightness(screenBrightnessModeSetting
+ == Settings.System.SCREEN_BRIGHTNESS_MODE_AUTOMATIC);
}
@@ -2430,9 +2428,13 @@
}
@Override
- public void setBrightness(float brightnessValue, int userSerial) {
- mDisplayBrightnessController.setBrightness(clampScreenBrightness(brightnessValue),
- userSerial);
+ public void setBrightness(float brightness) {
+ mDisplayBrightnessController.setBrightness(clampScreenBrightness(brightness));
+ }
+
+ @Override
+ public void setBrightness(float brightness, int userSerial) {
+ mDisplayBrightnessController.setBrightness(clampScreenBrightness(brightness), userSerial);
}
@Override
@@ -2966,7 +2968,7 @@
if (mStopped) {
return;
}
- handleSettingsChange(false /*userSwitch*/);
+ handleSettingsChange();
break;
case MSG_UPDATE_RBC:
@@ -2985,7 +2987,9 @@
break;
case MSG_SWITCH_USER:
- handleOnSwitchUser(msg.arg1);
+ float newBrightness = msg.obj instanceof Float ? (float) msg.obj
+ : PowerManager.BRIGHTNESS_INVALID_FLOAT;
+ handleOnSwitchUser(msg.arg1, msg.arg2, newBrightness);
break;
case MSG_BOOT_COMPLETED:
@@ -3023,7 +3027,10 @@
@Override
public void onChange(boolean selfChange, Uri uri) {
if (uri.equals(Settings.System.getUriFor(Settings.System.SCREEN_BRIGHTNESS_MODE))) {
- handleBrightnessModeChange();
+ mHandler.postAtTime(() -> {
+ handleBrightnessModeChange();
+ updatePowerState();
+ }, mClock.uptimeMillis());
} else if (uri.equals(Settings.System.getUriFor(
Settings.System.SCREEN_BRIGHTNESS_FOR_ALS))) {
int preset = Settings.System.getIntForUser(mContext.getContentResolver(),
@@ -3035,7 +3042,7 @@
setUpAutoBrightness(mContext, mHandler);
sendUpdatePowerState();
} else {
- handleSettingsChange(false /* userSwitch */);
+ handleSettingsChange();
}
}
}
diff --git a/services/core/java/com/android/server/display/DisplayPowerControllerInterface.java b/services/core/java/com/android/server/display/DisplayPowerControllerInterface.java
index 13acb3f..ecf1635 100644
--- a/services/core/java/com/android/server/display/DisplayPowerControllerInterface.java
+++ b/services/core/java/com/android/server/display/DisplayPowerControllerInterface.java
@@ -30,7 +30,6 @@
* An interface to manage the display's power state and brightness
*/
public interface DisplayPowerControllerInterface {
- int DEFAULT_USER_SERIAL = -1;
/**
* Notified when the display is changed.
*
@@ -100,15 +99,12 @@
* Set the screen brightness of the associated display
* @param brightness The value to which the brightness is to be set
*/
- default void setBrightness(float brightness) {
- setBrightness(brightness, DEFAULT_USER_SERIAL);
- }
+ void setBrightness(float brightness);
/**
* Set the screen brightness of the associated display
* @param brightness The value to which the brightness is to be set
- * @param userSerial The user for which the brightness value is to be set. Use userSerial = -1,
- * if brightness needs to be updated for the current user.
+ * @param userSerial The user for which the brightness value is to be set.
*/
void setBrightness(float brightness, int userSerial);
@@ -188,8 +184,10 @@
/**
* Handles the changes to be done to update the brightness when the user is changed
* @param newUserId The new userId
+ * @param userSerial The serial number of the new user
+ * @param newBrightness The brightness for the new user
*/
- void onSwitchUser(int newUserId);
+ void onSwitchUser(int newUserId, int userSerial, float newBrightness);
/**
* Get the ID of the display associated with this DPC.
diff --git a/services/core/java/com/android/server/display/brightness/DisplayBrightnessController.java b/services/core/java/com/android/server/display/brightness/DisplayBrightnessController.java
index 3bb7986..f6d02db 100644
--- a/services/core/java/com/android/server/display/brightness/DisplayBrightnessController.java
+++ b/services/core/java/com/android/server/display/brightness/DisplayBrightnessController.java
@@ -41,7 +41,6 @@
* display. Applies the chosen brightness.
*/
public final class DisplayBrightnessController {
- private static final int DEFAULT_USER_SERIAL = -1;
// The ID of the display tied to this DisplayBrightnessController
private final int mDisplayId;
@@ -302,16 +301,8 @@
* Notifies the brightnessSetting to persist the supplied brightness value.
*/
public void setBrightness(float brightnessValue) {
- setBrightness(brightnessValue, DEFAULT_USER_SERIAL);
- }
-
- /**
- * Notifies the brightnessSetting to persist the supplied brightness value for a user.
- */
- public void setBrightness(float brightnessValue, int userSerial) {
// Update the setting, which will eventually call back into DPC to have us actually
// update the display with the new value.
- mBrightnessSetting.setUserSerial(userSerial);
mBrightnessSetting.setBrightness(brightnessValue);
if (mDisplayId == Display.DEFAULT_DISPLAY && mPersistBrightnessNitsForDefaultDisplay) {
float nits = convertToNits(brightnessValue);
@@ -322,6 +313,14 @@
}
/**
+ * Notifies the brightnessSetting to persist the supplied brightness value for a user.
+ */
+ public void setBrightness(float brightnessValue, int userSerial) {
+ mBrightnessSetting.setUserSerial(userSerial);
+ setBrightness(brightnessValue);
+ }
+
+ /**
* Sets the current screen brightness, and notifies the BrightnessSetting about the change.
*/
public void updateScreenBrightnessSetting(float brightnessValue) {
diff --git a/services/core/java/com/android/server/display/brightness/strategy/AutomaticBrightnessStrategy.java b/services/core/java/com/android/server/display/brightness/strategy/AutomaticBrightnessStrategy.java
index 3c23b5c..3e6e09d 100644
--- a/services/core/java/com/android/server/display/brightness/strategy/AutomaticBrightnessStrategy.java
+++ b/services/core/java/com/android/server/display/brightness/strategy/AutomaticBrightnessStrategy.java
@@ -203,14 +203,11 @@
* Sets the pending auto-brightness adjustments in the system settings. Executed
* when there is a change in the brightness system setting, or when there is a user switch.
*/
- public void updatePendingAutoBrightnessAdjustments(boolean userSwitch) {
+ public void updatePendingAutoBrightnessAdjustments() {
final float adj = Settings.System.getFloatForUser(mContext.getContentResolver(),
Settings.System.SCREEN_AUTO_BRIGHTNESS_ADJ, 0.0f, UserHandle.USER_CURRENT);
mPendingAutoBrightnessAdjustment = Float.isNaN(adj) ? Float.NaN
: BrightnessUtils.clampBrightnessAdjustment(adj);
- if (userSwitch) {
- processPendingAutoBrightnessAdjustments();
- }
}
/**
diff --git a/services/core/java/com/android/server/grammaticalinflection/GrammaticalInflectionManagerInternal.java b/services/core/java/com/android/server/grammaticalinflection/GrammaticalInflectionManagerInternal.java
index c260f10..6a6e6ab 100644
--- a/services/core/java/com/android/server/grammaticalinflection/GrammaticalInflectionManagerInternal.java
+++ b/services/core/java/com/android/server/grammaticalinflection/GrammaticalInflectionManagerInternal.java
@@ -47,5 +47,19 @@
* @see Configuration#getGrammaticalGender
*/
public abstract @Configuration.GrammaticalGender int getSystemGrammaticalGender(int userId);
+
+ /**
+ * Retrieve the system grammatical gender.
+ *
+ * @return the value of grammatical gender
+ *
+ */
+ public abstract @Configuration.GrammaticalGender int retrieveSystemGrammaticalGender(
+ Configuration configuration);
+
+ /**
+ * Whether the package can get the system grammatical gender or not.
+ */
+ public abstract boolean canGetSystemGrammaticalGender(int uid, String packageName);
}
diff --git a/services/core/java/com/android/server/grammaticalinflection/GrammaticalInflectionService.java b/services/core/java/com/android/server/grammaticalinflection/GrammaticalInflectionService.java
index 6eb7e95..d01f54f 100644
--- a/services/core/java/com/android/server/grammaticalinflection/GrammaticalInflectionService.java
+++ b/services/core/java/com/android/server/grammaticalinflection/GrammaticalInflectionService.java
@@ -22,17 +22,21 @@
import static com.android.server.grammaticalinflection.GrammaticalInflectionUtils.checkSystemGrammaticalGenderPermission;
import android.annotation.Nullable;
+import android.app.ActivityTaskManager;
import android.app.GrammaticalInflectionManager;
import android.app.IGrammaticalInflectionManager;
import android.content.AttributionSource;
import android.content.Context;
import android.content.pm.PackageManagerInternal;
+import android.content.res.Configuration;
import android.os.Binder;
import android.os.Environment;
import android.os.Process;
+import android.os.RemoteException;
import android.os.ResultReceiver;
import android.os.ShellCallback;
import android.os.SystemProperties;
+import android.os.Trace;
import android.permission.PermissionManager;
import android.util.AtomicFile;
import android.util.Log;
@@ -43,6 +47,7 @@
import com.android.internal.util.XmlUtils;
import com.android.modules.utils.TypedXmlPullParser;
import com.android.modules.utils.TypedXmlSerializer;
+import com.android.server.IoThread;
import com.android.server.LocalServices;
import com.android.server.SystemService;
import com.android.server.wm.ActivityTaskManagerInternal;
@@ -71,6 +76,7 @@
private static final String TAG_GRAMMATICAL_INFLECTION = "grammatical_inflection";
private static final String GRAMMATICAL_INFLECTION_ENABLED =
"i18n.grammatical_Inflection.enabled";
+ private static final String GRAMMATICAL_GENDER_PROPERTY = "persist.sys.grammatical_gender";
private final GrammaticalInflectionBackupHelper mBackupHelper;
private final ActivityTaskManagerInternal mActivityTaskManagerInternal;
@@ -121,16 +127,16 @@
@Override
public void setSystemWideGrammaticalGender(int grammaticalGender, int userId) {
checkCallerIsSystem();
- checkSystemTermsOfAddressIsEnabled();
GrammaticalInflectionService.this.setSystemWideGrammaticalGender(grammaticalGender,
userId);
}
@Override
public int getSystemGrammaticalGender(AttributionSource attributionSource, int userId) {
- checkSystemTermsOfAddressIsEnabled();
- return GrammaticalInflectionService.this.getSystemGrammaticalGender(attributionSource,
- userId);
+ return canGetSystemGrammaticalGender(attributionSource)
+ ? GrammaticalInflectionService.this.getSystemGrammaticalGender(
+ attributionSource, userId)
+ : GRAMMATICAL_GENDER_NOT_SPECIFIED;
}
@Override
@@ -159,9 +165,33 @@
@Override
public int getSystemGrammaticalGender(int userId) {
- checkCallerIsSystem();
- return GrammaticalInflectionService.this.getSystemGrammaticalGender(
- mContext.getAttributionSource(), userId);
+ return checkSystemTermsOfAddressIsEnabled()
+ ? GrammaticalInflectionService.this.getSystemGrammaticalGender(
+ mContext.getAttributionSource(), userId)
+ : GRAMMATICAL_GENDER_NOT_SPECIFIED;
+ }
+
+ @Override
+ public int retrieveSystemGrammaticalGender(Configuration configuration) {
+ int systemGrammaticalGender = getSystemGrammaticalGender(mContext.getUserId());
+ // Retrieve the grammatical gender from system property, set it into
+ // configuration which will get updated later if the grammatical gender raw value of
+ // current configuration is {@link Configuration#GRAMMATICAL_GENDER_UNDEFINED}.
+ if (configuration.getGrammaticalGenderRaw()
+ == Configuration.GRAMMATICAL_GENDER_UNDEFINED
+ || systemGrammaticalGender <= Configuration.GRAMMATICAL_GENDER_NOT_SPECIFIED) {
+ systemGrammaticalGender = SystemProperties.getInt(GRAMMATICAL_GENDER_PROPERTY,
+ Configuration.GRAMMATICAL_GENDER_UNDEFINED);
+ }
+ return systemGrammaticalGender;
+ }
+
+ @Override
+ public boolean canGetSystemGrammaticalGender(int uid, String packageName) {
+ AttributionSource attributionSource = new AttributionSource.Builder(
+ uid).setPackageName(packageName).build();
+ return GrammaticalInflectionService.this.canGetSystemGrammaticalGender(
+ attributionSource);
}
}
@@ -202,11 +232,20 @@
}
protected void setSystemWideGrammaticalGender(int grammaticalGender, int userId) {
+ Trace.beginSection("GrammaticalInflectionService.setSystemWideGrammaticalGender");
if (!GrammaticalInflectionManager.VALID_GRAMMATICAL_GENDER_VALUES.contains(
grammaticalGender)) {
throw new IllegalArgumentException("Unknown grammatical gender");
}
+ if (!checkSystemTermsOfAddressIsEnabled()) {
+ if (grammaticalGender == GRAMMATICAL_GENDER_NOT_SPECIFIED) {
+ return;
+ }
+ Log.d(TAG, "Clearing the system grammatical gender setting");
+ grammaticalGender = GRAMMATICAL_GENDER_NOT_SPECIFIED;
+ }
+
synchronized (mLock) {
final File file = getGrammaticalGenderFile(userId);
final AtomicFile atomicFile = new AtomicFile(file);
@@ -224,6 +263,15 @@
throw new RuntimeException(e);
}
}
+
+ try {
+ Configuration config = new Configuration();
+ config.setGrammaticalGender(grammaticalGender);
+ ActivityTaskManager.getService().updateConfiguration(config);
+ } catch (RemoteException e) {
+ Log.w(TAG, "Can not update configuration", e);
+ }
+ Trace.endSection();
}
public int getSystemGrammaticalGender(AttributionSource attributionSource, int userId) {
@@ -233,34 +281,9 @@
return GRAMMATICAL_GENDER_NOT_SPECIFIED;
}
- int callingUid = Binder.getCallingUid();
- if (mPackageManagerInternal.getPackageUid(packageName, 0, userId) != callingUid) {
- Log.d(TAG,
- "Package " + packageName + " does not belong to the calling uid " + callingUid);
- return GRAMMATICAL_GENDER_NOT_SPECIFIED;
- }
-
- if (!checkSystemGrammaticalGenderPermission(mPermissionManager, attributionSource)) {
- return GRAMMATICAL_GENDER_NOT_SPECIFIED;
- }
-
synchronized (mLock) {
- final File file = getGrammaticalGenderFile(userId);
- if (!file.exists()) {
- Log.d(TAG, "User " + userId + "doesn't have the grammatical gender file.");
- return GRAMMATICAL_GENDER_NOT_SPECIFIED;
- }
-
- if (mGrammaticalGenderCache.indexOfKey(userId) < 0) {
- try {
- InputStream in = new FileInputStream(file);
- final TypedXmlPullParser parser = Xml.resolvePullParser(in);
- mGrammaticalGenderCache.put(userId, getGrammaticalGenderFromXml(parser));
- } catch (IOException | XmlPullParserException e) {
- Log.e(TAG, "Failed to parse XML configuration from " + file, e);
- }
- }
- return mGrammaticalGenderCache.get(userId);
+ int grammaticalGender = mGrammaticalGenderCache.get(userId);
+ return grammaticalGender < 0 ? GRAMMATICAL_GENDER_NOT_SPECIFIED : grammaticalGender;
}
}
@@ -311,9 +334,39 @@
}
}
- private void checkSystemTermsOfAddressIsEnabled() {
+ private boolean checkSystemTermsOfAddressIsEnabled() {
if (!systemTermsOfAddressEnabled()) {
- throw new RuntimeException("The flag must be enabled to allow calling the API.");
+ Log.d(TAG, "The flag must be enabled to allow calling the API.");
+ return false;
}
+ return true;
+ }
+
+ private boolean canGetSystemGrammaticalGender(AttributionSource attributionSource) {
+ return checkSystemTermsOfAddressIsEnabled() && checkSystemGrammaticalGenderPermission(
+ mPermissionManager, attributionSource);
+ }
+
+ @Override
+ public void onUserUnlocked(TargetUser user) {
+ IoThread.getHandler().post(() -> {
+ int userId = user.getUserIdentifier();
+ final File file = getGrammaticalGenderFile(userId);
+ synchronized (mLock) {
+ if (!file.exists()) {
+ Log.d(TAG, "User " + userId + "doesn't have the grammatical gender file.");
+ return;
+ }
+ if (mGrammaticalGenderCache.indexOfKey(userId) < 0) {
+ try {
+ InputStream in = new FileInputStream(file);
+ final TypedXmlPullParser parser = Xml.resolvePullParser(in);
+ mGrammaticalGenderCache.put(userId, getGrammaticalGenderFromXml(parser));
+ } catch (IOException | XmlPullParserException e) {
+ Log.e(TAG, "Failed to parse XML configuration from " + file, e);
+ }
+ }
+ }
+ });
}
}
diff --git a/services/core/java/com/android/server/hdmi/AbsoluteVolumeAudioStatusAction.java b/services/core/java/com/android/server/hdmi/AbsoluteVolumeAudioStatusAction.java
index 8278600..202c894 100644
--- a/services/core/java/com/android/server/hdmi/AbsoluteVolumeAudioStatusAction.java
+++ b/services/core/java/com/android/server/hdmi/AbsoluteVolumeAudioStatusAction.java
@@ -98,18 +98,21 @@
localDevice().getService().enableAbsoluteVolumeBehavior(audioStatus);
mState = STATE_MONITOR_AUDIO_STATUS;
} else if (mState == STATE_MONITOR_AUDIO_STATUS) {
- // On TV panels, we notify AudioService even if neither volume nor mute state changed.
- // This ensures that the user sees volume UI if they tried to adjust the AVR's volume,
- // even if the new volume level is the same as the previous one.
- boolean notifyAvbVolumeToShowUi = localDevice().getService().isTvDevice()
- && audioStatus.equals(mLastAudioStatus);
-
- if (audioStatus.getVolume() != mLastAudioStatus.getVolume()
- || notifyAvbVolumeToShowUi) {
+ // Update volume in AudioService if it has changed since the last <Report Audio Status>
+ boolean updateVolume = audioStatus.getVolume() != mLastAudioStatus.getVolume();
+ if (updateVolume) {
localDevice().getService().notifyAvbVolumeChange(audioStatus.getVolume());
}
- if (audioStatus.getMute() != mLastAudioStatus.getMute()) {
+ // Update mute in AudioService if any of the following conditions are met:
+ // - The mute status changed
+ // - The volume changed - we need to make sure mute is set correctly afterwards, since
+ // setting volume can affect mute status as well as a side effect.
+ // - We're a TV panel - we want to trigger volume UI on TV panels, so that the user
+ // always gets visual feedback when they attempt to adjust the AVR's volume/mute.
+ if ((audioStatus.getMute() != mLastAudioStatus.getMute())
+ || updateVolume
+ || localDevice().getService().isTvDevice()) {
localDevice().getService().notifyAvbMuteChange(audioStatus.getMute());
}
}
diff --git a/services/core/java/com/android/server/hdmi/HdmiCecMessageValidator.java b/services/core/java/com/android/server/hdmi/HdmiCecMessageValidator.java
index a15cb10..a23c08a 100644
--- a/services/core/java/com/android/server/hdmi/HdmiCecMessageValidator.java
+++ b/services/core/java/com/android/server/hdmi/HdmiCecMessageValidator.java
@@ -716,7 +716,10 @@
// Programmed
int programedInfo = params[offset] & 0x0F;
if (isValidProgrammedInfo(programedInfo)) {
- if (programedInfo == 0x09 || programedInfo == 0x0B) {
+ offset = offset + 1;
+ // Duration Available (2 bytes)
+ if ((programedInfo == 0x09 || programedInfo == 0x0B)
+ && params.length - offset >= 2) {
durationAvailable = true;
} else {
return true;
@@ -726,16 +729,17 @@
// Non programmed
int nonProgramedErrorInfo = params[offset] & 0x0F;
if (isValidNotProgrammedErrorInfo(nonProgramedErrorInfo)) {
- if (nonProgramedErrorInfo == 0x0E) {
+ offset = offset + 1;
+ // Duration Available (2 bytes)
+ if (nonProgramedErrorInfo == 0x0E && params.length - offset >= 2) {
durationAvailable = true;
} else {
return true;
}
}
}
- offset = offset + 1;
// Duration Available (2 bytes)
- if (durationAvailable && params.length - offset >= 2) {
+ if (durationAvailable) {
return (isValidDurationHours(params[offset]) && isValidMinute(params[offset + 1]));
}
return false;
diff --git a/services/core/java/com/android/server/hdmi/HdmiControlService.java b/services/core/java/com/android/server/hdmi/HdmiControlService.java
index e0e825d..c8c66238 100644
--- a/services/core/java/com/android/server/hdmi/HdmiControlService.java
+++ b/services/core/java/com/android/server/hdmi/HdmiControlService.java
@@ -1366,6 +1366,12 @@
// we don't call onInitializeCecComplete()
// since we reallocate the logical address only.
onInitializeCecComplete(initiatedBy);
+ } else if (initiatedBy == INITIATED_BY_HOTPLUG
+ && mDisplayStatusCallback == null) {
+ // Force to update display status for hotplug event.
+ synchronized (mLock) {
+ announceHdmiControlStatusChange(mHdmiControlEnabled);
+ }
}
// We remove local devices here, instead of before the start of
// address allocation, to prevent multiple local devices of the
diff --git a/services/core/java/com/android/server/input/InputManagerService.java b/services/core/java/com/android/server/input/InputManagerService.java
index 687def0..574be34 100644
--- a/services/core/java/com/android/server/input/InputManagerService.java
+++ b/services/core/java/com/android/server/input/InputManagerService.java
@@ -170,9 +170,6 @@
private InputMethodManagerInternal mInputMethodManagerInternal;
- // Context cache used for loading pointer resources.
- private Context mPointerIconDisplayContext;
-
private final File mDoubleTouchGestureEnableFile;
private WindowManagerCallbacks mWindowManagerCallbacks;
@@ -416,6 +413,8 @@
new SparseArray<>();
@GuardedBy("mLoadedPointerIconsByDisplayAndType")
boolean mUseLargePointerIcons = false;
+ @GuardedBy("mLoadedPointerIconsByDisplayAndType")
+ final SparseArray<Context> mDisplayContexts = new SparseArray<>();
final DisplayManager.DisplayListener mDisplayListener = new DisplayManager.DisplayListener() {
@Override
@@ -427,6 +426,7 @@
public void onDisplayRemoved(int displayId) {
synchronized (mLoadedPointerIconsByDisplayAndType) {
mLoadedPointerIconsByDisplayAndType.remove(displayId);
+ mDisplayContexts.remove(displayId);
}
}
@@ -440,6 +440,7 @@
return;
}
iconsByType.clear();
+ mDisplayContexts.remove(displayId);
}
mNative.reloadPointerIcons();
}
@@ -620,10 +621,6 @@
mBatteryController.systemRunning();
mKeyboardBacklightController.systemRunning();
mKeyRemapper.systemRunning();
-
- mNative.setStylusPointerIconEnabled(
- Objects.requireNonNull(mContext.getSystemService(InputManager.class))
- .isStylusPointerIconEnabled());
}
private void reloadDeviceAliases() {
@@ -1323,11 +1320,6 @@
/** Clean up input window handles of the given display. */
public void onDisplayRemoved(int displayId) {
- if (mPointerIconDisplayContext != null
- && mPointerIconDisplayContext.getDisplay().getDisplayId() == displayId) {
- mPointerIconDisplayContext = null;
- }
-
updateAdditionalDisplayInputProperties(displayId, AdditionalDisplayInputProperties::reset);
// TODO(b/320763728): Rely on WindowInfosListener to determine when a display has been
@@ -1392,6 +1384,11 @@
}
@Override // Binder call
+ public int getMousePointerSpeed() {
+ return mNative.getMousePointerSpeed();
+ }
+
+ @Override // Binder call
public void tryPointerSpeed(int speed) {
if (!checkCallingPermission(android.Manifest.permission.SET_POINTER_SPEED,
"tryPointerSpeed()")) {
@@ -2374,6 +2371,7 @@
synchronized (mLidSwitchLock) { /* Test if blocked by lid switch lock. */ }
synchronized (mInputMonitors) { /* Test if blocked by input monitor lock. */ }
synchronized (mAdditionalDisplayInputPropertiesLock) { /* Test if blocked by props lock */ }
+ synchronized (mLoadedPointerIconsByDisplayAndType) { /* Test if blocked by pointer lock */}
mBatteryController.monitor();
mNative.monitor();
}
@@ -2777,7 +2775,7 @@
}
PointerIcon icon = iconsByType.get(type);
if (icon == null) {
- icon = PointerIcon.getLoadedSystemIcon(getContextForPointerIcon(displayId), type,
+ icon = PointerIcon.getLoadedSystemIcon(getContextForDisplay(displayId), type,
mUseLargePointerIcons);
iconsByType.put(type, icon);
}
@@ -2795,40 +2793,31 @@
return sc.mNativeObject;
}
+ @GuardedBy("mLoadedPointerIconsByDisplayAndType")
@NonNull
- private Context getContextForPointerIcon(int displayId) {
- if (mPointerIconDisplayContext != null
- && mPointerIconDisplayContext.getDisplay().getDisplayId() == displayId) {
- return mPointerIconDisplayContext;
- }
-
- // Create and cache context for non-default display.
- mPointerIconDisplayContext = getContextForDisplay(displayId);
-
- // Fall back to default display if the requested displayId does not exist.
- if (mPointerIconDisplayContext == null) {
- mPointerIconDisplayContext = getContextForDisplay(Display.DEFAULT_DISPLAY);
- }
- return mPointerIconDisplayContext;
- }
-
- @Nullable
private Context getContextForDisplay(int displayId) {
if (displayId == Display.INVALID_DISPLAY) {
- return null;
+ // Fallback to using the default context.
+ return mContext;
}
- if (mContext.getDisplay().getDisplayId() == displayId) {
+ if (displayId == mContext.getDisplay().getDisplayId()) {
return mContext;
}
- final DisplayManager displayManager = Objects.requireNonNull(
- mContext.getSystemService(DisplayManager.class));
- final Display display = displayManager.getDisplay(displayId);
- if (display == null) {
- return null;
- }
+ Context displayContext = mDisplayContexts.get(displayId);
+ if (displayContext == null) {
+ final DisplayManager displayManager = Objects.requireNonNull(
+ mContext.getSystemService(DisplayManager.class));
+ final Display display = displayManager.getDisplay(displayId);
+ if (display == null) {
+ // Fallback to using the default context.
+ return mContext;
+ }
- return mContext.createDisplayContext(display);
+ displayContext = mContext.createDisplayContext(display);
+ mDisplayContexts.put(displayId, displayContext);
+ }
+ return displayContext;
}
// Native callback.
diff --git a/services/core/java/com/android/server/input/InputSettingsObserver.java b/services/core/java/com/android/server/input/InputSettingsObserver.java
index 5ffc380..c02d524 100644
--- a/services/core/java/com/android/server/input/InputSettingsObserver.java
+++ b/services/core/java/com/android/server/input/InputSettingsObserver.java
@@ -94,7 +94,9 @@
Map.entry(Settings.Secure.getUriFor(Settings.Secure.ACCESSIBILITY_SLOW_KEYS),
(reason) -> updateAccessibilitySlowKeys()),
Map.entry(Settings.Secure.getUriFor(Settings.Secure.ACCESSIBILITY_STICKY_KEYS),
- (reason) -> updateAccessibilityStickyKeys()));
+ (reason) -> updateAccessibilityStickyKeys()),
+ Map.entry(Settings.Secure.getUriFor(Settings.Secure.STYLUS_POINTER_ICON_ENABLED),
+ (reason) -> updateStylusPointerIconEnabled()));
}
/**
@@ -254,4 +256,8 @@
mNative.setMinTimeBetweenUserActivityPokes(intervalMillis);
}
}
+
+ private void updateStylusPointerIconEnabled() {
+ mNative.setStylusPointerIconEnabled(InputSettings.isStylusPointerIconEnabled(mContext));
+ }
}
diff --git a/services/core/java/com/android/server/input/NativeInputManagerService.java b/services/core/java/com/android/server/input/NativeInputManagerService.java
index e5f3484..b16df0f 100644
--- a/services/core/java/com/android/server/input/NativeInputManagerService.java
+++ b/services/core/java/com/android/server/input/NativeInputManagerService.java
@@ -119,6 +119,8 @@
*/
boolean transferTouch(IBinder destChannelToken, int displayId);
+ int getMousePointerSpeed();
+
void setPointerSpeed(int speed);
void setMousePointerAccelerationEnabled(int displayId, boolean enabled);
@@ -364,6 +366,9 @@
public native boolean transferTouch(IBinder destChannelToken, int displayId);
@Override
+ public native int getMousePointerSpeed();
+
+ @Override
public native void setPointerSpeed(int speed);
@Override
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
index 2ea662c..3bd1e1a 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
@@ -2385,12 +2385,6 @@
@StartInputReason int startInputReason,
int unverifiedTargetSdkVersion,
@NonNull ImeOnBackInvokedDispatcher imeDispatcher) {
- if (!InputMethodUtils.checkIfPackageBelongsToUid(mPackageManagerInternal, cs.mUid,
- editorInfo.packageName)) {
- Slog.e(TAG, "Rejecting this client as it reported an invalid package name."
- + " uid=" + cs.mUid + " package=" + editorInfo.packageName);
- return InputBindResult.INVALID_PACKAGE_NAME;
- }
// Compute the final shown display ID with validated cs.selfReportedDisplayId for this
// session & other conditions.
@@ -2521,6 +2515,7 @@
if (DEBUG) {
Slog.v(TAG, "Restoring default device input method: " + defaultDeviceMethodId);
}
+ mSettings.putSelectedDefaultDeviceInputMethod(null);
return defaultDeviceMethodId;
}
@@ -3200,6 +3195,8 @@
SecureSettingsWrapper.putString(
Settings.Secure.DEFAULT_INPUT_METHOD, defaultDeviceIme,
mSettings.getUserId());
+ SecureSettingsWrapper.putString(
+ Settings.Secure.DEFAULT_DEVICE_INPUT_METHOD, null, mSettings.getUserId());
}
}
@@ -3726,6 +3723,68 @@
return InputBindResult.INVALID_USER;
}
+ // Ensure that caller's focused window and display parameters are allowd to
+ // display input method.
+ final int imeClientFocus = mWindowManagerInternal.hasInputMethodClientFocus(
+ windowToken, cs.mUid, cs.mPid, cs.mSelfReportedDisplayId);
+ switch (imeClientFocus) {
+ case WindowManagerInternal.ImeClientFocusResult.DISPLAY_ID_MISMATCH:
+ Slog.e(TAG,
+ "startInputOrWindowGainedFocusInternal: display ID mismatch.");
+ return InputBindResult.DISPLAY_ID_MISMATCH;
+ case WindowManagerInternal.ImeClientFocusResult.NOT_IME_TARGET_WINDOW:
+ // Check with the window manager to make sure this client actually
+ // has a window with focus. If not, reject. This is thread safe
+ // because if the focus changes some time before or after, the
+ // next client receiving focus that has any interest in input will
+ // be calling through here after that change happens.
+ if (DEBUG) {
+ Slog.w(TAG, "Focus gain on non-focused client " + cs.mClient
+ + " (uid=" + cs.mUid + " pid=" + cs.mPid + ")");
+ }
+ return InputBindResult.NOT_IME_TARGET_WINDOW;
+ case WindowManagerInternal.ImeClientFocusResult.INVALID_DISPLAY_ID:
+ return InputBindResult.INVALID_DISPLAY_ID;
+ }
+
+ // In case mShowForced flag affects the next client to keep IME visible, when
+ // the current client is leaving due to the next focused client, we clear
+ // mShowForced flag when the next client's targetSdkVersion is T or higher.
+ final boolean shouldClearFlag =
+ mImePlatformCompatUtils.shouldClearShowForcedFlag(cs.mUid);
+ final boolean showForced = mVisibilityStateComputer.mShowForced;
+ if (mCurFocusedWindow != windowToken && showForced && shouldClearFlag) {
+ mVisibilityStateComputer.mShowForced = false;
+ }
+
+ // Verify if caller is a background user.
+ final int currentUserId = mSettings.getUserId();
+ if (userId != currentUserId) {
+ if (ArrayUtils.contains(
+ mUserManagerInternal.getProfileIds(currentUserId, false), userId)) {
+ // cross-profile access is always allowed here to allow
+ // profile-switching.
+ scheduleSwitchUserTaskLocked(userId, cs.mClient);
+ return InputBindResult.USER_SWITCHING;
+ }
+ Slog.w(TAG, "A background user is requesting window. Hiding IME.");
+ Slog.w(TAG, "If you need to impersonate a foreground user/profile from"
+ + " a background user, use EditorInfo.targetInputMethodUser with"
+ + " INTERACT_ACROSS_USERS_FULL permission.");
+ hideCurrentInputLocked(mCurFocusedWindow, null /* statsToken */,
+ 0 /* flags */,
+ null /* resultReceiver */,
+ SoftInputShowHideReason.HIDE_INVALID_USER);
+ return InputBindResult.INVALID_USER;
+ }
+
+ if (editorInfo != null && !InputMethodUtils.checkIfPackageBelongsToUid(
+ mPackageManagerInternal, cs.mUid, editorInfo.packageName)) {
+ Slog.e(TAG, "Rejecting this client as it reported an invalid package name."
+ + " uid=" + cs.mUid + " package=" + editorInfo.packageName);
+ return InputBindResult.INVALID_PACKAGE_NAME;
+ }
+
result = startInputOrWindowGainedFocusInternalLocked(startInputReason,
client, windowToken, startInputFlags, softInputMode, windowFlags,
editorInfo, inputConnection, remoteAccessibilityInputConnection,
@@ -3774,52 +3833,6 @@
+ " imeDispatcher=" + imeDispatcher
+ " cs=" + cs);
}
- final int imeClientFocus = mWindowManagerInternal.hasInputMethodClientFocus(
- windowToken, cs.mUid, cs.mPid, cs.mSelfReportedDisplayId);
- switch (imeClientFocus) {
- case WindowManagerInternal.ImeClientFocusResult.DISPLAY_ID_MISMATCH:
- Slog.e(TAG, "startInputOrWindowGainedFocusInternal: display ID mismatch.");
- return InputBindResult.DISPLAY_ID_MISMATCH;
- case WindowManagerInternal.ImeClientFocusResult.NOT_IME_TARGET_WINDOW:
- // Check with the window manager to make sure this client actually
- // has a window with focus. If not, reject. This is thread safe
- // because if the focus changes some time before or after, the
- // next client receiving focus that has any interest in input will
- // be calling through here after that change happens.
- if (DEBUG) {
- Slog.w(TAG, "Focus gain on non-focused client " + cs.mClient
- + " (uid=" + cs.mUid + " pid=" + cs.mPid + ")");
- }
- return InputBindResult.NOT_IME_TARGET_WINDOW;
- case WindowManagerInternal.ImeClientFocusResult.INVALID_DISPLAY_ID:
- return InputBindResult.INVALID_DISPLAY_ID;
- }
-
- final boolean shouldClearFlag = mImePlatformCompatUtils.shouldClearShowForcedFlag(cs.mUid);
- // In case mShowForced flag affects the next client to keep IME visible, when the current
- // client is leaving due to the next focused client, we clear mShowForced flag when the
- // next client's targetSdkVersion is T or higher.
- final boolean showForced = mVisibilityStateComputer.mShowForced;
- if (mCurFocusedWindow != windowToken && showForced && shouldClearFlag) {
- mVisibilityStateComputer.mShowForced = false;
- }
-
- final int currentUserId = mSettings.getUserId();
- if (userId != currentUserId) {
- if (ArrayUtils.contains(
- mUserManagerInternal.getProfileIds(currentUserId, false), userId)) {
- // cross-profile access is always allowed here to allow profile-switching.
- scheduleSwitchUserTaskLocked(userId, cs.mClient);
- return InputBindResult.USER_SWITCHING;
- }
- Slog.w(TAG, "A background user is requesting window. Hiding IME.");
- Slog.w(TAG, "If you need to impersonate a foreground user/profile from"
- + " a background user, use EditorInfo.targetInputMethodUser with"
- + " INTERACT_ACROSS_USERS_FULL permission.");
- hideCurrentInputLocked(mCurFocusedWindow, null /* statsToken */, 0 /* flags */,
- null /* resultReceiver */, SoftInputShowHideReason.HIDE_INVALID_USER);
- return InputBindResult.INVALID_USER;
- }
final boolean sameWindowFocused = mCurFocusedWindow == windowToken;
final boolean isTextEditor = (startInputFlags & StartInputFlags.IS_TEXT_EDITOR) != 0;
@@ -5347,7 +5360,7 @@
InputMethodInfoUtils.getMostApplicableDefaultIME(
mSettings.getEnabledInputMethodList());
mSettings.putSelectedDefaultDeviceInputMethod(
- newDefaultIme == null ? "" : newDefaultIme.getId());
+ newDefaultIme == null ? null : newDefaultIme.getId());
}
// Previous state was enabled.
return true;
@@ -5384,16 +5397,16 @@
if (!setSubtypeOnly) {
// Set InputMethod here
- final String imeId = imi != null ? imi.getId() : "";
- mSettings.putSelectedInputMethod(imeId);
- if (mDeviceIdToShowIme == DEVICE_ID_DEFAULT) {
- mSettings.putSelectedDefaultDeviceInputMethod(imeId);
- }
+ mSettings.putSelectedInputMethod(imi != null ? imi.getId() : "");
}
}
@GuardedBy("ImfLock.class")
private void resetSelectedInputMethodAndSubtypeLocked(String newDefaultIme) {
+ mDeviceIdToShowIme = DEVICE_ID_DEFAULT;
+ mDisplayIdToShowIme = INVALID_DISPLAY;
+ mSettings.putSelectedDefaultDeviceInputMethod(null);
+
InputMethodInfo imi = mSettings.getMethodMap().get(newDefaultIme);
int lastSubtypeId = NOT_A_SUBTYPE_ID;
// newDefaultIme is empty when there is no candidate for the selected IME.
@@ -5531,9 +5544,6 @@
return false; // IME is not found or not enabled.
}
settings.putSelectedInputMethod(imeId);
- if (mDeviceIdToShowIme == DEVICE_ID_DEFAULT) {
- settings.putSelectedDefaultDeviceInputMethod(imeId);
- }
settings.putSelectedSubtype(NOT_A_SUBTYPE_ID);
return true;
}
@@ -6580,9 +6590,7 @@
// Reset selected IME.
settings.putSelectedInputMethod(nextIme);
- if (mDeviceIdToShowIme == DEVICE_ID_DEFAULT) {
- settings.putSelectedDefaultDeviceInputMethod(nextIme);
- }
+ settings.putSelectedDefaultDeviceInputMethod(null);
settings.putSelectedSubtype(NOT_A_SUBTYPE_ID);
}
out.println("Reset current and enabled IMEs for user #" + userId);
diff --git a/services/core/java/com/android/server/location/altitude/AltitudeService.java b/services/core/java/com/android/server/location/altitude/AltitudeService.java
index 97bf354..289d4a2 100644
--- a/services/core/java/com/android/server/location/altitude/AltitudeService.java
+++ b/services/core/java/com/android/server/location/altitude/AltitudeService.java
@@ -71,31 +71,13 @@
@Override
public GetGeoidHeightResponse getGeoidHeight(GetGeoidHeightRequest request)
throws RemoteException {
- Location location = new Location("");
- location.setLatitude(request.latitudeDegrees);
- location.setLongitude(request.longitudeDegrees);
- location.setAltitude(0.0);
- location.setVerticalAccuracyMeters(0.0f);
-
- GetGeoidHeightResponse response = new GetGeoidHeightResponse();
try {
- mAltitudeConverter.addMslAltitudeToLocation(mContext, location);
+ return mAltitudeConverter.getGeoidHeight(mContext, request);
} catch (IOException e) {
+ GetGeoidHeightResponse response = new GetGeoidHeightResponse();
response.success = false;
return response;
}
- // The geoid height for a location with zero WGS84 altitude is equal in value to the
- // negative of the corresponding MSL altitude.
- response.geoidHeightMeters = -location.getMslAltitudeMeters();
- // The geoid height error for a location with zero vertical accuracy is equal in value to
- // the corresponding MSL altitude accuracy.
- response.geoidHeightErrorMeters = location.getMslAltitudeAccuracyMeters();
- // The expiration distance and additional error are currently set to constants used by
- // health services.
- response.expirationDistanceMeters = 10000.0;
- response.additionalGeoidHeightErrorMeters = 0.707f;
- response.success = true;
- return response;
}
@Override
diff --git a/services/core/java/com/android/server/locksettings/LockSettingsService.java b/services/core/java/com/android/server/locksettings/LockSettingsService.java
index a06607b..7fb3e00 100644
--- a/services/core/java/com/android/server/locksettings/LockSettingsService.java
+++ b/services/core/java/com/android/server/locksettings/LockSettingsService.java
@@ -93,7 +93,6 @@
import android.os.IBinder;
import android.os.IProgressListener;
import android.os.Process;
-import android.os.RemoteCallbackList;
import android.os.RemoteException;
import android.os.ResultReceiver;
import android.os.ServiceManager;
@@ -139,11 +138,11 @@
import com.android.internal.util.Preconditions;
import com.android.internal.widget.ICheckCredentialProgressCallback;
import com.android.internal.widget.ILockSettings;
-import com.android.internal.widget.ILockSettingsStateListener;
import com.android.internal.widget.IWeakEscrowTokenActivatedListener;
import com.android.internal.widget.IWeakEscrowTokenRemovedListener;
import com.android.internal.widget.LockPatternUtils;
import com.android.internal.widget.LockSettingsInternal;
+import com.android.internal.widget.LockSettingsStateListener;
import com.android.internal.widget.LockscreenCredential;
import com.android.internal.widget.RebootEscrowListener;
import com.android.internal.widget.VerifyCredentialResponse;
@@ -185,6 +184,7 @@
import java.util.Objects;
import java.util.Set;
import java.util.StringJoiner;
+import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
@@ -332,8 +332,8 @@
private HashMap<UserHandle, UserManager> mUserManagerCache = new HashMap<>();
- private final RemoteCallbackList<ILockSettingsStateListener> mLockSettingsStateListeners =
- new RemoteCallbackList<>();
+ private final CopyOnWriteArrayList<LockSettingsStateListener> mLockSettingsStateListeners =
+ new CopyOnWriteArrayList<>();
// This class manages life cycle events for encrypted users on File Based Encryption (FBE)
// devices. The most basic of these is to show/hide notifications about missing features until
@@ -2379,25 +2379,12 @@
}
private void notifyLockSettingsStateListeners(boolean success, int userId) {
- int i = mLockSettingsStateListeners.beginBroadcast();
- try {
- while (i > 0) {
- i--;
- try {
- if (success) {
- mLockSettingsStateListeners.getBroadcastItem(i)
- .onAuthenticationSucceeded(userId);
- } else {
- mLockSettingsStateListeners.getBroadcastItem(i)
- .onAuthenticationFailed(userId);
- }
- } catch (RemoteException e) {
- Slog.e(TAG, "Exception while notifying LockSettingsStateListener:"
- + " success = " + success + ", userId = " + userId, e);
- }
+ for (LockSettingsStateListener listener : mLockSettingsStateListeners) {
+ if (success) {
+ listener.onAuthenticationSucceeded(userId);
+ } else {
+ listener.onAuthenticationFailed(userId);
}
- } finally {
- mLockSettingsStateListeners.finishBroadcast();
}
}
@@ -3720,15 +3707,15 @@
}
@Override
- public void registerLockSettingsStateListener(
- @NonNull ILockSettingsStateListener listener) {
- mLockSettingsStateListeners.register(listener);
+ public void registerLockSettingsStateListener(@NonNull LockSettingsStateListener listener) {
+ Objects.requireNonNull(listener, "listener cannot be null");
+ mLockSettingsStateListeners.add(listener);
}
@Override
public void unregisterLockSettingsStateListener(
- @NonNull ILockSettingsStateListener listener) {
- mLockSettingsStateListeners.unregister(listener);
+ @NonNull LockSettingsStateListener listener) {
+ mLockSettingsStateListeners.remove(listener);
}
}
diff --git a/services/core/java/com/android/server/net/watchlist/FileHashCache.java b/services/core/java/com/android/server/net/watchlist/FileHashCache.java
new file mode 100644
index 0000000..f829bc6
--- /dev/null
+++ b/services/core/java/com/android/server/net/watchlist/FileHashCache.java
@@ -0,0 +1,243 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.net.watchlist;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Handler;
+import android.os.SystemClock;
+import android.system.ErrnoException;
+import android.system.Os;
+import android.util.Slog;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.HexDump;
+
+import java.io.BufferedReader;
+import java.io.BufferedWriter;
+import java.io.Closeable;
+import java.io.File;
+import java.io.FileReader;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.security.NoSuchAlgorithmException;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.StringTokenizer;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * @hide
+ * Utility class that keeps file hashes in cache. This cache is persistent across reboots.
+ * If requested hash does not exist in cache, it is calculated from the target file. Cache gets
+ * persisted once it is changed in deferred mode to prevent multiple savings per many small updates.
+ * Deleted files are detected and removed from the cache during the initial load. If file change is
+ * detected, it is hash is calculated during the next request.
+ * The synchronization is done using Handler. All requests for hashes must be done in context of
+ * handler thread.
+ */
+public class FileHashCache {
+ private static final String TAG = FileHashCache.class.getSimpleName();
+ private static final boolean DEBUG = false;
+ // Turns on the check that validates hash in cache matches one, calculated directly on the
+ // target file. Not to be used in production.
+ private static final boolean VERIFY = false;
+
+ // Used for logging wtf only once during load, see logWtfOnce()
+ private static boolean sLoggedWtf = false;
+
+ @VisibleForTesting
+ static String sPersistFileName = "/data/system/file_hash_cache";
+
+ static long sSaveDeferredDelayMillis = TimeUnit.SECONDS.toMillis(5);
+
+ private static class Entry {
+ public final long mLastModified;
+ public final byte[] mSha256Hash;
+
+ Entry(long lastModified, @NonNull byte[] sha256Hash) {
+ mLastModified = lastModified;
+ mSha256Hash = sha256Hash;
+ }
+ }
+
+ private Handler mHandler;
+ private final Map<File, Entry> mEntries = new HashMap<>();
+
+ private final Runnable mLoadTask = () -> {
+ load();
+ };
+ private final Runnable mSaveTask = () -> {
+ save();
+ };
+
+ /**
+ * @hide
+ */
+ public FileHashCache(@NonNull Handler handler) {
+ mHandler = handler;
+ mHandler.post(mLoadTask);
+ }
+
+ /**
+ * Requests sha256 for the provided file from the cache. If cache entry does not exist or
+ * file was modified, then null is returned.
+ * @hide
+ **/
+ @VisibleForTesting
+ @Nullable
+ byte[] getSha256HashFromCache(@NonNull File file) {
+ if (!mHandler.getLooper().isCurrentThread()) {
+ Slog.wtf(TAG, "Request from invalid thread", new Exception());
+ return null;
+ }
+
+ final Entry entry = mEntries.get(file);
+ if (entry == null) {
+ return null;
+ }
+
+ try {
+ if (entry.mLastModified == Os.stat(file.getAbsolutePath()).st_ctime) {
+ if (VERIFY) {
+ try {
+ if (!Arrays.equals(entry.mSha256Hash, DigestUtils.getSha256Hash(file))) {
+ Slog.wtf(TAG, "Failed to verify entry for " + file);
+ }
+ } catch (NoSuchAlgorithmException | IOException e) { }
+ }
+
+ return entry.mSha256Hash;
+ }
+ } catch (ErrnoException e) { }
+
+ if (DEBUG) Slog.v(TAG, "Found stale cached entry for " + file);
+ mEntries.remove(file);
+ return null;
+ }
+
+ /**
+ * Requests sha256 for the provided file. If cache entry does not exist or file was modified,
+ * hash is calculated from the requested file. Otherwise hash from cache is returned.
+ * @hide
+ **/
+ @NonNull
+ public byte[] getSha256Hash(@NonNull File file) throws NoSuchAlgorithmException, IOException {
+ byte[] sha256Hash = getSha256HashFromCache(file);
+ if (sha256Hash != null) {
+ return sha256Hash;
+ }
+
+ try {
+ sha256Hash = DigestUtils.getSha256Hash(file);
+ mEntries.put(file, new Entry(Os.stat(file.getAbsolutePath()).st_ctime, sha256Hash));
+ if (DEBUG) Slog.v(TAG, "New cache entry is created for " + file);
+ scheduleSave();
+ return sha256Hash;
+ } catch (ErrnoException e) {
+ throw new IOException(e);
+ }
+ }
+
+ private static void closeQuietly(@Nullable Closeable closeable) {
+ try {
+ if (closeable != null) {
+ closeable.close();
+ }
+ } catch (IOException e) { }
+ }
+
+ /**
+ * Log an error as wtf only the first instance, then log as warning.
+ */
+ private static void logWtfOnce(@NonNull final String s, final Exception e) {
+ if (!sLoggedWtf) {
+ Slog.wtf(TAG, s, e);
+ sLoggedWtf = true;
+ } else {
+ Slog.w(TAG, s, e);
+ }
+ }
+
+ private void load() {
+ mEntries.clear();
+
+ final long startTime = SystemClock.currentTimeMicro();
+ final File file = new File(sPersistFileName);
+ if (!file.exists()) {
+ if (DEBUG) Slog.v(TAG, "Storage file does not exist. Starting from scratch.");
+ return;
+ }
+
+ BufferedReader reader = null;
+ try {
+ reader = new BufferedReader(new FileReader(file));
+ // forEach rethrows IOException as UncheckedIOException
+ reader.lines().forEach((fileEntry)-> {
+ try {
+ final StringTokenizer tokenizer = new StringTokenizer(fileEntry, ",");
+ final File testFile = new File(tokenizer.nextToken());
+ final long lastModified = Long.parseLong(tokenizer.nextToken());
+ final byte[] sha256 = HexDump.hexStringToByteArray(tokenizer.nextToken());
+ mEntries.put(testFile, new Entry(lastModified, sha256));
+ if (DEBUG) Slog.v(TAG, "Loaded entry for " + testFile);
+ } catch (RuntimeException e) {
+ // hexStringToByteArray can throw raw RuntimeException on invalid input. Avoid
+ // potentially reporting one error per line if the data is corrupt.
+ logWtfOnce("Invalid entry for " + fileEntry, e);
+ return;
+ }
+ });
+ if (DEBUG) {
+ Slog.i(TAG, "Loaded " + mEntries.size() + " entries in "
+ + (SystemClock.currentTimeMicro() - startTime) + " mcs.");
+ }
+ } catch (IOException | UncheckedIOException e) {
+ Slog.e(TAG, "Failed to read storage file", e);
+ } finally {
+ closeQuietly(reader);
+ }
+ }
+
+ private void scheduleSave() {
+ mHandler.removeCallbacks(mSaveTask);
+ mHandler.postDelayed(mSaveTask, sSaveDeferredDelayMillis);
+ }
+
+ private void save() {
+ BufferedWriter writer = null;
+ final long startTime = SystemClock.currentTimeMicro();
+ try {
+ writer = new BufferedWriter(new FileWriter(sPersistFileName));
+ for (Map.Entry<File, Entry> entry : mEntries.entrySet()) {
+ writer.write(entry.getKey() + ","
+ + entry.getValue().mLastModified + ","
+ + HexDump.toHexString(entry.getValue().mSha256Hash) + "\n");
+ }
+ if (DEBUG) {
+ Slog.i(TAG, "Saved " + mEntries.size() + " entries in "
+ + (SystemClock.currentTimeMicro() - startTime) + " mcs.");
+ }
+ } catch (IOException e) {
+ Slog.e(TAG, "Failed to save.", e);
+ } finally {
+ closeQuietly(writer);
+ }
+ }
+}
diff --git a/services/core/java/com/android/server/net/watchlist/WatchlistLoggingHandler.java b/services/core/java/com/android/server/net/watchlist/WatchlistLoggingHandler.java
index 8ce7b57..c863cbf 100644
--- a/services/core/java/com/android/server/net/watchlist/WatchlistLoggingHandler.java
+++ b/services/core/java/com/android/server/net/watchlist/WatchlistLoggingHandler.java
@@ -83,6 +83,8 @@
private final ConcurrentHashMap<Integer, byte[]> mCachedUidDigestMap =
new ConcurrentHashMap<>();
+ private final FileHashCache mApkHashCache;
+
private interface WatchlistEventKeys {
String HOST = "host";
String IP_ADDRESSES = "ipAddresses";
@@ -100,6 +102,13 @@
mSettings = WatchlistSettings.getInstance();
mDropBoxManager = mContext.getSystemService(DropBoxManager.class);
mPrimaryUserId = getPrimaryUserId();
+ if (context.getResources().getBoolean(
+ com.android.internal.R.bool.config_watchlistUseFileHashesCache)) {
+ mApkHashCache = new FileHashCache(this);
+ Slog.i(TAG, "Using file hashes cache.");
+ } else {
+ mApkHashCache = null;
+ }
}
@Override
@@ -345,7 +354,9 @@
Slog.i(TAG, "Skipping incremental path: " + packageName);
continue;
}
- return DigestUtils.getSha256Hash(new File(apkPath));
+ return mApkHashCache != null
+ ? mApkHashCache.getSha256Hash(new File(apkPath))
+ : DigestUtils.getSha256Hash(new File(apkPath));
} catch (NameNotFoundException | NoSuchAlgorithmException | IOException e) {
Slog.e(TAG, "Cannot get digest from uid: " + key
+ ",pkg: " + packageName, e);
diff --git a/services/core/java/com/android/server/notification/GroupHelper.java b/services/core/java/com/android/server/notification/GroupHelper.java
index dff02bf..e349fa3 100644
--- a/services/core/java/com/android/server/notification/GroupHelper.java
+++ b/services/core/java/com/android/server/notification/GroupHelper.java
@@ -15,6 +15,7 @@
*/
package com.android.server.notification;
+import static android.app.Notification.COLOR_DEFAULT;
import static android.app.Notification.FLAG_AUTOGROUP_SUMMARY;
import static android.app.Notification.FLAG_AUTO_CANCEL;
import static android.app.Notification.FLAG_GROUP_SUMMARY;
@@ -23,15 +24,24 @@
import static android.app.Notification.FLAG_ONGOING_EVENT;
import android.annotation.NonNull;
+import android.app.Notification;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.graphics.drawable.AdaptiveIconDrawable;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.Icon;
import android.service.notification.StatusBarNotification;
import android.util.ArrayMap;
import android.util.Slog;
+import com.android.internal.R;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import java.util.ArrayList;
import java.util.List;
+import java.util.Objects;
/**
* NotificationManagerService helper for auto-grouping notifications.
@@ -41,6 +51,8 @@
protected static final String AUTOGROUP_KEY = "ranker_group";
+ protected static final int FLAG_INVALID = -1;
+
// Flags that all autogroup summaries have
protected static final int BASE_FLAGS =
FLAG_AUTOGROUP_SUMMARY | FLAG_GROUP_SUMMARY | FLAG_LOCAL_ONLY;
@@ -51,17 +63,22 @@
private final Callback mCallback;
private final int mAutoGroupAtCount;
+ private final Context mContext;
+ private final PackageManager mPackageManager;
// Only contains notifications that are not explicitly grouped by the app (aka no group or
// sort key).
// userId|packageName -> (keys of notifications that aren't in an explicit app group -> flags)
@GuardedBy("mUngroupedNotifications")
- private final ArrayMap<String, ArrayMap<String, Integer>> mUngroupedNotifications
+ private final ArrayMap<String, ArrayMap<String, NotificationAttributes>> mUngroupedNotifications
= new ArrayMap<>();
- public GroupHelper(int autoGroupAtCount, Callback callback) {
+ public GroupHelper(Context context, PackageManager packageManager, int autoGroupAtCount,
+ Callback callback) {
mAutoGroupAtCount = autoGroupAtCount;
mCallback = callback;
+ mContext = context;
+ mPackageManager = packageManager;
}
private String generatePackageKey(int userId, String pkg) {
@@ -70,15 +87,16 @@
@VisibleForTesting
@GuardedBy("mUngroupedNotifications")
- protected int getAutogroupSummaryFlags(@NonNull final ArrayMap<String, Integer> children) {
+ protected int getAutogroupSummaryFlags(
+ @NonNull final ArrayMap<String, NotificationAttributes> children) {
boolean allChildrenHasFlag = children.size() > 0;
int anyChildFlagSet = 0;
for (int i = 0; i < children.size(); i++) {
- if (!hasAnyFlag(children.valueAt(i), ALL_CHILDREN_FLAG)) {
+ if (!hasAnyFlag(children.valueAt(i).flags, ALL_CHILDREN_FLAG)) {
allChildrenHasFlag = false;
}
- if (hasAnyFlag(children.valueAt(i), ANY_CHILDREN_FLAGS)) {
- anyChildFlagSet |= (children.valueAt(i) & ANY_CHILDREN_FLAGS);
+ if (hasAnyFlag(children.valueAt(i).flags, ANY_CHILDREN_FLAGS)) {
+ anyChildFlagSet |= (children.valueAt(i).flags & ANY_CHILDREN_FLAGS);
}
}
return BASE_FLAGS | (allChildrenHasFlag ? ALL_CHILDREN_FLAG : 0) | anyChildFlagSet;
@@ -95,7 +113,6 @@
} else {
maybeUngroup(sbn, false, sbn.getUserId());
}
-
} catch (Exception e) {
Slog.e(TAG, "Failure processing new notification", e);
}
@@ -121,25 +138,47 @@
private void maybeGroup(StatusBarNotification sbn, boolean autogroupSummaryExists) {
int flags = 0;
List<String> notificationsToGroup = new ArrayList<>();
+ List<NotificationAttributes> childrenAttr = new ArrayList<>();
synchronized (mUngroupedNotifications) {
String key = generatePackageKey(sbn.getUserId(), sbn.getPackageName());
- final ArrayMap<String, Integer> children =
+ final ArrayMap<String, NotificationAttributes> children =
mUngroupedNotifications.getOrDefault(key, new ArrayMap<>());
- children.put(sbn.getKey(), sbn.getNotification().flags);
+ NotificationAttributes attr = new NotificationAttributes(sbn.getNotification().flags,
+ sbn.getNotification().getSmallIcon(), sbn.getNotification().color);
+ children.put(sbn.getKey(), attr);
mUngroupedNotifications.put(key, children);
if (children.size() >= mAutoGroupAtCount || autogroupSummaryExists) {
flags = getAutogroupSummaryFlags(children);
notificationsToGroup.addAll(children.keySet());
+ childrenAttr.addAll(children.values());
}
}
if (notificationsToGroup.size() > 0) {
if (autogroupSummaryExists) {
- mCallback.updateAutogroupSummary(sbn.getUserId(), sbn.getPackageName(), flags);
+ NotificationAttributes attr = new NotificationAttributes(flags,
+ sbn.getNotification().getSmallIcon(), sbn.getNotification().color);
+ if (Flags.autogroupSummaryIconUpdate()) {
+ attr = updateAutobundledSummaryIcon(sbn.getPackageName(), childrenAttr, attr);
+ }
+
+ mCallback.updateAutogroupSummary(sbn.getUserId(), sbn.getPackageName(), attr);
} else {
- mCallback.addAutoGroupSummary(
- sbn.getUserId(), sbn.getPackageName(), sbn.getKey(), flags);
+ Icon summaryIcon = sbn.getNotification().getSmallIcon();
+ int summaryIconColor = sbn.getNotification().color;
+ if (Flags.autogroupSummaryIconUpdate()) {
+ // Calculate the initial summary icon and icon color
+ NotificationAttributes iconAttr = getAutobundledSummaryIconAndColor(
+ sbn.getPackageName(), childrenAttr);
+ summaryIcon = iconAttr.icon;
+ summaryIconColor = iconAttr.iconColor;
+ }
+
+ NotificationAttributes attr = new NotificationAttributes(flags, summaryIcon,
+ summaryIconColor);
+ mCallback.addAutoGroupSummary(sbn.getUserId(), sbn.getPackageName(), sbn.getKey(),
+ attr);
}
for (String key : notificationsToGroup) {
mCallback.addAutoGroup(key);
@@ -154,16 +193,17 @@
* (b) if we need to remove our autogroup overlay for this notification
* (c) we need to remove the autogroup summary
*
- * And updates the internal state of un-app-grouped notifications and their flags
+ * And updates the internal state of un-app-grouped notifications and their flags.
*/
private void maybeUngroup(StatusBarNotification sbn, boolean notificationGone, int userId) {
boolean removeSummary = false;
- int summaryFlags = 0;
+ int summaryFlags = FLAG_INVALID;
boolean updateSummaryFlags = false;
boolean removeAutogroupOverlay = false;
+ List<NotificationAttributes> childrenAttrs = new ArrayList<>();
synchronized (mUngroupedNotifications) {
String key = generatePackageKey(sbn.getUserId(), sbn.getPackageName());
- final ArrayMap<String, Integer> children =
+ final ArrayMap<String, NotificationAttributes> children =
mUngroupedNotifications.getOrDefault(key, new ArrayMap<>());
if (children.size() == 0) {
return;
@@ -173,7 +213,7 @@
if (children.containsKey(sbn.getKey())) {
// if this notification was contributing flags that aren't covered by other
// children to the summary, reevaluate flags for the summary
- int flags = children.remove(sbn.getKey());
+ int flags = children.remove(sbn.getKey()).flags;
// this
if (hasAnyFlag(flags, ANY_CHILDREN_FLAGS)) {
updateSummaryFlags = true;
@@ -188,14 +228,29 @@
// If there are no more children left to autogroup, remove the summary
if (children.size() == 0) {
removeSummary = true;
+ } else {
+ childrenAttrs.addAll(children.values());
}
}
}
+
if (removeSummary) {
mCallback.removeAutoGroupSummary(userId, sbn.getPackageName());
} else {
- if (updateSummaryFlags) {
- mCallback.updateAutogroupSummary(userId, sbn.getPackageName(), summaryFlags);
+ NotificationAttributes attr = new NotificationAttributes(summaryFlags,
+ sbn.getNotification().getSmallIcon(), sbn.getNotification().color);
+ boolean iconUpdated = false;
+ if (Flags.autogroupSummaryIconUpdate()) {
+ NotificationAttributes newAttr = updateAutobundledSummaryIcon(sbn.getPackageName(),
+ childrenAttrs, attr);
+ if (!newAttr.equals(attr)) {
+ iconUpdated = true;
+ attr = newAttr;
+ }
+ }
+
+ if (updateSummaryFlags || iconUpdated) {
+ mCallback.updateAutogroupSummary(userId, sbn.getPackageName(), attr);
}
}
if (removeAutogroupOverlay) {
@@ -207,17 +262,139 @@
int getNotGroupedByAppCount(int userId, String pkg) {
synchronized (mUngroupedNotifications) {
String key = generatePackageKey(userId, pkg);
- final ArrayMap<String, Integer> children =
+ final ArrayMap<String, NotificationAttributes> children =
mUngroupedNotifications.getOrDefault(key, new ArrayMap<>());
return children.size();
}
}
+ NotificationAttributes getAutobundledSummaryIconAndColor(@NonNull String packageName,
+ @NonNull List<NotificationAttributes> childrenAttr) {
+ Icon newIcon = null;
+ boolean childrenHaveSameIcon = true;
+ int newColor = Notification.COLOR_INVALID;
+ boolean childrenHaveSameColor = true;
+
+ // Both the icon drawable and the icon background color are updated according to this rule:
+ // - if all child icons are identical => use the common icon
+ // - if child icons are different: use the monochromatic app icon, if exists.
+ // Otherwise fall back to a generic icon representing a stack.
+ for (NotificationAttributes state: childrenAttr) {
+ // Check for icon
+ if (newIcon == null) {
+ newIcon = state.icon;
+ } else {
+ if (!newIcon.sameAs(state.icon)) {
+ childrenHaveSameIcon = false;
+ }
+ }
+ // Check for color
+ if (newColor == Notification.COLOR_INVALID) {
+ newColor = state.iconColor;
+ } else {
+ if (newColor != state.iconColor) {
+ childrenHaveSameColor = false;
+ }
+ }
+ }
+ if (!childrenHaveSameIcon) {
+ newIcon = getMonochromeAppIcon(packageName);
+ }
+ if (!childrenHaveSameColor) {
+ newColor = COLOR_DEFAULT;
+ }
+
+ return new NotificationAttributes(0, newIcon, newColor);
+ }
+
+ NotificationAttributes updateAutobundledSummaryIcon(@NonNull String packageName,
+ @NonNull List<NotificationAttributes> childrenAttr,
+ @NonNull NotificationAttributes oldAttr) {
+ NotificationAttributes newAttr = getAutobundledSummaryIconAndColor(packageName,
+ childrenAttr);
+ Icon newIcon = newAttr.icon;
+ int newColor = newAttr.iconColor;
+ if (newAttr.icon == null) {
+ newIcon = oldAttr.icon;
+ }
+ if (newAttr.iconColor == Notification.COLOR_INVALID) {
+ newColor = oldAttr.iconColor;
+ }
+
+ return new NotificationAttributes(oldAttr.flags, newIcon, newColor);
+ }
+
+ /**
+ * Get the monochrome app icon for an app from the adaptive launcher icon
+ * or a fallback generic icon for autogroup summaries.
+ *
+ * @param pkg packageName of the app
+ * @return a monochrome app icon or a fallback generic icon
+ */
+ @NonNull
+ Icon getMonochromeAppIcon(@NonNull final String pkg) {
+ Icon monochromeIcon = null;
+ final int fallbackIconResId = R.drawable.ic_notification_summary_auto;
+ try {
+ final Drawable appIcon = mPackageManager.getApplicationIcon(pkg);
+ if (appIcon instanceof AdaptiveIconDrawable) {
+ if (((AdaptiveIconDrawable) appIcon).getMonochrome() != null) {
+ monochromeIcon = Icon.createWithResourceAdaptiveDrawable(pkg,
+ ((AdaptiveIconDrawable) appIcon).getSourceDrawableResId(), true,
+ -2.0f * AdaptiveIconDrawable.getExtraInsetFraction());
+ }
+ }
+ } catch (NameNotFoundException e) {
+ Slog.e(TAG, "Failed to getApplicationIcon() in getMonochromeAppIcon()", e);
+ }
+ if (monochromeIcon != null) {
+ return monochromeIcon;
+ } else {
+ return Icon.createWithResource(mContext, fallbackIconResId);
+ }
+ }
+
+ protected static class NotificationAttributes {
+ public final int flags;
+ public final int iconColor;
+ public final Icon icon;
+
+ public NotificationAttributes(int flags, Icon icon, int iconColor) {
+ this.flags = flags;
+ this.icon = icon;
+ this.iconColor = iconColor;
+ }
+
+ public NotificationAttributes(@NonNull NotificationAttributes attr) {
+ this.flags = attr.flags;
+ this.icon = attr.icon;
+ this.iconColor = attr.iconColor;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (!(o instanceof NotificationAttributes that)) {
+ return false;
+ }
+ return flags == that.flags && iconColor == that.iconColor && icon.sameAs(that.icon);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(flags, iconColor, icon);
+ }
+ }
+
protected interface Callback {
void addAutoGroup(String key);
void removeAutoGroup(String key);
- void addAutoGroupSummary(int userId, String pkg, String triggeringKey, int flags);
+
+ void addAutoGroupSummary(int userId, String pkg, String triggeringKey,
+ NotificationAttributes summaryAttr);
void removeAutoGroupSummary(int user, String pkg);
- void updateAutogroupSummary(int userId, String pkg, int flags);
+ void updateAutogroupSummary(int userId, String pkg, NotificationAttributes summaryAttr);
}
}
diff --git a/services/core/java/com/android/server/notification/NotificationAttentionHelper.java b/services/core/java/com/android/server/notification/NotificationAttentionHelper.java
index f852b81..097daf2 100644
--- a/services/core/java/com/android/server/notification/NotificationAttentionHelper.java
+++ b/services/core/java/com/android/server/notification/NotificationAttentionHelper.java
@@ -97,7 +97,7 @@
private static final float DEFAULT_VOLUME = 1.0f;
// TODO (b/291899544): remove for release
private static final int DEFAULT_NOTIFICATION_COOLDOWN_ENABLED = 1;
- private static final int DEFAULT_NOTIFICATION_COOLDOWN_ENABLED_FOR_WORK = 0;
+ private static final int DEFAULT_NOTIFICATION_COOLDOWN_ENABLED_FOR_WORK = 1;
private static final int DEFAULT_NOTIFICATION_COOLDOWN_ALL = 1;
private static final int DEFAULT_NOTIFICATION_COOLDOWN_VIBRATE_UNLOCKED = 0;
@@ -1405,6 +1405,7 @@
long timestampMillis) {
super.setLastNotificationUpdateTimeMs(record, timestampMillis);
mLastNotificationTimestamp = timestampMillis;
+ mAppStrategy.setLastNotificationUpdateTimeMs(record, timestampMillis);
}
long getLastNotificationUpdateTimeMs(final NotificationRecord record) {
diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java
index 1c9bbab..7dbe880 100755
--- a/services/core/java/com/android/server/notification/NotificationManagerService.java
+++ b/services/core/java/com/android/server/notification/NotificationManagerService.java
@@ -227,6 +227,7 @@
import android.content.pm.VersionedPackage;
import android.content.res.Resources;
import android.database.ContentObserver;
+import android.graphics.drawable.Icon;
import android.media.AudioAttributes;
import android.media.AudioManager;
import android.media.AudioManagerInternal;
@@ -341,6 +342,7 @@
import com.android.server.job.JobSchedulerInternal;
import com.android.server.lights.LightsManager;
import com.android.server.lights.LogicalLight;
+import com.android.server.notification.GroupHelper.NotificationAttributes;
import com.android.server.notification.ManagedServices.ManagedServiceInfo;
import com.android.server.notification.ManagedServices.UserProfiles;
import com.android.server.notification.toast.CustomToastRecord;
@@ -725,7 +727,7 @@
private static final int MY_UID = Process.myUid();
private static final int MY_PID = Process.myPid();
- private static final IBinder ALLOWLIST_TOKEN = new Binder();
+ static final IBinder ALLOWLIST_TOKEN = new Binder();
protected RankingHandler mRankingHandler;
private long mLastOverRateLogTime;
private float mMaxPackageEnqueueRate = DEFAULT_MAX_NOTIFICATION_ENQUEUE_RATE;
@@ -996,17 +998,19 @@
}
/**
- * This method will update the flags of the summary.
+ * This method will update the flags and/or the icon of the summary.
* It will set it to FLAG_ONGOING_EVENT if any of its group members
- * has the same flag. It will delete the flag otherwise
+ * has the same flag. It will delete the flag otherwise.
+ * It will update the summary notification icon if the group children's
+ * icons are different.
* @param userId user id of the autogroup summary
* @param pkg package of the autogroup summary
- * @param flags the new flags for this summary
+ * @param summaryAttr the new flags and/or icon & color for this summary
* @param isAppForeground true if the app is currently in the foreground.
*/
@GuardedBy("mNotificationLock")
- protected void updateAutobundledSummaryFlags(int userId, String pkg, int flags,
- boolean isAppForeground) {
+ protected void updateAutobundledSummaryLocked(int userId, String pkg,
+ NotificationAttributes summaryAttr, boolean isAppForeground) {
ArrayMap<String, String> summaries = mAutobundledSummaries.get(userId);
if (summaries == null) {
return;
@@ -1020,8 +1024,16 @@
return;
}
int oldFlags = summary.getSbn().getNotification().flags;
- if (oldFlags != flags) {
- summary.getSbn().getNotification().flags = flags;
+
+ boolean iconUpdated =
+ !summaryAttr.icon.sameAs(summary.getSbn().getNotification().getSmallIcon())
+ || summaryAttr.iconColor != summary.getSbn().getNotification().color;
+
+ if (oldFlags != summaryAttr.flags || iconUpdated) {
+ summary.getSbn().getNotification().flags =
+ summaryAttr.flags != GroupHelper.FLAG_INVALID ? summaryAttr.flags : oldFlags;
+ summary.getSbn().getNotification().setSmallIcon(summaryAttr.icon);
+ summary.getSbn().getNotification().color = summaryAttr.iconColor;
mHandler.post(new EnqueueNotificationRunnable(userId, summary, isAppForeground,
mPostNotificationTrackerFactory.newTracker(null)));
}
@@ -2873,7 +2885,8 @@
private GroupHelper getGroupHelper() {
mAutoGroupAtCount =
getContext().getResources().getInteger(R.integer.config_autoGroupAtCount);
- return new GroupHelper(mAutoGroupAtCount, new GroupHelper.Callback() {
+ return new GroupHelper(getContext(), getContext().getPackageManager(),
+ mAutoGroupAtCount, new GroupHelper.Callback() {
@Override
public void addAutoGroup(String key) {
synchronized (mNotificationLock) {
@@ -2890,8 +2903,9 @@
@Override
public void addAutoGroupSummary(int userId, String pkg, String triggeringKey,
- int flags) {
- NotificationRecord r = createAutoGroupSummary(userId, pkg, triggeringKey, flags);
+ NotificationAttributes summaryAttr) {
+ NotificationRecord r = createAutoGroupSummary(userId, pkg, triggeringKey,
+ summaryAttr.flags, summaryAttr.icon, summaryAttr.iconColor);
if (r != null) {
final boolean isAppForeground =
mActivityManager.getPackageImportance(pkg) == IMPORTANCE_FOREGROUND;
@@ -2908,11 +2922,12 @@
}
@Override
- public void updateAutogroupSummary(int userId, String pkg, int flags) {
+ public void updateAutogroupSummary(int userId, String pkg,
+ NotificationAttributes summaryAttr) {
boolean isAppForeground = pkg != null
&& mActivityManager.getPackageImportance(pkg) == IMPORTANCE_FOREGROUND;
synchronized (mNotificationLock) {
- updateAutobundledSummaryFlags(userId, pkg, flags, isAppForeground);
+ updateAutobundledSummaryLocked(userId, pkg, summaryAttr, isAppForeground);
}
}
});
@@ -4744,7 +4759,7 @@
// Remove background token before returning notification to untrusted app, this
// ensures the app isn't able to perform background operations that are
// associated with notification interactions.
- notification.clearAllowlistToken();
+ notification.overrideAllowlistToken(null);
return new StatusBarNotification(
sbn.getPackageName(),
sbn.getOpPkg(),
@@ -4880,14 +4895,14 @@
continue;
}
notificationsRapidlyCleared = notificationsRapidlyCleared
- || isNotificationRecent(r);
+ || isNotificationRecent(r.getUpdateTimeMs());
cancelNotificationFromListenerLocked(info, callingUid, callingPid,
r.getSbn().getPackageName(), r.getSbn().getTag(),
r.getSbn().getId(), userId, reason);
}
} else {
for (NotificationRecord notificationRecord : mNotificationList) {
- if (isNotificationRecent(notificationRecord)) {
+ if (isNotificationRecent(notificationRecord.getUpdateTimeMs())) {
notificationsRapidlyCleared = true;
break;
}
@@ -4913,14 +4928,6 @@
}
}
- private boolean isNotificationRecent(@NonNull NotificationRecord notificationRecord) {
- if (!rapidClearNotificationsByListenerAppOpEnabled()) {
- return false;
- }
- return notificationRecord.getFreshnessMs(System.currentTimeMillis())
- < NOTIFICATION_RAPID_CLEAR_THRESHOLD_MS;
- }
-
/**
* Handle request from an approved listener to re-enable itself.
*
@@ -5044,12 +5051,11 @@
@Override
public void snoozeNotificationUntilContextFromListener(INotificationListener token,
String key, String snoozeCriterionId) {
+ final int callingUid = Binder.getCallingUid();
final long identity = Binder.clearCallingIdentity();
try {
- synchronized (mNotificationLock) {
- final ManagedServiceInfo info = mListeners.checkServiceTokenLocked(token);
- snoozeNotificationInt(key, SNOOZE_UNTIL_UNSPECIFIED, snoozeCriterionId, info);
- }
+ snoozeNotificationInt(callingUid, token, key, SNOOZE_UNTIL_UNSPECIFIED,
+ snoozeCriterionId);
} finally {
Binder.restoreCallingIdentity(identity);
}
@@ -5063,12 +5069,10 @@
@Override
public void snoozeNotificationUntilFromListener(INotificationListener token, String key,
long duration) {
+ final int callingUid = Binder.getCallingUid();
final long identity = Binder.clearCallingIdentity();
try {
- synchronized (mNotificationLock) {
- final ManagedServiceInfo info = mListeners.checkServiceTokenLocked(token);
- snoozeNotificationInt(key, duration, null, info);
- }
+ snoozeNotificationInt(callingUid, token, key, duration, null);
} finally {
Binder.restoreCallingIdentity(identity);
}
@@ -6529,7 +6533,7 @@
// Creates a 'fake' summary for a package that has exceeded the solo-notification limit.
NotificationRecord createAutoGroupSummary(int userId, String pkg, String triggeringKey,
- int flagsToSet) {
+ int flagsToSet, Icon summaryIcon, int summaryIconColor) {
NotificationRecord summaryRecord = null;
boolean isPermissionFixed = mPermissionHelper.isPermissionFixed(pkg, userId);
synchronized (mNotificationLock) {
@@ -6555,14 +6559,15 @@
final Bundle extras = new Bundle();
extras.putParcelable(Notification.EXTRA_BUILDER_APPLICATION_INFO, appInfo);
final String channelId = notificationRecord.getChannel().getId();
+
final Notification summaryNotification =
- new Notification.Builder(getContext(), channelId)
- .setSmallIcon(adjustedSbn.getNotification().getSmallIcon())
+ new Notification.Builder(getContext(), channelId)
+ .setSmallIcon(summaryIcon)
.setGroupSummary(true)
.setGroupAlertBehavior(Notification.GROUP_ALERT_CHILDREN)
.setGroup(GroupHelper.AUTOGROUP_KEY)
.setFlag(flagsToSet, true)
- .setColor(adjustedSbn.getNotification().color)
+ .setColor(summaryIconColor)
.build();
summaryNotification.extras.putAll(extras);
Intent appIntent = getContext().getPackageManager().getLaunchIntentForPackage(pkg);
@@ -7618,6 +7623,8 @@
}
}
+ notification.overrideAllowlistToken(ALLOWLIST_TOKEN);
+
// Remote views? Are they too big?
checkRemoteViews(pkg, tag, id, notification);
}
@@ -7955,8 +7962,8 @@
&& mTelecomManager != null) {
try {
return mTelecomManager.isInManagedCall()
- || mTelecomManager.isInSelfManagedCall(
- pkg, UserHandle.getUserHandleForUid(uid));
+ || mTelecomManager.isInSelfManagedCall(pkg,
+ UserHandle.getUserHandleForUid(uid), /* hasCrossUserAccess */ true);
} catch (IllegalStateException ise) {
// Telecom is not ready (this is likely early boot), so there are no calls.
return false;
@@ -8844,19 +8851,26 @@
}
}
+ private PendingIntent getNotificationTimeoutPendingIntent(NotificationRecord record,
+ int flags) {
+ flags |= PendingIntent.FLAG_IMMUTABLE;
+ return PendingIntent.getBroadcast(getContext(),
+ REQUEST_CODE_TIMEOUT,
+ new Intent(ACTION_NOTIFICATION_TIMEOUT)
+ .setPackage(PackageManagerService.PLATFORM_PACKAGE_NAME)
+ .setData(new Uri.Builder().scheme(SCHEME_TIMEOUT)
+ .appendPath(record.getKey()).build())
+ .addFlags(Intent.FLAG_RECEIVER_FOREGROUND)
+ .putExtra(EXTRA_KEY, record.getKey()),
+ flags);
+ }
+
@VisibleForTesting
@GuardedBy("mNotificationLock")
void scheduleTimeoutLocked(NotificationRecord record) {
if (record.getNotification().getTimeoutAfter() > 0) {
- final PendingIntent pi = PendingIntent.getBroadcast(getContext(),
- REQUEST_CODE_TIMEOUT,
- new Intent(ACTION_NOTIFICATION_TIMEOUT)
- .setPackage(PackageManagerService.PLATFORM_PACKAGE_NAME)
- .setData(new Uri.Builder().scheme(SCHEME_TIMEOUT)
- .appendPath(record.getKey()).build())
- .addFlags(Intent.FLAG_RECEIVER_FOREGROUND)
- .putExtra(EXTRA_KEY, record.getKey()),
- PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
+ final PendingIntent pi = getNotificationTimeoutPendingIntent(
+ record, PendingIntent.FLAG_UPDATE_CURRENT);
mAlarmManager.setExactAndAllowWhileIdle(AlarmManager.ELAPSED_REALTIME_WAKEUP,
SystemClock.elapsedRealtime() + record.getNotification().getTimeoutAfter(), pi);
}
@@ -8864,6 +8878,16 @@
@VisibleForTesting
@GuardedBy("mNotificationLock")
+ void cancelScheduledTimeoutLocked(NotificationRecord record) {
+ final PendingIntent pi = getNotificationTimeoutPendingIntent(
+ record, PendingIntent.FLAG_CANCEL_CURRENT);
+ if (pi != null) {
+ mAlarmManager.cancel(pi);
+ }
+ }
+
+ @VisibleForTesting
+ @GuardedBy("mNotificationLock")
/**
* Determine whether this notification should attempt to make noise, vibrate, or flash the LED
* @return buzzBeepBlink - bitfield (buzz ? 1 : 0) | (beep ? 2 : 0) | (blink ? 4 : 0)
@@ -9894,21 +9918,7 @@
int rank, int count, boolean wasPosted, String listenerName,
@ElapsedRealtimeLong long cancellationElapsedTimeMs) {
final String canceledKey = r.getKey();
-
- // Get pending intent used to create alarm, use FLAG_NO_CREATE if PendingIntent
- // does not already exist, then null will be returned.
- final PendingIntent pi = PendingIntent.getBroadcast(getContext(),
- REQUEST_CODE_TIMEOUT,
- new Intent(ACTION_NOTIFICATION_TIMEOUT)
- .setData(new Uri.Builder().scheme(SCHEME_TIMEOUT)
- .appendPath(r.getKey()).build())
- .addFlags(Intent.FLAG_RECEIVER_FOREGROUND),
- PendingIntent.FLAG_NO_CREATE | PendingIntent.FLAG_IMMUTABLE);
-
- // Cancel alarm corresponding to pi.
- if (pi != null) {
- mAlarmManager.cancel(pi);
- }
+ cancelScheduledTimeoutLocked(r);
// Record caller.
recordCallerLocked(r);
@@ -10323,16 +10333,22 @@
}
}
- void snoozeNotificationInt(String key, long duration, String snoozeCriterionId,
- ManagedServiceInfo listener) {
- if (listener == null) {
- return;
- }
- String listenerName = listener.component.toShortString();
- if ((duration <= 0 && snoozeCriterionId == null) || key == null) {
- return;
- }
+ void snoozeNotificationInt(int callingUid, INotificationListener token, String key,
+ long duration, String snoozeCriterionId) {
+ final String packageName;
+ final long notificationUpdateTimeMs;
+
synchronized (mNotificationLock) {
+ final ManagedServiceInfo listener = mListeners.checkServiceTokenLocked(token);
+ if (listener == null) {
+ return;
+ }
+ packageName = listener.component.getPackageName();
+ String listenerName = listener.component.toShortString();
+ if ((duration <= 0 && snoozeCriterionId == null) || key == null) {
+ return;
+ }
+
final NotificationRecord r = findInCurrentAndSnoozedNotificationByKeyLocked(key);
if (r == null) {
return;
@@ -10340,14 +10356,20 @@
if (!listener.enabledAndUserMatches(r.getSbn().getNormalizedUserId())){
return;
}
+ notificationUpdateTimeMs = r.getUpdateTimeMs();
+
+ if (DBG) {
+ Slog.d(TAG, String.format("snooze event(%s, %d, %s, %s)", key, duration,
+ snoozeCriterionId, listenerName));
+ }
+ // Needs to post so that it can cancel notifications not yet enqueued.
+ mHandler.post(new SnoozeNotificationRunnable(key, duration, snoozeCriterionId));
}
- if (DBG) {
- Slog.d(TAG, String.format("snooze event(%s, %d, %s, %s)", key, duration,
- snoozeCriterionId, listenerName));
+ if (isNotificationRecent(notificationUpdateTimeMs)) {
+ mAppOps.noteOpNoThrow(AppOpsManager.OP_RAPID_CLEAR_NOTIFICATIONS_BY_LISTENER,
+ callingUid, packageName, /* attributionTag= */ null, /* message= */ null);
}
- // Needs to post so that it can cancel notifications not yet enqueued.
- mHandler.post(new SnoozeNotificationRunnable(key, duration, snoozeCriterionId));
}
void unsnoozeNotificationInt(String key, ManagedServiceInfo listener, boolean muteOnReturn) {
@@ -10359,6 +10381,14 @@
handleSavePolicyFile();
}
+ private boolean isNotificationRecent(long notificationUpdateTimeMs) {
+ if (!rapidClearNotificationsByListenerAppOpEnabled()) {
+ return false;
+ }
+ return System.currentTimeMillis() - notificationUpdateTimeMs
+ < NOTIFICATION_RAPID_CLEAR_THRESHOLD_MS;
+ }
+
@GuardedBy("mNotificationLock")
void cancelAllLocked(int callingUid, int callingPid, int userId, int reason,
ManagedServiceInfo listener, boolean includeCurrentProfiles, int mustNotHaveFlags) {
diff --git a/services/core/java/com/android/server/notification/NotificationRecord.java b/services/core/java/com/android/server/notification/NotificationRecord.java
index 1786ac5..6ab4b99 100644
--- a/services/core/java/com/android/server/notification/NotificationRecord.java
+++ b/services/core/java/com/android/server/notification/NotificationRecord.java
@@ -308,31 +308,43 @@
return light;
}
+ private VibrationEffect getVibrationForChannel(
+ NotificationChannel channel, VibratorHelper helper, boolean insistent) {
+ if (!channel.shouldVibrate()) {
+ return null;
+ }
+
+ if (Flags.notificationChannelVibrationEffectApi()) {
+ final VibrationEffect vibration = channel.getVibrationEffect();
+ if (vibration != null && helper.areEffectComponentsSupported(vibration)) {
+ // Adjust the vibration's repeat behavior based on the `insistent` property.
+ return vibration.applyRepeatingIndefinitely(insistent, /* loopDelayMs= */ 0);
+ }
+ }
+
+ final long[] vibrationPattern = channel.getVibrationPattern();
+ if (vibrationPattern == null) {
+ return helper.createDefaultVibration(insistent);
+ }
+ return helper.createWaveformVibration(vibrationPattern, insistent);
+ }
+
private VibrationEffect calculateVibration() {
VibratorHelper helper = new VibratorHelper(mContext);
final Notification notification = getSbn().getNotification();
final boolean insistent = (notification.flags & Notification.FLAG_INSISTENT) != 0;
- VibrationEffect defaultVibration = helper.createDefaultVibration(insistent);
- VibrationEffect vibration;
- if (getChannel().shouldVibrate()) {
- vibration = getChannel().getVibrationPattern() == null
- ? defaultVibration
- : helper.createWaveformVibration(getChannel().getVibrationPattern(), insistent);
- } else {
- vibration = null;
- }
+
if (mPreChannelsNotification
&& (getChannel().getUserLockedFields()
& NotificationChannel.USER_LOCKED_VIBRATION) == 0) {
final boolean useDefaultVibrate =
(notification.defaults & Notification.DEFAULT_VIBRATE) != 0;
if (useDefaultVibrate) {
- vibration = defaultVibration;
- } else {
- vibration = helper.createWaveformVibration(notification.vibrate, insistent);
+ return helper.createDefaultVibration(insistent);
}
+ return helper.createWaveformVibration(notification.vibrate, insistent);
}
- return vibration;
+ return getVibrationForChannel(getChannel(), helper, insistent);
}
private @NonNull AudioAttributes calculateAttributes() {
diff --git a/services/core/java/com/android/server/notification/PreferencesHelper.java b/services/core/java/com/android/server/notification/PreferencesHelper.java
index 1bafcfe..4f3cdbc 100644
--- a/services/core/java/com/android/server/notification/PreferencesHelper.java
+++ b/services/core/java/com/android/server/notification/PreferencesHelper.java
@@ -1212,6 +1212,8 @@
if ((conversation.getUserLockedFields() & NotificationChannel.USER_LOCKED_VIBRATION) == 0
&& (!Arrays.equals(oldParent.getVibrationPattern(),
updatedParent.getVibrationPattern())
+ || !Objects.equals(
+ oldParent.getVibrationEffect(), updatedParent.getVibrationEffect())
|| oldParent.shouldVibrate() != updatedParent.shouldVibrate())) {
// enableVibration must be 2nd because setVibrationPattern may toggle it.
conversation.setVibrationPattern(updatedParent.getVibrationPattern());
@@ -1972,6 +1974,7 @@
update.lockFields(NotificationChannel.USER_LOCKED_SOUND);
}
if (!Arrays.equals(original.getVibrationPattern(), update.getVibrationPattern())
+ || !Objects.equals(original.getVibrationEffect(), update.getVibrationEffect())
|| original.shouldVibrate() != update.shouldVibrate()) {
update.lockFields(NotificationChannel.USER_LOCKED_VIBRATION);
}
diff --git a/services/core/java/com/android/server/notification/VibratorHelper.java b/services/core/java/com/android/server/notification/VibratorHelper.java
index 7204d05..8a0e595 100644
--- a/services/core/java/com/android/server/notification/VibratorHelper.java
+++ b/services/core/java/com/android/server/notification/VibratorHelper.java
@@ -193,6 +193,11 @@
return createWaveformVibration(mDefaultPattern, insistent);
}
+ /** Returns if a given vibration can be played by the vibrator that does notification buzz. */
+ public boolean areEffectComponentsSupported(VibrationEffect effect) {
+ return mVibrator.areVibrationFeaturesSupported(effect);
+ }
+
@Nullable
private static float[] getFloatArray(Resources resources, int resId) {
TypedArray array = resources.obtainTypedArray(resId);
diff --git a/services/core/java/com/android/server/notification/flags.aconfig b/services/core/java/com/android/server/notification/flags.aconfig
index dbff442..722654a 100644
--- a/services/core/java/com/android/server/notification/flags.aconfig
+++ b/services/core/java/com/android/server/notification/flags.aconfig
@@ -43,6 +43,13 @@
}
flag {
+ name: "screenshare_notification_hiding"
+ namespace: "systemui"
+ description: "Enable hiding of notifications during screenshare"
+ bug: "312784809"
+}
+
+flag {
name: "sensitive_notification_app_protection"
namespace: "systemui"
description: "This flag controls the sensitive notification app protections while screen sharing"
diff --git a/services/core/java/com/android/server/pm/BackgroundInstallControlCallbackHelper.java b/services/core/java/com/android/server/pm/BackgroundInstallControlCallbackHelper.java
new file mode 100644
index 0000000..1553618
--- /dev/null
+++ b/services/core/java/com/android/server/pm/BackgroundInstallControlCallbackHelper.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright (C) 2024 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.pm;
+
+import static android.os.Process.THREAD_PRIORITY_BACKGROUND;
+
+import android.annotation.NonNull;
+import android.app.BackgroundInstallControlManager;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.IRemoteCallback;
+import android.os.RemoteCallbackList;
+import android.os.RemoteException;
+import android.util.Slog;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.ServiceThread;
+
+public class BackgroundInstallControlCallbackHelper {
+
+ @VisibleForTesting static final String FLAGGED_PACKAGE_NAME_KEY = "packageName";
+ @VisibleForTesting static final String FLAGGED_USER_ID_KEY = "userId";
+ private static final String TAG = "BackgroundInstallControlCallbackHelper";
+
+ private final Handler mHandler;
+
+ BackgroundInstallControlCallbackHelper() {
+ HandlerThread backgroundThread =
+ new ServiceThread(
+ "BackgroundInstallControlCallbackHelperBg",
+ THREAD_PRIORITY_BACKGROUND,
+ true);
+ backgroundThread.start();
+ mHandler = new Handler(backgroundThread.getLooper());
+ }
+
+ @NonNull @VisibleForTesting
+ final RemoteCallbackList<IRemoteCallback> mCallbacks = new RemoteCallbackList<>();
+
+ /** Registers callback that gets invoked upon detection of an MBA
+ *
+ * NOTE: The callback is user context agnostic and currently broadcasts to all users of other
+ * users app installs. This is fine because the API is for SystemServer use only.
+ */
+ public void registerBackgroundInstallCallback(IRemoteCallback callback) {
+ synchronized (mCallbacks) {
+ mCallbacks.register(callback, null);
+ }
+ }
+
+ /** Unregisters callback */
+ public void unregisterBackgroundInstallCallback(IRemoteCallback callback) {
+ synchronized (mCallbacks) {
+ mCallbacks.unregister(callback);
+ }
+ }
+
+ /**
+ * Invokes all registered callbacks Callbacks are processed through user provided-threads and
+ * parameters are passed in via {@link BackgroundInstallControlManager} InstallEvent
+ */
+ public void notifyAllCallbacks(int userId, String packageName) {
+ Bundle extras = new Bundle();
+ extras.putCharSequence(FLAGGED_PACKAGE_NAME_KEY, packageName);
+ extras.putInt(FLAGGED_USER_ID_KEY, userId);
+ synchronized (mCallbacks) {
+ mHandler.post(
+ () ->
+ mCallbacks.broadcast(
+ callback -> {
+ try {
+ callback.sendResult(extras);
+ } catch (RemoteException e) {
+ Slog.e(
+ TAG,
+ "error detected: " + e.getLocalizedMessage(),
+ e);
+ }
+ }));
+ }
+ }
+}
diff --git a/services/core/java/com/android/server/pm/BackgroundInstallControlService.java b/services/core/java/com/android/server/pm/BackgroundInstallControlService.java
index 200b17b..3468081 100644
--- a/services/core/java/com/android/server/pm/BackgroundInstallControlService.java
+++ b/services/core/java/com/android/server/pm/BackgroundInstallControlService.java
@@ -36,6 +36,7 @@
import android.os.Build;
import android.os.Environment;
import android.os.Handler;
+import android.os.IRemoteCallback;
import android.os.Looper;
import android.os.Message;
import android.os.SystemClock;
@@ -93,6 +94,8 @@
private final File mDiskFile;
private final Context mContext;
+ private final BackgroundInstallControlCallbackHelper mCallbackHelper;
+
private SparseSetArray<String> mBackgroundInstalledPackages = null;
// User ID -> package name -> set of foreground time frame
@@ -112,6 +115,7 @@
mHandler = new EventHandler(injector.getLooper(), this);
mDiskFile = injector.getDiskFile();
mContext = injector.getContext();
+ mCallbackHelper = injector.getBackgroundInstallControlCallbackHelper();
UsageStatsManagerInternal usageStatsManagerInternal =
injector.getUsageStatsManagerInternal();
usageStatsManagerInternal.registerListener(
@@ -150,6 +154,16 @@
}
}
+ @Override
+ public void registerBackgroundInstallCallback(IRemoteCallback callback) {
+ mService.mCallbackHelper.registerBackgroundInstallCallback(callback);
+ }
+
+ @Override
+ public void unregisterBackgroundInstallCallback(IRemoteCallback callback) {
+ mService.mCallbackHelper.unregisterBackgroundInstallCallback(callback);
+ }
+
}
@RequiresPermission(GET_BACKGROUND_INSTALLED_PACKAGES)
@@ -274,6 +288,7 @@
initBackgroundInstalledPackages();
mBackgroundInstalledPackages.add(userId, packageName);
+ mCallbackHelper.notifyAllCallbacks(userId, packageName);
writeBackgroundInstalledPackagesToDisk();
}
@@ -568,6 +583,8 @@
File getDiskFile();
+ BackgroundInstallControlCallbackHelper getBackgroundInstallControlCallbackHelper();
+
}
private static final class InjectorImpl implements Injector {
@@ -617,5 +634,10 @@
File file = new File(dir, DISK_FILE_NAME);
return file;
}
+
+ @Override
+ public BackgroundInstallControlCallbackHelper getBackgroundInstallControlCallbackHelper() {
+ return new BackgroundInstallControlCallbackHelper();
+ }
}
}
diff --git a/services/core/java/com/android/server/pm/ComputerEngine.java b/services/core/java/com/android/server/pm/ComputerEngine.java
index 0be8e6e..ac89fecc 100644
--- a/services/core/java/com/android/server/pm/ComputerEngine.java
+++ b/services/core/java/com/android/server/pm/ComputerEngine.java
@@ -3438,26 +3438,33 @@
}
}
} else {
- if (allowSetMutation) {
- Slog.i(TAG,
- "Result set changed, dropping preferred activity "
- + "for " + intent + " type "
- + resolvedType);
- if (DEBUG_PREFERRED) {
- Slog.v(TAG,
- "Removing preferred activity since set changed "
- + pa.mPref.mComponent);
+ final boolean isHomeActivity = ACTION_MAIN.equals(intent.getAction())
+ && intent.hasCategory(CATEGORY_HOME);
+ if (!Flags.improveHomeAppBehavior() || !isHomeActivity) {
+ // Don't reset the preferred activity just for the home intent, we
+ // should respect the default home app even though there any new
+ // home activity is enabled.
+ if (allowSetMutation) {
+ Slog.i(TAG,
+ "Result set changed, dropping preferred activity "
+ + "for " + intent + " type "
+ + resolvedType);
+ if (DEBUG_PREFERRED) {
+ Slog.v(TAG,
+ "Removing preferred activity since set changed "
+ + pa.mPref.mComponent);
+ }
+ pir.removeFilter(pa);
+ // Re-add the filter as a "last chosen" entry (!always)
+ PreferredActivity lastChosen = new PreferredActivity(
+ pa, pa.mPref.mMatch, null, pa.mPref.mComponent,
+ false);
+ pir.addFilter(this, lastChosen);
+ result.mChanged = true;
}
- pir.removeFilter(pa);
- // Re-add the filter as a "last chosen" entry (!always)
- PreferredActivity lastChosen = new PreferredActivity(
- pa, pa.mPref.mMatch, null, pa.mPref.mComponent,
- false);
- pir.addFilter(this, lastChosen);
- result.mChanged = true;
+ result.mPreferredResolveInfo = null;
+ return result;
}
- result.mPreferredResolveInfo = null;
- return result;
}
}
diff --git a/services/core/java/com/android/server/pm/CrossProfileAppsServiceImpl.java b/services/core/java/com/android/server/pm/CrossProfileAppsServiceImpl.java
index f1dc284..b9aa28e 100644
--- a/services/core/java/com/android/server/pm/CrossProfileAppsServiceImpl.java
+++ b/services/core/java/com/android/server/pm/CrossProfileAppsServiceImpl.java
@@ -49,6 +49,7 @@
import android.content.pm.PackageManager;
import android.content.pm.PackageManagerInternal;
import android.content.pm.ResolveInfo;
+import android.content.pm.UserProperties;
import android.os.Binder;
import android.os.Bundle;
import android.os.IBinder;
@@ -268,7 +269,8 @@
private boolean canRequestInteractAcrossProfilesUnchecked(String packageName) {
final int callingUserId = mInjector.getCallingUserId();
final int[] enabledProfileIds =
- mInjector.getUserManager().getEnabledProfileIds(callingUserId);
+ mInjector.getUserManager().getProfileIdsExcludingHidden(
+ callingUserId, /* enabled= */ true);
if (enabledProfileIds.length < 2) {
return false;
}
@@ -350,7 +352,8 @@
String packageName, @UserIdInt int userId) {
return mInjector.withCleanCallingIdentity(() -> {
final int[] enabledProfileIds =
- mInjector.getUserManager().getEnabledProfileIds(userId);
+ mInjector.getUserManager().getProfileIdsExcludingHidden(userId, /* enabled= */
+ true);
List<UserHandle> targetProfiles = new ArrayList<>();
for (final int profileId : enabledProfileIds) {
@@ -466,7 +469,8 @@
return;
}
final int[] profileIds =
- mInjector.getUserManager().getProfileIds(userId, /* enabledOnly= */ false);
+ mInjector.getUserManager().getProfileIdsExcludingHidden(userId, /* enabled= */
+ false);
for (int profileId : profileIds) {
if (!isPackageInstalled(packageName, profileId)) {
continue;
@@ -632,7 +636,8 @@
private boolean canUserAttemptToConfigureInteractAcrossProfiles(
String packageName, @UserIdInt int userId) {
final int[] profileIds =
- mInjector.getUserManager().getProfileIds(userId, /* enabledOnly= */ false);
+ mInjector.getUserManager().getProfileIdsExcludingHidden(userId, /* enabled= */
+ false);
if (profileIds.length < 2) {
return false;
}
@@ -676,7 +681,8 @@
private boolean hasOtherProfileWithPackageInstalled(String packageName, @UserIdInt int userId) {
return mInjector.withCleanCallingIdentity(() -> {
final int[] profileIds =
- mInjector.getUserManager().getProfileIds(userId, /* enabledOnly= */ false);
+ mInjector.getUserManager().getProfileIdsExcludingHidden(userId, /* enabled= */
+ false);
for (int profileId : profileIds) {
if (profileId != userId && isPackageInstalled(packageName, profileId)) {
return true;
diff --git a/services/core/java/com/android/server/pm/InstallPackageHelper.java b/services/core/java/com/android/server/pm/InstallPackageHelper.java
index 28f3d59..db5acc2 100644
--- a/services/core/java/com/android/server/pm/InstallPackageHelper.java
+++ b/services/core/java/com/android/server/pm/InstallPackageHelper.java
@@ -592,9 +592,11 @@
mPm.addAllPackageProperties(pkg);
if (oldPkgSetting == null || oldPkgSetting.getPkg() == null) {
- mPm.mDomainVerificationManager.addPackage(pkgSetting);
+ mPm.mDomainVerificationManager.addPackage(pkgSetting,
+ request.getPreVerifiedDomains());
} else {
- mPm.mDomainVerificationManager.migrateState(oldPkgSetting, pkgSetting);
+ mPm.mDomainVerificationManager.migrateState(oldPkgSetting, pkgSetting,
+ request.getPreVerifiedDomains());
}
int collectionSize = ArrayUtils.size(pkg.getInstrumentations());
@@ -2204,6 +2206,9 @@
File appMetadataFile = new File(ps.getPath(), APP_METADATA_FILE_NAME);
if (appMetadataFile.exists()) {
ps.setAppMetadataFilePath(appMetadataFile.getAbsolutePath());
+ if (Flags.aslInApkAppMetadataSource()) {
+ ps.setAppMetadataSource(PackageManager.APP_METADATA_SOURCE_INSTALLER);
+ }
} else {
ps.setAppMetadataFilePath(null);
}
diff --git a/services/core/java/com/android/server/pm/InstallRequest.java b/services/core/java/com/android/server/pm/InstallRequest.java
index ebaf516..4fb0c22 100644
--- a/services/core/java/com/android/server/pm/InstallRequest.java
+++ b/services/core/java/com/android/server/pm/InstallRequest.java
@@ -42,6 +42,7 @@
import android.content.pm.SharedLibraryInfo;
import android.content.pm.SigningDetails;
import android.content.pm.parsing.PackageLite;
+import android.content.pm.verify.domain.DomainSet;
import android.net.Uri;
import android.os.Build;
import android.os.Process;
@@ -155,6 +156,9 @@
@NonNull
private final ArrayList<String> mWarnings = new ArrayList<>();
+ @Nullable
+ private DomainSet mPreVerifiedDomains;
+
// New install
InstallRequest(InstallingSession params) {
mUserId = params.getUser().getIdentifier();
@@ -172,6 +176,7 @@
mIsInstallInherit = params.mIsInherit;
mSessionId = params.mSessionId;
mRequireUserAction = params.mRequireUserAction;
+ mPreVerifiedDomains = params.mPreVerifiedDomains;
}
// Install existing package as user
@@ -875,6 +880,11 @@
}
}
+ @Nullable
+ public DomainSet getPreVerifiedDomains() {
+ return mPreVerifiedDomains;
+ }
+
public void addWarning(@NonNull String warning) {
mWarnings.add(warning);
}
diff --git a/services/core/java/com/android/server/pm/InstallingSession.java b/services/core/java/com/android/server/pm/InstallingSession.java
index e970d2c..4cbd3ad 100644
--- a/services/core/java/com/android/server/pm/InstallingSession.java
+++ b/services/core/java/com/android/server/pm/InstallingSession.java
@@ -40,6 +40,7 @@
import android.content.pm.PackageManager;
import android.content.pm.SigningDetails;
import android.content.pm.parsing.PackageLite;
+import android.content.pm.verify.domain.DomainSet;
import android.os.Environment;
import android.os.Trace;
import android.os.UserHandle;
@@ -98,6 +99,8 @@
final int mSessionId;
final int mRequireUserAction;
final boolean mApplicationEnabledSettingPersistent;
+ @Nullable
+ final DomainSet mPreVerifiedDomains;
// For move install
InstallingSession(OriginInfo originInfo, MoveInfo moveInfo, IPackageInstallObserver2 observer,
@@ -130,12 +133,13 @@
mSessionId = -1;
mRequireUserAction = USER_ACTION_UNSPECIFIED;
mApplicationEnabledSettingPersistent = false;
+ mPreVerifiedDomains = null;
}
InstallingSession(int sessionId, File stagedDir, IPackageInstallObserver2 observer,
PackageInstaller.SessionParams sessionParams, InstallSource installSource,
UserHandle user, SigningDetails signingDetails, int installerUid,
- PackageLite packageLite, PackageManagerService pm) {
+ PackageLite packageLite, DomainSet preVerifiedDomains, PackageManagerService pm) {
mPm = pm;
mUser = user;
mOriginInfo = OriginInfo.fromStagedFile(stagedDir);
@@ -163,6 +167,7 @@
mSessionId = sessionId;
mRequireUserAction = sessionParams.requireUserAction;
mApplicationEnabledSettingPersistent = sessionParams.applicationEnabledSettingPersistent;
+ mPreVerifiedDomains = preVerifiedDomains;
}
@Override
diff --git a/services/core/java/com/android/server/pm/LauncherAppsService.java b/services/core/java/com/android/server/pm/LauncherAppsService.java
index 43328fc..984a629 100644
--- a/services/core/java/com/android/server/pm/LauncherAppsService.java
+++ b/services/core/java/com/android/server/pm/LauncherAppsService.java
@@ -1676,6 +1676,8 @@
private IntentSender buildAppMarketIntentSenderForUser(@NonNull UserHandle user) {
Intent appMarketIntent = new Intent(Intent.ACTION_MAIN);
appMarketIntent.addCategory(Intent.CATEGORY_APP_MARKET);
+ appMarketIntent.setFlags(
+ Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
return buildIntentSenderForUser(appMarketIntent, user);
}
diff --git a/services/core/java/com/android/server/pm/PackageArchiver.java b/services/core/java/com/android/server/pm/PackageArchiver.java
index 339b1e7..dc97e5f 100644
--- a/services/core/java/com/android/server/pm/PackageArchiver.java
+++ b/services/core/java/com/android/server/pm/PackageArchiver.java
@@ -28,6 +28,7 @@
import static android.content.pm.PackageInstaller.STATUS_PENDING_USER_ACTION;
import static android.content.pm.PackageInstaller.UNARCHIVAL_OK;
import static android.content.pm.PackageInstaller.UNARCHIVAL_STATUS_UNSET;
+import static android.content.pm.PackageManager.DELETE_ALL_USERS;
import static android.content.pm.PackageManager.DELETE_ARCHIVE;
import static android.content.pm.PackageManager.DELETE_KEEP_DATA;
import static android.content.pm.PackageManager.INSTALL_UNARCHIVE_DRAFT;
@@ -182,62 +183,76 @@
return Flags.archiving() || SystemProperties.getBoolean("pm.archiving.enabled", false);
}
+ @VisibleForTesting
void requestArchive(
@NonNull String packageName,
@NonNull String callerPackageName,
@NonNull IntentSender intentSender,
@NonNull UserHandle userHandle) {
+ requestArchive(packageName, callerPackageName, /*flags=*/ 0, intentSender, userHandle);
+ }
+
+ void requestArchive(
+ @NonNull String packageName,
+ @NonNull String callerPackageName,
+ int flags,
+ @NonNull IntentSender intentSender,
+ @NonNull UserHandle userHandle) {
Objects.requireNonNull(packageName);
Objects.requireNonNull(callerPackageName);
Objects.requireNonNull(intentSender);
Objects.requireNonNull(userHandle);
Computer snapshot = mPm.snapshotComputer();
- int userId = userHandle.getIdentifier();
+ int binderUserId = userHandle.getIdentifier();
int binderUid = Binder.getCallingUid();
int binderPid = Binder.getCallingPid();
if (!PackageManagerServiceUtils.isSystemOrRootOrShell(binderUid)) {
- verifyCaller(snapshot.getPackageUid(callerPackageName, 0, userId), binderUid);
+ verifyCaller(snapshot.getPackageUid(callerPackageName, 0, binderUserId), binderUid);
}
- snapshot.enforceCrossUserPermission(binderUid, userId, true, true,
- "archiveApp");
+
+ final boolean deleteAllUsers = (flags & PackageManager.DELETE_ALL_USERS) != 0;
+ final int[] users = deleteAllUsers ? mPm.mInjector.getUserManagerInternal().getUserIds()
+ : new int[]{binderUserId};
+ for (int userId : users) {
+ snapshot.enforceCrossUserPermission(binderUid, userId,
+ /*requireFullPermission=*/ true, /*checkShell=*/ true,
+ "archiveApp");
+ }
verifyUninstallPermissions();
- CompletableFuture<ArchiveState> archiveStateFuture;
+ CompletableFuture<Void>[] archiveStateStored = new CompletableFuture[users.length];
try {
- archiveStateFuture = createArchiveState(packageName, userId);
+ for (int i = 0, size = users.length; i < size; ++i) {
+ archiveStateStored[i] = createAndStoreArchiveState(packageName, users[i]);
+ }
} catch (PackageManager.NameNotFoundException e) {
Slog.d(TAG, TextUtils.formatSimple("Failed to archive %s with message %s",
packageName, e.getMessage()));
throw new ParcelableException(e);
}
- archiveStateFuture
- .thenAccept(
- archiveState -> {
- // TODO(b/282952870) Should be reverted if uninstall fails/cancels
- try {
- storeArchiveState(packageName, archiveState, userId);
- } catch (PackageManager.NameNotFoundException e) {
- sendFailureStatus(intentSender, packageName, e.getMessage());
- return;
- }
+ final int deleteFlags = DELETE_ARCHIVE | DELETE_KEEP_DATA
+ | (deleteAllUsers ? DELETE_ALL_USERS : 0);
- mPm.mInstallerService.uninstall(
- new VersionedPackage(packageName,
- PackageManager.VERSION_CODE_HIGHEST),
- callerPackageName,
- DELETE_ARCHIVE | DELETE_KEEP_DATA,
- intentSender,
- userId,
- binderUid,
- binderPid);
- })
- .exceptionally(
- e -> {
- sendFailureStatus(intentSender, packageName, e.getMessage());
- return null;
- });
+ CompletableFuture.allOf(archiveStateStored).thenAccept(ignored ->
+ mPm.mInstallerService.uninstall(
+ new VersionedPackage(packageName,
+ PackageManager.VERSION_CODE_HIGHEST),
+ callerPackageName,
+ deleteFlags,
+ intentSender,
+ binderUserId,
+ binderUid,
+ binderPid)
+ ).exceptionally(
+ e -> {
+ Slog.d(TAG, TextUtils.formatSimple("Failed to archive %s with message %s",
+ packageName, e.getMessage()));
+ sendFailureStatus(intentSender, packageName, e.getMessage());
+ return null;
+ }
+ );
}
/**
@@ -384,7 +399,7 @@
}
/** Creates archived state for the package and user. */
- private CompletableFuture<ArchiveState> createArchiveState(String packageName, int userId)
+ private CompletableFuture<Void> createAndStoreArchiveState(String packageName, int userId)
throws PackageManager.NameNotFoundException {
Computer snapshot = mPm.snapshotComputer();
PackageStateInternal ps = getPackageState(packageName, snapshot,
@@ -399,17 +414,18 @@
List<LauncherActivityInfo> mainActivities = getLauncherActivityInfos(ps.getPackageName(),
userId);
- final CompletableFuture<ArchiveState> archiveState = new CompletableFuture<>();
+ final CompletableFuture<Void> archiveStateStored = new CompletableFuture<>();
mPm.mHandler.post(() -> {
try {
- archiveState.complete(
- createArchiveStateInternal(packageName, userId, mainActivities,
- installerInfo.loadLabel(mContext.getPackageManager()).toString()));
- } catch (IOException e) {
- archiveState.completeExceptionally(e);
+ var archiveState = createArchiveStateInternal(packageName, userId, mainActivities,
+ installerInfo.loadLabel(mContext.getPackageManager()).toString());
+ storeArchiveState(packageName, archiveState, userId);
+ archiveStateStored.complete(null);
+ } catch (IOException | PackageManager.NameNotFoundException e) {
+ archiveStateStored.completeExceptionally(e);
}
});
- return archiveState;
+ return archiveStateStored;
}
@Nullable
@@ -803,6 +819,7 @@
* <p> The icon is returned without any treatment/overlay. In the rare case the app had multiple
* launcher activities, only one of the icons is returned arbitrarily.
*/
+ @Nullable
public Bitmap getArchivedAppIcon(@NonNull String packageName, @NonNull UserHandle user,
String callingPackageName) {
Objects.requireNonNull(packageName);
@@ -828,7 +845,7 @@
// In the rare case the archived app defined more than two launcher activities, we choose
// the first one arbitrarily.
Bitmap icon = decodeIcon(archiveState.getActivityInfos().get(0));
- if (getAppOpsManager().checkOp(
+ if (icon != null && getAppOpsManager().checkOp(
AppOpsManager.OP_ARCHIVE_ICON_OVERLAY, callingUid, callingPackageName)
== MODE_ALLOWED) {
icon = includeCloudOverlay(icon);
@@ -884,6 +901,7 @@
return bitmap;
}
+ @Nullable
Bitmap includeCloudOverlay(Bitmap bitmap) {
Drawable cloudDrawable =
mContext.getResources()
@@ -904,7 +922,9 @@
final int iconSize = mContext.getSystemService(
ActivityManager.class).getLauncherLargeIconSize();
Bitmap appIconWithCloudOverlay = drawableToBitmap(layerDrawable, iconSize);
- bitmap.recycle();
+ if (bitmap != null) {
+ bitmap.recycle();
+ }
return appIconWithCloudOverlay;
}
diff --git a/services/core/java/com/android/server/pm/PackageInstallerHistoricalSession.java b/services/core/java/com/android/server/pm/PackageInstallerHistoricalSession.java
index d40a715..4b98e34 100644
--- a/services/core/java/com/android/server/pm/PackageInstallerHistoricalSession.java
+++ b/services/core/java/com/android/server/pm/PackageInstallerHistoricalSession.java
@@ -19,6 +19,7 @@
import android.content.pm.PackageInstaller.PreapprovalDetails;
import android.content.pm.PackageInstaller.SessionInfo;
import android.content.pm.PackageInstaller.SessionParams;
+import android.content.pm.verify.domain.DomainSet;
import com.android.internal.util.IndentingPrintWriter;
@@ -76,6 +77,7 @@
private final boolean mSessionFailed;
private final int mSessionErrorCode;
private final String mSessionErrorMessage;
+ private final String mPreVerifiedDomains;
PackageInstallerHistoricalSession(int sessionId, int userId, int originalInstallerUid,
String originalInstallerPackageName, InstallSource installSource, int installerUid,
@@ -86,7 +88,7 @@
String finalMessage, SessionParams params, int parentSessionId,
int[] childSessionIds, boolean sessionApplied, boolean sessionFailed,
boolean sessionReady, int sessionErrorCode, String sessionErrorMessage,
- PreapprovalDetails preapprovalDetails) {
+ PreapprovalDetails preapprovalDetails, DomainSet preVerifiedDomains) {
this.sessionId = sessionId;
this.userId = userId;
this.mOriginalInstallerUid = originalInstallerUid;
@@ -128,6 +130,11 @@
} else {
this.mPreapprovalDetails = null;
}
+ if (preVerifiedDomains != null) {
+ this.mPreVerifiedDomains = String.join(",", preVerifiedDomains.getDomains());
+ } else {
+ this.mPreVerifiedDomains = null;
+ }
}
void dump(IndentingPrintWriter pw) {
@@ -170,6 +177,7 @@
pw.printPair("mSessionErrorCode", mSessionErrorCode);
pw.printPair("mSessionErrorMessage", mSessionErrorMessage);
pw.printPair("mPreapprovalDetails", mPreapprovalDetails);
+ pw.printPair("mPreVerifiedDomains", mPreVerifiedDomains);
pw.println();
pw.decreaseIndent();
diff --git a/services/core/java/com/android/server/pm/PackageInstallerService.java b/services/core/java/com/android/server/pm/PackageInstallerService.java
index c6d448d..6e4f199 100644
--- a/services/core/java/com/android/server/pm/PackageInstallerService.java
+++ b/services/core/java/com/android/server/pm/PackageInstallerService.java
@@ -1026,7 +1026,7 @@
mSilentUpdatePolicy, mInstallThread.getLooper(), mStagingManager, sessionId,
userId, callingUid, installSource, params, createdMillis, 0L, stageDir, stageCid,
null, null, false, false, false, false, null, SessionInfo.INVALID_ID,
- false, false, false, PackageManager.INSTALL_UNKNOWN, "");
+ false, false, false, PackageManager.INSTALL_UNKNOWN, "", null);
synchronized (mSessions) {
mSessions.put(sessionId, session);
@@ -1672,9 +1672,11 @@
public void requestArchive(
@NonNull String packageName,
@NonNull String callerPackageName,
+ int flags,
@NonNull IntentSender intentSender,
@NonNull UserHandle userHandle) {
- mPackageArchiver.requestArchive(packageName, callerPackageName, intentSender, userHandle);
+ mPackageArchiver.requestArchive(packageName, callerPackageName, flags, intentSender,
+ userHandle);
}
@Override
diff --git a/services/core/java/com/android/server/pm/PackageInstallerSession.java b/services/core/java/com/android/server/pm/PackageInstallerSession.java
index 27c3dad..c860b5a 100644
--- a/services/core/java/com/android/server/pm/PackageInstallerSession.java
+++ b/services/core/java/com/android/server/pm/PackageInstallerSession.java
@@ -113,6 +113,7 @@
import android.content.pm.parsing.PackageLite;
import android.content.pm.parsing.result.ParseResult;
import android.content.pm.parsing.result.ParseTypeImpl;
+import android.content.pm.verify.domain.DomainSet;
import android.content.res.ApkAssets;
import android.content.res.AssetManager;
import android.content.res.Configuration;
@@ -241,6 +242,8 @@
"whitelisted-restricted-permission";
private static final String TAG_AUTO_REVOKE_PERMISSIONS_MODE =
"auto-revoke-permissions-mode";
+
+ static final String TAG_PRE_VERIFIED_DOMAINS = "preVerifiedDomains";
private static final String ATTR_SESSION_ID = "sessionId";
private static final String ATTR_USER_ID = "userId";
private static final String ATTR_INSTALLER_PACKAGE_NAME = "installerPackageName";
@@ -298,6 +301,7 @@
private static final String ATTR_CHECKSUM_VALUE = "checksumValue";
private static final String ATTR_APPLICATION_ENABLED_SETTING_PERSISTENT =
"applicationEnabledSettingPersistent";
+ private static final String ATTR_DOMAIN = "domain";
private static final String PROPERTY_NAME_INHERIT_NATIVE = "pi.inherit_native_on_dont_kill";
private static final int[] EMPTY_CHILD_SESSION_ARRAY = EmptyArray.INT;
@@ -365,6 +369,25 @@
@EnabledSince(targetSdkVersion = Build.VERSION_CODES.VANILLA_ICE_CREAM)
private static final long THROW_EXCEPTION_COMMIT_WITH_IMMUTABLE_PENDING_INTENT = 240618202L;
+ /**
+ * Configurable maximum number of pre-verified domains allowed to be added to the session.
+ * Flag type: {@code long}
+ * Namespace: NAMESPACE_PACKAGE_MANAGER_SERVICE
+ */
+ private static final String PROPERTY_PRE_VERIFIED_DOMAINS_COUNT_LIMIT =
+ "pre_verified_domains_count_limit";
+ /**
+ * Configurable maximum string length of each pre-verified domain.
+ * Flag type: {@code long}
+ * Namespace: NAMESPACE_PACKAGE_MANAGER_SERVICE
+ */
+ private static final String PROPERTY_PRE_VERIFIED_DOMAIN_LENGTH_LIMIT =
+ "pre_verified_domain_length_limit";
+ /** Default max number of pre-verified domains */
+ private static final long DEFAULT_PRE_VERIFIED_DOMAINS_COUNT_LIMIT = 1000;
+ /** Default max string length of each pre-verified domain */
+ private static final long DEFAULT_PRE_VERIFIED_DOMAIN_LENGTH_LIMIT = 256;
+
// TODO: enforce INSTALL_ALLOW_TEST
// TODO: enforce INSTALL_ALLOW_DOWNGRADE
@@ -506,6 +529,9 @@
@GuardedBy("mLock")
private int mUserActionRequirement;
+ @GuardedBy("mLock")
+ private DomainSet mPreVerifiedDomains;
+
static class FileEntry {
private final int mIndex;
private final InstallationFile mFile;
@@ -936,6 +962,21 @@
getInstallSource().mInstallerPackageName, mInstallerUid);
}
+ private boolean isEmergencyInstallerEnabled(String packageName, Computer snapshot) {
+ final PackageStateInternal ps = snapshot.getPackageStateInternal(packageName);
+ if (ps == null || ps.getPkg() == null || !ps.isSystem()) {
+ return false;
+ }
+ String emergencyInstaller = ps.getPkg().getEmergencyInstaller();
+ if (emergencyInstaller == null || !ArrayUtils.contains(
+ snapshot.getPackagesForUid(mInstallerUid),
+ emergencyInstaller)) {
+ return false;
+ }
+ return (snapshot.checkUidPermission(Manifest.permission.EMERGENCY_INSTALL_PACKAGES,
+ mInstallerUid) == PackageManager.PERMISSION_GRANTED);
+ }
+
private static final int USER_ACTION_NOT_NEEDED = 0;
private static final int USER_ACTION_REQUIRED = 1;
private static final int USER_ACTION_PENDING_APK_PARSING = 2;
@@ -1020,6 +1061,8 @@
final boolean isUpdateOwner = TextUtils.equals(existingUpdateOwnerPackageName,
getInstallerPackageName());
final boolean isSelfUpdate = targetPackageUid == mInstallerUid;
+ final boolean isEmergencyInstall =
+ isEmergencyInstallerEnabled(packageName, snapshot);
final boolean isPermissionGranted = isInstallPermissionGranted
|| (isUpdatePermissionGranted && isUpdate)
|| (isSelfUpdatePermissionGranted && isSelfUpdate)
@@ -1036,7 +1079,7 @@
// Device owners and affiliated profile owners are allowed to silently install packages, so
// the permission check is waived if the installer is the device owner.
final boolean noUserActionNecessary = isInstallerRoot || isInstallerSystem
- || isInstallerDeviceOwnerOrAffiliatedProfileOwner();
+ || isInstallerDeviceOwnerOrAffiliatedProfileOwner() || isEmergencyInstall;
if (noUserActionNecessary) {
return userActionNotTypicallyNeededResponse;
@@ -1089,7 +1132,7 @@
boolean prepared, boolean committed, boolean destroyed, boolean sealed,
@Nullable int[] childSessionIds, int parentSessionId, boolean isReady,
boolean isFailed, boolean isApplied, int sessionErrorCode,
- String sessionErrorMessage) {
+ String sessionErrorMessage, DomainSet preVerifiedDomains) {
mCallback = callback;
mContext = context;
mPm = pm;
@@ -1151,6 +1194,7 @@
mSessionErrorMessage =
sessionErrorMessage != null ? sessionErrorMessage : "";
mStagedSession = params.isStaged ? new StagedSession() : null;
+ mPreVerifiedDomains = preVerifiedDomains;
if (isDataLoaderInstallation()) {
if (isApexSession()) {
@@ -1198,7 +1242,7 @@
mStageDirInUse, mDestroyed, mFds.size(), mBridges.size(), mFinalStatus,
mFinalMessage, params, mParentSessionId, getChildSessionIdsLocked(),
mSessionApplied, mSessionFailed, mSessionReady, mSessionErrorCode,
- mSessionErrorMessage, mPreapprovalDetails);
+ mSessionErrorMessage, mPreapprovalDetails, mPreVerifiedDomains);
}
}
@@ -3135,7 +3179,7 @@
synchronized (mLock) {
return new InstallingSession(sessionId, stageDir, localObserver, params, mInstallSource,
- user, mSigningDetails, mInstallerUid, mPackageLite, mPm);
+ user, mSigningDetails, mInstallerUid, mPackageLite, mPreVerifiedDomains, mPm);
}
}
@@ -5024,6 +5068,82 @@
return (params.installFlags & PackageManager.INSTALL_REQUEST_UPDATE_OWNERSHIP) != 0;
}
+ @Override
+ public void setPreVerifiedDomains(@NonNull DomainSet preVerifiedDomains) {
+ // First check permissions
+ final boolean exemptFromPermissionChecks =
+ (mInstallerUid == Process.ROOT_UID) || (mInstallerUid == Process.SHELL_UID);
+ if (!exemptFromPermissionChecks) {
+ final Computer snapshot = mPm.snapshotComputer();
+ if (PackageManager.PERMISSION_GRANTED != snapshot.checkUidPermission(
+ Manifest.permission.ACCESS_INSTANT_APPS, mInstallerUid)) {
+ throw new SecurityException("You need android.permission.ACCESS_INSTANT_APPS "
+ + "permission to set pre-verified domains.");
+ }
+ ComponentName instantAppInstallerComponent = snapshot.getInstantAppInstallerComponent();
+ if (instantAppInstallerComponent == null) {
+ // Shouldn't happen
+ throw new IllegalStateException("Instant app installer is not available. "
+ + "Only the instant app installer can call this API.");
+ }
+ if (!instantAppInstallerComponent.getPackageName().equals(getInstallerPackageName())) {
+ throw new SecurityException("Only the instant app installer can call this API.");
+ }
+ }
+ // Then check size limits
+ final long preVerifiedDomainsCountLimit = getPreVerifiedDomainsCountLimit();
+ if (preVerifiedDomains.getDomains().size() > preVerifiedDomainsCountLimit) {
+ throw new IllegalArgumentException(
+ "The number of pre-verified domains have exceeded the maximum of "
+ + preVerifiedDomainsCountLimit);
+ }
+ final long preVerifiedDomainLengthLimit = getPreVerifiedDomainLengthLimit();
+ for (String domain : preVerifiedDomains.getDomains()) {
+ if (domain.length() > preVerifiedDomainLengthLimit) {
+ throw new IllegalArgumentException(
+ "Pre-verified domain: [" + domain + " ] exceeds maximum length allowed: "
+ + preVerifiedDomainLengthLimit);
+ }
+ }
+ // Okay to proceed
+ synchronized (mLock) {
+ mPreVerifiedDomains = preVerifiedDomains;
+ }
+ }
+
+ private static long getPreVerifiedDomainsCountLimit() {
+ final long token = Binder.clearCallingIdentity();
+ try {
+ return DeviceConfig.getLong(NAMESPACE_PACKAGE_MANAGER_SERVICE,
+ PROPERTY_PRE_VERIFIED_DOMAINS_COUNT_LIMIT,
+ DEFAULT_PRE_VERIFIED_DOMAINS_COUNT_LIMIT);
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ }
+
+ private static long getPreVerifiedDomainLengthLimit() {
+ final long token = Binder.clearCallingIdentity();
+ try {
+ return DeviceConfig.getLong(NAMESPACE_PACKAGE_MANAGER_SERVICE,
+ PROPERTY_PRE_VERIFIED_DOMAIN_LENGTH_LIMIT,
+ DEFAULT_PRE_VERIFIED_DOMAIN_LENGTH_LIMIT);
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ }
+
+ @Override
+ @Nullable
+ public DomainSet getPreVerifiedDomains() {
+ assertCallerIsOwnerOrRoot();
+ synchronized (mLock) {
+ assertPreparedAndNotCommittedOrDestroyedLocked("getPreVerifiedDomains");
+ return mPreVerifiedDomains;
+ }
+ }
+
+
void setSessionReady() {
synchronized (mLock) {
// Do not allow destroyed/failed session to change state
@@ -5250,6 +5370,9 @@
pw.printPair("mSessionErrorCode", mSessionErrorCode);
pw.printPair("mSessionErrorMessage", mSessionErrorMessage);
pw.printPair("mPreapprovalDetails", mPreapprovalDetails);
+ if (mPreVerifiedDomains != null) {
+ pw.printPair("mPreVerifiedDomains", mPreVerifiedDomains);
+ }
pw.println();
pw.decreaseIndent();
@@ -5546,7 +5669,13 @@
writeByteArrayAttribute(out, ATTR_SIGNATURE, signature);
out.endTag(null, TAG_SESSION_CHECKSUM_SIGNATURE);
}
-
+ if (mPreVerifiedDomains != null) {
+ for (String domain : mPreVerifiedDomains.getDomains()) {
+ out.startTag(null, TAG_PRE_VERIFIED_DOMAINS);
+ writeStringAttribute(out, ATTR_DOMAIN, domain);
+ out.endTag(null, TAG_PRE_VERIFIED_DOMAINS);
+ }
+ }
}
out.endTag(null, TAG_SESSION);
@@ -5673,6 +5802,7 @@
List<InstallationFile> files = new ArrayList<>();
ArrayMap<String, List<Checksum>> checksums = new ArrayMap<>();
ArrayMap<String, byte[]> signatures = new ArrayMap<>();
+ ArraySet<String> preVerifiedDomainSet = new ArraySet<>();
int outerDepth = in.getDepth();
int type;
while ((type = in.next()) != XmlPullParser.END_DOCUMENT
@@ -5726,6 +5856,9 @@
final byte[] signature = readByteArrayAttribute(in, ATTR_SIGNATURE);
signatures.put(fileName1, signature);
break;
+ case TAG_PRE_VERIFIED_DOMAINS:
+ preVerifiedDomainSet.add(readStringAttribute(in, ATTR_DOMAIN));
+ break;
}
}
@@ -5769,6 +5902,9 @@
}
}
+ DomainSet preVerifiedDomains =
+ preVerifiedDomainSet.isEmpty() ? null : new DomainSet(preVerifiedDomainSet);
+
InstallSource installSource = InstallSource.create(installInitiatingPackageName,
installOriginatingPackageName, installerPackageName, installPackageUid,
updateOwnerPackageName, installerAttributionTag, params.packageSource);
@@ -5777,6 +5913,6 @@
installerUid, installSource, params, createdMillis, committedMillis, stageDir,
stageCid, fileArray, checksumsMap, prepared, committed, destroyed, sealed,
childSessionIdsArray, parentSessionId, isReady, isFailed, isApplied,
- sessionErrorCode, sessionErrorMessage);
+ sessionErrorCode, sessionErrorMessage, preVerifiedDomains);
}
}
diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java
index b705e84..afd4fb1 100644
--- a/services/core/java/com/android/server/pm/PackageManagerService.java
+++ b/services/core/java/com/android/server/pm/PackageManagerService.java
@@ -569,7 +569,8 @@
* target sdk apps as malware can target older sdk versions to avoid
* the enforcement of new API behavior.
*/
- public static final int MIN_INSTALLABLE_TARGET_SDK = Build.VERSION_CODES.M;
+ public static final int MIN_INSTALLABLE_TARGET_SDK =
+ Flags.minTargetSdk24() ? Build.VERSION_CODES.N : Build.VERSION_CODES.M;
// Compilation reasons.
// TODO(b/260124949): Clean this up with the legacy dexopt code.
@@ -2559,12 +2560,20 @@
PackageSetting pkgSetting = mSettings.getPackageLPr(pkgName);
if (pkgSetting != null) {
pkgSetting.setAppMetadataFilePath(path);
+ if (Flags.aslInApkAppMetadataSource()) {
+ pkgSetting.setAppMetadataSource(
+ PackageManager.APP_METADATA_SOURCE_SYSTEM_IMAGE);
+ }
} else {
Slog.w(TAG, "Cannot set app metadata file for nonexistent package "
+ pkgName);
}
} else {
disabledPkgSetting.setAppMetadataFilePath(path);
+ if (Flags.aslInApkAppMetadataSource()) {
+ disabledPkgSetting.setAppMetadataSource(
+ PackageManager.APP_METADATA_SOURCE_SYSTEM_IMAGE);
+ }
}
}
}
@@ -5231,6 +5240,21 @@
return null;
}
+ @android.annotation.EnforcePermission(android.Manifest.permission.GET_APP_METADATA)
+ @Override
+ public int getAppMetadataSource(String packageName, int userId) {
+ getAppMetadataSource_enforcePermission();
+ final int callingUid = Binder.getCallingUid();
+ final Computer snapshot = snapshotComputer();
+ final PackageStateInternal ps = snapshot.getPackageStateForInstalledAndFiltered(
+ packageName, callingUid, userId);
+ if (ps == null) {
+ throw new ParcelableException(
+ new PackageManager.NameNotFoundException(packageName));
+ }
+ return ps.getAppMetadataSource();
+ }
+
@Override
public String getPermissionControllerPackageName() {
final int callingUid = Binder.getCallingUid();
@@ -6460,6 +6484,17 @@
}
@Override
+ @Nullable
+ public ComponentName getDomainVerificationAgent() {
+ final int callerUid = Binder.getCallingUid();
+ if (!PackageManagerServiceUtils.isRootOrShell(callerUid)) {
+ throw new SecurityException("Not allowed to query domain verification agent");
+ }
+ final Computer snapshot = snapshotComputer();
+ return getDomainVerificationAgentComponentNameLPr(snapshot);
+ }
+
+ @Override
public boolean onTransact(int code, Parcel data, Parcel reply, int flags)
throws RemoteException {
try {
diff --git a/services/core/java/com/android/server/pm/PackageManagerShellCommand.java b/services/core/java/com/android/server/pm/PackageManagerShellCommand.java
index 5c9c8c6..89589ed 100644
--- a/services/core/java/com/android/server/pm/PackageManagerShellCommand.java
+++ b/services/core/java/com/android/server/pm/PackageManagerShellCommand.java
@@ -107,6 +107,7 @@
import android.text.TextUtils;
import android.text.format.DateUtils;
import android.util.ArrayMap;
+import android.util.ArraySet;
import android.util.IntArray;
import android.util.Pair;
import android.util.PrintWriterPrinter;
@@ -270,6 +271,10 @@
return runGetInstallLocation();
case "install-add-session":
return runInstallAddSession();
+ case "install-set-pre-verified-domains":
+ return runInstallSetPreVerifiedDomains();
+ case "install-get-pre-verified-domains":
+ return runInstallGetPreVerifiedDomains();
case "move-package":
return runMovePackage();
case "move-primary-storage":
@@ -391,6 +396,8 @@
return runArchive();
case "request-unarchive":
return runUnarchive();
+ case "get-domain-verification-agent":
+ return runGetDomainVerificationAgent();
default: {
if (ART_SERVICE_COMMANDS.contains(cmd)) {
if (DexOptHelper.useArtService()) {
@@ -1808,6 +1815,41 @@
true /*logSuccess*/);
}
+ private int runInstallSetPreVerifiedDomains() throws RemoteException {
+ final PrintWriter pw = getOutPrintWriter();
+ final int sessionId = Integer.parseInt(getNextArg());
+ final String preVerifiedDomainsStr = getNextArg();
+ final String[] preVerifiedDomains = preVerifiedDomainsStr.split(",");
+ PackageInstaller.Session session = null;
+ try {
+ session = new PackageInstaller.Session(
+ mInterface.getPackageInstaller().openSession(sessionId));
+ session.setPreVerifiedDomains(new ArraySet<>(preVerifiedDomains));
+ } finally {
+ IoUtils.closeQuietly(session);
+ }
+ return 0;
+ }
+
+ private int runInstallGetPreVerifiedDomains() throws RemoteException {
+ final PrintWriter pw = getOutPrintWriter();
+ final int sessionId = Integer.parseInt(getNextArg());
+ PackageInstaller.Session session = null;
+ try {
+ session = new PackageInstaller.Session(
+ mInterface.getPackageInstaller().openSession(sessionId));
+ Set<String> preVerifiedDomains = session.getPreVerifiedDomains();
+ if (preVerifiedDomains.isEmpty()) {
+ pw.println("The session doesn't have any pre-verified domains specified.");
+ } else {
+ pw.println(String.join(",", preVerifiedDomains));
+ }
+ } finally {
+ IoUtils.closeQuietly(session);
+ }
+ return 0;
+ }
+
private int runInstallRemove() throws RemoteException {
final PrintWriter pw = getOutPrintWriter();
@@ -4651,6 +4693,7 @@
private int runArchive() throws RemoteException {
final PrintWriter pw = getOutPrintWriter();
+ int flags = 0;
int userId = UserHandle.USER_ALL;
String opt;
@@ -4678,13 +4721,16 @@
return 1;
}
+ if (userId == UserHandle.USER_ALL) {
+ flags |= PackageManager.DELETE_ALL_USERS;
+ }
final int translatedUserId =
translateUserId(userId, UserHandle.USER_SYSTEM, "runArchive");
final LocalIntentReceiver receiver = new LocalIntentReceiver();
try {
mInterface.getPackageInstaller().requestArchive(packageName,
- /* callerPackageName= */ "", receiver.getIntentSender(),
+ /* callerPackageName= */ "", flags, receiver.getIntentSender(),
new UserHandle(translatedUserId));
} catch (Exception e) {
pw.println("Failure [" + e.getMessage() + "]");
@@ -4750,6 +4796,19 @@
return 0;
}
+ private int runGetDomainVerificationAgent() throws RemoteException {
+ final PrintWriter pw = getOutPrintWriter();
+ try {
+ final ComponentName domainVerificationAgent = mInterface.getDomainVerificationAgent();
+ pw.println(domainVerificationAgent == null
+ ? "No Domain Verifier available!" : domainVerificationAgent.toString());
+ } catch (Exception e) {
+ pw.println("Failure [" + e.getMessage() + "]");
+ return 1;
+ }
+ return 0;
+ }
+
@Override
public void onHelp() {
final PrintWriter pw = getOutPrintWriter();
@@ -4930,6 +4989,13 @@
pw.println(" install-add-session MULTI_PACKAGE_SESSION_ID CHILD_SESSION_IDs");
pw.println(" Add one or more session IDs to a multi-package session.");
pw.println("");
+ pw.println(" install-set-pre-verified-domains SESSION_ID PRE_VERIFIED_DOMAIN... ");
+ pw.println(" Specify a comma separated list of pre-verified domains for a session.");
+ pw.println("");
+ pw.println(" install-get-pre-verified-domains SESSION_ID");
+ pw.println(" List all the pre-verified domains that are specified in a session.");
+ pw.println(" The result list is comma separated.");
+ pw.println("");
pw.println(" install-commit SESSION_ID");
pw.println(" Commit the given active install session, installing the app.");
pw.println("");
@@ -5143,6 +5209,9 @@
pw.println(" to unarchive an app to the responsible installer. Options are:");
pw.println(" --user: request unarchival of the app from the given user.");
pw.println("");
+ pw.println(" get-domain-verification-agent");
+ pw.println(" Displays the component name of the domain verification agent on device.");
+ pw.println("");
if (DexOptHelper.useArtService()) {
printArtServiceHelp();
} else {
diff --git a/services/core/java/com/android/server/pm/PackageSetting.java b/services/core/java/com/android/server/pm/PackageSetting.java
index f474d32..12eb88e 100644
--- a/services/core/java/com/android/server/pm/PackageSetting.java
+++ b/services/core/java/com/android/server/pm/PackageSetting.java
@@ -224,6 +224,8 @@
@Nullable
private String mAppMetadataFilePath;
+ private int mAppMetadataSource = PackageManager.APP_METADATA_SOURCE_UNKNOWN;
+
private int mTargetSdkVersion;
@Nullable
@@ -716,6 +718,7 @@
categoryOverride = other.categoryOverride;
mDomainSetId = other.mDomainSetId;
mAppMetadataFilePath = other.mAppMetadataFilePath;
+ mAppMetadataSource = other.mAppMetadataSource;
mTargetSdkVersion = other.mTargetSdkVersion;
mRestrictUpdateHash = other.mRestrictUpdateHash == null
? null : other.mRestrictUpdateHash.clone();
@@ -1378,6 +1381,15 @@
return this;
}
+ /**
+ * @param source the source of the app metadata that is currently associated with
+ */
+ public PackageSetting setAppMetadataSource(int source) {
+ mAppMetadataSource = source;
+ onChanged();
+ return this;
+ }
+
@NonNull
@Override
public long getVersionCode() {
@@ -1818,6 +1830,11 @@
}
@DataClass.Generated.Member
+ public int getAppMetadataSource() {
+ return mAppMetadataSource;
+ }
+
+ @DataClass.Generated.Member
public int getTargetSdkVersion() {
return mTargetSdkVersion;
}
@@ -1828,10 +1845,10 @@
}
@DataClass.Generated(
- time = 1702666890740L,
+ time = 1706698406378L,
codegenVersion = "1.0.23",
sourceFile = "frameworks/base/services/core/java/com/android/server/pm/PackageSetting.java",
- inputSignatures = "private int mBooleans\nprivate int mSharedUserAppId\nprivate @android.annotation.Nullable java.util.Map<java.lang.String,java.util.Set<java.lang.String>> mimeGroups\nprivate @android.annotation.Nullable java.lang.String[] usesSdkLibraries\nprivate @android.annotation.Nullable long[] usesSdkLibrariesVersionsMajor\nprivate @android.annotation.Nullable boolean[] usesSdkLibrariesOptional\nprivate @android.annotation.Nullable java.lang.String[] usesStaticLibraries\nprivate @android.annotation.Nullable long[] usesStaticLibrariesVersions\nprivate @android.annotation.Nullable @java.lang.Deprecated java.lang.String legacyNativeLibraryPath\nprivate @android.annotation.NonNull java.lang.String mName\nprivate @android.annotation.Nullable java.lang.String mRealName\nprivate int mAppId\nprivate @android.annotation.Nullable com.android.internal.pm.parsing.pkg.AndroidPackageInternal pkg\nprivate @android.annotation.NonNull java.io.File mPath\nprivate @android.annotation.NonNull java.lang.String mPathString\nprivate @android.annotation.Nullable java.util.LinkedHashSet<java.io.File> mOldPaths\nprivate float mLoadingProgress\nprivate long mLoadingCompletedTime\nprivate @android.annotation.Nullable java.lang.String mPrimaryCpuAbi\nprivate @android.annotation.Nullable java.lang.String mSecondaryCpuAbi\nprivate @android.annotation.Nullable java.lang.String mCpuAbiOverride\nprivate long mLastModifiedTime\nprivate long lastUpdateTime\nprivate long versionCode\nprivate @android.annotation.NonNull com.android.server.pm.PackageSignatures signatures\nprivate @android.annotation.NonNull com.android.server.pm.PackageKeySetData keySetData\nprivate final @android.annotation.NonNull android.util.SparseArray<com.android.server.pm.pkg.PackageUserStateImpl> mUserStates\nprivate @android.annotation.NonNull com.android.server.pm.InstallSource installSource\nprivate @android.annotation.Nullable java.lang.String volumeUuid\nprivate int categoryOverride\nprivate final @android.annotation.NonNull com.android.server.pm.pkg.PackageStateUnserialized pkgState\nprivate @android.annotation.NonNull java.util.UUID mDomainSetId\nprivate @android.annotation.Nullable java.lang.String mAppMetadataFilePath\nprivate int mTargetSdkVersion\nprivate @android.annotation.Nullable byte[] mRestrictUpdateHash\nprivate final @android.annotation.NonNull com.android.server.utils.SnapshotCache<com.android.server.pm.PackageSetting> mSnapshot\nprivate void setBoolean(int,boolean)\nprivate boolean getBoolean(int)\nprivate com.android.server.utils.SnapshotCache<com.android.server.pm.PackageSetting> makeCache()\npublic com.android.server.pm.PackageSetting snapshot()\npublic void dumpDebug(android.util.proto.ProtoOutputStream,long,java.util.List<android.content.pm.UserInfo>,com.android.server.pm.permission.LegacyPermissionDataProvider)\npublic com.android.server.pm.PackageSetting setAppId(int)\npublic com.android.server.pm.PackageSetting setCpuAbiOverride(java.lang.String)\npublic com.android.server.pm.PackageSetting setFirstInstallTimeFromReplaced(com.android.server.pm.pkg.PackageStateInternal,int[])\npublic com.android.server.pm.PackageSetting setFirstInstallTime(long,int)\npublic com.android.server.pm.PackageSetting setForceQueryableOverride(boolean)\npublic com.android.server.pm.PackageSetting setInstallerPackage(java.lang.String,int)\npublic com.android.server.pm.PackageSetting setUpdateOwnerPackage(java.lang.String)\npublic com.android.server.pm.PackageSetting setInstallSource(com.android.server.pm.InstallSource)\n com.android.server.pm.PackageSetting removeInstallerPackage(java.lang.String)\npublic com.android.server.pm.PackageSetting setIsOrphaned(boolean)\npublic com.android.server.pm.PackageSetting setKeySetData(com.android.server.pm.PackageKeySetData)\npublic com.android.server.pm.PackageSetting setLastModifiedTime(long)\npublic com.android.server.pm.PackageSetting setLastUpdateTime(long)\npublic com.android.server.pm.PackageSetting setLongVersionCode(long)\npublic boolean setMimeGroup(java.lang.String,android.util.ArraySet<java.lang.String>)\npublic com.android.server.pm.PackageSetting setPkg(com.android.server.pm.pkg.AndroidPackage)\npublic com.android.server.pm.PackageSetting setPkgStateLibraryFiles(java.util.Collection<java.lang.String>)\npublic com.android.server.pm.PackageSetting setPrimaryCpuAbi(java.lang.String)\npublic com.android.server.pm.PackageSetting setSecondaryCpuAbi(java.lang.String)\npublic com.android.server.pm.PackageSetting setSignatures(com.android.server.pm.PackageSignatures)\npublic com.android.server.pm.PackageSetting setVolumeUuid(java.lang.String)\npublic @java.lang.Override boolean isExternalStorage()\npublic com.android.server.pm.PackageSetting setUpdateAvailable(boolean)\npublic com.android.server.pm.PackageSetting setSharedUserAppId(int)\npublic com.android.server.pm.PackageSetting setTargetSdkVersion(int)\npublic com.android.server.pm.PackageSetting setRestrictUpdateHash(byte[])\npublic @java.lang.Override int getSharedUserAppId()\npublic @java.lang.Override boolean hasSharedUser()\npublic @java.lang.Override java.lang.String toString()\nprivate void copyMimeGroups(java.util.Map<java.lang.String,java.util.Set<java.lang.String>>)\npublic void updateFrom(com.android.server.pm.PackageSetting)\n com.android.server.pm.PackageSetting updateMimeGroups(java.util.Set<java.lang.String>)\npublic @java.lang.Deprecated @java.lang.Override com.android.server.pm.permission.LegacyPermissionState getLegacyPermissionState()\npublic com.android.server.pm.PackageSetting setInstallPermissionsFixed(boolean)\npublic boolean isPrivileged()\npublic boolean isOem()\npublic boolean isVendor()\npublic boolean isProduct()\npublic @java.lang.Override boolean isRequiredForSystemUser()\npublic boolean isSystemExt()\npublic boolean isOdm()\npublic boolean isSystem()\npublic boolean isRequestLegacyExternalStorage()\npublic boolean isUserDataFragile()\npublic android.content.pm.SigningDetails getSigningDetails()\npublic com.android.server.pm.PackageSetting setSigningDetails(android.content.pm.SigningDetails)\npublic void copyPackageSetting(com.android.server.pm.PackageSetting,boolean)\n @com.android.internal.annotations.VisibleForTesting com.android.server.pm.pkg.PackageUserStateImpl modifyUserState(int)\npublic com.android.server.pm.pkg.PackageUserStateImpl getOrCreateUserState(int)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateInternal readUserState(int)\n void setEnabled(int,int,java.lang.String)\n int getEnabled(int)\n void setInstalled(boolean,int)\n void setArchiveState(com.android.server.pm.pkg.ArchiveState,int)\n boolean getInstalled(int)\n boolean isArchived(int)\n int getInstallReason(int)\n void setInstallReason(int,int)\n int getUninstallReason(int)\n void setUninstallReason(int,int)\n @android.annotation.NonNull android.content.pm.overlay.OverlayPaths getOverlayPaths(int)\n boolean setOverlayPathsForLibrary(java.lang.String,android.content.pm.overlay.OverlayPaths,int)\n boolean isInstalledOrHasDataOnAnyOtherUser(int[],int)\n int[] queryInstalledUsers(int[],boolean)\n int[] queryUsersInstalledOrHasData(int[])\n long getCeDataInode(int)\n long getDeDataInode(int)\n void setCeDataInode(long,int)\n void setDeDataInode(long,int)\n boolean getStopped(int)\n void setStopped(boolean,int)\npublic com.android.server.pm.PackageSetting setScannedAsStoppedSystemApp(boolean)\n boolean getNotLaunched(int)\n void setNotLaunched(boolean,int)\n boolean getHidden(int)\n void setHidden(boolean,int)\n int getDistractionFlags(int)\n void setDistractionFlags(int,int)\npublic boolean getInstantApp(int)\n void setInstantApp(boolean,int)\n boolean getVirtualPreload(int)\n void setVirtualPreload(boolean,int)\n void setUserState(int,long,long,int,boolean,boolean,boolean,boolean,int,android.util.ArrayMap<java.lang.String,com.android.server.pm.pkg.SuspendParams>,boolean,boolean,java.lang.String,android.util.ArraySet<java.lang.String>,android.util.ArraySet<java.lang.String>,int,int,java.lang.String,java.lang.String,long,int,com.android.server.pm.pkg.ArchiveState)\n void setUserState(int,com.android.server.pm.pkg.PackageUserStateInternal)\n com.android.server.utils.WatchedArraySet<java.lang.String> getEnabledComponents(int)\n com.android.server.utils.WatchedArraySet<java.lang.String> getDisabledComponents(int)\n void setEnabledComponents(com.android.server.utils.WatchedArraySet<java.lang.String>,int)\n void setDisabledComponents(com.android.server.utils.WatchedArraySet<java.lang.String>,int)\n void setEnabledComponentsCopy(com.android.server.utils.WatchedArraySet<java.lang.String>,int)\n void setDisabledComponentsCopy(com.android.server.utils.WatchedArraySet<java.lang.String>,int)\n com.android.server.pm.pkg.PackageUserStateImpl modifyUserStateComponents(int,boolean,boolean)\n void addDisabledComponent(java.lang.String,int)\n void addEnabledComponent(java.lang.String,int)\n boolean enableComponentLPw(java.lang.String,int)\n boolean disableComponentLPw(java.lang.String,int)\n boolean restoreComponentLPw(java.lang.String,int)\n int getCurrentEnabledStateLPr(java.lang.String,int)\n void removeUser(int)\npublic int[] getNotInstalledUserIds()\n void writePackageUserPermissionsProto(android.util.proto.ProtoOutputStream,long,java.util.List<android.content.pm.UserInfo>,com.android.server.pm.permission.LegacyPermissionDataProvider)\nprotected void writeUsersInfoToProto(android.util.proto.ProtoOutputStream,long)\nprivate static void writeArchiveState(android.util.proto.ProtoOutputStream,com.android.server.pm.pkg.ArchiveState)\npublic @com.android.internal.annotations.VisibleForTesting com.android.server.pm.PackageSetting setPath(java.io.File)\npublic @com.android.internal.annotations.VisibleForTesting com.android.server.pm.PackageSetting addOldPath(java.io.File)\npublic @com.android.internal.annotations.VisibleForTesting com.android.server.pm.PackageSetting removeOldPath(java.io.File)\npublic @com.android.internal.annotations.VisibleForTesting boolean overrideNonLocalizedLabelAndIcon(android.content.ComponentName,java.lang.String,java.lang.Integer,int)\npublic void resetOverrideComponentLabelIcon(int)\npublic @android.annotation.Nullable java.lang.String getSplashScreenTheme(int)\npublic boolean isIncremental()\npublic boolean isLoading()\npublic com.android.server.pm.PackageSetting setLoadingProgress(float)\npublic com.android.server.pm.PackageSetting setLoadingCompletedTime(long)\npublic com.android.server.pm.PackageSetting setAppMetadataFilePath(java.lang.String)\npublic @android.annotation.NonNull @java.lang.Override long getVersionCode()\npublic @android.annotation.Nullable @java.lang.Override java.util.Map<java.lang.String,java.util.Set<java.lang.String>> getMimeGroups()\npublic @android.annotation.NonNull @java.lang.Override java.lang.String getPackageName()\npublic @android.annotation.Nullable @java.lang.Override com.android.server.pm.pkg.AndroidPackage getAndroidPackage()\npublic @android.annotation.NonNull android.content.pm.SigningInfo getSigningInfo()\npublic @android.annotation.NonNull @java.lang.Override java.lang.String[] getUsesSdkLibraries()\npublic @android.annotation.NonNull @java.lang.Override long[] getUsesSdkLibrariesVersionsMajor()\npublic @android.annotation.NonNull @java.lang.Override boolean[] getUsesSdkLibrariesOptional()\npublic @android.annotation.NonNull @java.lang.Override java.lang.String[] getUsesStaticLibraries()\npublic @android.annotation.NonNull @java.lang.Override long[] getUsesStaticLibrariesVersions()\npublic @android.annotation.NonNull @java.lang.Override java.util.List<com.android.server.pm.pkg.SharedLibrary> getSharedLibraryDependencies()\npublic @android.annotation.NonNull com.android.server.pm.PackageSetting addUsesLibraryInfo(android.content.pm.SharedLibraryInfo)\npublic @android.annotation.NonNull @java.lang.Override java.util.List<java.lang.String> getUsesLibraryFiles()\npublic @android.annotation.NonNull com.android.server.pm.PackageSetting addUsesLibraryFile(java.lang.String)\npublic @java.lang.Override boolean isHiddenUntilInstalled()\npublic @android.annotation.NonNull @java.lang.Override long[] getLastPackageUsageTime()\npublic @java.lang.Override boolean isUpdatedSystemApp()\npublic @java.lang.Override boolean isApkInUpdatedApex()\npublic @android.annotation.Nullable @java.lang.Override java.lang.String getApexModuleName()\npublic com.android.server.pm.PackageSetting setDomainSetId(java.util.UUID)\npublic com.android.server.pm.PackageSetting setCategoryOverride(int)\npublic com.android.server.pm.PackageSetting setLegacyNativeLibraryPath(java.lang.String)\npublic com.android.server.pm.PackageSetting setMimeGroups(java.util.Map<java.lang.String,java.util.Set<java.lang.String>>)\npublic com.android.server.pm.PackageSetting setUsesSdkLibraries(java.lang.String[])\npublic com.android.server.pm.PackageSetting setUsesSdkLibrariesVersionsMajor(long[])\npublic com.android.server.pm.PackageSetting setUsesSdkLibrariesOptional(boolean[])\npublic com.android.server.pm.PackageSetting setUsesStaticLibraries(java.lang.String[])\npublic com.android.server.pm.PackageSetting setUsesStaticLibrariesVersions(long[])\npublic com.android.server.pm.PackageSetting setApexModuleName(java.lang.String)\npublic @android.annotation.NonNull @java.lang.Override com.android.server.pm.pkg.PackageStateUnserialized getTransientState()\npublic @android.annotation.NonNull android.util.SparseArray<? extends PackageUserStateInternal> getUserStates()\npublic com.android.server.pm.PackageSetting addMimeTypes(java.lang.String,java.util.Set<java.lang.String>)\npublic @android.annotation.NonNull @java.lang.Override com.android.server.pm.pkg.PackageUserState getStateForUser(android.os.UserHandle)\npublic @android.annotation.Nullable java.lang.String getPrimaryCpuAbi()\npublic @android.annotation.Nullable java.lang.String getSecondaryCpuAbi()\npublic @android.annotation.Nullable @java.lang.Override java.lang.String getSeInfo()\npublic @android.annotation.Nullable java.lang.String getPrimaryCpuAbiLegacy()\npublic @android.annotation.Nullable java.lang.String getSecondaryCpuAbiLegacy()\npublic @android.content.pm.ApplicationInfo.HiddenApiEnforcementPolicy @java.lang.Override int getHiddenApiEnforcementPolicy()\npublic @java.lang.Override boolean isApex()\npublic @java.lang.Override boolean isForceQueryableOverride()\npublic @java.lang.Override boolean isUpdateAvailable()\npublic @java.lang.Override boolean isInstallPermissionsFixed()\npublic @java.lang.Override boolean isDefaultToDeviceProtectedStorage()\npublic @java.lang.Override boolean isPersistent()\npublic @java.lang.Override boolean isScannedAsStoppedSystemApp()\nclass PackageSetting extends com.android.server.pm.SettingBase implements [com.android.server.pm.pkg.PackageStateInternal]\nprivate static final int INSTALL_PERMISSION_FIXED\nprivate static final int UPDATE_AVAILABLE\nprivate static final int FORCE_QUERYABLE_OVERRIDE\nprivate static final int SCANNED_AS_STOPPED_SYSTEM_APP\nclass Booleans extends java.lang.Object implements []\n@com.android.internal.util.DataClass(genGetters=true, genConstructor=false, genSetters=false, genBuilder=false)")
+ inputSignatures = "private int mBooleans\nprivate int mSharedUserAppId\nprivate @android.annotation.Nullable java.util.Map<java.lang.String,java.util.Set<java.lang.String>> mimeGroups\nprivate @android.annotation.Nullable java.lang.String[] usesSdkLibraries\nprivate @android.annotation.Nullable long[] usesSdkLibrariesVersionsMajor\nprivate @android.annotation.Nullable boolean[] usesSdkLibrariesOptional\nprivate @android.annotation.Nullable java.lang.String[] usesStaticLibraries\nprivate @android.annotation.Nullable long[] usesStaticLibrariesVersions\nprivate @android.annotation.Nullable @java.lang.Deprecated java.lang.String legacyNativeLibraryPath\nprivate @android.annotation.NonNull java.lang.String mName\nprivate @android.annotation.Nullable java.lang.String mRealName\nprivate int mAppId\nprivate @android.annotation.Nullable com.android.internal.pm.parsing.pkg.AndroidPackageInternal pkg\nprivate @android.annotation.NonNull java.io.File mPath\nprivate @android.annotation.NonNull java.lang.String mPathString\nprivate @android.annotation.Nullable java.util.LinkedHashSet<java.io.File> mOldPaths\nprivate float mLoadingProgress\nprivate long mLoadingCompletedTime\nprivate @android.annotation.Nullable java.lang.String mPrimaryCpuAbi\nprivate @android.annotation.Nullable java.lang.String mSecondaryCpuAbi\nprivate @android.annotation.Nullable java.lang.String mCpuAbiOverride\nprivate long mLastModifiedTime\nprivate long lastUpdateTime\nprivate long versionCode\nprivate @android.annotation.NonNull com.android.server.pm.PackageSignatures signatures\nprivate @android.annotation.NonNull com.android.server.pm.PackageKeySetData keySetData\nprivate final @android.annotation.NonNull android.util.SparseArray<com.android.server.pm.pkg.PackageUserStateImpl> mUserStates\nprivate @android.annotation.NonNull com.android.server.pm.InstallSource installSource\nprivate @android.annotation.Nullable java.lang.String volumeUuid\nprivate int categoryOverride\nprivate final @android.annotation.NonNull com.android.server.pm.pkg.PackageStateUnserialized pkgState\nprivate @android.annotation.NonNull java.util.UUID mDomainSetId\nprivate @android.annotation.Nullable java.lang.String mAppMetadataFilePath\nprivate int mAppMetadataSource\nprivate int mTargetSdkVersion\nprivate @android.annotation.Nullable byte[] mRestrictUpdateHash\nprivate final @android.annotation.NonNull com.android.server.utils.SnapshotCache<com.android.server.pm.PackageSetting> mSnapshot\nprivate void setBoolean(int,boolean)\nprivate boolean getBoolean(int)\nprivate com.android.server.utils.SnapshotCache<com.android.server.pm.PackageSetting> makeCache()\npublic com.android.server.pm.PackageSetting snapshot()\npublic void dumpDebug(android.util.proto.ProtoOutputStream,long,java.util.List<android.content.pm.UserInfo>,com.android.server.pm.permission.LegacyPermissionDataProvider)\npublic com.android.server.pm.PackageSetting setAppId(int)\npublic com.android.server.pm.PackageSetting setCpuAbiOverride(java.lang.String)\npublic com.android.server.pm.PackageSetting setFirstInstallTimeFromReplaced(com.android.server.pm.pkg.PackageStateInternal,int[])\npublic com.android.server.pm.PackageSetting setFirstInstallTime(long,int)\npublic com.android.server.pm.PackageSetting setForceQueryableOverride(boolean)\npublic com.android.server.pm.PackageSetting setInstallerPackage(java.lang.String,int)\npublic com.android.server.pm.PackageSetting setUpdateOwnerPackage(java.lang.String)\npublic com.android.server.pm.PackageSetting setInstallSource(com.android.server.pm.InstallSource)\n com.android.server.pm.PackageSetting removeInstallerPackage(java.lang.String)\npublic com.android.server.pm.PackageSetting setIsOrphaned(boolean)\npublic com.android.server.pm.PackageSetting setKeySetData(com.android.server.pm.PackageKeySetData)\npublic com.android.server.pm.PackageSetting setLastModifiedTime(long)\npublic com.android.server.pm.PackageSetting setLastUpdateTime(long)\npublic com.android.server.pm.PackageSetting setLongVersionCode(long)\npublic boolean setMimeGroup(java.lang.String,android.util.ArraySet<java.lang.String>)\npublic com.android.server.pm.PackageSetting setPkg(com.android.server.pm.pkg.AndroidPackage)\npublic com.android.server.pm.PackageSetting setPkgStateLibraryFiles(java.util.Collection<java.lang.String>)\npublic com.android.server.pm.PackageSetting setPrimaryCpuAbi(java.lang.String)\npublic com.android.server.pm.PackageSetting setSecondaryCpuAbi(java.lang.String)\npublic com.android.server.pm.PackageSetting setSignatures(com.android.server.pm.PackageSignatures)\npublic com.android.server.pm.PackageSetting setVolumeUuid(java.lang.String)\npublic @java.lang.Override boolean isExternalStorage()\npublic com.android.server.pm.PackageSetting setUpdateAvailable(boolean)\npublic com.android.server.pm.PackageSetting setSharedUserAppId(int)\npublic com.android.server.pm.PackageSetting setTargetSdkVersion(int)\npublic com.android.server.pm.PackageSetting setRestrictUpdateHash(byte[])\npublic @java.lang.Override int getSharedUserAppId()\npublic @java.lang.Override boolean hasSharedUser()\npublic @java.lang.Override java.lang.String toString()\nprivate void copyMimeGroups(java.util.Map<java.lang.String,java.util.Set<java.lang.String>>)\npublic void updateFrom(com.android.server.pm.PackageSetting)\n com.android.server.pm.PackageSetting updateMimeGroups(java.util.Set<java.lang.String>)\npublic @java.lang.Deprecated @java.lang.Override com.android.server.pm.permission.LegacyPermissionState getLegacyPermissionState()\npublic com.android.server.pm.PackageSetting setInstallPermissionsFixed(boolean)\npublic boolean isPrivileged()\npublic boolean isOem()\npublic boolean isVendor()\npublic boolean isProduct()\npublic @java.lang.Override boolean isRequiredForSystemUser()\npublic boolean isSystemExt()\npublic boolean isOdm()\npublic boolean isSystem()\npublic boolean isRequestLegacyExternalStorage()\npublic boolean isUserDataFragile()\npublic android.content.pm.SigningDetails getSigningDetails()\npublic com.android.server.pm.PackageSetting setSigningDetails(android.content.pm.SigningDetails)\npublic void copyPackageSetting(com.android.server.pm.PackageSetting,boolean)\n @com.android.internal.annotations.VisibleForTesting com.android.server.pm.pkg.PackageUserStateImpl modifyUserState(int)\npublic com.android.server.pm.pkg.PackageUserStateImpl getOrCreateUserState(int)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateInternal readUserState(int)\n void setEnabled(int,int,java.lang.String)\n int getEnabled(int)\n void setInstalled(boolean,int)\n void setArchiveState(com.android.server.pm.pkg.ArchiveState,int)\n boolean getInstalled(int)\n boolean isArchived(int)\n int getInstallReason(int)\n void setInstallReason(int,int)\n int getUninstallReason(int)\n void setUninstallReason(int,int)\n @android.annotation.NonNull android.content.pm.overlay.OverlayPaths getOverlayPaths(int)\n boolean setOverlayPathsForLibrary(java.lang.String,android.content.pm.overlay.OverlayPaths,int)\n boolean isInstalledOnAnyOtherUser(int[],int)\n boolean hasDataOnAnyOtherUser(int[],int)\n int[] queryInstalledUsers(int[],boolean)\n int[] queryUsersInstalledOrHasData(int[])\n long getCeDataInode(int)\n long getDeDataInode(int)\n void setCeDataInode(long,int)\n void setDeDataInode(long,int)\n boolean getStopped(int)\n void setStopped(boolean,int)\npublic com.android.server.pm.PackageSetting setScannedAsStoppedSystemApp(boolean)\n boolean getNotLaunched(int)\n void setNotLaunched(boolean,int)\n boolean getHidden(int)\n void setHidden(boolean,int)\n int getDistractionFlags(int)\n void setDistractionFlags(int,int)\npublic boolean getInstantApp(int)\n void setInstantApp(boolean,int)\n boolean getVirtualPreload(int)\n void setVirtualPreload(boolean,int)\n void setUserState(int,long,long,int,boolean,boolean,boolean,boolean,int,android.util.ArrayMap<android.content.pm.UserPackage,com.android.server.pm.pkg.SuspendParams>,boolean,boolean,java.lang.String,android.util.ArraySet<java.lang.String>,android.util.ArraySet<java.lang.String>,int,int,java.lang.String,java.lang.String,long,int,com.android.server.pm.pkg.ArchiveState)\n void setUserState(int,com.android.server.pm.pkg.PackageUserStateInternal)\n com.android.server.utils.WatchedArraySet<java.lang.String> getEnabledComponents(int)\n com.android.server.utils.WatchedArraySet<java.lang.String> getDisabledComponents(int)\n void setEnabledComponents(com.android.server.utils.WatchedArraySet<java.lang.String>,int)\n void setDisabledComponents(com.android.server.utils.WatchedArraySet<java.lang.String>,int)\n void setEnabledComponentsCopy(com.android.server.utils.WatchedArraySet<java.lang.String>,int)\n void setDisabledComponentsCopy(com.android.server.utils.WatchedArraySet<java.lang.String>,int)\n com.android.server.pm.pkg.PackageUserStateImpl modifyUserStateComponents(int,boolean,boolean)\n void addDisabledComponent(java.lang.String,int)\n void addEnabledComponent(java.lang.String,int)\n boolean enableComponentLPw(java.lang.String,int)\n boolean disableComponentLPw(java.lang.String,int)\n boolean restoreComponentLPw(java.lang.String,int)\n void restoreComponentSettings(int)\n int getCurrentEnabledStateLPr(java.lang.String,int)\n void removeUser(int)\npublic int[] getNotInstalledUserIds()\n void writePackageUserPermissionsProto(android.util.proto.ProtoOutputStream,long,java.util.List<android.content.pm.UserInfo>,com.android.server.pm.permission.LegacyPermissionDataProvider)\nprotected void writeUsersInfoToProto(android.util.proto.ProtoOutputStream,long)\nprivate static void writeArchiveState(android.util.proto.ProtoOutputStream,com.android.server.pm.pkg.ArchiveState)\npublic @com.android.internal.annotations.VisibleForTesting com.android.server.pm.PackageSetting setPath(java.io.File)\npublic @com.android.internal.annotations.VisibleForTesting com.android.server.pm.PackageSetting addOldPath(java.io.File)\npublic @com.android.internal.annotations.VisibleForTesting com.android.server.pm.PackageSetting removeOldPath(java.io.File)\npublic @com.android.internal.annotations.VisibleForTesting boolean overrideNonLocalizedLabelAndIcon(android.content.ComponentName,java.lang.String,java.lang.Integer,int)\npublic void resetOverrideComponentLabelIcon(int)\npublic @android.annotation.Nullable java.lang.String getSplashScreenTheme(int)\npublic boolean isIncremental()\npublic boolean isLoading()\npublic com.android.server.pm.PackageSetting setLoadingProgress(float)\npublic com.android.server.pm.PackageSetting setLoadingCompletedTime(long)\npublic com.android.server.pm.PackageSetting setAppMetadataFilePath(java.lang.String)\npublic com.android.server.pm.PackageSetting setAppMetadataSource(int)\npublic @android.annotation.NonNull @java.lang.Override long getVersionCode()\npublic @android.annotation.Nullable @java.lang.Override java.util.Map<java.lang.String,java.util.Set<java.lang.String>> getMimeGroups()\npublic @android.annotation.NonNull @java.lang.Override java.lang.String getPackageName()\npublic @android.annotation.Nullable @java.lang.Override com.android.server.pm.pkg.AndroidPackage getAndroidPackage()\npublic @android.annotation.NonNull android.content.pm.SigningInfo getSigningInfo()\npublic @android.annotation.NonNull @java.lang.Override java.lang.String[] getUsesSdkLibraries()\npublic @android.annotation.NonNull @java.lang.Override long[] getUsesSdkLibrariesVersionsMajor()\npublic @android.annotation.NonNull @java.lang.Override boolean[] getUsesSdkLibrariesOptional()\npublic @android.annotation.NonNull @java.lang.Override java.lang.String[] getUsesStaticLibraries()\npublic @android.annotation.NonNull @java.lang.Override long[] getUsesStaticLibrariesVersions()\npublic @android.annotation.NonNull @java.lang.Override java.util.List<com.android.server.pm.pkg.SharedLibrary> getSharedLibraryDependencies()\npublic @android.annotation.NonNull com.android.server.pm.PackageSetting addUsesLibraryInfo(android.content.pm.SharedLibraryInfo)\npublic @android.annotation.NonNull @java.lang.Override java.util.List<java.lang.String> getUsesLibraryFiles()\npublic @android.annotation.NonNull com.android.server.pm.PackageSetting addUsesLibraryFile(java.lang.String)\npublic @java.lang.Override boolean isHiddenUntilInstalled()\npublic @android.annotation.NonNull @java.lang.Override long[] getLastPackageUsageTime()\npublic @java.lang.Override boolean isUpdatedSystemApp()\npublic @java.lang.Override boolean isApkInUpdatedApex()\npublic @android.annotation.Nullable @java.lang.Override java.lang.String getApexModuleName()\npublic com.android.server.pm.PackageSetting setDomainSetId(java.util.UUID)\npublic com.android.server.pm.PackageSetting setCategoryOverride(int)\npublic com.android.server.pm.PackageSetting setLegacyNativeLibraryPath(java.lang.String)\npublic com.android.server.pm.PackageSetting setMimeGroups(java.util.Map<java.lang.String,java.util.Set<java.lang.String>>)\npublic com.android.server.pm.PackageSetting setUsesSdkLibraries(java.lang.String[])\npublic com.android.server.pm.PackageSetting setUsesSdkLibrariesVersionsMajor(long[])\npublic com.android.server.pm.PackageSetting setUsesSdkLibrariesOptional(boolean[])\npublic com.android.server.pm.PackageSetting setUsesStaticLibraries(java.lang.String[])\npublic com.android.server.pm.PackageSetting setUsesStaticLibrariesVersions(long[])\npublic com.android.server.pm.PackageSetting setApexModuleName(java.lang.String)\npublic @android.annotation.NonNull @java.lang.Override com.android.server.pm.pkg.PackageStateUnserialized getTransientState()\npublic @android.annotation.NonNull android.util.SparseArray<? extends PackageUserStateInternal> getUserStates()\npublic com.android.server.pm.PackageSetting addMimeTypes(java.lang.String,java.util.Set<java.lang.String>)\npublic @android.annotation.NonNull @java.lang.Override com.android.server.pm.pkg.PackageUserState getStateForUser(android.os.UserHandle)\npublic @android.annotation.Nullable java.lang.String getPrimaryCpuAbi()\npublic @android.annotation.Nullable java.lang.String getSecondaryCpuAbi()\npublic @android.annotation.Nullable @java.lang.Override java.lang.String getSeInfo()\npublic @android.annotation.Nullable java.lang.String getPrimaryCpuAbiLegacy()\npublic @android.annotation.Nullable java.lang.String getSecondaryCpuAbiLegacy()\npublic @android.content.pm.ApplicationInfo.HiddenApiEnforcementPolicy @java.lang.Override int getHiddenApiEnforcementPolicy()\npublic @java.lang.Override boolean isApex()\npublic @java.lang.Override boolean isForceQueryableOverride()\npublic @java.lang.Override boolean isUpdateAvailable()\npublic @java.lang.Override boolean isInstallPermissionsFixed()\npublic @java.lang.Override boolean isDefaultToDeviceProtectedStorage()\npublic @java.lang.Override boolean isPersistent()\npublic @java.lang.Override boolean isScannedAsStoppedSystemApp()\nclass PackageSetting extends com.android.server.pm.SettingBase implements [com.android.server.pm.pkg.PackageStateInternal]\nprivate static final int INSTALL_PERMISSION_FIXED\nprivate static final int UPDATE_AVAILABLE\nprivate static final int FORCE_QUERYABLE_OVERRIDE\nprivate static final int SCANNED_AS_STOPPED_SYSTEM_APP\nclass Booleans extends java.lang.Object implements []\n@com.android.internal.util.DataClass(genGetters=true, genConstructor=false, genSetters=false, genBuilder=false)")
@Deprecated
private void __metadata() {}
diff --git a/services/core/java/com/android/server/pm/Settings.java b/services/core/java/com/android/server/pm/Settings.java
index c7ee649..5575f52 100644
--- a/services/core/java/com/android/server/pm/Settings.java
+++ b/services/core/java/com/android/server/pm/Settings.java
@@ -924,6 +924,7 @@
ret.setUsesStaticLibrariesVersions(p.getUsesStaticLibrariesVersions());
ret.setMimeGroups(p.getMimeGroups());
ret.setAppMetadataFilePath(p.getAppMetadataFilePath());
+ ret.setAppMetadataSource(p.getAppMetadataSource());
ret.getPkgState().setUpdatedSystemApp(false);
ret.setTargetSdkVersion(p.getTargetSdkVersion());
ret.setRestrictUpdateHash(p.getRestrictUpdateHash());
@@ -3122,6 +3123,9 @@
pkg.getAppMetadataFilePath());
}
+ serializer.attributeInt(null, "appMetadataSource",
+ pkg.getAppMetadataSource());
+
writeUsesSdkLibLPw(serializer, pkg.getUsesSdkLibraries(),
pkg.getUsesSdkLibrariesVersionsMajor(), pkg.getUsesSdkLibrariesOptional());
@@ -3226,6 +3230,9 @@
if (pkg.getAppMetadataFilePath() != null) {
serializer.attribute(null, "appMetadataFilePath", pkg.getAppMetadataFilePath());
}
+ serializer.attributeInt(null, "appMetadataSource",
+ pkg.getAppMetadataSource());
+
writeUsesSdkLibLPw(serializer, pkg.getUsesSdkLibraries(),
pkg.getUsesSdkLibrariesVersionsMajor(), pkg.getUsesSdkLibrariesOptional());
@@ -3942,6 +3949,8 @@
}
ps.setAppMetadataFilePath(parser.getAttributeValue(null, "appMetadataFilePath"));
+ ps.setAppMetadataSource(parser.getAttributeInt(null,
+ "appMetadataSource", PackageManager.APP_METADATA_SOURCE_UNKNOWN));
int outerDepth = parser.getDepth();
int type;
@@ -4021,6 +4030,7 @@
long loadingCompletedTime = 0;
UUID domainSetId;
String appMetadataFilePath = null;
+ int appMetadataSource = PackageManager.APP_METADATA_SOURCE_UNKNOWN;
int targetSdkVersion = 0;
byte[] restrictUpdateHash = null;
boolean isScannedAsStoppedSystemApp = false;
@@ -4066,6 +4076,9 @@
categoryHint = parser.getAttributeInt(null, "categoryHint",
ApplicationInfo.CATEGORY_UNDEFINED);
appMetadataFilePath = parser.getAttributeValue(null, "appMetadataFilePath");
+ appMetadataSource = parser.getAttributeInt(null, "appMetadataSource",
+ PackageManager.APP_METADATA_SOURCE_UNKNOWN);
+
isScannedAsStoppedSystemApp = parser.getAttributeBoolean(null,
"scannedAsStoppedSystemApp", false);
@@ -4214,6 +4227,7 @@
.setLoadingProgress(loadingProgress)
.setLoadingCompletedTime(loadingCompletedTime)
.setAppMetadataFilePath(appMetadataFilePath)
+ .setAppMetadataSource(appMetadataSource)
.setTargetSdkVersion(targetSdkVersion)
.setRestrictUpdateHash(restrictUpdateHash)
.setScannedAsStoppedSystemApp(isScannedAsStoppedSystemApp);
@@ -5030,6 +5044,10 @@
pw.print(prefix); pw.print(" updatableSystem=false");
pw.println();
}
+ if (pkg.getEmergencyInstaller() != null) {
+ pw.print(prefix); pw.print(" emergencyInstaller=");
+ pw.println(pkg.getEmergencyInstaller());
+ }
if (pkg.hasPreserveLegacyExternalStorage()) {
pw.print(prefix); pw.print(" hasPreserveLegacyExternalStorage=true");
pw.println();
@@ -5223,6 +5241,8 @@
}
pw.print(prefix); pw.print(" appMetadataFilePath=");
pw.println(ps.getAppMetadataFilePath());
+ pw.print(prefix); pw.print(" appMetadataSource=");
+ pw.println(ps.getAppMetadataSource());
if (ps.getVolumeUuid() != null) {
pw.print(prefix); pw.print(" volumeUuid=");
pw.println(ps.getVolumeUuid());
diff --git a/services/core/java/com/android/server/pm/ShortcutUser.java b/services/core/java/com/android/server/pm/ShortcutUser.java
index e9c6aab..b35f9c2 100644
--- a/services/core/java/com/android/server/pm/ShortcutUser.java
+++ b/services/core/java/com/android/server/pm/ShortcutUser.java
@@ -209,9 +209,8 @@
if (ret == null) {
ret = new ShortcutLauncher(this, mUserId, packageName, launcherUserId);
mLaunchers.put(key, ret);
- } else {
- ret.attemptToRestoreIfNeededAndSave();
}
+ ret.attemptToRestoreIfNeededAndSave();
return ret;
}
diff --git a/services/core/java/com/android/server/pm/UserManagerService.java b/services/core/java/com/android/server/pm/UserManagerService.java
index c0596bb..796edde 100644
--- a/services/core/java/com/android/server/pm/UserManagerService.java
+++ b/services/core/java/com/android/server/pm/UserManagerService.java
@@ -1385,7 +1385,7 @@
@Override
public int[] getProfileIds(@UserIdInt int userId, boolean enabledOnly) {
- return getProfileIds(userId, null, enabledOnly);
+ return getProfileIds(userId, null, enabledOnly, /* excludeHidden */ false);
}
// TODO(b/142482943): Probably @Override and make this accessible in UserManager.
@@ -1397,14 +1397,14 @@
* If enabledOnly, only returns users that are not {@link UserInfo#FLAG_DISABLED}.
*/
public int[] getProfileIds(@UserIdInt int userId, @Nullable String userType,
- boolean enabledOnly) {
+ boolean enabledOnly, boolean excludeHidden) {
if (userId != UserHandle.getCallingUserId()) {
checkQueryOrCreateUsersPermission("getting profiles related to user " + userId);
}
final long ident = Binder.clearCallingIdentity();
try {
synchronized (mUsersLock) {
- return getProfileIdsLU(userId, userType, enabledOnly).toArray();
+ return getProfileIdsLU(userId, userType, enabledOnly, excludeHidden).toArray();
}
} finally {
Binder.restoreCallingIdentity(ident);
@@ -1415,7 +1415,8 @@
@GuardedBy("mUsersLock")
private List<UserInfo> getProfilesLU(@UserIdInt int userId, @Nullable String userType,
boolean enabledOnly, boolean fullInfo) {
- IntArray profileIds = getProfileIdsLU(userId, userType, enabledOnly);
+ IntArray profileIds = getProfileIdsLU(userId, userType, enabledOnly, /* excludeHidden */
+ false);
ArrayList<UserInfo> users = new ArrayList<>(profileIds.size());
for (int i = 0; i < profileIds.size(); i++) {
int profileId = profileIds.get(i);
@@ -1440,7 +1441,7 @@
*/
@GuardedBy("mUsersLock")
private IntArray getProfileIdsLU(@UserIdInt int userId, @Nullable String userType,
- boolean enabledOnly) {
+ boolean enabledOnly, boolean excludeHidden) {
UserInfo user = getUserInfoLU(userId);
IntArray result = new IntArray(mUsers.size());
if (user == null) {
@@ -1465,11 +1466,36 @@
if (userType != null && !userType.equals(profile.userType)) {
continue;
}
+ if (excludeHidden && isProfileHidden(userId)) {
+ continue;
+ }
result.add(profile.id);
}
return result;
}
+ /*
+ * Returns all the users that are in the same profile group as userId excluding those with
+ * {@link UserProperties#getProfileApiVisibility()} set to hidden. The returned list includes
+ * the user itself.
+ */
+ // TODO (b/323011770): Add a permission check to make an exception for App stores if we end
+ // up supporting Private Space on COPE devices
+ @Override
+ public int[] getProfileIdsExcludingHidden(@UserIdInt int userId, boolean enabledOnly) {
+ return getProfileIds(userId, null, enabledOnly, /* excludeHidden */ true);
+ }
+
+ private boolean isProfileHidden(int userId) {
+ UserProperties userProperties = getUserPropertiesCopy(userId);
+ if (android.os.Flags.allowPrivateProfile()
+ && android.multiuser.Flags.enableHidingProfiles()) {
+ return userProperties.getProfileApiVisibility()
+ == UserProperties.PROFILE_API_VISIBILITY_HIDDEN;
+ }
+ return false;
+ }
+
@Override
public int getCredentialOwnerProfile(@UserIdInt int userId) {
checkManageUsersPermission("get the credential owner");
@@ -3630,7 +3656,8 @@
return 0;
}
- final int userTypeCount = getProfileIds(userId, userType, false).length;
+ final int userTypeCount = getProfileIds(userId, userType, false, /* excludeHidden */
+ false).length;
final int profilesRemovedCount = userTypeCount > 0 && allowedToRemoveOne ? 1 : 0;
final int usersCountAfterRemoving = getAliveUsersExcludingGuestsCountLU()
- profilesRemovedCount;
@@ -5931,7 +5958,8 @@
}
userData = mUsers.get(userId);
isProfile = userData.info.isProfile();
- profileIds = isProfile ? null : getProfileIdsLU(userId, null, false);
+ profileIds = isProfile ? null : getProfileIdsLU(userId, null, false, /* excludeHidden */
+ false);
}
if (!isProfile) {
@@ -7458,7 +7486,8 @@
@Override
public @NonNull int[] getProfileIds(@UserIdInt int userId, boolean enabledOnly) {
synchronized (mUsersLock) {
- return getProfileIdsLU(userId, null /* userType */, enabledOnly).toArray();
+ return getProfileIdsLU(userId, null /* userType */, enabledOnly, /* excludeHidden */
+ false).toArray();
}
}
diff --git a/services/core/java/com/android/server/pm/UserTypeFactory.java b/services/core/java/com/android/server/pm/UserTypeFactory.java
index 23d0230..067a012 100644
--- a/services/core/java/com/android/server/pm/UserTypeFactory.java
+++ b/services/core/java/com/android/server/pm/UserTypeFactory.java
@@ -288,28 +288,6 @@
* configuration.
*/
private static UserTypeDetails.Builder getDefaultTypeProfilePrivate() {
- UserProperties.Builder userPropertiesBuilder = new UserProperties.Builder()
- .setStartWithParent(true)
- .setCredentialShareableWithParent(true)
- .setAuthAlwaysRequiredToDisableQuietMode(true)
- .setAllowStoppingUserWithDelayedLocking(true)
- .setMediaSharedWithParent(false)
- .setShowInLauncher(UserProperties.SHOW_IN_LAUNCHER_SEPARATE)
- .setShowInSettings(UserProperties.SHOW_IN_SETTINGS_SEPARATE)
- .setShowInQuietMode(
- UserProperties.SHOW_IN_QUIET_MODE_HIDDEN)
- .setShowInSharingSurfaces(
- UserProperties.SHOW_IN_SHARING_SURFACES_SEPARATE)
- .setCrossProfileIntentFilterAccessControl(
- UserProperties.CROSS_PROFILE_INTENT_FILTER_ACCESS_LEVEL_SYSTEM)
- .setInheritDevicePolicy(UserProperties.INHERIT_DEVICE_POLICY_FROM_PARENT)
- .setCrossProfileContentSharingStrategy(
- UserProperties.CROSS_PROFILE_CONTENT_SHARING_DELEGATE_FROM_PARENT);
- if (android.multiuser.Flags.supportHidingProfiles()) {
- userPropertiesBuilder.setProfileApiVisibility(
- UserProperties.PROFILE_API_VISIBILITY_HIDDEN);
- }
-
return new UserTypeDetails.Builder()
.setName(USER_TYPE_PROFILE_PRIVATE)
.setBaseType(FLAG_PROFILE)
@@ -328,7 +306,26 @@
.setDarkThemeBadgeColors(
R.color.white)
.setDefaultRestrictions(getDefaultProfileRestrictions())
- .setDefaultUserProperties(userPropertiesBuilder);
+ .setDefaultUserProperties(new UserProperties.Builder()
+ .setStartWithParent(true)
+ .setCredentialShareableWithParent(true)
+ .setAuthAlwaysRequiredToDisableQuietMode(true)
+ .setAllowStoppingUserWithDelayedLocking(true)
+ .setMediaSharedWithParent(false)
+ .setShowInLauncher(UserProperties.SHOW_IN_LAUNCHER_SEPARATE)
+ .setShowInSettings(UserProperties.SHOW_IN_SETTINGS_SEPARATE)
+ .setShowInQuietMode(
+ UserProperties.SHOW_IN_QUIET_MODE_HIDDEN)
+ .setShowInSharingSurfaces(
+ UserProperties.SHOW_IN_SHARING_SURFACES_SEPARATE)
+ .setCrossProfileIntentFilterAccessControl(
+ UserProperties.CROSS_PROFILE_INTENT_FILTER_ACCESS_LEVEL_SYSTEM)
+ .setInheritDevicePolicy(UserProperties.INHERIT_DEVICE_POLICY_FROM_PARENT)
+ .setCrossProfileContentSharingStrategy(
+ UserProperties.CROSS_PROFILE_CONTENT_SHARING_DELEGATE_FROM_PARENT)
+ .setProfileApiVisibility(
+ UserProperties.PROFILE_API_VISIBILITY_HIDDEN)
+ .setItemsRestrictedOnHomeScreen(true));
}
/**
diff --git a/services/core/java/com/android/server/pm/dex/PackageDynamicCodeLoading.java b/services/core/java/com/android/server/pm/dex/PackageDynamicCodeLoading.java
index cc26c9b..9159851 100644
--- a/services/core/java/com/android/server/pm/dex/PackageDynamicCodeLoading.java
+++ b/services/core/java/com/android/server/pm/dex/PackageDynamicCodeLoading.java
@@ -538,9 +538,10 @@
} else {
if (fileInfo.mUserId != userId) {
// This should be impossible: private app files are always user-specific and
- // can't be accessed from different users.
- throw new IllegalArgumentException("Cannot change userId for '" + path
- + "' from " + fileInfo.mUserId + " to " + userId);
+ // can't be accessed from different users. But it does very occasionally happen
+ // (b/323665257). Ignore such cases - we shouldn't record data from a different
+ // user.
+ return false;
}
// Changing file type (i.e. loading the same file in different ways is possible if
// unlikely. We allow it but ignore it.
diff --git a/services/core/java/com/android/server/pm/parsing/PackageInfoUtils.java b/services/core/java/com/android/server/pm/parsing/PackageInfoUtils.java
index 6ed2d31..1c70af0 100644
--- a/services/core/java/com/android/server/pm/parsing/PackageInfoUtils.java
+++ b/services/core/java/com/android/server/pm/parsing/PackageInfoUtils.java
@@ -594,6 +594,7 @@
}
ai.applicationInfo = applicationInfo;
ai.requiredDisplayCategory = a.getRequiredDisplayCategory();
+ ai.requireContentUriPermissionFromCaller = a.getRequireContentUriPermissionFromCaller();
ai.setKnownActivityEmbeddingCerts(a.getKnownActivityEmbeddingCerts());
assignFieldsComponentInfoParsedMainComponent(ai, a, pkgSetting, userId);
return ai;
@@ -831,7 +832,7 @@
retProcs.put(proc.getName(),
new ProcessInfo(proc.getName(), new ArraySet<>(proc.getDeniedPermissions()),
proc.getGwpAsanMode(), proc.getMemtagMode(),
- proc.getNativeHeapZeroInitialized()));
+ proc.getNativeHeapZeroInitialized(), proc.isUseEmbeddedDex()));
}
return retProcs;
}
diff --git a/services/core/java/com/android/server/pm/pkg/PackageStateInternal.java b/services/core/java/com/android/server/pm/pkg/PackageStateInternal.java
index f7603b5..85ea83a 100644
--- a/services/core/java/com/android/server/pm/pkg/PackageStateInternal.java
+++ b/services/core/java/com/android/server/pm/pkg/PackageStateInternal.java
@@ -116,4 +116,9 @@
@Nullable
Set<File> getOldPaths();
+
+ /**
+ * @return the source of the app metadata that is currently associated with the given package.
+ */
+ int getAppMetadataSource();
}
diff --git a/services/core/java/com/android/server/pm/verify/domain/DomainVerificationManagerInternal.java b/services/core/java/com/android/server/pm/verify/domain/DomainVerificationManagerInternal.java
index 53ee189..7ca449a 100644
--- a/services/core/java/com/android/server/pm/verify/domain/DomainVerificationManagerInternal.java
+++ b/services/core/java/com/android/server/pm/verify/domain/DomainVerificationManagerInternal.java
@@ -26,6 +26,7 @@
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.pm.ResolveInfo;
+import android.content.pm.verify.domain.DomainSet;
import android.content.pm.verify.domain.DomainVerificationInfo;
import android.content.pm.verify.domain.DomainVerificationManager;
import android.content.pm.verify.domain.DomainVerificationState;
@@ -230,13 +231,20 @@
* broadcast will be sent to the domain verification agent so it may re-run any verification
* logic for the newly associated domains.
* <p>
- * This will mutate internal {@link DomainVerificationPkgState} and so will hold the internal
- * lock. This should never be called from within the domain verification classes themselves.
+ * Optionally, the caller can specify a set of domains that are already pre-verified by the
+ * installer. These domains, if specified with autoVerify in the manifest, will be regarded as
+ * verified as soon as the app is installed, until the domain verification agent sends back the
+ * real verification results.
+ * <p>
+ * This method will mutate internal {@link DomainVerificationPkgState} and so will hold the
+ * internal lock. This should never be called from within the domain verification classes
+ * themselves.
* <p>
* This will NOT call {@link #writeSettings(Computer, TypedXmlSerializer, boolean, int)}. That must be
* handled by the caller.
*/
- void addPackage(@NonNull PackageStateInternal newPkgSetting);
+ void addPackage(@NonNull PackageStateInternal newPkgSetting,
+ @Nullable DomainSet preVerifiedDomains);
/**
* Migrates verification state from a previous install to a new one. It is expected that the
@@ -245,14 +253,20 @@
* domains under the assumption that the new package will pass the same server side config as
* the previous package, as they have matching signatures.
* <p>
- * This will mutate internal {@link DomainVerificationPkgState} and so will hold the internal
- * lock. This should never be called from within the domain verification classes themselves.
+ * Optionally, the caller can specify a set of domains that are already pre-verified by the
+ * installer. These domains, if specified with autoVerify in the manifest, will be regarded as
+ * verified as soon as the app is updated, until the domain verification agent sends back the
+ * real verification results.
+ * <p>
+ * This method will mutate internal {@link DomainVerificationPkgState} and so will hold the
+ * internal lock. This should never be called from within the domain verification classes
+ * themselves.
* <p>
* This will NOT call {@link #writeSettings(Computer, TypedXmlSerializer, boolean, int)}. That must be
* handled by the caller.
*/
void migrateState(@NonNull PackageStateInternal oldPkgSetting,
- @NonNull PackageStateInternal newPkgSetting);
+ @NonNull PackageStateInternal newPkgSetting, @Nullable DomainSet preVerifiedDomains);
/**
* Serializes the entire internal state. This is equivalent to a full backup of the existing
diff --git a/services/core/java/com/android/server/pm/verify/domain/DomainVerificationService.java b/services/core/java/com/android/server/pm/verify/domain/DomainVerificationService.java
index 6150099..c796b40 100644
--- a/services/core/java/com/android/server/pm/verify/domain/DomainVerificationService.java
+++ b/services/core/java/com/android/server/pm/verify/domain/DomainVerificationService.java
@@ -32,6 +32,7 @@
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.pm.ResolveInfo;
import android.content.pm.verify.domain.DomainOwner;
+import android.content.pm.verify.domain.DomainSet;
import android.content.pm.verify.domain.DomainVerificationInfo;
import android.content.pm.verify.domain.DomainVerificationManager;
import android.content.pm.verify.domain.DomainVerificationState;
@@ -859,7 +860,7 @@
@Override
public void migrateState(@NonNull PackageStateInternal oldPkgSetting,
- @NonNull PackageStateInternal newPkgSetting) {
+ @NonNull PackageStateInternal newPkgSetting, @Nullable DomainSet preVerifiedDomains) {
String pkgName = newPkgSetting.getPackageName();
boolean sendBroadcast;
@@ -935,6 +936,9 @@
sendBroadcast = hasAutoVerifyDomains && needsBroadcast;
+ // Apply pre-verified states as the last step of migration
+ applyPreVerifiedState(newStateMap, newAutoVerifyDomains, preVerifiedDomains);
+
mAttachedPkgStates.put(pkgName, newDomainSetId, new DomainVerificationPkgState(
pkgName, newDomainSetId, hasAutoVerifyDomains, newStateMap, newUserStates,
null /* signature */));
@@ -947,7 +951,8 @@
// TODO(b/159952358): Handle valid domainSetIds for PackageStateInternals with no AndroidPackage
@Override
- public void addPackage(@NonNull PackageStateInternal newPkgSetting) {
+ public void addPackage(@NonNull PackageStateInternal newPkgSetting,
+ @Nullable DomainSet preVerifiedDomains) {
// TODO(b/159952358): Optimize packages without any domains. Those wouldn't have to be in
// the state map, but it would require handling the "migration" case where an app either
// gains or loses all domains.
@@ -1029,6 +1034,9 @@
DomainVerificationState.STATE_MIGRATED);
}
}
+
+ // Apply pre-verified states before sending out broadcast
+ applyPreVerifiedState(pkgState.getStateMap(), autoVerifyDomains, preVerifiedDomains);
}
synchronized (mLock) {
@@ -1040,6 +1048,27 @@
}
}
+ private void applyPreVerifiedState(ArrayMap<String, Integer> stateMap,
+ ArraySet<String> autoVerifyDomains,
+ DomainSet preVerifiedDomains) {
+ // If any pre-verified domains are provided, treating them as verified as well. This
+ // allows the app to be opened immediately by the corresponding app links, but the
+ // pre-verified state can still be overwritten by the domain verification agent in the
+ // future.
+ if (preVerifiedDomains != null && !autoVerifyDomains.isEmpty()) {
+ for (String preVerifiedDomain : preVerifiedDomains.getDomains()) {
+ if (autoVerifyDomains.contains(preVerifiedDomain)
+ && !stateMap.containsKey(preVerifiedDomain)) {
+ // Only set the pre-verified state if there's no existing state
+ stateMap.put(preVerifiedDomain, DomainVerificationState.STATE_PRE_VERIFIED);
+ if (DEBUG_APPROVAL) {
+ Slog.d(TAG, "Inserted pre-verified domain: " + preVerifiedDomain);
+ }
+ }
+ }
+ }
+ }
+
/**
* Applies any immutable state as the final step when adding or migrating state. Currently only
* applies {@link SystemConfig#getLinkedApps()}, which approves all domains for a system app.
diff --git a/services/core/java/com/android/server/pm/verify/domain/DomainVerificationShell.java b/services/core/java/com/android/server/pm/verify/domain/DomainVerificationShell.java
index 466c4c9..13b072b 100644
--- a/services/core/java/com/android/server/pm/verify/domain/DomainVerificationShell.java
+++ b/services/core/java/com/android/server/pm/verify/domain/DomainVerificationShell.java
@@ -62,6 +62,7 @@
pw.println(" - restored: preserved verification from a user data restore");
pw.println(" - legacy_failure: rejected by a legacy verifier, unknown reason");
pw.println(" - system_configured: automatically approved by the device config");
+ pw.println(" - pre_verified: the domain was pre-verified by the installer");
pw.println(" - >= 1024: Custom error code which is specific to the device verifier");
pw.println(" --user <USER_ID>: include user selections (includes all domains, not");
pw.println(" just autoVerify ones)");
diff --git a/services/core/java/com/android/server/policy/DeviceStatePolicyImpl.java b/services/core/java/com/android/server/policy/DeviceStatePolicyImpl.java
index 7754944..07cc775 100644
--- a/services/core/java/com/android/server/policy/DeviceStatePolicyImpl.java
+++ b/services/core/java/com/android/server/policy/DeviceStatePolicyImpl.java
@@ -17,11 +17,14 @@
package com.android.server.policy;
import android.annotation.NonNull;
+import android.annotation.Nullable;
import android.content.Context;
import com.android.server.devicestate.DeviceStatePolicy;
import com.android.server.devicestate.DeviceStateProvider;
+import java.io.PrintWriter;
+
/**
* Default empty implementation of {@link DeviceStatePolicy}.
*
@@ -43,4 +46,9 @@
public void configureDeviceForState(int state, @NonNull Runnable onComplete) {
onComplete.run();
}
+
+ @Override
+ public void dump(@NonNull PrintWriter writer, @Nullable String[] args) {
+ mProvider.dump(writer, args);
+ }
}
diff --git a/services/core/java/com/android/server/policy/DeviceStateProviderImpl.java b/services/core/java/com/android/server/policy/DeviceStateProviderImpl.java
index 3644054..afcf5a0 100644
--- a/services/core/java/com/android/server/policy/DeviceStateProviderImpl.java
+++ b/services/core/java/com/android/server/policy/DeviceStateProviderImpl.java
@@ -58,6 +58,7 @@
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
+import java.io.PrintWriter;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Arrays;
@@ -503,6 +504,24 @@
// Do nothing.
}
+ @Override
+ public void dump(@NonNull PrintWriter writer, @Nullable String[] args) {
+ writer.println("DeviceStateProviderImpl");
+
+ synchronized (mLock) {
+ writer.println(" mLastReportedState = " + mLastReportedState);
+ writer.println(" mPowerSaveModeEnabled = " + mPowerSaveModeEnabled);
+ writer.println(" mThermalStatus = " + mThermalStatus);
+ writer.println(" mIsLidOpen = " + mIsLidOpen);
+ writer.println(" Sensor values:");
+
+ for (Sensor sensor : mLatestSensorEvent.keySet()) {
+ SensorEvent sensorEvent = mLatestSensorEvent.get(sensor);
+ writer.println(" - " + toSensorValueString(sensor, sensorEvent));
+ }
+ }
+ }
+
/**
* Implementation of {@link BooleanSupplier} that returns {@code true} if the expected lid
* switch open state matches {@link #mIsLidOpen}.
@@ -669,14 +688,16 @@
Slog.i(TAG, "Sensor values:");
for (Sensor sensor : mLatestSensorEvent.keySet()) {
SensorEvent sensorEvent = mLatestSensorEvent.get(sensor);
- if (sensorEvent != null) {
- Slog.i(TAG, sensor.getName() + ": " + Arrays.toString(sensorEvent.values));
- } else {
- Slog.i(TAG, sensor.getName() + ": null");
- }
+ Slog.i(TAG, toSensorValueString(sensor, sensorEvent));
}
}
+ private String toSensorValueString(Sensor sensor, @Nullable SensorEvent event) {
+ String sensorString = sensor == null ? "null" : sensor.getName();
+ String eventValues = event == null ? "null" : Arrays.toString(event.values);
+ return sensorString + " : " + eventValues;
+ }
+
/**
* Tries to parse the provided file into a {@link DeviceStateConfig} object. Returns
* {@code null} if the file could not be successfully parsed.
diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java
index 51790b8..775a361 100644
--- a/services/core/java/com/android/server/policy/PhoneWindowManager.java
+++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java
@@ -4708,6 +4708,9 @@
case KeyEvent.KEYCODE_BACK: {
logKeyboardSystemsEventOnActionUp(event, KeyboardLogEvent.BACK);
if (down) {
+ // There may have other embedded activities on the same Task. Try to move the
+ // focus before processing the back event.
+ mWindowManagerInternal.moveFocusToTopEmbeddedWindowIfNeeded();
mBackKeyHandled = false;
} else {
if (!hasLongPressOnBackBehavior()) {
@@ -5016,6 +5019,8 @@
case KeyEvent.KEYCODE_STYLUS_BUTTON_SECONDARY:
case KeyEvent.KEYCODE_STYLUS_BUTTON_TERTIARY:
case KeyEvent.KEYCODE_STYLUS_BUTTON_TAIL: {
+ Slog.i(TAG, "Stylus buttons event: " + keyCode + " received. Should handle event? "
+ + mStylusButtonsEnabled);
if (mStylusButtonsEnabled) {
sendSystemKeyToStatusBarAsync(event);
}
diff --git a/services/core/java/com/android/server/policy/TalkbackShortcutController.java b/services/core/java/com/android/server/policy/TalkbackShortcutController.java
index 906da2f..b05a421 100644
--- a/services/core/java/com/android/server/policy/TalkbackShortcutController.java
+++ b/services/core/java/com/android/server/policy/TalkbackShortcutController.java
@@ -59,6 +59,10 @@
final Set<ComponentName> enabledServices =
AccessibilityUtils.getEnabledServicesFromSettings(mContext, userId);
ComponentName componentName = getTalkbackComponent();
+ if (componentName == null) {
+ return false;
+ }
+
boolean isTalkbackAlreadyEnabled = enabledServices.contains(componentName);
if (isTalkBackShortcutGestureEnabled()) {
@@ -67,7 +71,7 @@
isTalkbackAlreadyEnabled);
// log stem triple press telemetry if it's a talkback enabled event.
- if (componentName != null && isTalkbackAlreadyEnabled) {
+ if (isTalkbackAlreadyEnabled) {
logStemTriplePressAccessibilityTelemetry(componentName);
}
}
diff --git a/services/core/java/com/android/server/power/hint/HintManagerService.java b/services/core/java/com/android/server/power/hint/HintManagerService.java
index 6f75439..35717af 100644
--- a/services/core/java/com/android/server/power/hint/HintManagerService.java
+++ b/services/core/java/com/android/server/power/hint/HintManagerService.java
@@ -691,16 +691,26 @@
TextUtils.formatSimple("Actual total duration (%d) should be greater than 0",
workDuration.getActualTotalDurationNanos()));
}
- if (workDuration.getActualCpuDurationNanos() <= 0) {
+ if (workDuration.getActualCpuDurationNanos() < 0) {
throw new IllegalArgumentException(
- TextUtils.formatSimple("Actual CPU duration (%d) should be greater than 0",
+ TextUtils.formatSimple(
+ "Actual CPU duration (%d) should be greater than or equal to 0",
workDuration.getActualCpuDurationNanos()));
}
if (workDuration.getActualGpuDurationNanos() < 0) {
throw new IllegalArgumentException(
- TextUtils.formatSimple("Actual GPU duration (%d) should be non negative",
+ TextUtils.formatSimple(
+ "Actual GPU duration (%d) should greater than or equal to 0",
workDuration.getActualGpuDurationNanos()));
}
+ if (workDuration.getActualCpuDurationNanos()
+ + workDuration.getActualGpuDurationNanos() <= 0) {
+ throw new IllegalArgumentException(
+ TextUtils.formatSimple(
+ "The actual CPU duration (%d) and the actual GPU duration (%d)"
+ + " should not both be 0", workDuration.getActualCpuDurationNanos(),
+ workDuration.getActualGpuDurationNanos()));
+ }
}
private void onProcStateChanged(boolean updateAllowed) {
diff --git a/services/core/java/com/android/server/power/stats/flags.aconfig b/services/core/java/com/android/server/power/stats/flags.aconfig
index 6546646..b2e01c5 100644
--- a/services/core/java/com/android/server/power/stats/flags.aconfig
+++ b/services/core/java/com/android/server/power/stats/flags.aconfig
@@ -21,3 +21,10 @@
bug: "311793616"
is_fixed_read_only: true
}
+
+flag {
+ name: "streamlined_connectivity_battery_stats"
+ namespace: "backstage_power"
+ description: "Feature flag for streamlined connectivity battery stats"
+ bug: "323970018"
+}
diff --git a/services/core/java/com/android/server/tv/TvInputManagerService.java b/services/core/java/com/android/server/tv/TvInputManagerService.java
index 089a886..e0a8226 100644
--- a/services/core/java/com/android/server/tv/TvInputManagerService.java
+++ b/services/core/java/com/android/server/tv/TvInputManagerService.java
@@ -2095,17 +2095,17 @@
}
@Override
- public void startPlayback(IBinder sessionToken, int userId) {
+ public void resumePlayback(IBinder sessionToken, int userId) {
final int callingUid = Binder.getCallingUid();
final int resolvedUserId = resolveCallingUserId(Binder.getCallingPid(), callingUid,
- userId, "stopPlayback");
+ userId, "resumePlayback");
final long identity = Binder.clearCallingIdentity();
try {
synchronized (mLock) {
try {
- getSessionLocked(sessionToken, callingUid, resolvedUserId).startPlayback();
+ getSessionLocked(sessionToken, callingUid, resolvedUserId).resumePlayback();
} catch (RemoteException | SessionNotFoundException e) {
- Slog.e(TAG, "error in startPlayback()", e);
+ Slog.e(TAG, "error in resumePlayback()", e);
}
}
} finally {
diff --git a/services/core/java/com/android/server/uri/UriGrantsManagerInternal.java b/services/core/java/com/android/server/uri/UriGrantsManagerInternal.java
index 03c75e0..195e91c 100644
--- a/services/core/java/com/android/server/uri/UriGrantsManagerInternal.java
+++ b/services/core/java/com/android/server/uri/UriGrantsManagerInternal.java
@@ -19,6 +19,7 @@
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.Intent;
+import android.content.pm.ActivityInfo.RequiredContentUriPermission;
import android.content.pm.ProviderInfo;
import android.net.Uri;
import android.os.IBinder;
@@ -63,6 +64,15 @@
String targetPkg, int targetUserId);
/**
+ * Same as {@link #checkGrantUriPermissionFromIntent(Intent, int, String, int)}, but with an
+ * extra parameter {@code requireContentUriPermissionFromCaller}, which is the value from {@link
+ * android.R.attr#requireContentUriPermissionFromCaller} attribute.
+ */
+ NeededUriGrants checkGrantUriPermissionFromIntent(Intent intent, int callingUid,
+ String targetPkg, int targetUserId,
+ @RequiredContentUriPermission int requireContentUriPermissionFromCaller);
+
+ /**
* Extend a previously calculated set of permissions grants to the given
* owner. All security checks will have already been performed as part of
* calculating {@link NeededUriGrants}.
diff --git a/services/core/java/com/android/server/uri/UriGrantsManagerService.java b/services/core/java/com/android/server/uri/UriGrantsManagerService.java
index ce2cbed..d2f6701 100644
--- a/services/core/java/com/android/server/uri/UriGrantsManagerService.java
+++ b/services/core/java/com/android/server/uri/UriGrantsManagerService.java
@@ -23,6 +23,10 @@
import static android.app.ActivityManagerInternal.ALLOW_FULL_ONLY;
import static android.content.Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION;
import static android.content.Intent.FLAG_GRANT_PREFIX_URI_PERMISSION;
+import static android.content.pm.ActivityInfo.CONTENT_URI_PERMISSION_NONE;
+import static android.content.pm.ActivityInfo.CONTENT_URI_PERMISSION_READ_OR_WRITE;
+import static android.content.pm.ActivityInfo.isRequiredContentUriPermissionRead;
+import static android.content.pm.ActivityInfo.isRequiredContentUriPermissionWrite;
import static android.content.pm.PackageManager.MATCH_ANY_USER;
import static android.content.pm.PackageManager.MATCH_DEBUG_TRIAGED_MISSING;
import static android.content.pm.PackageManager.MATCH_DIRECT_BOOT_AUTO;
@@ -53,6 +57,8 @@
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
+import android.content.pm.ActivityInfo;
+import android.content.pm.ActivityInfo.RequiredContentUriPermission;
import android.content.pm.PackageManager;
import android.content.pm.PackageManagerInternal;
import android.content.pm.ParceledListSlice;
@@ -609,7 +615,8 @@
/** Like checkGrantUriPermission, but takes an Intent. */
private NeededUriGrants checkGrantUriPermissionFromIntentUnlocked(int callingUid,
- String targetPkg, Intent intent, int mode, NeededUriGrants needed, int targetUserId) {
+ String targetPkg, Intent intent, int mode, NeededUriGrants needed, int targetUserId,
+ @RequiredContentUriPermission Integer requireContentUriPermissionFromCaller) {
if (DEBUG) Slog.v(TAG,
"Checking URI perm to data=" + (intent != null ? intent.getData() : null)
+ " clip=" + (intent != null ? intent.getClipData() : null)
@@ -647,6 +654,10 @@
}
if (data != null) {
GrantUri grantUri = GrantUri.resolve(contentUserHint, data, mode);
+ if (android.security.Flags.contentUriPermissionApis()) {
+ enforceRequireContentUriPermissionFromCaller(requireContentUriPermissionFromCaller,
+ grantUri, callingUid);
+ }
targetUid = checkGrantUriPermissionUnlocked(callingUid, targetPkg, grantUri, mode,
targetUid);
if (targetUid > 0) {
@@ -661,6 +672,10 @@
Uri uri = clip.getItemAt(i).getUri();
if (uri != null) {
GrantUri grantUri = GrantUri.resolve(contentUserHint, uri, mode);
+ if (android.security.Flags.contentUriPermissionApis()) {
+ enforceRequireContentUriPermissionFromCaller(
+ requireContentUriPermissionFromCaller, grantUri, callingUid);
+ }
targetUid = checkGrantUriPermissionUnlocked(callingUid, targetPkg,
grantUri, mode, targetUid);
if (targetUid > 0) {
@@ -673,7 +688,8 @@
Intent clipIntent = clip.getItemAt(i).getIntent();
if (clipIntent != null) {
NeededUriGrants newNeeded = checkGrantUriPermissionFromIntentUnlocked(
- callingUid, targetPkg, clipIntent, mode, needed, targetUserId);
+ callingUid, targetPkg, clipIntent, mode, needed, targetUserId,
+ requireContentUriPermissionFromCaller);
if (newNeeded != null) {
needed = newNeeded;
}
@@ -685,6 +701,38 @@
return needed;
}
+ private void enforceRequireContentUriPermissionFromCaller(
+ @RequiredContentUriPermission Integer requireContentUriPermissionFromCaller,
+ GrantUri grantUri, int uid) {
+ // Ignore if requireContentUriPermissionFromCaller hasn't been set or the URI is a
+ // non-content URI.
+ if (requireContentUriPermissionFromCaller == null
+ || requireContentUriPermissionFromCaller == CONTENT_URI_PERMISSION_NONE
+ || !ContentResolver.SCHEME_CONTENT.equals(grantUri.uri.getScheme())) {
+ return;
+ }
+
+ final boolean readMet = !isRequiredContentUriPermissionRead(
+ requireContentUriPermissionFromCaller)
+ || checkContentUriPermissionFullUnlocked(grantUri, uid,
+ Intent.FLAG_GRANT_READ_URI_PERMISSION);
+
+ final boolean writeMet = !isRequiredContentUriPermissionWrite(
+ requireContentUriPermissionFromCaller)
+ || checkContentUriPermissionFullUnlocked(grantUri, uid,
+ Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
+
+ boolean hasPermission =
+ requireContentUriPermissionFromCaller == CONTENT_URI_PERMISSION_READ_OR_WRITE
+ ? (readMet || writeMet) : (readMet && writeMet);
+
+ if (!hasPermission) {
+ throw new SecurityException("You can't launch this activity because you don't have the"
+ + " required " + ActivityInfo.requiredContentUriPermissionToShortString(
+ requireContentUriPermissionFromCaller) + " access to " + grantUri.uri);
+ }
+ }
+
@GuardedBy("mLock")
private void readGrantedUriPermissionsLocked() {
if (DEBUG) Slog.v(TAG, "readGrantedUriPermissions()");
@@ -1560,9 +1608,24 @@
@Override
public NeededUriGrants checkGrantUriPermissionFromIntent(Intent intent, int callingUid,
String targetPkg, int targetUserId) {
+ return internalCheckGrantUriPermissionFromIntent(intent, callingUid, targetPkg,
+ targetUserId, /* requireContentUriPermissionFromCaller */ null);
+ }
+
+ @Override
+ public NeededUriGrants checkGrantUriPermissionFromIntent(Intent intent, int callingUid,
+ String targetPkg, int targetUserId, int requireContentUriPermissionFromCaller) {
+ return internalCheckGrantUriPermissionFromIntent(intent, callingUid, targetPkg,
+ targetUserId, requireContentUriPermissionFromCaller);
+ }
+
+ private NeededUriGrants internalCheckGrantUriPermissionFromIntent(Intent intent,
+ int callingUid, String targetPkg, int targetUserId,
+ @Nullable Integer requireContentUriPermissionFromCaller) {
final int mode = (intent != null) ? intent.getFlags() : 0;
return UriGrantsManagerService.this.checkGrantUriPermissionFromIntentUnlocked(
- callingUid, targetPkg, intent, mode, null, targetUserId);
+ callingUid, targetPkg, intent, mode, null, targetUserId,
+ requireContentUriPermissionFromCaller);
}
@Override
diff --git a/services/core/java/com/android/server/vibrator/VibratorControlService.java b/services/core/java/com/android/server/vibrator/VibratorControlService.java
index 8f8fe3c..17a9e33 100644
--- a/services/core/java/com/android/server/vibrator/VibratorControlService.java
+++ b/services/core/java/com/android/server/vibrator/VibratorControlService.java
@@ -248,8 +248,6 @@
IVibratorController vibratorController =
mVibratorControllerHolder.getVibratorController();
if (vibratorController == null) {
- Slog.d(TAG, "Unable to check if should request vibration params. "
- + "There is no registered IVibrationController.");
return false;
}
diff --git a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java
index 8c27bb8..118985a 100644
--- a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java
+++ b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java
@@ -145,7 +145,6 @@
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
-import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Consumer;
import java.util.function.Predicate;
@@ -613,8 +612,8 @@
}
private final Context mContext;
- private final AtomicBoolean mIsInitialBinding = new AtomicBoolean(true);
- private final ServiceThread mHandlerThread;
+ private boolean mInitialUserSwitch = true;
+ private ServiceThread mHandlerThread;
private final WindowManagerInternal mWindowManagerInternal;
private final PackageManagerInternal mPackageManagerInternal;
private final IPackageManager mIPackageManager;
@@ -1474,12 +1473,6 @@
public WallpaperManagerService(Context context) {
if (DEBUG) Slog.v(TAG, "WallpaperService startup");
mContext = context;
- if (Flags.bindWallpaperServiceOnItsOwnThreadDuringAUserSwitch()) {
- mHandlerThread = new ServiceThread(TAG, THREAD_PRIORITY_FOREGROUND, true /*allowIo*/);
- mHandlerThread.start();
- } else {
- mHandlerThread = null;
- }
mShuttingDown = false;
mImageWallpaper = ComponentName.unflattenFromString(
context.getResources().getString(R.string.image_wallpaper_component));
@@ -1803,6 +1796,7 @@
switchWallpaper(lockWallpaper, null);
}
switchWallpaper(systemWallpaper, reply);
+ mInitialUserSwitch = false;
}
// Offload color extraction to another thread since switchUser will be called
@@ -3326,11 +3320,8 @@
com.android.internal.R.bool.config_wallpaperTopApp)) {
bindFlags |= Context.BIND_SCHEDULE_LIKE_TOP_APP;
}
- Handler handler = Flags.bindWallpaperServiceOnItsOwnThreadDuringAUserSwitch()
- && !mIsInitialBinding.compareAndSet(true, false)
- ? mHandlerThread.getThreadHandler() : mContext.getMainThreadHandler();
- boolean bindSuccess = mContext.bindServiceAsUser(intent, newConn, bindFlags, handler,
- new UserHandle(serviceUserId));
+ boolean bindSuccess = mContext.bindServiceAsUser(intent, newConn, bindFlags,
+ getHandlerForBindingWallpaperLocked(), new UserHandle(serviceUserId));
if (!bindSuccess) {
String msg = "Unable to bind service: " + componentName;
if (fromUser) {
@@ -3358,6 +3349,20 @@
return true;
}
+ private Handler getHandlerForBindingWallpaperLocked() {
+ if (!Flags.bindWallpaperServiceOnItsOwnThreadDuringAUserSwitch()) {
+ return mContext.getMainThreadHandler();
+ }
+ if (mInitialUserSwitch) {
+ return mContext.getMainThreadHandler();
+ }
+ if (mHandlerThread == null) {
+ mHandlerThread = new ServiceThread(TAG, THREAD_PRIORITY_FOREGROUND, true /*allowIo*/);
+ mHandlerThread.start();
+ }
+ return mHandlerThread.getThreadHandler();
+ }
+
// Updates tracking of the currently bound wallpapers.
private void updateCurrentWallpapers(WallpaperData newWallpaper) {
if (newWallpaper.userId != mCurrentUserId || newWallpaper.equals(mFallbackWallpaper)) {
diff --git a/services/core/java/com/android/server/wearable/RemoteWearableSensingService.java b/services/core/java/com/android/server/wearable/RemoteWearableSensingService.java
index b2bbcda..88d3daf 100644
--- a/services/core/java/com/android/server/wearable/RemoteWearableSensingService.java
+++ b/services/core/java/com/android/server/wearable/RemoteWearableSensingService.java
@@ -32,6 +32,8 @@
import com.android.internal.infra.ServiceConnector;
+import java.io.IOException;
+
/** Manages the connection to the remote wearable sensing service. */
final class RemoteWearableSensingService extends ServiceConnector.Impl<IWearableSensingService> {
private static final String TAG =
@@ -56,6 +58,29 @@
}
/**
+ * Provides a secure connection to the wearable.
+ *
+ * @param secureWearableConnection The secure connection to the wearable
+ * @param callback The callback for service status
+ */
+ public void provideSecureWearableConnection(
+ ParcelFileDescriptor secureWearableConnection, RemoteCallback callback) {
+ if (DEBUG) {
+ Slog.i(TAG, "Providing secure wearable connection.");
+ }
+ var unused = post(
+ service -> {
+ service.provideSecureWearableConnection(secureWearableConnection, callback);
+ try {
+ // close the local fd after it has been sent to the WSS process
+ secureWearableConnection.close();
+ } catch (IOException ex) {
+ Slog.w(TAG, "Unable to close the local parcelFileDescriptor.", ex);
+ }
+ });
+ }
+
+ /**
* Provides the implementation a data stream to the wearable.
*
* @param parcelFileDescriptor The data stream to the wearable
@@ -66,7 +91,16 @@
if (DEBUG) {
Slog.i(TAG, "Providing data stream.");
}
- post(service -> service.provideDataStream(parcelFileDescriptor, callback));
+ var unused = post(
+ service -> {
+ service.provideDataStream(parcelFileDescriptor, callback);
+ try {
+ // close the local fd after it has been sent to the WSS process
+ parcelFileDescriptor.close();
+ } catch (IOException ex) {
+ Slog.w(TAG, "Unable to close the local parcelFileDescriptor.", ex);
+ }
+ });
}
/**
@@ -84,4 +118,62 @@
}
post(service -> service.provideData(data, sharedMemory, callback));
}
+
+ /**
+ * Registers a data request observer with WearableSensingService.
+ *
+ * @param dataType The data type to listen to. Values are defined by the application that
+ * implements WearableSensingService.
+ * @param dataRequestCallback The observer to send data requests to.
+ * @param dataRequestObserverId The unique ID for the data request observer. It will be used for
+ * unregistering the observer.
+ * @param packageName The package name of the app that will receive the data requests.
+ * @param statusCallback The callback for status of the method call.
+ */
+ public void registerDataRequestObserver(
+ int dataType,
+ RemoteCallback dataRequestCallback,
+ int dataRequestObserverId,
+ String packageName,
+ RemoteCallback statusCallback) {
+ if (DEBUG) {
+ Slog.i(TAG, "Registering data request observer.");
+ }
+ var unused =
+ post(
+ service ->
+ service.registerDataRequestObserver(
+ dataType,
+ dataRequestCallback,
+ dataRequestObserverId,
+ packageName,
+ statusCallback));
+ }
+
+ /**
+ * Unregisters a previously registered data request observer.
+ *
+ * @param dataType The data type the observer was registered against.
+ * @param dataRequestObserverId The unique ID of the observer to unregister.
+ * @param packageName The package name of the app that will receive requests sent to the
+ * observer.
+ * @param statusCallback The callback for status of the method call.
+ */
+ public void unregisterDataRequestObserver(
+ int dataType,
+ int dataRequestObserverId,
+ String packageName,
+ RemoteCallback statusCallback) {
+ if (DEBUG) {
+ Slog.i(TAG, "Unregistering data request observer.");
+ }
+ var unused =
+ post(
+ service ->
+ service.unregisterDataRequestObserver(
+ dataType,
+ dataRequestObserverId,
+ packageName,
+ statusCallback));
+ }
}
diff --git a/services/core/java/com/android/server/wearable/WearableSensingManagerPerUserService.java b/services/core/java/com/android/server/wearable/WearableSensingManagerPerUserService.java
index e73fd0f..0e8b82f 100644
--- a/services/core/java/com/android/server/wearable/WearableSensingManagerPerUserService.java
+++ b/services/core/java/com/android/server/wearable/WearableSensingManagerPerUserService.java
@@ -22,17 +22,19 @@
import android.annotation.UserIdInt;
import android.app.AppGlobals;
import android.app.ambientcontext.AmbientContextEvent;
+import android.app.wearable.Flags;
import android.app.wearable.WearableSensingManager;
+import android.companion.CompanionDeviceManager;
import android.content.ComponentName;
import android.content.pm.PackageManager;
import android.content.pm.ServiceInfo;
import android.os.Bundle;
-import android.system.OsConstants;
import android.os.ParcelFileDescriptor;
import android.os.PersistableBundle;
import android.os.RemoteCallback;
import android.os.RemoteException;
import android.os.SharedMemory;
+import android.system.OsConstants;
import android.util.IndentingPrintWriter;
import android.util.Slog;
@@ -40,6 +42,7 @@
import com.android.internal.annotations.VisibleForTesting;
import com.android.server.infra.AbstractPerUserSystemService;
+import java.io.IOException;
import java.io.PrintWriter;
/**
@@ -55,6 +58,10 @@
RemoteWearableSensingService mRemoteService;
private ComponentName mComponentName;
+ private final Object mSecureChannelLock = new Object();
+
+ @GuardedBy("mSecureChannelLock")
+ private WearableSensingSecureChannel mSecureChannel;
WearableSensingManagerPerUserService(
@NonNull WearableSensingManagerService master, Object lock, @UserIdInt int userId) {
@@ -76,6 +83,11 @@
mRemoteService = null;
}
}
+ synchronized (mSecureChannelLock) {
+ if (mSecureChannel != null) {
+ mSecureChannel.close();
+ }
+ }
}
@GuardedBy("mLock")
@@ -156,6 +168,63 @@
}
/**
+ * Creates a CompanionDeviceManager secure channel and sends a proxy to the wearable sensing
+ * service.
+ */
+ public void onProvideWearableConnection(
+ ParcelFileDescriptor wearableConnection, RemoteCallback callback) {
+ Slog.i(TAG, "onProvideWearableConnection in per user service.");
+ synchronized (mLock) {
+ if (!setUpServiceIfNeeded()) {
+ Slog.w(TAG, "Detection service is not available at this moment.");
+ notifyStatusCallback(callback, WearableSensingManager.STATUS_SERVICE_UNAVAILABLE);
+ return;
+ }
+ }
+ synchronized (mSecureChannelLock) {
+ if (mSecureChannel != null) {
+ // TODO(b/321012559): Kill the WearableSensingService process if it has not been
+ // killed from onError
+ mSecureChannel.close();
+ }
+ try {
+ mSecureChannel =
+ WearableSensingSecureChannel.create(
+ getContext().getSystemService(CompanionDeviceManager.class),
+ wearableConnection,
+ new WearableSensingSecureChannel.SecureTransportListener() {
+ @Override
+ public void onSecureTransportAvailable(
+ ParcelFileDescriptor secureTransport) {
+ Slog.i(TAG, "calling over to remote service.");
+ synchronized (mLock) {
+ ensureRemoteServiceInitiated();
+ mRemoteService.provideSecureWearableConnection(
+ secureTransport, callback);
+ }
+ }
+
+ @Override
+ public void onError() {
+ // TODO(b/321012559): Kill the WearableSensingService
+ // process if mSecureChannel has not been reassigned
+ if (Flags.enableProvideWearableConnectionApi()) {
+ notifyStatusCallback(
+ callback,
+ WearableSensingManager.STATUS_CHANNEL_ERROR);
+ }
+ }
+ });
+ } catch (IOException ex) {
+ Slog.e(TAG, "Unable to create the secure channel.", ex);
+ if (Flags.enableProvideWearableConnectionApi()) {
+ notifyStatusCallback(callback, WearableSensingManager.STATUS_CHANNEL_ERROR);
+ }
+ }
+ }
+ }
+
+ /**
* Handles sending the provided data stream for the wearable to the wearable sensing service.
*/
public void onProvideDataStream(
@@ -193,4 +262,65 @@
mRemoteService.provideData(data, sharedMemory, callback);
}
}
+
+ /**
+ * Handles registering a data request observer.
+ *
+ * @param dataType The data type to listen to. Values are defined by the application that
+ * implements WearableSensingService.
+ * @param dataRequestObserver The observer to register.
+ * @param dataRequestObserverId The unique ID for the data request observer. It will be used for
+ * unregistering the observer.
+ * @param packageName The package name of the app that will receive the data requests.
+ * @param statusCallback The callback for status of the method call.
+ */
+ public void onRegisterDataRequestObserver(
+ int dataType,
+ RemoteCallback dataRequestObserver,
+ int dataRequestObserverId,
+ String packageName,
+ RemoteCallback statusCallback) {
+ synchronized (mLock) {
+ if (!setUpServiceIfNeeded()) {
+ Slog.w(TAG, "Detection service is not available at this moment.");
+ notifyStatusCallback(
+ statusCallback, WearableSensingManager.STATUS_SERVICE_UNAVAILABLE);
+ return;
+ }
+ ensureRemoteServiceInitiated();
+ mRemoteService.registerDataRequestObserver(
+ dataType,
+ dataRequestObserver,
+ dataRequestObserverId,
+ packageName,
+ statusCallback);
+ }
+ }
+
+ /**
+ * Handles unregistering a previously registered data request observer.
+ *
+ * @param dataType The data type the observer was registered against.
+ * @param dataRequestObserverId The unique ID of the observer to unregister.
+ * @param packageName The package name of the app that will receive requests sent to the
+ * observer.
+ * @param statusCallback The callback for status of the method call.
+ */
+ public void onUnregisterDataRequestObserver(
+ int dataType,
+ int dataRequestObserverId,
+ String packageName,
+ RemoteCallback statusCallback) {
+ synchronized (mLock) {
+ if (!setUpServiceIfNeeded()) {
+ Slog.w(TAG, "Detection service is not available at this moment.");
+ notifyStatusCallback(
+ statusCallback, WearableSensingManager.STATUS_SERVICE_UNAVAILABLE);
+ return;
+ }
+ ensureRemoteServiceInitiated();
+ mRemoteService.unregisterDataRequestObserver(
+ dataType, dataRequestObserverId, packageName, statusCallback);
+ }
+ }
}
diff --git a/services/core/java/com/android/server/wearable/WearableSensingManagerService.java b/services/core/java/com/android/server/wearable/WearableSensingManagerService.java
index 4cc2c02..78952fa 100644
--- a/services/core/java/com/android/server/wearable/WearableSensingManagerService.java
+++ b/services/core/java/com/android/server/wearable/WearableSensingManagerService.java
@@ -21,12 +21,18 @@
import android.Manifest;
import android.annotation.NonNull;
import android.annotation.UserIdInt;
+import android.app.BroadcastOptions;
+import android.app.ComponentOptions;
+import android.app.PendingIntent;
import android.app.ambientcontext.AmbientContextEvent;
import android.app.wearable.IWearableSensingManager;
+import android.app.wearable.WearableSensingDataRequest;
import android.app.wearable.WearableSensingManager;
import android.content.ComponentName;
import android.content.Context;
+import android.content.Intent;
import android.content.pm.PackageManagerInternal;
+import android.os.Binder;
import android.os.ParcelFileDescriptor;
import android.os.PersistableBundle;
import android.os.RemoteCallback;
@@ -35,6 +41,8 @@
import android.os.ShellCallback;
import android.os.UserHandle;
import android.provider.DeviceConfig;
+import android.service.wearable.WearableSensingDataRequester;
+import android.text.TextUtils;
import android.util.Slog;
import com.android.internal.R;
@@ -44,10 +52,13 @@
import com.android.server.infra.AbstractMasterSystemService;
import com.android.server.infra.FrameworkResourcesServiceNameResolver;
import com.android.server.pm.KnownPackages;
+import com.android.server.utils.quota.MultiRateLimiter;
import java.io.FileDescriptor;
+import java.util.HashSet;
import java.util.Objects;
import java.util.Set;
+import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Consumer;
/**
@@ -64,9 +75,38 @@
/** Default value in absence of {@link DeviceConfig} override. */
private static final boolean DEFAULT_SERVICE_ENABLED = true;
+
public static final int MAX_TEMPORARY_SERVICE_DURATION_MS = 30000;
+ private static final String RATE_LIMITER_PACKAGE_NAME = "android";
+ private static final String RATE_LIMITER_TAG =
+ WearableSensingManagerService.class.getSimpleName();
+
+ private static final class DataRequestObserverContext {
+ final int mDataType;
+ final int mUserId;
+ final int mDataRequestObserverId;
+ @NonNull final PendingIntent mDataRequestPendingIntent;
+ @NonNull final RemoteCallback mDataRequestRemoteCallback;
+
+ DataRequestObserverContext(
+ int dataType,
+ int userId,
+ int dataRequestObserverId,
+ PendingIntent dataRequestPendingIntent,
+ RemoteCallback dataRequestRemoteCallback) {
+ mDataType = dataType;
+ mUserId = userId;
+ mDataRequestObserverId = dataRequestObserverId;
+ mDataRequestPendingIntent = dataRequestPendingIntent;
+ mDataRequestRemoteCallback = dataRequestRemoteCallback;
+ }
+ }
+
private final Context mContext;
+ private final AtomicInteger mNextDataRequestObserverId = new AtomicInteger(1);
+ private final Set<DataRequestObserverContext> mDataRequestObserverContexts = new HashSet<>();
+ private final MultiRateLimiter mDataRequestRateLimiter;
volatile boolean mIsServiceEnabled;
public WearableSensingManagerService(Context context) {
@@ -78,6 +118,12 @@
PACKAGE_UPDATE_POLICY_REFRESH_EAGER
| /*To avoid high latency*/ PACKAGE_RESTART_POLICY_REFRESH_EAGER);
mContext = context;
+ mDataRequestRateLimiter =
+ new MultiRateLimiter.Builder(context)
+ .addRateLimit(
+ WearableSensingDataRequest.getRateLimit(),
+ WearableSensingDataRequest.getRateLimitWindowSize())
+ .build();
}
@Override
@@ -192,6 +238,96 @@
}
}
+ private DataRequestObserverContext getDataRequestObserverContext(
+ int dataType, int userId, PendingIntent dataRequestPendingIntent) {
+ synchronized (mDataRequestObserverContexts) {
+ for (DataRequestObserverContext observerContext : mDataRequestObserverContexts) {
+ if (observerContext.mDataType == dataType
+ && observerContext.mUserId == userId
+ && observerContext.mDataRequestPendingIntent.equals(
+ dataRequestPendingIntent)) {
+ return observerContext;
+ }
+ }
+ }
+ return null;
+ }
+
+ @NonNull
+ private RemoteCallback createDataRequestRemoteCallback(
+ PendingIntent dataRequestPendingIntent, int userId) {
+ return new RemoteCallback(
+ bundle -> {
+ WearableSensingDataRequest dataRequest =
+ bundle.getParcelable(
+ WearableSensingDataRequest.REQUEST_BUNDLE_KEY,
+ WearableSensingDataRequest.class);
+ if (dataRequest == null) {
+ Slog.e(TAG, "Received data request callback without a request.");
+ return;
+ }
+ RemoteCallback dataRequestStatusCallback =
+ bundle.getParcelable(
+ WearableSensingDataRequest.REQUEST_STATUS_CALLBACK_BUNDLE_KEY,
+ RemoteCallback.class);
+ if (dataRequestStatusCallback == null) {
+ Slog.e(TAG, "Received data request callback without a status callback.");
+ return;
+ }
+ if (dataRequest.getDataSize()
+ > WearableSensingDataRequest.getMaxRequestSize()) {
+ Slog.w(
+ TAG,
+ TextUtils.formatSimple(
+ "WearableSensingDataRequest size exceeds the maximum"
+ + " allowed size of %s bytes. Dropping the request.",
+ WearableSensingDataRequest.getMaxRequestSize()));
+ WearableSensingManagerPerUserService.notifyStatusCallback(
+ dataRequestStatusCallback,
+ WearableSensingDataRequester.STATUS_TOO_LARGE);
+ return;
+ }
+ if (!mDataRequestRateLimiter.isWithinQuota(
+ userId, RATE_LIMITER_PACKAGE_NAME, RATE_LIMITER_TAG)) {
+ Slog.w(TAG, "Data request exceeded rate limit. Dropping the request.");
+ WearableSensingManagerPerUserService.notifyStatusCallback(
+ dataRequestStatusCallback,
+ WearableSensingDataRequester.STATUS_TOO_FREQUENT);
+ return;
+ }
+ Intent intent = new Intent();
+ intent.putExtra(
+ WearableSensingManager.EXTRA_WEARABLE_SENSING_DATA_REQUEST,
+ dataRequest);
+ BroadcastOptions options = BroadcastOptions.makeBasic();
+ options.setPendingIntentBackgroundActivityStartMode(
+ ComponentOptions.MODE_BACKGROUND_ACTIVITY_START_DENIED);
+ mDataRequestRateLimiter.noteEvent(
+ userId, RATE_LIMITER_PACKAGE_NAME, RATE_LIMITER_TAG);
+ final long previousCallingIdentity = Binder.clearCallingIdentity();
+ try {
+ dataRequestPendingIntent.send(
+ getContext(), 0, intent, null, null, null, options.toBundle());
+ WearableSensingManagerPerUserService.notifyStatusCallback(
+ dataRequestStatusCallback,
+ WearableSensingDataRequester.STATUS_SUCCESS);
+ Slog.i(
+ TAG,
+ TextUtils.formatSimple(
+ "Sending data request to %s: %s",
+ dataRequestPendingIntent.getCreatorPackage(),
+ dataRequest.toExpandedString()));
+ } catch (PendingIntent.CanceledException e) {
+ Slog.w(TAG, "Could not deliver pendingIntent: " + dataRequestPendingIntent);
+ WearableSensingManagerPerUserService.notifyStatusCallback(
+ dataRequestStatusCallback,
+ WearableSensingDataRequester.STATUS_OBSERVER_CANCELLED);
+ } finally {
+ Binder.restoreCallingIdentity(previousCallingIdentity);
+ }
+ });
+ }
+
private void callPerUserServiceIfExist(
Consumer<WearableSensingManagerPerUserService> serviceConsumer,
RemoteCallback statusCallback) {
@@ -211,9 +347,27 @@
private final class WearableSensingManagerInternal extends IWearableSensingManager.Stub {
@Override
+ public void provideWearableConnection(
+ ParcelFileDescriptor wearableConnection, RemoteCallback callback) {
+ Slog.i(TAG, "WearableSensingManagerInternal provideWearableConnection.");
+ Objects.requireNonNull(wearableConnection);
+ Objects.requireNonNull(callback);
+ mContext.enforceCallingOrSelfPermission(
+ Manifest.permission.MANAGE_WEARABLE_SENSING_SERVICE, TAG);
+ if (!mIsServiceEnabled) {
+ Slog.w(TAG, "Service not available.");
+ WearableSensingManagerPerUserService.notifyStatusCallback(
+ callback, WearableSensingManager.STATUS_SERVICE_UNAVAILABLE);
+ return;
+ }
+ callPerUserServiceIfExist(
+ service -> service.onProvideWearableConnection(wearableConnection, callback),
+ callback);
+ }
+
+ @Override
public void provideDataStream(
- ParcelFileDescriptor parcelFileDescriptor,
- RemoteCallback callback) {
+ ParcelFileDescriptor parcelFileDescriptor, RemoteCallback callback) {
Slog.i(TAG, "WearableSensingManagerInternal provideDataStream.");
Objects.requireNonNull(parcelFileDescriptor);
Objects.requireNonNull(callback);
@@ -242,8 +396,8 @@
Manifest.permission.MANAGE_WEARABLE_SENSING_SERVICE, TAG);
if (!mIsServiceEnabled) {
Slog.w(TAG, "Service not available.");
- WearableSensingManagerPerUserService.notifyStatusCallback(callback,
- WearableSensingManager.STATUS_SERVICE_UNAVAILABLE);
+ WearableSensingManagerPerUserService.notifyStatusCallback(
+ callback, WearableSensingManager.STATUS_SERVICE_UNAVAILABLE);
return;
}
callPerUserServiceIfExist(
@@ -252,6 +406,96 @@
}
@Override
+ public void registerDataRequestObserver(
+ int dataType,
+ PendingIntent dataRequestPendingIntent,
+ RemoteCallback statusCallback) {
+ Slog.i(TAG, "WearableSensingManagerInternal registerDataRequestObserver.");
+ Objects.requireNonNull(dataRequestPendingIntent);
+ mContext.enforceCallingOrSelfPermission(
+ Manifest.permission.MANAGE_WEARABLE_SENSING_SERVICE, TAG);
+ if (!mIsServiceEnabled) {
+ Slog.w(TAG, "Service not available.");
+ WearableSensingManagerPerUserService.notifyStatusCallback(
+ statusCallback, WearableSensingManager.STATUS_SERVICE_UNAVAILABLE);
+ return;
+ }
+ int userId = UserHandle.getCallingUserId();
+ RemoteCallback dataRequestCallback;
+ int dataRequestObserverId;
+ synchronized (mDataRequestObserverContexts) {
+ DataRequestObserverContext previousObserverContext =
+ getDataRequestObserverContext(dataType, userId, dataRequestPendingIntent);
+ if (previousObserverContext != null) {
+ Slog.i(TAG, "Received duplicate data request observer.");
+ dataRequestCallback = previousObserverContext.mDataRequestRemoteCallback;
+ dataRequestObserverId = previousObserverContext.mDataRequestObserverId;
+ } else {
+ dataRequestCallback =
+ createDataRequestRemoteCallback(dataRequestPendingIntent, userId);
+ dataRequestObserverId = mNextDataRequestObserverId.getAndIncrement();
+ mDataRequestObserverContexts.add(
+ new DataRequestObserverContext(
+ dataType,
+ userId,
+ dataRequestObserverId,
+ dataRequestPendingIntent,
+ dataRequestCallback));
+ }
+ }
+ callPerUserServiceIfExist(
+ service ->
+ service.onRegisterDataRequestObserver(
+ dataType,
+ dataRequestCallback,
+ dataRequestObserverId,
+ dataRequestPendingIntent.getCreatorPackage(),
+ statusCallback),
+ statusCallback);
+ }
+
+ @Override
+ public void unregisterDataRequestObserver(
+ int dataType,
+ PendingIntent dataRequestPendingIntent,
+ RemoteCallback statusCallback) {
+ Slog.i(TAG, "WearableSensingManagerInternal unregisterDataRequestObserver.");
+ Objects.requireNonNull(dataRequestPendingIntent);
+ Objects.requireNonNull(statusCallback);
+ mContext.enforceCallingOrSelfPermission(
+ Manifest.permission.MANAGE_WEARABLE_SENSING_SERVICE, TAG);
+ if (!mIsServiceEnabled) {
+ Slog.w(TAG, "Service not available.");
+ WearableSensingManagerPerUserService.notifyStatusCallback(
+ statusCallback, WearableSensingManager.STATUS_SERVICE_UNAVAILABLE);
+ return;
+ }
+ int userId = UserHandle.getCallingUserId();
+ int previousDataRequestObserverId;
+ String pendingIntentCreatorPackage;
+ synchronized (mDataRequestObserverContexts) {
+ DataRequestObserverContext previousObserverContext =
+ getDataRequestObserverContext(dataType, userId, dataRequestPendingIntent);
+ if (previousObserverContext == null) {
+ Slog.w(TAG, "Previous observer not found, cannot unregister.");
+ return;
+ }
+ mDataRequestObserverContexts.remove(previousObserverContext);
+ previousDataRequestObserverId = previousObserverContext.mDataRequestObserverId;
+ pendingIntentCreatorPackage =
+ previousObserverContext.mDataRequestPendingIntent.getCreatorPackage();
+ }
+ callPerUserServiceIfExist(
+ service ->
+ service.onUnregisterDataRequestObserver(
+ dataType,
+ previousDataRequestObserverId,
+ pendingIntentCreatorPackage,
+ statusCallback),
+ statusCallback);
+ }
+
+ @Override
public void onShellCommand(FileDescriptor in, FileDescriptor out, FileDescriptor err,
String[] args, ShellCallback callback, ResultReceiver resultReceiver) {
new WearableSensingShellCommand(WearableSensingManagerService.this).exec(
diff --git a/services/core/java/com/android/server/wearable/WearableSensingSecureChannel.java b/services/core/java/com/android/server/wearable/WearableSensingSecureChannel.java
new file mode 100644
index 0000000..a16ff51
--- /dev/null
+++ b/services/core/java/com/android/server/wearable/WearableSensingSecureChannel.java
@@ -0,0 +1,350 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.wearable;
+
+import android.annotation.NonNull;
+import android.companion.AssociationInfo;
+import android.companion.AssociationRequest;
+import android.companion.CompanionDeviceManager;
+import android.os.Binder;
+import android.os.ParcelFileDescriptor;
+import android.os.ParcelFileDescriptor.AutoCloseInputStream;
+import android.os.ParcelFileDescriptor.AutoCloseOutputStream;
+import android.util.Slog;
+
+import com.android.internal.annotations.GuardedBy;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.List;
+import java.util.Objects;
+import java.util.concurrent.Executor;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.RejectedExecutionException;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.function.BiConsumer;
+import java.util.function.Consumer;
+
+/**
+ * A wrapper that manages a CompanionDeviceManager secure channel for wearable sensing.
+ *
+ * <p>This wrapper accepts a connection to a wearable from the caller. It then attaches the
+ * connection to the CompanionDeviceManager via {@link
+ * CompanionDeviceManager#attachSystemDataTransport(int, InputStream, OutputStream)}, which will
+ * create an encrypted channel using the provided connection as the raw underlying connection. The
+ * wearable device is expected to attach its side of the raw connection to its
+ * CompanionDeviceManager via the same method so that the two CompanionDeviceManagers on the two
+ * devices can perform attestation and set up the encrypted channel. Attestation requirements are
+ * listed in {@link com.android.server.security.AttestationVerificationPeerDeviceVerifier}.
+ *
+ * <p>When the encrypted channel is available, it will be provided to the caller via the
+ * SecureTransportListener.
+ */
+final class WearableSensingSecureChannel {
+
+ /** A listener for secure transport and its error signal. */
+ interface SecureTransportListener {
+
+ /** Called when the secure transport is available. */
+ void onSecureTransportAvailable(ParcelFileDescriptor secureTransport);
+
+ /**
+ * Called when there is a non-recoverable error. The secure channel will be automatically
+ * closed.
+ */
+ void onError();
+ }
+
+ private static final String TAG = WearableSensingSecureChannel.class.getSimpleName();
+ private static final String CDM_ASSOCIATION_DISPLAY_NAME = "PlaceholderDisplayNameFromWSM";
+ // The batch size of reading from the ParcelFileDescriptor returned to mSecureTransportListener
+ private static final int READ_BUFFER_SIZE = 8192;
+
+ private final Object mLock = new Object();
+ // CompanionDeviceManager (CDM) can continue to call these ExecutorServices even after the
+ // corresponding cleanup methods in CDM have been called (e.g.
+ // removeOnTransportsChangedListener). Since we shut down these ExecutorServices after
+ // clean up, we use SoftShutdownExecutor to suppress RejectedExecutionExceptions.
+ private final SoftShutdownExecutor mMessageFromWearableExecutor =
+ new SoftShutdownExecutor(Executors.newSingleThreadExecutor());
+ private final SoftShutdownExecutor mMessageToWearableExecutor =
+ new SoftShutdownExecutor(Executors.newSingleThreadExecutor());
+ private final SoftShutdownExecutor mLightWeightExecutor =
+ new SoftShutdownExecutor(Executors.newSingleThreadExecutor());
+ private final CompanionDeviceManager mCompanionDeviceManager;
+ private final ParcelFileDescriptor mUnderlyingTransport;
+ private final SecureTransportListener mSecureTransportListener;
+ private final AtomicBoolean mTransportAvailable = new AtomicBoolean(false);
+ private final Consumer<List<AssociationInfo>> mOnTransportsChangedListener =
+ this::onTransportsChanged;
+ private final BiConsumer<Integer, byte[]> mOnMessageReceivedListener = this::onMessageReceived;
+ private final ParcelFileDescriptor mRemoteFd; // To be returned to mSecureTransportListener
+ // read input received from the ParcelFileDescriptor returned to mSecureTransportListener
+ private final InputStream mLocalIn;
+ // send output to the ParcelFileDescriptor returned to mSecureTransportListener
+ private final OutputStream mLocalOut;
+
+ @GuardedBy("mLock")
+ private boolean mClosed = false;
+
+ private Integer mAssociationId = null;
+
+ /**
+ * Creates a WearableSensingSecureChannel. When the secure transport is ready,
+ * secureTransportListener will be notified.
+ *
+ * @param companionDeviceManager The CompanionDeviceManager system service.
+ * @param underlyingTransport The underlying transport to create the secure channel on.
+ * @param secureTransportListener The listener to receive the secure transport when it is ready.
+ * @throws IOException if it cannot create a {@link ParcelFileDescriptor} socket pair.
+ */
+ static WearableSensingSecureChannel create(
+ @NonNull CompanionDeviceManager companionDeviceManager,
+ @NonNull ParcelFileDescriptor underlyingTransport,
+ @NonNull SecureTransportListener secureTransportListener)
+ throws IOException {
+ Objects.requireNonNull(companionDeviceManager);
+ Objects.requireNonNull(underlyingTransport);
+ Objects.requireNonNull(secureTransportListener);
+ ParcelFileDescriptor[] pair = ParcelFileDescriptor.createSocketPair();
+ WearableSensingSecureChannel channel =
+ new WearableSensingSecureChannel(
+ companionDeviceManager,
+ underlyingTransport,
+ secureTransportListener,
+ pair[0],
+ pair[1]);
+ channel.initialize();
+ return channel;
+ }
+
+ private WearableSensingSecureChannel(
+ CompanionDeviceManager companionDeviceManager,
+ ParcelFileDescriptor underlyingTransport,
+ SecureTransportListener secureTransportListener,
+ ParcelFileDescriptor remoteFd,
+ ParcelFileDescriptor localFd) {
+ mCompanionDeviceManager = companionDeviceManager;
+ mUnderlyingTransport = underlyingTransport;
+ mSecureTransportListener = secureTransportListener;
+ mRemoteFd = remoteFd;
+ mLocalIn = new AutoCloseInputStream(localFd);
+ mLocalOut = new AutoCloseOutputStream(localFd);
+ }
+
+ private void initialize() {
+ final long originalCallingIdentity = Binder.clearCallingIdentity();
+ try {
+ Slog.d(TAG, "Requesting CDM association.");
+ mCompanionDeviceManager.associate(
+ new AssociationRequest.Builder()
+ .setDisplayName(CDM_ASSOCIATION_DISPLAY_NAME)
+ .setSelfManaged(true)
+ .build(),
+ mLightWeightExecutor,
+ new CompanionDeviceManager.Callback() {
+ @Override
+ public void onAssociationCreated(AssociationInfo associationInfo) {
+ WearableSensingSecureChannel.this.onAssociationCreated(
+ associationInfo.getId());
+ }
+
+ @Override
+ public void onFailure(CharSequence error) {
+ Slog.e(
+ TAG,
+ "Failed to create CompanionDeviceManager association: "
+ + error);
+ onError();
+ }
+ });
+ } finally {
+ Binder.restoreCallingIdentity(originalCallingIdentity);
+ }
+ }
+
+ private void onAssociationCreated(int associationId) {
+ Slog.i(TAG, "CDM association created.");
+ synchronized (mLock) {
+ if (mClosed) {
+ return;
+ }
+ mAssociationId = associationId;
+ mCompanionDeviceManager.addOnMessageReceivedListener(
+ mMessageFromWearableExecutor,
+ CompanionDeviceManager.MESSAGE_ONEWAY_FROM_WEARABLE,
+ mOnMessageReceivedListener);
+ mCompanionDeviceManager.addOnTransportsChangedListener(
+ mLightWeightExecutor, mOnTransportsChangedListener);
+ mCompanionDeviceManager.attachSystemDataTransport(
+ associationId,
+ new AutoCloseInputStream(mUnderlyingTransport),
+ new AutoCloseOutputStream(mUnderlyingTransport));
+ }
+ }
+
+ private void onTransportsChanged(List<AssociationInfo> associationInfos) {
+ synchronized (mLock) {
+ if (mClosed) {
+ return;
+ }
+ if (mAssociationId == null) {
+ Slog.e(TAG, "mAssociationId is null when transport changed");
+ return;
+ }
+ }
+ // Do not call onTransportAvailable() or onError() when holding the lock because it can
+ // cause a deadlock if the callback holds another lock.
+ boolean transportAvailable =
+ associationInfos.stream().anyMatch(info -> info.getId() == mAssociationId);
+ if (transportAvailable && mTransportAvailable.compareAndSet(false, true)) {
+ onTransportAvailable();
+ } else if (!transportAvailable && mTransportAvailable.compareAndSet(true, false)) {
+ Slog.i(TAG, "CDM transport is detached. This is not recoverable.");
+ onError();
+ }
+ }
+
+ private void onTransportAvailable() {
+ // Start sending data received from the remote stream to the wearable.
+ Slog.i(TAG, "Transport available");
+ mMessageToWearableExecutor.execute(
+ () -> {
+ int[] associationIdsToSendMessageTo = new int[] {mAssociationId};
+ byte[] buffer = new byte[READ_BUFFER_SIZE];
+ int readLen;
+ try {
+ while ((readLen = mLocalIn.read(buffer)) != -1) {
+ byte[] data = new byte[readLen];
+ System.arraycopy(buffer, 0, data, 0, readLen);
+ Slog.v(TAG, "Sending message to wearable");
+ mCompanionDeviceManager.sendMessage(
+ CompanionDeviceManager.MESSAGE_ONEWAY_TO_WEARABLE,
+ data,
+ associationIdsToSendMessageTo);
+ }
+ } catch (IOException e) {
+ Slog.i(TAG, "IOException while reading from remote stream.");
+ onError();
+ return;
+ }
+ Slog.i(
+ TAG,
+ "Reached EOF when reading from remote stream. Reporting this as an"
+ + " error.");
+ onError();
+ });
+ mSecureTransportListener.onSecureTransportAvailable(mRemoteFd);
+ }
+
+ private void onMessageReceived(int associationIdForMessage, byte[] data) {
+ if (associationIdForMessage == mAssociationId) {
+ Slog.v(TAG, "Received message from wearable.");
+ try {
+ mLocalOut.write(data);
+ mLocalOut.flush();
+ } catch (IOException e) {
+ Slog.i(
+ TAG,
+ "IOException when writing to remote stream. Closing the secure channel.");
+ onError();
+ }
+ } else {
+ Slog.v(
+ TAG,
+ "Received CDM message of type MESSAGE_ONEWAY_FROM_WEARABLE, but it is for"
+ + " another association. Ignoring the message.");
+ }
+ }
+
+ private void onError() {
+ synchronized (mLock) {
+ if (mClosed) {
+ return;
+ }
+ }
+ mSecureTransportListener.onError();
+ close();
+ }
+
+ /** Closes this secure channel and releases all resources. */
+ void close() {
+ synchronized (mLock) {
+ if (mClosed) {
+ return;
+ }
+ Slog.i(TAG, "Closing WearableSensingSecureChannel.");
+ mClosed = true;
+ if (mAssociationId != null) {
+ final long originalCallingIdentity = Binder.clearCallingIdentity();
+ try {
+ mCompanionDeviceManager.removeOnTransportsChangedListener(
+ mOnTransportsChangedListener);
+ mCompanionDeviceManager.removeOnMessageReceivedListener(
+ CompanionDeviceManager.MESSAGE_ONEWAY_FROM_WEARABLE,
+ mOnMessageReceivedListener);
+ mCompanionDeviceManager.detachSystemDataTransport(mAssociationId);
+ mCompanionDeviceManager.disassociate(mAssociationId);
+ } finally {
+ Binder.restoreCallingIdentity(originalCallingIdentity);
+ }
+ }
+ try {
+ mLocalIn.close();
+ } catch (IOException ex) {
+ Slog.e(TAG, "Encountered IOException when closing local input stream.", ex);
+ }
+ try {
+ mLocalOut.close();
+ } catch (IOException ex) {
+ Slog.e(TAG, "Encountered IOException when closing local output stream.", ex);
+ }
+ mMessageFromWearableExecutor.shutdown();
+ mMessageToWearableExecutor.shutdown();
+ mLightWeightExecutor.shutdown();
+ }
+ }
+
+ /**
+ * An executor that can be shutdown. Unlike an ExecutorService, it will not throw a
+ * RejectedExecutionException if {@link #execute(Runnable)} is called after shutdown.
+ */
+ private static class SoftShutdownExecutor implements Executor {
+
+ private final ExecutorService mExecutorService;
+
+ SoftShutdownExecutor(ExecutorService executorService) {
+ mExecutorService = executorService;
+ }
+
+ @Override
+ public void execute(Runnable runnable) {
+ try {
+ mExecutorService.execute(runnable);
+ } catch (RejectedExecutionException ex) {
+ Slog.d(TAG, "Received new runnable after shutdown. Ignoring.");
+ }
+ }
+
+ /** Shutdown the underlying ExecutorService. */
+ void shutdown() {
+ mExecutorService.shutdown();
+ }
+ }
+}
diff --git a/services/core/java/com/android/server/wm/AccessibilityController.java b/services/core/java/com/android/server/wm/AccessibilityController.java
index 19ea9f9..ee865d3 100644
--- a/services/core/java/com/android/server/wm/AccessibilityController.java
+++ b/services/core/java/com/android/server/wm/AccessibilityController.java
@@ -1610,7 +1610,7 @@
Slog.i(LOG_TAG, "computeChangedWindows()");
}
- final List<WindowInfo> windows = new ArrayList<>();
+ final List<WindowInfo> windows;
final List<AccessibilityWindow> visibleWindows = new ArrayList<>();
final int topFocusedDisplayId;
IBinder topFocusedWindowToken = null;
@@ -1640,69 +1640,11 @@
}
final Display display = dc.getDisplay();
display.getRealSize(mTempPoint);
- final int screenWidth = mTempPoint.x;
- final int screenHeight = mTempPoint.y;
-
- Region unaccountedSpace = mTempRegion;
- unaccountedSpace.set(0, 0, screenWidth, screenHeight);
mA11yWindowsPopulator.populateVisibleWindowsOnScreenLocked(
mDisplayId, visibleWindows);
- Set<IBinder> addedWindows = mTempBinderSet;
- addedWindows.clear();
- boolean focusedWindowAdded = false;
-
- final int visibleWindowCount = visibleWindows.size();
-
- // Iterate until we figure out what is touchable for the entire screen.
- for (int i = 0; i < visibleWindowCount; i++) {
- final AccessibilityWindow a11yWindow = visibleWindows.get(i);
- final Region regionInWindow = new Region();
- a11yWindow.getTouchableRegionInWindow(regionInWindow);
- if (windowMattersToAccessibility(a11yWindow, regionInWindow,
- unaccountedSpace)) {
- addPopulatedWindowInfo(a11yWindow, regionInWindow, windows, addedWindows);
- if (windowMattersToUnaccountedSpaceComputation(a11yWindow)) {
- updateUnaccountedSpace(a11yWindow, unaccountedSpace);
- }
- focusedWindowAdded |= a11yWindow.isFocused();
- } else if (a11yWindow.isUntouchableNavigationBar()) {
- // If this widow is navigation bar without touchable region, accounting the
- // region of navigation bar inset because all touch events from this region
- // would be received by launcher, i.e. this region is a un-touchable one
- // for the application.
- unaccountedSpace.op(
- getSystemBarInsetsFrame(
- mService.mWindowMap.get(a11yWindow.getWindowInfo().token)),
- unaccountedSpace,
- Region.Op.REVERSE_DIFFERENCE);
- }
-
- if (unaccountedSpace.isEmpty() && focusedWindowAdded) {
- break;
- }
- }
-
- // Remove child/parent references to windows that were not added.
- final int windowCount = windows.size();
- for (int i = 0; i < windowCount; i++) {
- WindowInfo window = windows.get(i);
- if (!addedWindows.contains(window.parentToken)) {
- window.parentToken = null;
- }
- if (window.childTokens != null) {
- final int childTokenCount = window.childTokens.size();
- for (int j = childTokenCount - 1; j >= 0; j--) {
- if (!addedWindows.contains(window.childTokens.get(j))) {
- window.childTokens.remove(j);
- }
- }
- // Leave the child token list if empty.
- }
- }
-
- addedWindows.clear();
+ windows = buildWindowInfoListLocked(visibleWindows, mTempPoint);
// Gets the top focused display Id and window token for supporting multi-display.
topFocusedDisplayId = mService.mRoot.getTopFocusedDisplayContent().getDisplayId();
@@ -1718,6 +1660,74 @@
mInitialized = true;
}
+ /**
+ * From a list of windows, decides windows to be exposed to accessibility based on touchable
+ * region in the screen.
+ */
+ private List<WindowInfo> buildWindowInfoListLocked(List<AccessibilityWindow> visibleWindows,
+ Point screenSize) {
+ final List<WindowInfo> windows = new ArrayList<>();
+ final Set<IBinder> addedWindows = mTempBinderSet;
+ addedWindows.clear();
+
+ boolean focusedWindowAdded = false;
+
+ final int visibleWindowCount = visibleWindows.size();
+
+ Region unaccountedSpace = mTempRegion;
+ unaccountedSpace.set(0, 0, screenSize.x, screenSize.y);
+
+ // Iterate until we figure out what is touchable for the entire screen.
+ for (int i = 0; i < visibleWindowCount; i++) {
+ final AccessibilityWindow a11yWindow = visibleWindows.get(i);
+ final Region regionInWindow = new Region();
+ a11yWindow.getTouchableRegionInWindow(regionInWindow);
+ if (windowMattersToAccessibility(a11yWindow, regionInWindow, unaccountedSpace)) {
+ addPopulatedWindowInfo(a11yWindow, regionInWindow, windows, addedWindows);
+ if (windowMattersToUnaccountedSpaceComputation(a11yWindow)) {
+ updateUnaccountedSpace(a11yWindow, unaccountedSpace);
+ }
+ focusedWindowAdded |= a11yWindow.isFocused();
+ } else if (a11yWindow.isUntouchableNavigationBar()) {
+ // If this widow is navigation bar without touchable region, accounting the
+ // region of navigation bar inset because all touch events from this region
+ // would be received by launcher, i.e. this region is a un-touchable one
+ // for the application.
+ unaccountedSpace.op(
+ getSystemBarInsetsFrame(
+ mService.mWindowMap.get(a11yWindow.getWindowInfo().token)),
+ unaccountedSpace,
+ Region.Op.REVERSE_DIFFERENCE);
+ }
+
+ if (unaccountedSpace.isEmpty() && focusedWindowAdded) {
+ break;
+ }
+ }
+
+ // Remove child/parent references to windows that were not added.
+ final int windowCount = windows.size();
+ for (int i = 0; i < windowCount; i++) {
+ WindowInfo window = windows.get(i);
+ if (!addedWindows.contains(window.parentToken)) {
+ window.parentToken = null;
+ }
+ if (window.childTokens != null) {
+ final int childTokenCount = window.childTokens.size();
+ for (int j = childTokenCount - 1; j >= 0; j--) {
+ if (!addedWindows.contains(window.childTokens.get(j))) {
+ window.childTokens.remove(j);
+ }
+ }
+ // Leave the child token list if empty.
+ }
+ }
+
+ addedWindows.clear();
+
+ return windows;
+ }
+
// Some windows should be excluded from unaccounted space computation, though they still
// should be reported
private boolean windowMattersToUnaccountedSpaceComputation(AccessibilityWindow a11yWindow) {
diff --git a/services/core/java/com/android/server/wm/ActivityClientController.java b/services/core/java/com/android/server/wm/ActivityClientController.java
index 1128d0c..2e0546e 100644
--- a/services/core/java/com/android/server/wm/ActivityClientController.java
+++ b/services/core/java/com/android/server/wm/ActivityClientController.java
@@ -1319,33 +1319,9 @@
try {
synchronized (mGlobalLock) {
final ActivityRecord r = ActivityRecord.isInRootTaskLocked(token);
- if (r == null) return;
- final TransitionController controller = r.mTransitionController;
- if (!controller.isShellTransitionsEnabled()) {
+ if (r != null) {
r.setShowWhenLocked(showWhenLocked);
- return;
}
- if (controller.isCollecting()
- && !mService.mKeyguardController.isKeyguardLocked(r.getDisplayId())) {
- // Keyguard isn't locked, so this can be done as part of the collecting
- // transition.
- r.setShowWhenLocked(showWhenLocked);
- return;
- }
- final Transition transition = new Transition(
- showWhenLocked ? TRANSIT_TO_FRONT : TRANSIT_TO_BACK,
- 0 /* flags */, controller, mService.mWindowManager.mSyncEngine);
- r.mTransitionController.startCollectOrQueue(transition, (deferred) -> {
- transition.collect(r);
- r.setShowWhenLocked(showWhenLocked);
- if (transition.isNoOp()) {
- transition.abort();
- return;
- }
- controller.requestStartTransition(transition, null /* trigger */,
- null /* remoteTransition */, null /* displayChange */);
- transition.setReady(r, true);
- });
}
} finally {
Binder.restoreCallingIdentity(origId);
@@ -1358,34 +1334,9 @@
try {
synchronized (mGlobalLock) {
final ActivityRecord r = ActivityRecord.isInRootTaskLocked(token);
- if (r == null) return;
- final TransitionController controller = r.mTransitionController;
- // If shell transitions is not enabled just set it directly.
- if (!controller.isShellTransitionsEnabled()) {
+ if (r != null) {
r.setInheritShowWhenLocked(inheritShowWhenLocked);
- return;
}
- if (controller.isCollecting()
- && !mService.mKeyguardController.isKeyguardLocked(r.getDisplayId())) {
- // Keyguard isn't locked, so this can be done as part of the collecting
- // transition.
- r.setInheritShowWhenLocked(inheritShowWhenLocked);
- return;
- }
- final Transition transition = new Transition(
- inheritShowWhenLocked ? TRANSIT_TO_FRONT : TRANSIT_TO_BACK,
- 0 /* flags */, controller, mService.mWindowManager.mSyncEngine);
- r.mTransitionController.startCollectOrQueue(transition, (deferred) -> {
- transition.collect(r);
- r.setInheritShowWhenLocked(inheritShowWhenLocked);
- if (transition.isNoOp()) {
- transition.abort();
- return;
- }
- controller.requestStartTransition(transition, null /* trigger */,
- null /* remoteTransition */, null /* displayChange */);
- transition.setReady(r, true);
- });
}
} finally {
Binder.restoreCallingIdentity(origId);
diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java
index b1d04c9..c2117ea 100644
--- a/services/core/java/com/android/server/wm/ActivityRecord.java
+++ b/services/core/java/com/android/server/wm/ActivityRecord.java
@@ -122,6 +122,7 @@
import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_STARTING;
import static android.view.WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
import static android.view.WindowManager.PROPERTY_ACTIVITY_EMBEDDING_SPLITS_ENABLED;
+import static android.view.WindowManager.PROPERTY_ALLOW_UNTRUSTED_ACTIVITY_EMBEDDING_STATE_SHARING;
import static android.view.WindowManager.TRANSIT_CLOSE;
import static android.view.WindowManager.TRANSIT_FLAG_OPEN_BEHIND;
import static android.view.WindowManager.TRANSIT_OLD_UNSET;
@@ -386,6 +387,7 @@
import com.android.server.wm.SurfaceAnimator.AnimationType;
import com.android.server.wm.WindowManagerService.H;
import com.android.server.wm.utils.InsetUtils;
+import com.android.window.flags.Flags;
import dalvik.annotation.optimization.NeverCompile;
@@ -960,6 +962,9 @@
// without security checks
final Binder shareableActivityToken = new Binder();
+ // Token for accessing the initial caller who started the activity.
+ final IBinder initialCallerInfoAccessToken = new Binder();
+
// Tracking cookie for the launch of this activity and it's task.
IBinder mLaunchCookie;
@@ -986,6 +991,9 @@
// Whether the ActivityEmbedding is enabled on the app.
private final boolean mAppActivityEmbeddingSplitsEnabled;
+ // Whether the Activity allows state sharing in untrusted embedding
+ private final boolean mAllowUntrustedEmbeddingStateSharing;
+
// Records whether client has overridden the WindowAnimation_(Open/Close)(Enter/Exit)Animation.
private CustomAppTransition mCustomOpenTransition;
private CustomAppTransition mCustomCloseTransition;
@@ -2223,6 +2231,7 @@
// No such property name.
}
mAppActivityEmbeddingSplitsEnabled = appActivityEmbeddingEnabled;
+ mAllowUntrustedEmbeddingStateSharing = getAllowUntrustedEmbeddingStateSharingProperty();
mOptInOnBackInvoked = WindowOnBackInvokedDispatcher
.isOnBackInvokedCallbackEnabled(info, info.applicationInfo,
@@ -2432,11 +2441,11 @@
return false;
}
- if (mStartingData != null) {
+ if (hasStartingWindow()) {
return false;
}
- final WindowState mainWin = findMainWindow();
+ final WindowState mainWin = findMainWindow(false /* includeStartingApp */);
if (mainWin != null && mainWin.mWinAnimator.getShown()) {
// App already has a visible window...why would you want a starting window?
return false;
@@ -3078,6 +3087,32 @@
return parent != null && parent.isEmbedded();
}
+ /**
+ * Returns {@code true} if the system is allowed to share this activity's state with the host
+ * app when this activity is embedded in untrusted mode.
+ */
+ boolean isUntrustedEmbeddingStateSharingAllowed() {
+ if (!Flags.untrustedEmbeddingStateSharing()) {
+ return false;
+ }
+ return mAllowUntrustedEmbeddingStateSharing;
+ }
+
+ private boolean getAllowUntrustedEmbeddingStateSharingProperty() {
+ if (!Flags.untrustedEmbeddingStateSharing()) {
+ return false;
+ }
+ try {
+ return mAtmService.mContext.getPackageManager()
+ .getProperty(PROPERTY_ALLOW_UNTRUSTED_ACTIVITY_EMBEDDING_STATE_SHARING,
+ mActivityComponent)
+ .getBoolean();
+ } catch (PackageManager.NameNotFoundException e) {
+ // No such property name.
+ return false;
+ }
+ }
+
@Override
@Nullable
TaskDisplayArea getDisplayArea() {
diff --git a/services/core/java/com/android/server/wm/ActivityStarter.java b/services/core/java/com/android/server/wm/ActivityStarter.java
index 85580ac..d99000e 100644
--- a/services/core/java/com/android/server/wm/ActivityStarter.java
+++ b/services/core/java/com/android/server/wm/ActivityStarter.java
@@ -591,9 +591,18 @@
// Carefully collect grants without holding lock
if (activityInfo != null) {
- intentGrants = supervisor.mService.mUgmInternal.checkGrantUriPermissionFromIntent(
- intent, resolvedCallingUid, activityInfo.applicationInfo.packageName,
- UserHandle.getUserId(activityInfo.applicationInfo.uid));
+ if (android.security.Flags.contentUriPermissionApis()) {
+ intentGrants = supervisor.mService.mUgmInternal
+ .checkGrantUriPermissionFromIntent(intent, resolvedCallingUid,
+ activityInfo.applicationInfo.packageName,
+ UserHandle.getUserId(activityInfo.applicationInfo.uid),
+ activityInfo.requireContentUriPermissionFromCaller);
+ } else {
+ intentGrants = supervisor.mService.mUgmInternal
+ .checkGrantUriPermissionFromIntent(intent, resolvedCallingUid,
+ activityInfo.applicationInfo.packageName,
+ UserHandle.getUserId(activityInfo.applicationInfo.uid));
+ }
}
}
diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
index 3397a3d..8773366 100644
--- a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
+++ b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
@@ -276,6 +276,7 @@
import com.android.server.am.PendingIntentRecord;
import com.android.server.am.UserState;
import com.android.server.firewall.IntentFirewall;
+import com.android.server.grammaticalinflection.GrammaticalInflectionManagerInternal;
import com.android.server.pm.UserManagerService;
import com.android.server.policy.PermissionPolicyInternal;
import com.android.server.sdksandbox.SdkSandboxManagerLocal;
@@ -317,7 +318,6 @@
* {@hide}
*/
public class ActivityTaskManagerService extends IActivityTaskManager.Stub {
- private static final String GRAMMATICAL_GENDER_PROPERTY = "persist.sys.grammatical_gender";
private static final String TAG = TAG_WITH_CLASS_NAME ? "ActivityTaskManagerService" : TAG_ATM;
static final String TAG_ROOT_TASK = TAG + POSTFIX_ROOT_TASK;
static final String TAG_SWITCH = TAG + POSTFIX_SWITCH;
@@ -381,6 +381,7 @@
private PowerManagerInternal mPowerManagerInternal;
private UsageStatsManagerInternal mUsageStatsInternal;
+ GrammaticalInflectionManagerInternal mGrammaticalManagerInternal;
PendingIntentController mPendingIntentController;
IntentFirewall mIntentFirewall;
@@ -881,6 +882,8 @@
mActivityClientController.onSystemReady();
// TODO(b/258792202) Cleanup once ASM is ready to launch
ActivitySecurityModelFeatureFlags.initialize(mContext.getMainExecutor(), pm);
+ mGrammaticalManagerInternal = LocalServices.getService(
+ GrammaticalInflectionManagerInternal.class);
}
}
@@ -938,13 +941,8 @@
configuration.setLayoutDirection(configuration.locale);
}
- // Retrieve the grammatical gender from system property, set it into configuration which
- // will get updated later if the grammatical gender raw value of current configuration is
- // {@link Configuration#GRAMMATICAL_GENDER_UNDEFINED}.
- if (configuration.getGrammaticalGenderRaw() == Configuration.GRAMMATICAL_GENDER_UNDEFINED) {
- configuration.setGrammaticalGender(SystemProperties.getInt(GRAMMATICAL_GENDER_PROPERTY,
- Configuration.GRAMMATICAL_GENDER_UNDEFINED));
- }
+ configuration.setGrammaticalGender(
+ mGrammaticalManagerInternal.retrieveSystemGrammaticalGender(configuration));
synchronized (mGlobalLock) {
mForceResizableActivities = forceResizable;
@@ -3505,8 +3503,9 @@
}
@Override
- public boolean isAssistDataAllowedOnCurrentActivity() {
+ public boolean isAssistDataAllowed() {
int userId;
+ boolean hasRestrictedWindow;
synchronized (mGlobalLock) {
final Task focusedRootTask = getTopDisplayFocusedRootTask();
if (focusedRootTask == null || focusedRootTask.isActivityTypeAssistant()) {
@@ -3518,8 +3517,17 @@
return false;
}
userId = activity.mUserId;
+ DisplayContent displayContent = activity.getDisplayContent();
+ if (displayContent == null) {
+ return false;
+ }
+ hasRestrictedWindow = displayContent.forAllWindows(windowState -> {
+ return windowState.isOnScreen() && UserManager.isUserTypePrivateProfile(
+ getUserManager().getProfileType(windowState.mShowUserId));
+ }, true /* traverseTopToBottom */);
}
- return DevicePolicyCache.getInstance().isScreenCaptureAllowed(userId);
+ return DevicePolicyCache.getInstance().isScreenCaptureAllowed(userId)
+ && !hasRestrictedWindow;
}
private void onLocalVoiceInteractionStartedLocked(IBinder activity,
diff --git a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java
index ec0e3e7..49df396 100644
--- a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java
+++ b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java
@@ -930,7 +930,8 @@
proc.getReportedProcState(), r.getSavedState(), r.getPersistentSavedState(),
results, newIntents, r.takeSceneTransitionInfo(), isTransitionForward,
proc.createProfilerInfoIfNeeded(), r.assistToken, activityClientController,
- r.shareableActivityToken, r.getLaunchedFromBubble(), fragmentToken);
+ r.shareableActivityToken, r.getLaunchedFromBubble(), fragmentToken,
+ r.initialCallerInfoAccessToken);
// Set desired final state.
final ActivityLifecycleItem lifecycleItem;
@@ -943,7 +944,10 @@
// Schedule transaction.
mService.getLifecycleManager().scheduleTransactionAndLifecycleItems(
- proc.getThread(), launchActivityItem, lifecycleItem);
+ proc.getThread(), launchActivityItem, lifecycleItem,
+ // Immediately dispatch the transaction, so that if it fails, the server can
+ // restart the process and retry now.
+ true /* shouldDispatchImmediately */);
if (procConfig.seq > mRootWindowContainer.getConfiguration().seq) {
// If the seq is increased, there should be something changed (e.g. registered
diff --git a/services/core/java/com/android/server/wm/BackNavigationController.java b/services/core/java/com/android/server/wm/BackNavigationController.java
index 13f6a5f..c2dfa21 100644
--- a/services/core/java/com/android/server/wm/BackNavigationController.java
+++ b/services/core/java/com/android/server/wm/BackNavigationController.java
@@ -19,7 +19,6 @@
import static android.app.ActivityTaskManager.INVALID_TASK_ID;
import static android.view.RemoteAnimationTarget.MODE_CLOSING;
import static android.view.RemoteAnimationTarget.MODE_OPENING;
-import static android.view.View.FOCUS_FORWARD;
import static android.view.WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
import static android.view.WindowManager.TRANSIT_CLOSE;
import static android.view.WindowManager.TRANSIT_OLD_NONE;
@@ -61,7 +60,6 @@
import com.android.internal.policy.TransitionAnimation;
import com.android.internal.protolog.common.ProtoLog;
import com.android.server.wm.utils.InsetUtils;
-import com.android.window.flags.Flags;
import java.io.PrintWriter;
import java.util.ArrayList;
@@ -165,21 +163,12 @@
return null;
}
- // Move focus to the adjacent embedded window if it is higher than this window
- final TaskFragment taskFragment = window.getTaskFragment();
- final TaskFragment adjacentTaskFragment =
- taskFragment != null ? taskFragment.getAdjacentTaskFragment() : null;
- if (adjacentTaskFragment != null && taskFragment.isEmbedded()
- && Flags.embeddedActivityBackNavFlag()) {
- final WindowContainer parent = taskFragment.getParent();
- if (parent.mChildren.indexOf(taskFragment) < parent.mChildren.indexOf(
- adjacentTaskFragment)) {
- mWindowManagerService.moveFocusToAdjacentWindow(window, FOCUS_FORWARD);
- window = wmService.getFocusedWindowLocked();
- if (window == null) {
- Slog.e(TAG, "Adjacent window is null, returning null.");
- return null;
- }
+ // Move focus to the top embedded window if possible
+ if (mWindowManagerService.moveFocusToTopEmbeddedWindow(window)) {
+ window = wmService.getFocusedWindowLocked();
+ if (window == null) {
+ Slog.e(TAG, "New focused window is null, returning null.");
+ return null;
}
}
diff --git a/services/core/java/com/android/server/wm/ClientLifecycleManager.java b/services/core/java/com/android/server/wm/ClientLifecycleManager.java
index c7df83a..5b4fb3e 100644
--- a/services/core/java/com/android/server/wm/ClientLifecycleManager.java
+++ b/services/core/java/com/android/server/wm/ClientLifecycleManager.java
@@ -64,6 +64,10 @@
final IApplicationThread client = transaction.getClient();
try {
transaction.schedule();
+ } catch (RemoteException e) {
+ Slog.w(TAG, "Failed to deliver transaction for " + client
+ + "\ntransaction=" + transaction);
+ throw e;
} finally {
if (!(client instanceof Binder)) {
// If client is not an instance of Binder - it's a remote call and at this point it
@@ -106,7 +110,8 @@
final ClientTransaction clientTransaction = getOrCreatePendingTransaction(client);
clientTransaction.addTransactionItem(transactionItem);
- onClientTransactionItemScheduled(clientTransaction);
+ onClientTransactionItemScheduled(clientTransaction,
+ false /* shouldDispatchImmediately */);
} else {
// TODO(b/260873529): cleanup after launch.
final ClientTransaction clientTransaction = ClientTransaction.obtain(client);
@@ -119,14 +124,30 @@
}
}
+ void scheduleTransactionAndLifecycleItems(@NonNull IApplicationThread client,
+ @NonNull ClientTransactionItem transactionItem,
+ @NonNull ActivityLifecycleItem lifecycleItem) throws RemoteException {
+ scheduleTransactionAndLifecycleItems(client, transactionItem, lifecycleItem,
+ false /* shouldDispatchImmediately */);
+ }
+
/**
* Schedules a single transaction item with a lifecycle request, delivery to client application.
+ *
+ * @param shouldDispatchImmediately whether or not to dispatch the transaction immediately. This
+ * should only be {@code true} when it is important to know the
+ * result of dispatching immediately. For example, when cold
+ * launches an app, the server needs to know if the transaction
+ * is dispatched successfully, and may restart the process if
+ * not.
+ *
* @throws RemoteException
* @see ClientTransactionItem
*/
void scheduleTransactionAndLifecycleItems(@NonNull IApplicationThread client,
@NonNull ClientTransactionItem transactionItem,
- @NonNull ActivityLifecycleItem lifecycleItem) throws RemoteException {
+ @NonNull ActivityLifecycleItem lifecycleItem,
+ boolean shouldDispatchImmediately) throws RemoteException {
// The behavior is different depending on the flag.
// When flag is on, we wait until RootWindowContainer#performSurfacePlacementNoTrace to
// dispatch all pending transactions at once.
@@ -135,7 +156,7 @@
clientTransaction.addTransactionItem(transactionItem);
clientTransaction.addTransactionItem(lifecycleItem);
- onClientTransactionItemScheduled(clientTransaction);
+ onClientTransactionItemScheduled(clientTransaction, shouldDispatchImmediately);
} else {
// TODO(b/260873529): cleanup after launch.
final ClientTransaction clientTransaction = ClientTransaction.obtain(client);
@@ -157,7 +178,8 @@
try {
scheduleTransaction(transaction);
} catch (RemoteException e) {
- Slog.e(TAG, "Failed to deliver transaction for " + transaction.getClient());
+ Slog.e(TAG, "Failed to deliver pending transaction", e);
+ // TODO(b/323801078): apply cleanup for individual transaction item if needed.
}
}
mPendingTransactions.clear();
@@ -194,8 +216,9 @@
/** Must only be called with WM lock. */
private void onClientTransactionItemScheduled(
- @NonNull ClientTransaction clientTransaction) throws RemoteException {
- if (shouldDispatchPendingTransactionsImmediately()) {
+ @NonNull ClientTransaction clientTransaction,
+ boolean shouldDispatchImmediately) throws RemoteException {
+ if (shouldDispatchImmediately || shouldDispatchPendingTransactionsImmediately()) {
// Dispatch the pending transaction immediately.
mPendingTransactions.remove(clientTransaction.getClient().asBinder());
scheduleTransaction(clientTransaction);
diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java
index 716aee3..e743172 100644
--- a/services/core/java/com/android/server/wm/DisplayContent.java
+++ b/services/core/java/com/android/server/wm/DisplayContent.java
@@ -5150,6 +5150,15 @@
/** @return the orientation of the display when it's rotation is ROTATION_0. */
int getNaturalOrientation() {
+ return mBaseDisplayWidth <= mBaseDisplayHeight
+ ? ORIENTATION_PORTRAIT : ORIENTATION_LANDSCAPE;
+ }
+
+ /**
+ * Returns the orientation which is used for app's Configuration (excluding decor insets) when
+ * the display rotation is ROTATION_0.
+ */
+ int getNaturalConfigurationOrientation() {
final Configuration config = getConfiguration();
if (config.windowConfiguration.getDisplayRotation() == ROTATION_0) {
return config.orientation;
diff --git a/services/core/java/com/android/server/wm/InputConsumerImpl.java b/services/core/java/com/android/server/wm/InputConsumerImpl.java
index 34d7651..4204670 100644
--- a/services/core/java/com/android/server/wm/InputConsumerImpl.java
+++ b/services/core/java/com/android/server/wm/InputConsumerImpl.java
@@ -132,7 +132,7 @@
void show(SurfaceControl.Transaction t, WindowContainer w) {
t.show(mInputSurface);
t.setInputWindowInfo(mInputSurface, mWindowHandle);
- t.setRelativeLayer(mInputSurface, w.getSurfaceControl(), 1);
+ t.setRelativeLayer(mInputSurface, w.getSurfaceControl(), 1 + w.getChildCount());
}
void show(SurfaceControl.Transaction t, int layer) {
diff --git a/services/core/java/com/android/server/wm/InsetsSourceProvider.java b/services/core/java/com/android/server/wm/InsetsSourceProvider.java
index d9dda4a..cd96806 100644
--- a/services/core/java/com/android/server/wm/InsetsSourceProvider.java
+++ b/services/core/java/com/android/server/wm/InsetsSourceProvider.java
@@ -178,6 +178,7 @@
mWindowContainer.cancelAnimation();
mWindowContainer.getInsetsSourceProviders().remove(mSource.getId());
mSeamlessRotating = false;
+ mHasPendingPosition = false;
}
ProtoLog.d(WM_DEBUG_WINDOW_INSETS, "InsetsSource setWin %s for type %s",
windowContainer, WindowInsets.Type.toString(mSource.getType()));
diff --git a/services/core/java/com/android/server/wm/Letterbox.java b/services/core/java/com/android/server/wm/Letterbox.java
index f6aad4c..e66321a 100644
--- a/services/core/java/com/android/server/wm/Letterbox.java
+++ b/services/core/java/com/android/server/wm/Letterbox.java
@@ -23,6 +23,7 @@
import android.graphics.Color;
import android.graphics.Point;
import android.graphics.Rect;
+import android.os.Handler;
import android.os.IBinder;
import android.os.InputConfig;
import android.view.GestureDetector;
@@ -258,11 +259,12 @@
private final GestureDetector mDoubleTapDetector;
private final DoubleTapListener mDoubleTapListener;
- TapEventReceiver(InputChannel inputChannel, WindowManagerService wmService) {
- super(inputChannel, UiThread.getHandler().getLooper());
+ TapEventReceiver(InputChannel inputChannel, WindowManagerService wmService,
+ Handler uiHandler) {
+ super(inputChannel, uiHandler.getLooper());
mDoubleTapListener = new DoubleTapListener(wmService);
- mDoubleTapDetector = new GestureDetector(
- wmService.mContext, mDoubleTapListener, UiThread.getHandler());
+ mDoubleTapDetector = new GestureDetector(wmService.mContext, mDoubleTapListener,
+ uiHandler);
}
@Override
@@ -294,19 +296,21 @@
}
}
- private final class InputInterceptor {
+ private final class InputInterceptor implements Runnable {
private final InputChannel mClientChannel;
private final InputWindowHandle mWindowHandle;
private final InputEventReceiver mInputEventReceiver;
private final WindowManagerService mWmService;
private final IBinder mToken;
+ private final Handler mHandler;
InputInterceptor(String namePrefix, WindowState win) {
mWmService = win.mWmService;
+ mHandler = UiThread.getHandler();
final String name = namePrefix + (win.mActivityRecord != null ? win.mActivityRecord : win);
mClientChannel = mWmService.mInputManager.createInputChannel(name);
- mInputEventReceiver = new TapEventReceiver(mClientChannel, mWmService);
+ mInputEventReceiver = new TapEventReceiver(mClientChannel, mWmService, mHandler);
mToken = mClientChannel.getToken();
@@ -335,11 +339,17 @@
mWindowHandle.touchableRegion.translate(-frame.left, -frame.top);
}
- void dispose() {
- mWmService.mInputManager.removeInputChannel(mToken);
+ @Override
+ public void run() {
mInputEventReceiver.dispose();
mClientChannel.dispose();
}
+
+ void dispose() {
+ mWmService.mInputManager.removeInputChannel(mToken);
+ // Perform dispose on the same thread that dispatches input event
+ mHandler.post(this);
+ }
}
private class LetterboxSurface {
diff --git a/services/core/java/com/android/server/wm/LetterboxUiController.java b/services/core/java/com/android/server/wm/LetterboxUiController.java
index 0e2d3d1..edf9da1 100644
--- a/services/core/java/com/android/server/wm/LetterboxUiController.java
+++ b/services/core/java/com/android/server/wm/LetterboxUiController.java
@@ -58,6 +58,8 @@
import static android.content.res.Configuration.SCREEN_WIDTH_DP_UNDEFINED;
import static android.content.res.Configuration.SMALLEST_SCREEN_WIDTH_DP_UNDEFINED;
import static android.view.WindowManager.LayoutParams.FLAG_SHOW_WALLPAPER;
+import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_STARTING;
+import static android.view.WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
import static android.view.WindowManager.PROPERTY_CAMERA_COMPAT_ALLOW_FORCE_ROTATION;
import static android.view.WindowManager.PROPERTY_CAMERA_COMPAT_ALLOW_REFRESH;
import static android.view.WindowManager.PROPERTY_CAMERA_COMPAT_ENABLE_REFRESH_VIA_PAUSE;
@@ -927,8 +929,7 @@
}
void updateLetterboxSurface(WindowState winHint, Transaction t) {
- final WindowState w = mActivityRecord.findMainWindow();
- if (w != winHint && winHint != null && w != null) {
+ if (shouldNotLayoutLetterbox(winHint)) {
return;
}
layoutLetterbox(winHint);
@@ -937,20 +938,11 @@
}
}
- void layoutLetterbox(WindowState winHint) {
- final WindowState w = mActivityRecord.findMainWindow();
- if (w == null || winHint != null && w != winHint) {
+ void layoutLetterbox(WindowState w) {
+ if (shouldNotLayoutLetterbox(w)) {
return;
}
updateRoundedCornersIfNeeded(w);
- // If there is another main window that is not an application-starting window, we should
- // update rounded corners for it as well, to avoid flickering rounded corners.
- final WindowState nonStartingAppW = mActivityRecord.findMainWindow(
- /* includeStartingApp= */ false);
- if (nonStartingAppW != null && nonStartingAppW != w) {
- updateRoundedCornersIfNeeded(nonStartingAppW);
- }
-
updateWallpaperForLetterbox(w);
if (shouldShowLetterboxUi(w)) {
if (mLetterbox == null) {
@@ -1023,6 +1015,18 @@
return mActivityRecord.getSurfaceControl();
}
+ private static boolean shouldNotLayoutLetterbox(WindowState w) {
+ if (w == null) {
+ return true;
+ }
+ final int type = w.mAttrs.type;
+ // Allow letterbox to be displayed early for base application or application starting
+ // windows even if it is not on the top z order to prevent flickering when the
+ // letterboxed window is brought to the top
+ return (type != TYPE_BASE_APPLICATION && type != TYPE_APPLICATION_STARTING)
+ || w.mAnimatingExit;
+ }
+
private boolean shouldLetterboxHaveRoundedCorners() {
// TODO(b/214030873): remove once background is drawn for transparent activities
// Letterbox shouldn't have rounded corners if the activity is transparent
diff --git a/services/core/java/com/android/server/wm/OWNERS b/services/core/java/com/android/server/wm/OWNERS
index cd70447..e06f215 100644
--- a/services/core/java/com/android/server/wm/OWNERS
+++ b/services/core/java/com/android/server/wm/OWNERS
@@ -17,6 +17,7 @@
rgl@google.com
yunfanc@google.com
wilsonshih@google.com
+jiamingliu@google.com
# Files related to background activity launches
per-file Background*Start* = set noparent
diff --git a/services/core/java/com/android/server/wm/PerfettoTransitionTracer.java b/services/core/java/com/android/server/wm/PerfettoTransitionTracer.java
index eae9951..d08f043 100644
--- a/services/core/java/com/android/server/wm/PerfettoTransitionTracer.java
+++ b/services/core/java/com/android/server/wm/PerfettoTransitionTracer.java
@@ -19,6 +19,7 @@
import android.annotation.NonNull;
import android.internal.perfetto.protos.PerfettoTrace;
import android.os.SystemClock;
+import android.os.Trace;
import android.tracing.perfetto.DataSourceParams;
import android.tracing.perfetto.InitArguments;
import android.tracing.perfetto.Producer;
@@ -57,6 +58,16 @@
return;
}
+ Trace.traceBegin(Trace.TRACE_TAG_WINDOW_MANAGER, "logSentTransition");
+ try {
+ doLogSentTransition(transition, targets);
+ } finally {
+ Trace.traceEnd(Trace.TRACE_TAG_WINDOW_MANAGER);
+ }
+ }
+
+ private void doLogSentTransition(
+ Transition transition, ArrayList<Transition.ChangeInfo> targets) {
mDataSource.trace((ctx) -> {
final ProtoOutputStream os = ctx.newTracePacket();
@@ -91,6 +102,15 @@
return;
}
+ Trace.traceBegin(Trace.TRACE_TAG_WINDOW_MANAGER, "logFinishedTransition");
+ try {
+ doLogFinishTransition(transition);
+ } finally {
+ Trace.traceEnd(Trace.TRACE_TAG_WINDOW_MANAGER);
+ }
+ }
+
+ private void doLogFinishTransition(Transition transition) {
mDataSource.trace((ctx) -> {
final ProtoOutputStream os = ctx.newTracePacket();
@@ -114,6 +134,15 @@
return;
}
+ Trace.traceBegin(Trace.TRACE_TAG_WINDOW_MANAGER, "logAbortedTransition");
+ try {
+ doLogAbortedTransition(transition);
+ } finally {
+ Trace.traceEnd(Trace.TRACE_TAG_WINDOW_MANAGER);
+ }
+ }
+
+ private void doLogAbortedTransition(Transition transition) {
mDataSource.trace((ctx) -> {
final ProtoOutputStream os = ctx.newTracePacket();
@@ -131,6 +160,15 @@
return;
}
+ Trace.traceBegin(Trace.TRACE_TAG_WINDOW_MANAGER, "logRemovingStartingWindow");
+ try {
+ doLogRemovingStartingWindow(startingData);
+ } finally {
+ Trace.traceEnd(Trace.TRACE_TAG_WINDOW_MANAGER);
+ }
+ }
+
+ public void doLogRemovingStartingWindow(@NonNull StartingData startingData) {
mDataSource.trace((ctx) -> {
final ProtoOutputStream os = ctx.newTracePacket();
diff --git a/services/core/java/com/android/server/wm/RootWindowContainer.java b/services/core/java/com/android/server/wm/RootWindowContainer.java
index 25646f1..609ad1e 100644
--- a/services/core/java/com/android/server/wm/RootWindowContainer.java
+++ b/services/core/java/com/android/server/wm/RootWindowContainer.java
@@ -83,6 +83,7 @@
import static com.android.server.wm.WindowManagerService.WINDOWS_FREEZING_SCREENS_NONE;
import static com.android.server.wm.WindowSurfacePlacer.SET_UPDATE_ROTATION;
import static com.android.server.wm.WindowSurfacePlacer.SET_WALLPAPER_ACTION_PENDING;
+import static com.android.systemui.shared.Flags.enableHomeDelay;
import static java.lang.Integer.MAX_VALUE;
@@ -1444,6 +1445,7 @@
aInfo = info.first;
homeIntent = info.second;
}
+
if (aInfo == null || homeIntent == null) {
return false;
}
@@ -1452,6 +1454,11 @@
return false;
}
+ if (enableHomeDelay() && !mService.mAmInternal.getThemeOverlayReadiness()) {
+ Slog.d(TAG, "ThemeHomeDelay: Home launch was deferred.");
+ return false;
+ }
+
// Updates the home component of the intent.
homeIntent.setComponent(new ComponentName(aInfo.applicationInfo.packageName, aInfo.name));
homeIntent.setFlags(homeIntent.getFlags() | FLAG_ACTIVITY_NEW_TASK);
diff --git a/services/core/java/com/android/server/wm/SafeActivityOptions.java b/services/core/java/com/android/server/wm/SafeActivityOptions.java
index 4ced5d5..f2dc55f 100644
--- a/services/core/java/com/android/server/wm/SafeActivityOptions.java
+++ b/services/core/java/com/android/server/wm/SafeActivityOptions.java
@@ -333,7 +333,9 @@
if (aInfo != null && overrideTaskTransition) {
final int startTasksFromRecentsPerm = ActivityTaskManagerService.checkPermission(
START_TASKS_FROM_RECENTS, callingPid, callingUid);
- if (startTasksFromRecentsPerm != PERMISSION_GRANTED) {
+ // Allow if calling uid is from assistant, or start task from recents
+ if (startTasksFromRecentsPerm != PERMISSION_GRANTED
+ && !isAssistant(supervisor.mService, callingUid)) {
final String msg = "Permission Denial: starting " + getIntentString(intent)
+ " from " + callerApp + " (pid=" + callingPid
+ ", uid=" + callingUid + ") with overrideTaskTransition=true";
diff --git a/services/core/java/com/android/server/wm/ScreenRecordingCallbackController.java b/services/core/java/com/android/server/wm/ScreenRecordingCallbackController.java
index bdb4588..967f415 100644
--- a/services/core/java/com/android/server/wm/ScreenRecordingCallbackController.java
+++ b/services/core/java/com/android/server/wm/ScreenRecordingCallbackController.java
@@ -36,8 +36,7 @@
import com.android.internal.protolog.common.ProtoLog;
import java.io.PrintWriter;
-import java.util.Map;
-import java.util.Set;
+import java.util.ArrayList;
public class ScreenRecordingCallbackController {
@@ -57,10 +56,10 @@
}
@GuardedBy("WindowManagerService.mGlobalLock")
- private final Map<IBinder, Callback> mCallbacks = new ArrayMap<>();
+ private final ArrayMap<IBinder, Callback> mCallbacks = new ArrayMap<>();
@GuardedBy("WindowManagerService.mGlobalLock")
- private final Map<Integer /*UID*/, Boolean> mLastInvokedStateByUid = new ArrayMap<>();
+ private final ArrayMap<Integer /*UID*/, Boolean> mLastInvokedStateByUid = new ArrayMap<>();
private final WindowManagerService mWms;
@@ -108,8 +107,8 @@
}
IBinder binder = ServiceManager.getService(MEDIA_PROJECTION_SERVICE);
- IMediaProjectionManager mediaProjectionManager =
- IMediaProjectionManager.Stub.asInterface(binder);
+ IMediaProjectionManager mediaProjectionManager = IMediaProjectionManager.Stub.asInterface(
+ binder);
long identityToken = Binder.clearCallingIdentity();
MediaProjectionInfo mediaProjectionInfo = null;
@@ -157,11 +156,14 @@
synchronized (mWms.mGlobalLock) {
IBinder binder = callback.asBinder();
Callback callbackInfo = mCallbacks.remove(binder);
+ if (callbackInfo == null) {
+ return;
+ }
binder.unlinkToDeath(callbackInfo, 0);
boolean uidHasCallback = false;
- for (Callback cb : mCallbacks.values()) {
- if (cb.mUid == callbackInfo.mUid) {
+ for (int i = 0; i < mCallbacks.size(); i++) {
+ if (mCallbacks.valueAt(i).mUid == callbackInfo.mUid) {
uidHasCallback = true;
break;
}
@@ -213,7 +215,9 @@
return;
}
- dispatchCallbacks(Set.of(uid), processVisible);
+ ArraySet<Integer> uidSet = new ArraySet<>();
+ uidSet.add(uid);
+ dispatchCallbacks(uidSet, processVisible);
}
@GuardedBy("WindowManagerService.mGlobalLock")
@@ -233,8 +237,8 @@
}
@GuardedBy("WindowManagerService.mGlobalLock")
- private Set<Integer> getRecordedUids() {
- Set<Integer> result = new ArraySet<>();
+ private ArraySet<Integer> getRecordedUids() {
+ ArraySet<Integer> result = new ArraySet<>();
if (mRecordedWC == null) {
return result;
}
@@ -248,37 +252,43 @@
}
@GuardedBy("WindowManagerService.mGlobalLock")
- private void dispatchCallbacks(Set<Integer> uids, boolean visibleInScreenRecording) {
+ private void dispatchCallbacks(ArraySet<Integer> uids, boolean visibleInScreenRecording) {
if (uids.isEmpty()) {
return;
}
- for (Integer uid : uids) {
- mLastInvokedStateByUid.put(uid, visibleInScreenRecording);
+ for (int i = 0; i < uids.size(); i++) {
+ mLastInvokedStateByUid.put(uids.valueAt(i), visibleInScreenRecording);
}
- for (Callback callback : mCallbacks.values()) {
- if (!uids.contains(callback.mUid)) {
- continue;
- }
- try {
- callback.mCallback.onScreenRecordingStateChanged(visibleInScreenRecording);
- } catch (RemoteException e) {
- // Client has died. Cleanup is handled via DeathRecipient.
+ ArrayList<IScreenRecordingCallback> callbacks = new ArrayList<>();
+ for (int i = 0; i < mCallbacks.size(); i++) {
+ if (uids.contains(mCallbacks.valueAt(i).mUid)) {
+ callbacks.add(mCallbacks.valueAt(i).mCallback);
}
}
+
+ mWms.mH.post(() -> {
+ for (int i = 0; i < callbacks.size(); i++) {
+ try {
+ callbacks.get(i).onScreenRecordingStateChanged(visibleInScreenRecording);
+ } catch (RemoteException e) {
+ // Client has died. Cleanup is handled via DeathRecipient.
+ }
+ }
+ });
}
void dump(PrintWriter pw) {
pw.format("ScreenRecordingCallbackController:\n");
pw.format(" Registered callbacks:\n");
- for (Map.Entry<IBinder, Callback> entry : mCallbacks.entrySet()) {
- pw.format(" callback=%s uid=%s\n", entry.getKey(), entry.getValue().mUid);
+ for (int i = 0; i < mCallbacks.size(); i++) {
+ pw.format(" callback=%s uid=%s\n", mCallbacks.keyAt(i), mCallbacks.valueAt(i).mUid);
}
pw.format(" Last invoked states:\n");
- for (Map.Entry<Integer, Boolean> entry : mLastInvokedStateByUid.entrySet()) {
- pw.format(" uid=%s isVisibleInScreenRecording=%s\n", entry.getKey(),
- entry.getValue());
+ for (int i = 0; i < mLastInvokedStateByUid.size(); i++) {
+ pw.format(" uid=%s isVisibleInScreenRecording=%s\n", mLastInvokedStateByUid.keyAt(i),
+ mLastInvokedStateByUid.valueAt(i));
}
}
}
diff --git a/services/core/java/com/android/server/wm/Session.java b/services/core/java/com/android/server/wm/Session.java
index 083872a..95e6ca6 100644
--- a/services/core/java/com/android/server/wm/Session.java
+++ b/services/core/java/com/android/server/wm/Session.java
@@ -1005,7 +1005,14 @@
public boolean moveFocusToAdjacentWindow(IWindow fromWindow, @FocusDirection int direction) {
final long identity = Binder.clearCallingIdentity();
try {
- return mService.moveFocusToAdjacentWindow(this, fromWindow, direction);
+ synchronized (mService.mGlobalLock) {
+ final WindowState win =
+ mService.windowForClientLocked(this, fromWindow, false /* throwOnError */);
+ if (win == null) {
+ return false;
+ }
+ return mService.moveFocusToAdjacentWindow(win, direction);
+ }
} finally {
Binder.restoreCallingIdentity(identity);
}
diff --git a/services/core/java/com/android/server/wm/TaskFragmentOrganizerController.java b/services/core/java/com/android/server/wm/TaskFragmentOrganizerController.java
index 81fe453..8d054db 100644
--- a/services/core/java/com/android/server/wm/TaskFragmentOrganizerController.java
+++ b/services/core/java/com/android/server/wm/TaskFragmentOrganizerController.java
@@ -376,10 +376,15 @@
+ " is not in a task belong to the organizer app.");
return null;
}
- if (task.isAllowedToEmbedActivity(activity, mOrganizerUid) != EMBEDDING_ALLOWED
- || !task.isAllowedToEmbedActivityInTrustedMode(activity, mOrganizerUid)) {
+ if (task.isAllowedToEmbedActivity(activity, mOrganizerUid) != EMBEDDING_ALLOWED) {
Slog.d(TAG, "Reparent activity=" + activity.token
- + " is not allowed to be embedded in trusted mode.");
+ + " is not allowed to be embedded.");
+ return null;
+ }
+ if (!task.isAllowedToEmbedActivityInTrustedMode(activity, mOrganizerUid)
+ && !activity.isUntrustedEmbeddingStateSharingAllowed()) {
+ Slog.d(TAG, "Reparent activity=" + activity.token
+ + " is not allowed to be shared with untrusted host.");
return null;
}
diff --git a/services/core/java/com/android/server/wm/TaskOrganizerController.java b/services/core/java/com/android/server/wm/TaskOrganizerController.java
index 27cc2d6..7fc61e1 100644
--- a/services/core/java/com/android/server/wm/TaskOrganizerController.java
+++ b/services/core/java/com/android/server/wm/TaskOrganizerController.java
@@ -66,7 +66,6 @@
import java.util.HashSet;
import java.util.List;
import java.util.WeakHashMap;
-import java.util.function.Consumer;
/**
* Stores the TaskOrganizers associated with a given windowing mode and
@@ -102,11 +101,8 @@
*/
private static class TaskOrganizerCallbacks {
final ITaskOrganizer mTaskOrganizer;
- final Consumer<Runnable> mDeferTaskOrgCallbacksConsumer;
- TaskOrganizerCallbacks(ITaskOrganizer taskOrg,
- Consumer<Runnable> deferTaskOrgCallbacksConsumer) {
- mDeferTaskOrgCallbacksConsumer = deferTaskOrgCallbacksConsumer;
+ TaskOrganizerCallbacks(ITaskOrganizer taskOrg) {
mTaskOrganizer = taskOrg;
}
@@ -335,11 +331,7 @@
private final int mUid;
TaskOrganizerState(ITaskOrganizer organizer, int uid) {
- final Consumer<Runnable> deferTaskOrgCallbacksConsumer =
- mDeferTaskOrgCallbacksConsumer != null
- ? mDeferTaskOrgCallbacksConsumer
- : mService.mWindowManager.mAnimator::addAfterPrepareSurfacesRunnable;
- mOrganizer = new TaskOrganizerCallbacks(organizer, deferTaskOrgCallbacksConsumer);
+ mOrganizer = new TaskOrganizerCallbacks(organizer);
mDeathRecipient = new DeathRecipient(organizer);
mPendingEventsQueue = new TaskOrganizerPendingEventsQueue(this);
try {
@@ -484,8 +476,6 @@
// Set of organized tasks (by taskId) that dispatch back pressed to their organizers
private final HashSet<Integer> mInterceptBackPressedOnRootTasks = new HashSet<>();
- private Consumer<Runnable> mDeferTaskOrgCallbacksConsumer;
-
TaskOrganizerController(ActivityTaskManagerService atm) {
mService = atm;
mGlobalLock = atm.mGlobalLock;
@@ -502,15 +492,6 @@
}
/**
- * Specifies the consumer to run to defer the task org callbacks. Can be overridden while
- * testing to allow the callbacks to be sent synchronously.
- */
- @VisibleForTesting
- public void setDeferTaskOrgCallbacksConsumer(Consumer<Runnable> consumer) {
- mDeferTaskOrgCallbacksConsumer = consumer;
- }
-
- /**
* Register a TaskOrganizer to manage tasks as they enter the a supported windowing mode.
*/
@Override
diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java
index 1e58306..2accf9a 100644
--- a/services/core/java/com/android/server/wm/Transition.java
+++ b/services/core/java/com/android/server/wm/Transition.java
@@ -2917,26 +2917,6 @@
Trace.asyncTraceForTrackEnd(Trace.TRACE_TAG_WINDOW_MANAGER, TAG, cookie);
}
- /**
- * Get whether the transition, in its current state, is a no-op. This should be avoided. It is
- * only here for legacy usages where we can't tell ahead-of-time whether something will
- * generate a change.
- */
- boolean isNoOp() {
- for (int i = mParticipants.size() - 1; i >= 0; --i) {
- // This is the same criteria as the rejection logic in calculateTargets
- final WindowContainer<?> wc = mParticipants.valueAt(i);
- if (!wc.isAttached()) continue;
- // The level of transition target should be at least window token.
- if (wc.asWindowState() != null) continue;
- final ChangeInfo changeInfo = mChanges.get(wc);
- // Reject no-ops
- if (!changeInfo.hasChanged()) continue;
- return false;
- }
- return true;
- }
-
@VisibleForTesting
static class ChangeInfo {
private static final int FLAG_NONE = 0;
diff --git a/services/core/java/com/android/server/wm/TrustedOverlayHost.java b/services/core/java/com/android/server/wm/TrustedOverlayHost.java
index f8edc2b..debe794 100644
--- a/services/core/java/com/android/server/wm/TrustedOverlayHost.java
+++ b/services/core/java/com/android/server/wm/TrustedOverlayHost.java
@@ -88,7 +88,17 @@
void addOverlay(SurfaceControlViewHost.SurfacePackage p, SurfaceControl currentParent) {
requireOverlaySurfaceControl();
- mOverlays.add(p);
+
+ boolean hasExistingOverlay = false;
+ for (int i = mOverlays.size() - 1; i >= 0; i--) {
+ SurfaceControlViewHost.SurfacePackage l = mOverlays.get(i);
+ if (l.getSurfaceControl().isSameSurface(p.getSurfaceControl())) {
+ hasExistingOverlay = true;
+ }
+ }
+ if (!hasExistingOverlay) {
+ mOverlays.add(p);
+ }
SurfaceControl.Transaction t = mWmService.mTransactionFactory.get();
t.reparent(p.getSurfaceControl(), mSurfaceControl)
diff --git a/services/core/java/com/android/server/wm/TrustedPresentationListenerController.java b/services/core/java/com/android/server/wm/TrustedPresentationListenerController.java
index 1688a1a..817901f 100644
--- a/services/core/java/com/android/server/wm/TrustedPresentationListenerController.java
+++ b/services/core/java/com/android/server/wm/TrustedPresentationListenerController.java
@@ -32,7 +32,6 @@
import android.os.IBinder;
import android.os.RemoteException;
import android.util.ArrayMap;
-import android.util.ArraySet;
import android.util.IntArray;
import android.util.Pair;
import android.util.Size;
@@ -159,10 +158,6 @@
private InputWindowHandle[] mLastWindowHandles;
- private final Object mIgnoredWindowTokensLock = new Object();
-
- private final ArraySet<IBinder> mIgnoredWindowTokens = new ArraySet<>();
-
private void startHandlerThreadIfNeeded() {
synchronized (mHandlerThreadLock) {
if (mHandler == null) {
@@ -173,18 +168,6 @@
}
}
- void addIgnoredWindowTokens(IBinder token) {
- synchronized (mIgnoredWindowTokensLock) {
- mIgnoredWindowTokens.add(token);
- }
- }
-
- void removeIgnoredWindowTokens(IBinder token) {
- synchronized (mIgnoredWindowTokensLock) {
- mIgnoredWindowTokens.remove(token);
- }
- }
-
void registerListener(IBinder window, ITrustedPresentationListener listener,
TrustedPresentationThresholds thresholds, int id) {
startHandlerThreadIfNeeded();
@@ -271,12 +254,8 @@
ArrayMap<ITrustedPresentationListener, Pair<IntArray, IntArray>> listenerUpdates =
new ArrayMap<>();
- ArraySet<IBinder> ignoredWindowTokens;
- synchronized (mIgnoredWindowTokensLock) {
- ignoredWindowTokens = new ArraySet<>(mIgnoredWindowTokens);
- }
for (var windowHandle : mLastWindowHandles) {
- if (ignoredWindowTokens.contains(windowHandle.getWindowToken())) {
+ if (!windowHandle.canOccludePresentation) {
ProtoLog.v(WM_DEBUG_TPL, "Skipping %s", windowHandle.name);
continue;
}
diff --git a/services/core/java/com/android/server/wm/WindowContainer.java b/services/core/java/com/android/server/wm/WindowContainer.java
index 2d2857a..80894b2 100644
--- a/services/core/java/com/android/server/wm/WindowContainer.java
+++ b/services/core/java/com/android/server/wm/WindowContainer.java
@@ -1589,7 +1589,7 @@
if (requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_NOSENSOR) {
// NOSENSOR means the display's "natural" orientation, so return that.
if (mDisplayContent != null) {
- return mDisplayContent.getNaturalOrientation();
+ return mDisplayContent.getNaturalConfigurationOrientation();
}
} else if (requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_LOCKED) {
// LOCKED means the activity's orientation remains unchanged, so return existing value.
diff --git a/services/core/java/com/android/server/wm/WindowList.java b/services/core/java/com/android/server/wm/WindowList.java
index dfeba40..1e888f5 100644
--- a/services/core/java/com/android/server/wm/WindowList.java
+++ b/services/core/java/com/android/server/wm/WindowList.java
@@ -24,7 +24,7 @@
*/
class WindowList<E> extends ArrayList<E> {
- void addFirst(E e) {
+ public void addFirst(E e) {
add(0, e);
}
diff --git a/services/core/java/com/android/server/wm/WindowManagerInternal.java b/services/core/java/com/android/server/wm/WindowManagerInternal.java
index d0b9a6e..ae4c3b9 100644
--- a/services/core/java/com/android/server/wm/WindowManagerInternal.java
+++ b/services/core/java/com/android/server/wm/WindowManagerInternal.java
@@ -1047,4 +1047,9 @@
* * {@link #addBlockScreenCaptureForApps(ArraySet)}
*/
public abstract void clearBlockedApps();
+
+ /**
+ * Moves the current focus to the top activity window if the top activity is embedded.
+ */
+ public abstract boolean moveFocusToTopEmbeddedWindowIfNeeded();
}
diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java
index fc23700..366e1bb 100644
--- a/services/core/java/com/android/server/wm/WindowManagerService.java
+++ b/services/core/java/com/android/server/wm/WindowManagerService.java
@@ -158,6 +158,7 @@
import android.Manifest;
import android.Manifest.permission;
import android.animation.ValueAnimator;
+import android.annotation.EnforcePermission;
import android.annotation.IntDef;
import android.annotation.IntRange;
import android.annotation.NonNull;
@@ -345,6 +346,7 @@
import com.android.server.power.ShutdownThread;
import com.android.server.utils.PriorityDump;
import com.android.server.wallpaper.WallpaperCropper.WallpaperCropUtils;
+import com.android.window.flags.Flags;
import dalvik.annotation.optimization.NeverCompile;
@@ -1112,8 +1114,11 @@
public static WindowManagerService main(final Context context, final InputManagerService im,
final boolean showBootMsgs, WindowManagerPolicy policy,
ActivityTaskManagerService atm) {
- return main(context, im, showBootMsgs, policy, atm, new DisplayWindowSettingsProvider(),
- SurfaceControl.Transaction::new, SurfaceControl.Builder::new);
+ final WindowManagerService wms = main(context, im, showBootMsgs, policy, atm,
+ new DisplayWindowSettingsProvider(), SurfaceControl.Transaction::new,
+ SurfaceControl.Builder::new);
+ WindowManagerGlobal.setWindowManagerServiceForSystemProcess(wms);
+ return wms;
}
/**
@@ -3285,7 +3290,7 @@
}
}
- @android.annotation.EnforcePermission(android.Manifest.permission.DISABLE_KEYGUARD)
+ @EnforcePermission(android.Manifest.permission.DISABLE_KEYGUARD)
/**
* @see android.app.KeyguardManager#exitKeyguardSecurely
*/
@@ -4513,7 +4518,7 @@
}
}
- @android.annotation.EnforcePermission(android.Manifest.permission.MANAGE_APP_TOKENS)
+ @EnforcePermission(android.Manifest.permission.MANAGE_APP_TOKENS)
@Override
public SurfaceControl addShellRoot(int displayId, IWindow client,
@WindowManager.ShellRootLayer int shellRootLayer) {
@@ -4532,7 +4537,7 @@
}
}
- @android.annotation.EnforcePermission(android.Manifest.permission.MANAGE_APP_TOKENS)
+ @EnforcePermission(android.Manifest.permission.MANAGE_APP_TOKENS)
@Override
public void setShellRootAccessibilityWindow(int displayId,
@WindowManager.ShellRootLayer int shellRootLayer, IWindow target) {
@@ -4555,7 +4560,7 @@
}
}
- @android.annotation.EnforcePermission(android.Manifest.permission.MANAGE_APP_TOKENS)
+ @EnforcePermission(android.Manifest.permission.MANAGE_APP_TOKENS)
@Override
public void setDisplayWindowInsetsController(
int displayId, IDisplayWindowInsetsController insetsController) {
@@ -4574,7 +4579,7 @@
}
}
- @android.annotation.EnforcePermission(android.Manifest.permission.MANAGE_APP_TOKENS)
+ @EnforcePermission(android.Manifest.permission.MANAGE_APP_TOKENS)
@Override
public void updateDisplayWindowRequestedVisibleTypes(
int displayId, @InsetsType int requestedVisibleTypes) {
@@ -5834,7 +5839,7 @@
}
}
- @android.annotation.EnforcePermission(android.Manifest.permission.WRITE_SECURE_SETTINGS)
+ @EnforcePermission(android.Manifest.permission.WRITE_SECURE_SETTINGS)
@Override
public void setForcedDisplaySize(int displayId, int width, int height) {
setForcedDisplaySize_enforcePermission();
@@ -5852,7 +5857,7 @@
}
}
- @android.annotation.EnforcePermission(android.Manifest.permission.WRITE_SECURE_SETTINGS)
+ @EnforcePermission(android.Manifest.permission.WRITE_SECURE_SETTINGS)
@Override
public void setForcedDisplayScalingMode(int displayId, int mode) {
setForcedDisplayScalingMode_enforcePermission();
@@ -5940,7 +5945,7 @@
return changed;
}
- @android.annotation.EnforcePermission(android.Manifest.permission.WRITE_SECURE_SETTINGS)
+ @EnforcePermission(android.Manifest.permission.WRITE_SECURE_SETTINGS)
@Override
public void clearForcedDisplaySize(int displayId) {
clearForcedDisplaySize_enforcePermission();
@@ -6003,7 +6008,7 @@
return -1;
}
- @android.annotation.EnforcePermission(android.Manifest.permission.WRITE_SECURE_SETTINGS)
+ @EnforcePermission(android.Manifest.permission.WRITE_SECURE_SETTINGS)
@Override
public void setForcedDisplayDensityForUser(int displayId, int density, int userId) {
setForcedDisplayDensityForUser_enforcePermission();
@@ -6029,7 +6034,7 @@
}
}
- @android.annotation.EnforcePermission(android.Manifest.permission.WRITE_SECURE_SETTINGS)
+ @EnforcePermission(android.Manifest.permission.WRITE_SECURE_SETTINGS)
@Override
public void clearForcedDisplayDensityForUser(int displayId, int userId) {
clearForcedDisplayDensityForUser_enforcePermission();
@@ -6529,7 +6534,7 @@
}
}
- @android.annotation.EnforcePermission(android.Manifest.permission.STATUS_BAR)
+ @EnforcePermission(android.Manifest.permission.STATUS_BAR)
public void setNavBarVirtualKeyHapticFeedbackEnabled(boolean enabled) {
setNavBarVirtualKeyHapticFeedbackEnabled_enforcePermission();
@@ -6571,7 +6576,7 @@
}
}
- @android.annotation.EnforcePermission(android.Manifest.permission.RESTRICTED_VR_ACCESS)
+ @EnforcePermission(android.Manifest.permission.RESTRICTED_VR_ACCESS)
@Override
public Region getCurrentImeTouchRegion() {
getCurrentImeTouchRegion_enforcePermission();
@@ -8461,12 +8466,13 @@
SurfaceControlViewHost.SurfacePackage overlay) {
if (overlay == null) {
throw new IllegalArgumentException("Invalid overlay passed in for task=" + taskId);
- } else if (overlay.getSurfaceControl() == null
- || !overlay.getSurfaceControl().isValid()) {
- throw new IllegalArgumentException(
- "Invalid overlay surfacecontrol passed in for task=" + taskId);
}
synchronized (mGlobalLock) {
+ if (overlay.getSurfaceControl() == null
+ || !overlay.getSurfaceControl().isValid()) {
+ throw new IllegalArgumentException(
+ "Invalid overlay surfacecontrol passed in for task=" + taskId);
+ }
final Task task = mRoot.getRootTask(taskId);
if (task == null) {
throw new IllegalArgumentException("no task with taskId" + taskId);
@@ -8627,6 +8633,24 @@
}
}
}
+
+ @Override
+ public boolean moveFocusToTopEmbeddedWindowIfNeeded() {
+ synchronized (mGlobalLock) {
+ final WindowState focusedWindow = getFocusedWindow();
+ if (focusedWindow == null) {
+ return false;
+ }
+
+ if (moveFocusToTopEmbeddedWindow(focusedWindow)) {
+ // Sync the input transactions to ensure the input focus updates as well.
+ syncInputTransactions(false);
+ return true;
+ }
+
+ return false;
+ }
+ }
}
private final class ImeTargetVisibilityPolicyImpl extends ImeTargetVisibilityPolicy {
@@ -9160,18 +9184,48 @@
win.mClient);
}
- boolean moveFocusToAdjacentWindow(Session session, IWindow fromWindow,
- @FocusDirection int direction) {
- synchronized (mGlobalLock) {
- final WindowState fromWin = windowForClientLocked(session, fromWindow, false);
- if (fromWin == null || !fromWin.isFocused()) {
- return false;
- }
- return moveFocusToAdjacentWindow(fromWin, direction);
+ /**
+ * Move focus to the top embedded window if possible.
+ */
+ boolean moveFocusToTopEmbeddedWindow(@NonNull WindowState focusedWindow) {
+ final TaskFragment taskFragment = focusedWindow.getTaskFragment();
+ if (taskFragment == null) {
+ // Skip if not an Activity window.
+ return false;
}
+
+ if (!Flags.embeddedActivityBackNavFlag()) {
+ // Skip if flag is not enabled.
+ return false;
+ }
+
+ final ActivityRecord topActivity =
+ taskFragment.getTask().topRunningActivity(true /* focusableOnly */);
+ if (topActivity == null || topActivity == focusedWindow.mActivityRecord) {
+ // Skip if the focused activity is already the top-most activity on the Task.
+ return false;
+ }
+
+ if (!topActivity.isEmbedded()) {
+ // Skip if the top activity is not embedded
+ return false;
+ }
+
+ final TaskFragment topTaskFragment = topActivity.getTaskFragment();
+ if (topTaskFragment.isIsolatedNav()
+ && taskFragment.getAdjacentTaskFragment() == topTaskFragment) {
+ // Skip if the top TaskFragment is adjacent to current focus and is set to isolated nav.
+ return false;
+ }
+
+ moveFocusToActivity(topActivity);
+ return !focusedWindow.isFocused();
}
- boolean moveFocusToAdjacentWindow(WindowState fromWin, @FocusDirection int direction) {
+ boolean moveFocusToAdjacentWindow(@NonNull WindowState fromWin, @FocusDirection int direction) {
+ if (!fromWin.isFocused()) {
+ return false;
+ }
final TaskFragment fromFragment = fromWin.getTaskFragment();
if (fromFragment == null) {
return false;
@@ -9220,12 +9274,14 @@
if (topRunningActivity == null) {
return false;
}
- moveDisplayToTopInternal(topRunningActivity.getDisplayId());
- handleTaskFocusChange(topRunningActivity.getTask(), topRunningActivity);
- if (fromWin.isFocused()) {
- return false;
- }
- return true;
+ moveFocusToActivity(topRunningActivity);
+ return !fromWin.isFocused();
+ }
+
+ @VisibleForTesting
+ void moveFocusToActivity(@NonNull ActivityRecord activity) {
+ moveDisplayToTopInternal(activity.getDisplayId());
+ handleTaskFocusChange(activity.getTask(), activity);
}
/** Return whether layer tracing is enabled */
@@ -9949,13 +10005,17 @@
mTrustedPresentationListenerController.unregisterListener(listener, id);
}
+ @EnforcePermission(android.Manifest.permission.DETECT_SCREEN_RECORDING)
@Override
public boolean registerScreenRecordingCallback(IScreenRecordingCallback callback) {
+ registerScreenRecordingCallback_enforcePermission();
return mScreenRecordingCallbackController.register(callback);
}
+ @EnforcePermission(android.Manifest.permission.DETECT_SCREEN_RECORDING)
@Override
public void unregisterScreenRecordingCallback(IScreenRecordingCallback callback) {
+ unregisterScreenRecordingCallback_enforcePermission();
mScreenRecordingCallbackController.unregister(callback);
}
diff --git a/services/core/java/com/android/server/wm/WindowProcessController.java b/services/core/java/com/android/server/wm/WindowProcessController.java
index 6d2e8cc..6acf1f3 100644
--- a/services/core/java/com/android/server/wm/WindowProcessController.java
+++ b/services/core/java/com/android/server/wm/WindowProcessController.java
@@ -28,6 +28,7 @@
import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_CONFIGURATION;
import static com.android.internal.util.Preconditions.checkArgument;
import static com.android.server.am.ProcessList.INVALID_ADJ;
+import static com.android.server.grammaticalinflection.GrammaticalInflectionUtils.checkSystemGrammaticalGenderPermission;
import static com.android.server.wm.ActivityRecord.State.DESTROYED;
import static com.android.server.wm.ActivityRecord.State.DESTROYING;
import static com.android.server.wm.ActivityRecord.State.PAUSED;
@@ -298,6 +299,8 @@
*/
private volatile int mActivityStateFlags = ACTIVITY_STATE_FLAG_MASK_MIN_TASK_LAYER;
+ private boolean mCanUseSystemGrammaticalGender;
+
public WindowProcessController(@NonNull ActivityTaskManagerService atm,
@NonNull ApplicationInfo info, String name, int uid, int userId, Object owner,
@NonNull WindowProcessListener listener) {
@@ -319,6 +322,9 @@
mIsActivityConfigOverrideAllowed = false;
}
+ mCanUseSystemGrammaticalGender = mAtm.mGrammaticalManagerInternal != null
+ && mAtm.mGrammaticalManagerInternal.canGetSystemGrammaticalGender(mUid,
+ mInfo.packageName);
onConfigurationChanged(atm.getGlobalConfiguration());
mAtm.mPackageConfigPersister.updateConfigIfNeeded(this, mUserId, mInfo.packageName);
}
@@ -1568,6 +1574,11 @@
return;
}
+ if (mCanUseSystemGrammaticalGender) {
+ config.setGrammaticalGender(
+ mAtm.mGrammaticalManagerInternal.getSystemGrammaticalGender(mUserId));
+ }
+
if (mPauseConfigurationDispatchCount > 0) {
mHasPendingConfigurationChange = true;
return;
diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java
index 24e50c5..68dade0 100644
--- a/services/core/java/com/android/server/wm/WindowState.java
+++ b/services/core/java/com/android/server/wm/WindowState.java
@@ -1148,10 +1148,6 @@
parentWindow.addChild(this, sWindowSubLayerComparator);
}
- if (token.mRoundedCornerOverlay) {
- mWmService.mTrustedPresentationListenerController.addIgnoredWindowTokens(
- getWindowToken());
- }
}
@Override
@@ -1163,6 +1159,9 @@
if (secureWindowState()) {
getPendingTransaction().setSecure(mSurfaceControl, isSecureLocked());
}
+ // All apps should be considered as occluding when computing TrustedPresentation Thresholds.
+ final boolean canOccludePresentation = !mSession.mCanAddInternalSystemWindow;
+ getPendingTransaction().setCanOccludePresentation(mSurfaceControl, canOccludePresentation);
}
void updateTrustedOverlay() {
@@ -2344,9 +2343,6 @@
mSession.onWindowRemoved(this);
mWmService.postWindowRemoveCleanupLocked(this);
- mWmService.mTrustedPresentationListenerController.removeIgnoredWindowTokens(
- getWindowToken());
-
consumeInsetsChange();
}
@@ -3024,8 +3020,10 @@
return false;
}
if (doAnimation) {
- mWinAnimator.applyAnimationLocked(TRANSIT_EXIT, false);
- if (!isAnimating(TRANSITION | PARENTS)) {
+ // If a hide animation is applied, then let onAnimationFinished
+ // -> checkPolicyVisibilityChange hide the window. Otherwise make doAnimation false
+ // to commit invisible immediately.
+ if (!mWinAnimator.applyAnimationLocked(TRANSIT_EXIT, false /* isEntrance */)) {
doAnimation = false;
}
}
diff --git a/services/core/java/com/android/server/wm/WindowStateAnimator.java b/services/core/java/com/android/server/wm/WindowStateAnimator.java
index 44cd23d..6428591 100644
--- a/services/core/java/com/android/server/wm/WindowStateAnimator.java
+++ b/services/core/java/com/android/server/wm/WindowStateAnimator.java
@@ -509,8 +509,10 @@
// We don't apply animation for application main window here since this window type
// should be controlled by ActivityRecord in general. Wallpaper is also excluded because
- // WallpaperController should handle it.
- if (mAttrType != TYPE_BASE_APPLICATION && !mIsWallpaper) {
+ // WallpaperController should handle it. Also skip play enter animation for the window
+ // below starting window.
+ if (mAttrType != TYPE_BASE_APPLICATION && !mIsWallpaper
+ && !(mWin.mActivityRecord != null && mWin.mActivityRecord.hasStartingWindow())) {
applyAnimationLocked(transit, true);
}
diff --git a/services/core/jni/OWNERS b/services/core/jni/OWNERS
index cc08488..df7fb99 100644
--- a/services/core/jni/OWNERS
+++ b/services/core/jni/OWNERS
@@ -31,5 +31,8 @@
per-file com_android_server_am_CachedAppOptimizer.cpp = timmurray@google.com, edgararriaga@google.com, dualli@google.com, carmenjackson@google.com, philipcuadra@google.com
per-file com_android_server_companion_virtual_InputController.cpp = file:/services/companion/java/com/android/server/companion/virtual/OWNERS
+# Memory
+per-file com_android_server_am_OomConnection.cpp = file:/MEMORY_OWNERS
+
# Bug component : 158088 = per-file *AnrTimer*
per-file *AnrTimer* = file:/PERFORMANCE_OWNERS
diff --git a/services/core/jni/com_android_server_am_OomConnection.cpp b/services/core/jni/com_android_server_am_OomConnection.cpp
index e892d23..49a3ad3 100644
--- a/services/core/jni/com_android_server_am_OomConnection.cpp
+++ b/services/core/jni/com_android_server_am_OomConnection.cpp
@@ -22,13 +22,15 @@
namespace android {
+using namespace ::android::bpf::memevents;
+
// Used to cache the results of the JNI name lookup
static struct {
jclass clazz;
jmethodID ctor;
} sOomKillRecordInfo;
-static memevents::MemEventListener memevent_listener;
+static MemEventListener memevent_listener(MemEventClient::AMS);
/**
* Initialize listening and waiting for new out-of-memory (OOM) events to occur.
@@ -42,25 +44,20 @@
* @throws java.lang.RuntimeException
*/
static jobjectArray android_server_am_OomConnection_waitOom(JNIEnv* env, jobject) {
- const memevents::MemEvent oom_event = memevents::MemEvent::OOM_KILL;
- if (!memevent_listener.registerEvent(oom_event)) {
+ if (!memevent_listener.registerEvent(MEM_EVENT_OOM_KILL)) {
memevent_listener.deregisterAllEvents();
jniThrowRuntimeException(env, "listener failed to register to OOM events");
return nullptr;
}
- memevents::MemEvent event_received;
- do {
- event_received = memevent_listener.listen();
- if (event_received == memevents::MemEvent::ERROR) {
- memevent_listener.deregisterAllEvents();
- jniThrowRuntimeException(env, "listener received error event");
- return nullptr;
- }
- } while (event_received != oom_event);
+ if (!memevent_listener.listen()) {
+ memevent_listener.deregisterAllEvents();
+ jniThrowRuntimeException(env, "listener failed waiting for OOM event");
+ return nullptr;
+ }
- std::vector<memevents::OomKill> oom_events;
- if (!memevent_listener.getOomEvents(oom_events)) {
+ std::vector<mem_event_t> oom_events;
+ if (!memevent_listener.getMemEvents(oom_events)) {
memevent_listener.deregisterAllEvents();
jniThrowRuntimeException(env, "Failed to get OOM events");
return nullptr;
@@ -75,15 +72,23 @@
}
for (int i = 0; i < oom_events.size(); i++) {
- const memevents::OomKill oom_event = oom_events[i];
- jstring process_name = env->NewStringUTF(oom_event.process_name);
+ const mem_event_t mem_event = oom_events[i];
+ if (mem_event.type != MEM_EVENT_OOM_KILL) {
+ memevent_listener.deregisterAllEvents();
+ jniThrowRuntimeException(env, "Received invalid memory event");
+ return java_oom_array;
+ }
+
+ const auto oom_kill = mem_event.event_data.oom_kill;
+
+ jstring process_name = env->NewStringUTF(oom_kill.process_name);
if (process_name == NULL) {
memevent_listener.deregisterAllEvents();
jniThrowRuntimeException(env, "Failed creating java string for process name");
}
jobject java_oom_kill = env->NewObject(sOomKillRecordInfo.clazz, sOomKillRecordInfo.ctor,
- oom_event.timestamp_ms, oom_event.pid, oom_event.uid,
- process_name, oom_event.oom_score_adj);
+ oom_kill.timestamp_ms, oom_kill.pid, oom_kill.uid,
+ process_name, oom_kill.oom_score_adj);
if (java_oom_kill == NULL) {
memevent_listener.deregisterAllEvents();
jniThrowRuntimeException(env, "Failed to create OomKillRecord object");
diff --git a/services/core/jni/com_android_server_input_InputManagerService.cpp b/services/core/jni/com_android_server_input_InputManagerService.cpp
index 4a6b31c..810090a 100644
--- a/services/core/jni/com_android_server_input_InputManagerService.cpp
+++ b/services/core/jni/com_android_server_input_InputManagerService.cpp
@@ -285,6 +285,7 @@
void setInputDispatchMode(bool enabled, bool frozen);
void setSystemUiLightsOut(bool lightsOut);
void setPointerDisplayId(int32_t displayId);
+ int32_t getMousePointerSpeed();
void setPointerSpeed(int32_t speed);
void setMousePointerAccelerationEnabled(int32_t displayId, bool enabled);
void setTouchpadPointerSpeed(int32_t speed);
@@ -1219,6 +1220,11 @@
}
}
+int32_t NativeInputManager::getMousePointerSpeed() {
+ std::scoped_lock _l(mLock);
+ return mLocked.pointerSpeed;
+}
+
void NativeInputManager::setPointerSpeed(int32_t speed) {
{ // acquire lock
std::scoped_lock _l(mLock);
@@ -2211,6 +2217,12 @@
}
}
+static jint nativeGetMousePointerSpeed(JNIEnv* env, jobject nativeImplObj) {
+ NativeInputManager* im = getNativeInputManager(env, nativeImplObj);
+
+ return static_cast<jint>(im->getMousePointerSpeed());
+}
+
static void nativeSetPointerSpeed(JNIEnv* env, jobject nativeImplObj, jint speed) {
NativeInputManager* im = getNativeInputManager(env, nativeImplObj);
@@ -2865,6 +2877,7 @@
{"transferTouchFocus", "(Landroid/os/IBinder;Landroid/os/IBinder;Z)Z",
(void*)nativeTransferTouchFocus},
{"transferTouch", "(Landroid/os/IBinder;I)Z", (void*)nativeTransferTouch},
+ {"getMousePointerSpeed", "()I", (void*)nativeGetMousePointerSpeed},
{"setPointerSpeed", "(I)V", (void*)nativeSetPointerSpeed},
{"setMousePointerAccelerationEnabled", "(IZ)V",
(void*)nativeSetMousePointerAccelerationEnabled},
diff --git a/services/credentials/java/com/android/server/credentials/ClearRequestSession.java b/services/credentials/java/com/android/server/credentials/ClearRequestSession.java
index b1349ea..f5ba50d 100644
--- a/services/credentials/java/com/android/server/credentials/ClearRequestSession.java
+++ b/services/credentials/java/com/android/server/credentials/ClearRequestSession.java
@@ -27,6 +27,7 @@
import android.credentials.selection.RequestInfo;
import android.os.CancellationSignal;
import android.os.RemoteException;
+import android.os.ResultReceiver;
import android.service.credentials.CallingAppInfo;
import android.util.Slog;
@@ -149,7 +150,7 @@
}
@Override
- public void onUiCancellation(boolean isUserCancellation) {
+ public void onUiCancellation(boolean isUserCancellation, ResultReceiver resultReceiver) {
// Not needed since UI is not involved
}
diff --git a/services/credentials/java/com/android/server/credentials/CreateRequestSession.java b/services/credentials/java/com/android/server/credentials/CreateRequestSession.java
index 3dcf42d..be4b9e1 100644
--- a/services/credentials/java/com/android/server/credentials/CreateRequestSession.java
+++ b/services/credentials/java/com/android/server/credentials/CreateRequestSession.java
@@ -31,6 +31,7 @@
import android.credentials.selection.RequestInfo;
import android.os.CancellationSignal;
import android.os.RemoteException;
+import android.os.ResultReceiver;
import android.service.credentials.CallingAppInfo;
import android.service.credentials.PermissionUtils;
import android.util.Slog;
@@ -103,14 +104,16 @@
flattenedPrimaryProviders.add(cn.flattenToString());
}
+ final boolean isShowAllOptionsRequested = false;
mPendingIntent = mCredentialManagerUi.createPendingIntent(
RequestInfo.newCreateRequestInfo(
mRequestId, mClientRequest,
mClientAppInfo.getPackageName(),
PermissionUtils.hasPermission(mContext, mClientAppInfo.getPackageName(),
Manifest.permission.CREDENTIAL_MANAGER_SET_ALLOWED_PROVIDERS),
- /*defaultProviderId=*/flattenedPrimaryProviders),
- providerDataList, /*isRequestForAllOptions=*/ false);
+ /*defaultProviderId=*/flattenedPrimaryProviders,
+ isShowAllOptionsRequested),
+ providerDataList, /*isRequestForAllOptions=*/ isShowAllOptionsRequested);
mClientCallback.onPendingIntent(mPendingIntent);
} catch (RemoteException e) {
mRequestSessionMetric.collectUiReturnedFinalPhase(/*uiReturned=*/ false);
@@ -161,7 +164,7 @@
}
@Override
- public void onUiCancellation(boolean isUserCancellation) {
+ public void onUiCancellation(boolean isUserCancellation, ResultReceiver resultReceiver) {
String exception = CreateCredentialException.TYPE_USER_CANCELED;
String message = "User cancelled the selector";
if (!isUserCancellation) {
diff --git a/services/credentials/java/com/android/server/credentials/CredentialManagerUi.java b/services/credentials/java/com/android/server/credentials/CredentialManagerUi.java
index 4203576..534c842 100644
--- a/services/credentials/java/com/android/server/credentials/CredentialManagerUi.java
+++ b/services/credentials/java/com/android/server/credentials/CredentialManagerUi.java
@@ -15,6 +15,8 @@
*/
package com.android.server.credentials;
+import static android.credentials.selection.Constants.EXTRA_FINAL_RESPONSE_RECEIVER;
+
import android.annotation.NonNull;
import android.app.PendingIntent;
import android.content.ComponentName;
@@ -34,7 +36,6 @@
import android.os.ResultReceiver;
import android.os.UserHandle;
import android.service.credentials.CredentialProviderInfoFactory;
-import android.util.Slog;
import java.util.ArrayList;
import java.util.HashSet;
@@ -79,20 +80,25 @@
UserSelectionDialogResult selection = UserSelectionDialogResult
.fromResultData(resultData);
if (selection != null) {
- mCallbacks.onUiSelection(selection);
- } else {
- Slog.i(TAG, "No selection found in UI result");
+ ResultReceiver resultReceiver = resultData.getParcelable(
+ EXTRA_FINAL_RESPONSE_RECEIVER,
+ ResultReceiver.class);
+ mCallbacks.onUiSelection(selection, resultReceiver);
}
break;
case UserSelectionDialogResult.RESULT_CODE_DIALOG_USER_CANCELED:
mStatus = UiStatus.TERMINATED;
- mCallbacks.onUiCancellation(/* isUserCancellation= */ true);
+ mCallbacks.onUiCancellation(/* isUserCancellation= */ true,
+ resultData.getParcelable(EXTRA_FINAL_RESPONSE_RECEIVER,
+ ResultReceiver.class));
break;
case UserSelectionDialogResult.RESULT_CODE_CANCELED_AND_LAUNCHED_SETTINGS:
mStatus = UiStatus.TERMINATED;
- mCallbacks.onUiCancellation(/* isUserCancellation= */ false);
+ mCallbacks.onUiCancellation(/* isUserCancellation= */ false,
+ resultData.getParcelable(EXTRA_FINAL_RESPONSE_RECEIVER,
+ ResultReceiver.class));
break;
case UserSelectionDialogResult.RESULT_CODE_DATA_PARSING_FAILURE:
mStatus = UiStatus.TERMINATED;
@@ -116,10 +122,10 @@
*/
public interface CredentialManagerUiCallback {
/** Called when the user makes a selection. */
- void onUiSelection(UserSelectionDialogResult selection);
+ void onUiSelection(UserSelectionDialogResult selection, ResultReceiver resultReceiver);
/** Called when the UI is canceled without a successful provider result. */
- void onUiCancellation(boolean isUserCancellation);
+ void onUiCancellation(boolean isUserCancellation, ResultReceiver resultReceiver);
/** Called when the selector UI fails to come up (mostly due to parsing issue today). */
void onUiSelectorInvocationFailure();
@@ -148,8 +154,8 @@
* Creates a {@link PendingIntent} to be used to invoke the credential manager selector UI,
* by the calling app process.
*
- * @param requestInfo the information about the request
- * @param providerDataList the list of provider data from remote providers
+ * @param requestInfo the information about the request
+ * @param providerDataList the list of provider data from remote providers
* @param isRequestForAllOptions whether the bottom sheet should directly navigate to the
* all options page
*/
@@ -170,7 +176,8 @@
.map(disabledProvider -> new DisabledProviderData(
disabledProvider.getComponentName().flattenToString())).toList();
- Intent intent = IntentFactory.createCredentialSelectorIntent(requestInfo, providerDataList,
+ Intent intent = IntentFactory.createCredentialSelectorIntent(mContext, requestInfo,
+ providerDataList,
new ArrayList<>(disabledProviderDataList), mResultReceiver,
isRequestForAllOptions)
.setAction(UUID.randomUUID().toString());
@@ -178,7 +185,7 @@
// intents
return PendingIntent.getActivityAsUser(
mContext, /*requestCode=*/0, intent,
- PendingIntent.FLAG_IMMUTABLE, /*options=*/null,
+ PendingIntent.FLAG_MUTABLE, /*options=*/null,
UserHandle.of(mUserId));
}
}
diff --git a/services/credentials/java/com/android/server/credentials/GetCandidateRequestSession.java b/services/credentials/java/com/android/server/credentials/GetCandidateRequestSession.java
index 7e709fe..adb1b72 100644
--- a/services/credentials/java/com/android/server/credentials/GetCandidateRequestSession.java
+++ b/services/credentials/java/com/android/server/credentials/GetCandidateRequestSession.java
@@ -20,6 +20,7 @@
import android.annotation.Nullable;
import android.content.ComponentName;
import android.content.Context;
+import android.credentials.Constants;
import android.credentials.CredentialProviderInfo;
import android.credentials.GetCandidateCredentialsException;
import android.credentials.GetCandidateCredentialsResponse;
@@ -29,10 +30,13 @@
import android.credentials.selection.GetCredentialProviderData;
import android.credentials.selection.ProviderData;
import android.credentials.selection.RequestInfo;
+import android.os.Bundle;
import android.os.CancellationSignal;
import android.os.IBinder;
import android.os.RemoteException;
+import android.os.ResultReceiver;
import android.service.credentials.CallingAppInfo;
+import android.service.credentials.CredentialProviderService;
import android.service.credentials.PermissionUtils;
import android.util.Slog;
@@ -111,13 +115,15 @@
}
cancelExistingPendingIntent();
+ final boolean isShowAllOptionsRequested = true;
mPendingIntent = mCredentialManagerUi.createPendingIntent(
RequestInfo.newGetRequestInfo(
mRequestId, mClientRequest, mClientAppInfo.getPackageName(),
PermissionUtils.hasPermission(mContext, mClientAppInfo.getPackageName(),
- Manifest.permission.CREDENTIAL_MANAGER_SET_ALLOWED_PROVIDERS)),
- providerDataList,
- /*isRequestForAllOptions=*/ true);
+ Manifest.permission.CREDENTIAL_MANAGER_SET_ALLOWED_PROVIDERS),
+ isShowAllOptionsRequested),
+ /*providerDataList=*/ null,
+ /*isRequestForAllOptions=*/ isShowAllOptionsRequested);
List<GetCredentialProviderData> candidateProviderDataList = new ArrayList<>();
for (ProviderData providerData : providerDataList) {
@@ -151,7 +157,8 @@
}
@Override
- public void onUiCancellation(boolean isUserCancellation) {
+ public void onUiCancellation(boolean isUserCancellation,
+ @Nullable ResultReceiver finalResponseReceiver) {
String exception = GetCandidateCredentialsException.TYPE_USER_CANCELED;
String message = "User cancelled the selector";
if (!isUserCancellation) {
@@ -159,7 +166,12 @@
message = "The UI was interrupted - please try again.";
}
mRequestSessionMetric.collectFrameworkException(exception);
- respondToClientWithErrorAndFinish(exception, message);
+ if (finalResponseReceiver != null) {
+ Bundle resultData = new Bundle();
+ finalResponseReceiver.send(Constants.FAILURE_CREDMAN_SELECTOR, resultData);
+ } else {
+ respondToClientWithErrorAndFinish(exception, message);
+ }
}
@Override
@@ -195,7 +207,16 @@
public void onFinalResponseReceived(ComponentName componentName,
GetCredentialResponse response) {
Slog.d(TAG, "onFinalResponseReceived");
- respondToClientWithResponseAndFinish(new GetCandidateCredentialsResponse(response));
+ if (this.mFinalResponseReceiver != null) {
+ Slog.d(TAG, "onFinalResponseReceived sending through final receiver");
+ Bundle resultData = new Bundle();
+ resultData.putParcelable(
+ CredentialProviderService.EXTRA_GET_CREDENTIAL_RESPONSE, response);
+ mFinalResponseReceiver.send(Constants.SUCCESS_CREDMAN_SELECTOR, resultData);
+ finishSession(/*propagateCancellation=*/ false);
+ } else {
+ Slog.w(TAG, "onFinalResponseReceived result receiver not found for pinned entry");
+ }
}
/**
@@ -210,6 +231,5 @@
*/
public int getAutofillRequestId() {
return mAutofillRequestId;
-
}
}
diff --git a/services/credentials/java/com/android/server/credentials/GetRequestSession.java b/services/credentials/java/com/android/server/credentials/GetRequestSession.java
index b33f531..a279337 100644
--- a/services/credentials/java/com/android/server/credentials/GetRequestSession.java
+++ b/services/credentials/java/com/android/server/credentials/GetRequestSession.java
@@ -31,6 +31,7 @@
import android.os.Binder;
import android.os.CancellationSignal;
import android.os.RemoteException;
+import android.os.ResultReceiver;
import android.service.credentials.CallingAppInfo;
import android.service.credentials.PermissionUtils;
import android.util.Slog;
@@ -102,15 +103,17 @@
Binder.withCleanCallingIdentity(() -> {
try {
cancelExistingPendingIntent();
+ final boolean isShowAllOptionsRequested = false;
mPendingIntent = mCredentialManagerUi.createPendingIntent(
RequestInfo.newGetRequestInfo(
mRequestId, mClientRequest, mClientAppInfo.getPackageName(),
PermissionUtils.hasPermission(mContext,
mClientAppInfo.getPackageName(),
Manifest.permission
- .CREDENTIAL_MANAGER_SET_ALLOWED_PROVIDERS)),
+ .CREDENTIAL_MANAGER_SET_ALLOWED_PROVIDERS),
+ isShowAllOptionsRequested),
providerDataList,
- /*isRequestForAllOptions=*/ false);
+ /*isRequestForAllOptions=*/ isShowAllOptionsRequested);
mClientCallback.onPendingIntent(mPendingIntent);
} catch (RemoteException e) {
mRequestSessionMetric.collectUiReturnedFinalPhase(/*uiReturned=*/ false);
@@ -163,7 +166,7 @@
}
@Override
- public void onUiCancellation(boolean isUserCancellation) {
+ public void onUiCancellation(boolean isUserCancellation, ResultReceiver resultReceiver) {
String exception = GetCredentialException.TYPE_USER_CANCELED;
String message = "User cancelled the selector";
if (!isUserCancellation) {
diff --git a/services/credentials/java/com/android/server/credentials/PrepareGetRequestSession.java b/services/credentials/java/com/android/server/credentials/PrepareGetRequestSession.java
index 30af567..6b313fd 100644
--- a/services/credentials/java/com/android/server/credentials/PrepareGetRequestSession.java
+++ b/services/credentials/java/com/android/server/credentials/PrepareGetRequestSession.java
@@ -187,12 +187,14 @@
}
}
if (!providerDataList.isEmpty()) {
+ final boolean isShowAllOptionsRequested = false;
return mCredentialManagerUi.createPendingIntent(
RequestInfo.newGetRequestInfo(
mRequestId, mClientRequest, mClientAppInfo.getPackageName(),
PermissionUtils.hasPermission(mContext, mClientAppInfo.getPackageName(),
- Manifest.permission.CREDENTIAL_MANAGER_SET_ALLOWED_PROVIDERS)),
- providerDataList, /*isRequestForAllOptions=*/ false);
+ Manifest.permission.CREDENTIAL_MANAGER_SET_ALLOWED_PROVIDERS),
+ isShowAllOptionsRequested),
+ providerDataList, /*isRequestForAllOptions=*/ isShowAllOptionsRequested);
} else {
return null;
}
diff --git a/services/credentials/java/com/android/server/credentials/ProviderGetSession.java b/services/credentials/java/com/android/server/credentials/ProviderGetSession.java
index fcaef9f..ca23d62 100644
--- a/services/credentials/java/com/android/server/credentials/ProviderGetSession.java
+++ b/services/credentials/java/com/android/server/credentials/ProviderGetSession.java
@@ -30,6 +30,7 @@
import android.credentials.selection.Entry;
import android.credentials.selection.GetCredentialProviderData;
import android.credentials.selection.ProviderPendingIntentResponse;
+import android.os.Bundle;
import android.os.ICancellationSignal;
import android.service.autofill.Flags;
import android.service.credentials.Action;
@@ -76,6 +77,9 @@
@NonNull
private final Map<String, CredentialOption> mBeginGetOptionToCredentialOptionMap;
+ @NonNull
+ private final Map<String, AutofillId> mCredentialEntryKeyToAutofilLIdMap;
+
/** The complete request to be used in the second round. */
private final android.credentials.GetCredentialRequest mCompleteRequest;
@@ -245,6 +249,7 @@
mBeginGetOptionToCredentialOptionMap = new HashMap<>(beginGetOptionToCredentialOptionMap);
mProviderResponseDataHandler = new ProviderResponseDataHandler(
ComponentName.unflattenFromString(hybridService));
+ mCredentialEntryKeyToAutofilLIdMap = new HashMap<>();
}
/** Called when the provider response has been updated by an external source. */
@@ -298,7 +303,7 @@
invokeCallbackOnInternalInvalidState();
return;
}
- onCredentialEntrySelected(providerPendingIntentResponse);
+ onCredentialEntrySelected(providerPendingIntentResponse, entryKey);
break;
case ACTION_ENTRY_KEY:
Action actionEntry = mProviderResponseDataHandler.getActionEntry(entryKey);
@@ -307,7 +312,7 @@
invokeCallbackOnInternalInvalidState();
return;
}
- onActionEntrySelected(providerPendingIntentResponse);
+ onActionEntrySelected(providerPendingIntentResponse, entryKey);
break;
case AUTHENTICATION_ACTION_ENTRY_KEY:
Action authenticationEntry = mProviderResponseDataHandler
@@ -337,7 +342,7 @@
break;
case REMOTE_ENTRY_KEY:
if (mProviderResponseDataHandler.getRemoteEntry(entryKey) != null) {
- onRemoteEntrySelected(providerPendingIntentResponse);
+ onRemoteEntrySelected(providerPendingIntentResponse, entryKey);
} else {
Slog.i(TAG, "Unexpected remote entry key");
invokeCallbackOnInternalInvalidState();
@@ -376,7 +381,7 @@
return null;
}
- private Intent setUpFillInIntentWithFinalRequest(@NonNull String id) {
+ private Intent setUpFillInIntentWithFinalRequest(@NonNull String id, String entryKey) {
// TODO: Determine if we should skip this entry if entry id is not set, or is set
// but does not resolve to a valid option. For now, not skipping it because
// it may be possible that the provider adds their own extras and expects to receive
@@ -392,6 +397,7 @@
.getParcelable(CredentialProviderService.EXTRA_AUTOFILL_ID, AutofillId.class);
if (autofillId != null && Flags.autofillCredmanIntegration()) {
intent.putExtra(CredentialProviderService.EXTRA_AUTOFILL_ID, autofillId);
+ mCredentialEntryKeyToAutofilLIdMap.put(entryKey, autofillId);
}
return intent.putExtra(
CredentialProviderService.EXTRA_GET_CREDENTIAL_REQUEST,
@@ -408,12 +414,13 @@
}
private void onRemoteEntrySelected(
- ProviderPendingIntentResponse providerPendingIntentResponse) {
- onCredentialEntrySelected(providerPendingIntentResponse);
+ ProviderPendingIntentResponse providerPendingIntentResponse, String entryKey) {
+ onCredentialEntrySelected(providerPendingIntentResponse, entryKey);
}
private void onCredentialEntrySelected(
- ProviderPendingIntentResponse providerPendingIntentResponse) {
+ ProviderPendingIntentResponse providerPendingIntentResponse,
+ String entryKey) {
if (providerPendingIntentResponse == null) {
invokeCallbackOnInternalInvalidState();
return;
@@ -430,7 +437,18 @@
GetCredentialResponse getCredentialResponse = PendingIntentResultHandler
.extractGetCredentialResponse(
providerPendingIntentResponse.getResultData());
- if (getCredentialResponse != null) {
+ if (getCredentialResponse != null && getCredentialResponse.getCredential() != null) {
+ Bundle credentialData = getCredentialResponse.getCredential().getData();
+ AutofillId autofillId = mCredentialEntryKeyToAutofilLIdMap.get(entryKey);
+ if (Flags.autofillCredmanIntegration()
+ && entryKey != null && autofillId != null && credentialData != null
+ ) {
+ Slog.d(TAG, "Adding autofillId to credential response: " + autofillId);
+ credentialData.putParcelable(
+ CredentialProviderService.EXTRA_AUTOFILL_ID,
+ mCredentialEntryKeyToAutofilLIdMap.get(entryKey)
+ );
+ }
mCallbacks.onFinalResponseReceived(mComponentName,
getCredentialResponse);
return;
@@ -514,9 +532,9 @@
/** Returns true if either an exception or a response is found. */
private void onActionEntrySelected(ProviderPendingIntentResponse
- providerPendingIntentResponse) {
+ providerPendingIntentResponse, String entryKey) {
Slog.i(TAG, "onActionEntrySelected");
- onCredentialEntrySelected(providerPendingIntentResponse);
+ onCredentialEntrySelected(providerPendingIntentResponse, entryKey);
}
@@ -614,7 +632,7 @@
Entry entry = new Entry(CREDENTIAL_ENTRY_KEY,
id, credentialEntry.getSlice(),
setUpFillInIntentWithFinalRequest(credentialEntry
- .getBeginGetCredentialOptionId()));
+ .getBeginGetCredentialOptionId(), id));
mUiCredentialEntries.put(id, new Pair<>(credentialEntry, entry));
mCredentialEntryTypes.add(credentialEntry.getType());
}
diff --git a/services/credentials/java/com/android/server/credentials/RequestSession.java b/services/credentials/java/com/android/server/credentials/RequestSession.java
index bf7df86..633c9c4 100644
--- a/services/credentials/java/com/android/server/credentials/RequestSession.java
+++ b/services/credentials/java/com/android/server/credentials/RequestSession.java
@@ -17,6 +17,7 @@
package com.android.server.credentials;
import android.annotation.NonNull;
+import android.annotation.Nullable;
import android.annotation.UserIdInt;
import android.app.PendingIntent;
import android.content.ComponentName;
@@ -33,6 +34,7 @@
import android.os.IInterface;
import android.os.Looper;
import android.os.RemoteException;
+import android.os.ResultReceiver;
import android.os.UserHandle;
import android.service.credentials.CallingAppInfo;
import android.util.Slog;
@@ -101,6 +103,9 @@
protected PendingIntent mPendingIntent;
+ @Nullable
+ protected ResultReceiver mFinalResponseReceiver;
+
@NonNull
protected RequestSessionStatus mRequestSessionStatus =
RequestSessionStatus.IN_PROGRESS;
@@ -219,7 +224,8 @@
// UI callbacks
@Override // from CredentialManagerUiCallbacks
- public void onUiSelection(UserSelectionDialogResult selection) {
+ public void onUiSelection(UserSelectionDialogResult selection,
+ ResultReceiver finalResponseReceiver) {
if (mRequestSessionStatus == RequestSessionStatus.COMPLETE) {
Slog.w(TAG, "Request has already been completed. This is strange.");
return;
@@ -234,6 +240,7 @@
Slog.w(TAG, "providerSession not found in onUiSelection. This is strange.");
return;
}
+ mFinalResponseReceiver = finalResponseReceiver;
ProviderSessionMetric providerSessionMetric = providerSession.mProviderSessionMetric;
int initialAuthMetricsProvider = providerSessionMetric.getBrowsedAuthenticationMetric()
.size();
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
index 519c9bb..f87fd8d 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
@@ -34,6 +34,7 @@
import static android.Manifest.permission.MANAGE_DEVICE_POLICY_CAMERA;
import static android.Manifest.permission.MANAGE_DEVICE_POLICY_CERTIFICATES;
import static android.Manifest.permission.MANAGE_DEVICE_POLICY_COMMON_CRITERIA_MODE;
+import static android.Manifest.permission.MANAGE_DEVICE_POLICY_CONTENT_PROTECTION;
import static android.Manifest.permission.MANAGE_DEVICE_POLICY_DEBUGGING_FEATURES;
import static android.Manifest.permission.MANAGE_DEVICE_POLICY_DEFAULT_SMS;
import static android.Manifest.permission.MANAGE_DEVICE_POLICY_DISPLAY;
@@ -110,6 +111,8 @@
import static android.app.admin.DevicePolicyManager.ACTION_PROVISION_MANAGED_PROFILE;
import static android.app.admin.DevicePolicyManager.ACTION_PROVISION_MANAGED_USER;
import static android.app.admin.DevicePolicyManager.ACTION_SYSTEM_UPDATE_POLICY_CHANGED;
+import static android.app.admin.DevicePolicyManager.CONTENT_PROTECTION_DISABLED;
+import static android.app.admin.DevicePolicyManager.ContentProtectionPolicy;
import static android.app.admin.DevicePolicyManager.DELEGATION_APP_RESTRICTIONS;
import static android.app.admin.DevicePolicyManager.DELEGATION_BLOCK_UNINSTALL;
import static android.app.admin.DevicePolicyManager.DELEGATION_CERT_INSTALL;
@@ -220,6 +223,7 @@
import static android.app.admin.ProvisioningException.ERROR_SETTING_PROFILE_OWNER_FAILED;
import static android.app.admin.ProvisioningException.ERROR_SET_DEVICE_OWNER_FAILED;
import static android.app.admin.ProvisioningException.ERROR_STARTING_PROFILE_FAILED;
+import static android.app.admin.flags.Flags.backupServiceSecurityLogEventEnabled;
import static android.app.admin.flags.Flags.dumpsysPolicyEngineMigrationEnabled;
import static android.app.admin.flags.Flags.policyEngineMigrationV2Enabled;
import static android.content.Intent.ACTION_MANAGED_PROFILE_AVAILABLE;
@@ -6012,10 +6016,10 @@
// Make sure the caller has any active admin with the right policy or
// the required permission.
if (isUnicornFlagEnabled()) {
- admin = enforcePermissionAndGetEnforcingAdmin(
+ admin = enforcePermissionsAndGetEnforcingAdmin(
/* admin= */ null,
- /* permission= */ MANAGE_DEVICE_POLICY_LOCK,
- USES_POLICY_FORCE_LOCK,
+ /* permissions= */ new String[]{MANAGE_DEVICE_POLICY_LOCK, LOCK_DEVICE},
+ /* deviceAdminPolicy= */ USES_POLICY_FORCE_LOCK,
caller.getPackageName(),
getAffectedUser(parent)
).getActiveAdmin();
@@ -17926,6 +17930,13 @@
|| isProfileOwner(caller) || isFinancedDeviceOwner(caller));
toggleBackupServiceActive(caller.getUserId(), enabled);
+
+ if (backupServiceSecurityLogEventEnabled()) {
+ if (SecurityLog.isLoggingEnabled()) {
+ SecurityLog.writeEvent(SecurityLog.TAG_BACKUP_SERVICE_TOGGLED,
+ caller.getPackageName(), caller.getUserId(), enabled ? 1 : 0);
+ }
+ }
}
@Override
@@ -23128,6 +23139,10 @@
mInjector.systemPropertiesSet(memtagProperty, "memtag");
} else if (flags == DevicePolicyManager.MTE_DISABLED) {
mInjector.systemPropertiesSet(memtagProperty, "memtag-off");
+ } else if (flags == DevicePolicyManager.MTE_NOT_CONTROLLED_BY_POLICY) {
+ if (admin.mtePolicy != DevicePolicyManager.MTE_NOT_CONTROLLED_BY_POLICY) {
+ mInjector.systemPropertiesSet(memtagProperty, "default");
+ }
}
admin.mtePolicy = flags;
saveSettingsLocked(caller.getUserId());
@@ -23161,6 +23176,90 @@
}
}
+ private EnforcingAdmin enforceCanCallContentProtectionLocked(
+ ComponentName who, String callerPackageName) {
+ CallerIdentity caller = getCallerIdentity(who, callerPackageName);
+ final int userId = caller.getUserId();
+
+ EnforcingAdmin enforcingAdmin = enforcePermissionAndGetEnforcingAdmin(
+ who,
+ MANAGE_DEVICE_POLICY_CONTENT_PROTECTION,
+ caller.getPackageName(),
+ userId
+ );
+ if ((isDeviceOwner(caller) || isProfileOwner(caller))
+ && !canDPCManagedUserUseLockTaskLocked(userId)) {
+ throw new SecurityException(
+ "User " + userId + " is not allowed to use content protection");
+ }
+ return enforcingAdmin;
+ }
+
+ private void enforceCanQueryContentProtectionLocked(
+ ComponentName who, String callerPackageName) {
+ CallerIdentity caller = getCallerIdentity(who, callerPackageName);
+ final int userId = caller.getUserId();
+
+ enforceCanQuery(MANAGE_DEVICE_POLICY_CONTENT_PROTECTION, caller.getPackageName(), userId);
+ if ((isDeviceOwner(caller) || isProfileOwner(caller))
+ && !canDPCManagedUserUseLockTaskLocked(userId)) {
+ throw new SecurityException(
+ "User " + userId + " is not allowed to use content protection");
+ }
+ }
+
+ @Override
+ public void setContentProtectionPolicy(
+ ComponentName who, String callerPackageName, @ContentProtectionPolicy int policy)
+ throws SecurityException {
+ if (!android.view.contentprotection.flags.Flags.manageDevicePolicyEnabled()) {
+ return;
+ }
+
+ CallerIdentity caller = getCallerIdentity(who, callerPackageName);
+ checkCanExecuteOrThrowUnsafe(DevicePolicyManager.OPERATION_SET_CONTENT_PROTECTION_POLICY);
+
+ EnforcingAdmin enforcingAdmin;
+ synchronized (getLockObject()) {
+ enforcingAdmin = enforceCanCallContentProtectionLocked(who, caller.getPackageName());
+ }
+
+ if (policy == CONTENT_PROTECTION_DISABLED) {
+ mDevicePolicyEngine.removeLocalPolicy(
+ PolicyDefinition.CONTENT_PROTECTION,
+ enforcingAdmin,
+ caller.getUserId());
+ } else {
+ mDevicePolicyEngine.setLocalPolicy(
+ PolicyDefinition.CONTENT_PROTECTION,
+ enforcingAdmin,
+ new IntegerPolicyValue(policy),
+ caller.getUserId());
+ }
+ }
+
+ @Override
+ public @ContentProtectionPolicy int getContentProtectionPolicy(
+ ComponentName who, String callerPackageName) {
+ if (!android.view.contentprotection.flags.Flags.manageDevicePolicyEnabled()) {
+ return CONTENT_PROTECTION_DISABLED;
+ }
+
+ CallerIdentity caller = getCallerIdentity(who, callerPackageName);
+ final int userHandle = caller.getUserId();
+
+ synchronized (getLockObject()) {
+ enforceCanQueryContentProtectionLocked(who, caller.getPackageName());
+ }
+ Integer policy = mDevicePolicyEngine.getResolvedPolicy(
+ PolicyDefinition.CONTENT_PROTECTION, userHandle);
+ if (policy == null) {
+ return CONTENT_PROTECTION_DISABLED;
+ } else {
+ return policy;
+ }
+ }
+
@Override
public ManagedSubscriptionsPolicy getManagedSubscriptionsPolicy() {
synchronized (getLockObject()) {
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/PolicyDefinition.java b/services/devicepolicy/java/com/android/server/devicepolicy/PolicyDefinition.java
index 0fc8c5e..27f1834 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/PolicyDefinition.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/PolicyDefinition.java
@@ -341,6 +341,13 @@
PolicyEnforcerCallbacks.setUsbDataSignalingEnabled(value, context),
new BooleanPolicySerializer());
+ static PolicyDefinition<Integer> CONTENT_PROTECTION = new PolicyDefinition<>(
+ new NoArgsPolicyKey(DevicePolicyIdentifiers.CONTENT_PROTECTION_POLICY),
+ new MostRecent<>(),
+ POLICY_FLAG_LOCAL_ONLY_POLICY,
+ (Integer value, Context context, Integer userId, PolicyKey policyKey) -> true,
+ new IntegerPolicySerializer());
+
private static final Map<String, PolicyDefinition<?>> POLICY_DEFINITIONS = new HashMap<>();
private static Map<String, Integer> USER_RESTRICTION_FLAGS = new HashMap<>();
@@ -374,6 +381,8 @@
PERSONAL_APPS_SUSPENDED);
POLICY_DEFINITIONS.put(DevicePolicyIdentifiers.USB_DATA_SIGNALING_POLICY,
USB_DATA_SIGNALING);
+ POLICY_DEFINITIONS.put(DevicePolicyIdentifiers.CONTENT_PROTECTION_POLICY,
+ CONTENT_PROTECTION);
// User Restriction Policies
USER_RESTRICTION_FLAGS.put(UserManager.DISALLOW_MODIFY_ACCOUNTS, /* flags= */ 0);
diff --git a/services/foldables/devicestateprovider/src/com/android/server/policy/BookStyleClosedStatePredicate.java b/services/foldables/devicestateprovider/src/com/android/server/policy/BookStyleClosedStatePredicate.java
index d5a3cff..82d5247 100644
--- a/services/foldables/devicestateprovider/src/com/android/server/policy/BookStyleClosedStatePredicate.java
+++ b/services/foldables/devicestateprovider/src/com/android/server/policy/BookStyleClosedStatePredicate.java
@@ -35,6 +35,7 @@
import android.hardware.display.DisplayManager;
import android.os.Handler;
import android.util.ArraySet;
+import android.util.Dumpable;
import android.view.Display;
import android.view.Surface;
@@ -43,7 +44,9 @@
import com.android.server.policy.BookStylePreferredScreenCalculator.StateTransition;
import com.android.server.policy.BookStyleClosedStatePredicate.ConditionSensorListener.SensorSubscription;
+import java.io.PrintWriter;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.function.Predicate;
@@ -56,7 +59,7 @@
* See {@link BookStyleStateTransitions} for detailed description of the default behavior.
*/
public class BookStyleClosedStatePredicate implements Predicate<FoldableDeviceStateProvider>,
- DisplayManager.DisplayListener {
+ DisplayManager.DisplayListener, Dumpable {
private final BookStylePreferredScreenCalculator mClosedStateCalculator;
private final Handler mHandler = new Handler();
@@ -154,6 +157,14 @@
}
+ @Override
+ public void dump(@NonNull PrintWriter writer, @Nullable String[] args) {
+ writer.println(" " + getDumpableName());
+
+ mPostureEstimator.dump(writer, args);
+ mClosedStateCalculator.dump(writer, args);
+ }
+
public interface ClosedStateUpdatesListener {
void onClosedStateUpdated();
}
@@ -161,7 +172,7 @@
/**
* Estimates if the device is going to enter wedge/tent mode based on the sensor data
*/
- private static class PostureEstimator implements SensorEventListener {
+ private static class PostureEstimator implements SensorEventListener, Dumpable {
private static final int FLAT_INCLINATION_THRESHOLD_DEGREES = 8;
@@ -356,6 +367,23 @@
mDeviceClosed = deviceClosed;
mConditionedSensorListener.updateListeningState();
}
+
+ @Override
+ public void dump(@NonNull PrintWriter writer, @Nullable String[] args) {
+ writer.println(" " + getDumpableName());
+ writer.println(" isLikelyTentOrWedgeMode = " + isLikelyTentOrWedgeMode());
+ writer.println(" mScreenTurnedOn = " + mScreenTurnedOn);
+ writer.println(" mLastScreenRotation = " + mLastScreenRotation);
+ writer.println(" mDeviceClosed = " + mDeviceClosed);
+ writer.println(" mLeftGravityVector = " + Arrays.toString(mLeftGravityVector));
+ writer.println(" mRightGravityVector = " + Arrays.toString(mRightGravityVector));
+ }
+
+ @NonNull
+ @Override
+ public String getDumpableName() {
+ return "PostureEstimator";
+ }
}
/**
diff --git a/services/foldables/devicestateprovider/src/com/android/server/policy/BookStyleDeviceStatePolicy.java b/services/foldables/devicestateprovider/src/com/android/server/policy/BookStyleDeviceStatePolicy.java
index ad938af..8b22718 100644
--- a/services/foldables/devicestateprovider/src/com/android/server/policy/BookStyleDeviceStatePolicy.java
+++ b/services/foldables/devicestateprovider/src/com/android/server/policy/BookStyleDeviceStatePolicy.java
@@ -38,6 +38,7 @@
import com.android.server.policy.feature.flags.FeatureFlags;
import com.android.server.policy.feature.flags.FeatureFlagsImpl;
+import java.io.PrintWriter;
import java.util.function.Predicate;
/**
@@ -182,4 +183,9 @@
public void configureDeviceForState(int state, @NonNull Runnable onComplete) {
onComplete.run();
}
+
+ @Override
+ public void dump(@NonNull PrintWriter writer, @Nullable String[] args) {
+ mProvider.dump(writer, args);
+ }
}
diff --git a/services/foldables/devicestateprovider/src/com/android/server/policy/BookStylePreferredScreenCalculator.java b/services/foldables/devicestateprovider/src/com/android/server/policy/BookStylePreferredScreenCalculator.java
index 8977422..69d793e 100644
--- a/services/foldables/devicestateprovider/src/com/android/server/policy/BookStylePreferredScreenCalculator.java
+++ b/services/foldables/devicestateprovider/src/com/android/server/policy/BookStylePreferredScreenCalculator.java
@@ -16,8 +16,14 @@
package com.android.server.policy;
+import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.os.Build;
+import android.util.Dumpable;
+import android.util.Slog;
+
+import java.io.PrintWriter;
import java.util.List;
import java.util.Objects;
@@ -31,7 +37,12 @@
*
* See {@link BookStyleStateTransitions} for detailed description of the default behavior.
*/
-public class BookStylePreferredScreenCalculator {
+public class BookStylePreferredScreenCalculator implements Dumpable {
+
+ private static final String TAG = "BookStylePreferredScreenCalculator";
+
+ // TODO(b/322137477): disable by default on all builds after flag clean-up
+ private static final boolean DEBUG = Build.IS_USERDEBUG || Build.IS_ENG;
/**
* When calculating the new state we will re-calculate it until it settles down. We re-calculate
@@ -77,6 +88,9 @@
*/
public PreferredScreen calculatePreferredScreen(HingeAngle angle, boolean likelyTentOrWedge,
boolean likelyReverseWedge) {
+
+ final State oldState = mState;
+
int attempts = 0;
State newState = calculateNewState(mState, angle, likelyTentOrWedge, likelyReverseWedge);
while (attempts < MAX_STATE_CHANGES && !Objects.equals(mState, newState)) {
@@ -92,7 +106,6 @@
+ ", likelyReverseWedge = " + likelyReverseWedge);
}
- final State oldState = mState;
mState = newState;
if (mState.mPreferredScreen == PreferredScreen.INVALID) {
@@ -103,6 +116,13 @@
+ oldState);
}
+ if (DEBUG && !Objects.equals(oldState, newState)) {
+ Slog.d(TAG, "Moving to state " + mState
+ + " (hingeAngle = " + angle
+ + ", likelyTentOrWedge = " + likelyTentOrWedge
+ + ", likelyReverseWedge = " + likelyReverseWedge + ")");
+ }
+
return mState.mPreferredScreen;
}
@@ -129,6 +149,18 @@
+ likelyReverseWedge);
}
+ @Override
+ public void dump(@NonNull PrintWriter writer, @Nullable String[] args) {
+ writer.println(" " + getDumpableName());
+ writer.println(" mState = " + mState);
+ }
+
+ @NonNull
+ @Override
+ public String getDumpableName() {
+ return TAG;
+ }
+
/**
* The angle between two halves of the foldable device in degrees. The angle is '0' when
* the device is fully closed and '180' when the device is fully open and flat.
diff --git a/services/foldables/devicestateprovider/src/com/android/server/policy/FoldableDeviceStateProvider.java b/services/foldables/devicestateprovider/src/com/android/server/policy/FoldableDeviceStateProvider.java
index ba72977..021a667 100644
--- a/services/foldables/devicestateprovider/src/com/android/server/policy/FoldableDeviceStateProvider.java
+++ b/services/foldables/devicestateprovider/src/com/android/server/policy/FoldableDeviceStateProvider.java
@@ -39,6 +39,7 @@
import android.os.Looper;
import android.os.PowerManager;
import android.os.Trace;
+import android.util.Dumpable;
import android.util.Slog;
import android.util.SparseArray;
import android.util.SparseBooleanArray;
@@ -52,6 +53,7 @@
import com.android.server.policy.feature.flags.FeatureFlags;
import com.android.server.policy.feature.flags.FeatureFlagsImpl;
+import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
@@ -87,6 +89,8 @@
// the conditions needed for availability.
private final SparseArray<BooleanSupplier> mStateAvailabilityConditions = new SparseArray<>();
+ private final DeviceStateConfiguration[] mConfigurations;
+
@GuardedBy("mLock")
private final SparseBooleanArray mExternalDisplaysConnected = new SparseBooleanArray();
@@ -142,6 +146,7 @@
mHingeAngleSensor = hingeAngleSensor;
mHallSensor = hallSensor;
mDisplayManager = displayManager;
+ mConfigurations = deviceStateConfigurations;
mIsDualDisplayBlockingEnabled = featureFlags.enableDualDisplayBlocking();
sensorManager.registerListener(this, mHingeAngleSensor, SENSOR_DELAY_FASTEST);
@@ -350,16 +355,20 @@
@GuardedBy("mLock")
private void dumpSensorValues() {
Slog.i(TAG, "Sensor values:");
- dumpSensorValues("Hall Sensor", mHallSensor, mLastHallSensorEvent);
- dumpSensorValues("Hinge Angle Sensor", mHingeAngleSensor, mLastHingeAngleSensorEvent);
+ dumpSensorValues(mHallSensor, mLastHallSensorEvent);
+ dumpSensorValues(mHingeAngleSensor, mLastHingeAngleSensorEvent);
Slog.i(TAG, "isScreenOn: " + isScreenOn());
}
@GuardedBy("mLock")
- private void dumpSensorValues(String sensorType, Sensor sensor, @Nullable SensorEvent event) {
+ private void dumpSensorValues(Sensor sensor, @Nullable SensorEvent event) {
+ Slog.i(TAG, toSensorValueString(sensor, event));
+ }
+
+ private String toSensorValueString(Sensor sensor, @Nullable SensorEvent event) {
String sensorString = sensor == null ? "null" : sensor.getName();
String eventValues = event == null ? "null" : Arrays.toString(event.values);
- Slog.i(TAG, sensorType + " : " + sensorString + " : " + eventValues);
+ return sensorString + " : " + eventValues;
}
@Override
@@ -414,6 +423,34 @@
}
}
+ @Override
+ public void dump(@NonNull PrintWriter writer, @Nullable String[] args) {
+ writer.println("FoldableDeviceStateProvider");
+
+ synchronized (mLock) {
+ writer.println(" mLastReportedState = " + mLastReportedState);
+ writer.println(" mPowerSaveModeEnabled = " + mPowerSaveModeEnabled);
+ writer.println(" mThermalStatus = " + mThermalStatus);
+ writer.println(" mLastHingeAngleSensorEvent = " +
+ toSensorValueString(mHingeAngleSensor, mLastHingeAngleSensorEvent));
+ writer.println(" mLastHallSensorEvent = " +
+ toSensorValueString(mHallSensor, mLastHallSensorEvent));
+ }
+
+ writer.println();
+ writer.println(" Predicates:");
+
+ for (int i = 0; i < mConfigurations.length; i++) {
+ final DeviceStateConfiguration configuration = mConfigurations[i];
+ final Predicate<FoldableDeviceStateProvider> predicate =
+ configuration.mActiveStatePredicate;
+
+ if (predicate instanceof Dumpable dumpable) {
+ dumpable.dump(writer, /* args= */ null);
+ }
+ }
+ }
+
/**
* Configuration for a single device state, contains information about the state like
* identifier, name, flags and a predicate that should return true if the state should
diff --git a/services/java/com/android/server/SystemConfigService.java b/services/java/com/android/server/SystemConfigService.java
index fd21a32..2359422c9 100644
--- a/services/java/com/android/server/SystemConfigService.java
+++ b/services/java/com/android/server/SystemConfigService.java
@@ -22,6 +22,8 @@
import android.content.ComponentName;
import android.content.Context;
import android.content.pm.PackageManagerInternal;
+import android.content.pm.SignedPackage;
+import android.content.pm.SignedPackageParcel;
import android.os.Binder;
import android.os.ISystemConfig;
import android.util.ArrayMap;
@@ -119,6 +121,26 @@
pmi.canQueryPackage(Binder.getCallingUid(), preventUserDisablePackage))
.collect(toList());
}
+
+ @Override
+ public List<SignedPackageParcel> getEnhancedConfirmationTrustedPackages() {
+ getContext().enforceCallingOrSelfPermission(
+ Manifest.permission.MANAGE_ENHANCED_CONFIRMATION_STATES,
+ "Caller must hold " + Manifest.permission.MANAGE_ENHANCED_CONFIRMATION_STATES);
+
+ return SystemConfig.getInstance().getEnhancedConfirmationTrustedPackages().stream()
+ .map(SignedPackage::getData).toList();
+ }
+
+ @Override
+ public List<SignedPackageParcel> getEnhancedConfirmationTrustedInstallers() {
+ getContext().enforceCallingOrSelfPermission(
+ Manifest.permission.MANAGE_ENHANCED_CONFIRMATION_STATES,
+ "Caller must hold " + Manifest.permission.MANAGE_ENHANCED_CONFIRMATION_STATES);
+
+ return SystemConfig.getInstance().getEnhancedConfirmationTrustedInstallers().stream()
+ .map(SignedPackage::getData).toList();
+ }
};
public SystemConfigService(Context context) {
diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java
index 2b8bcc7..c55d709 100644
--- a/services/java/com/android/server/SystemServer.java
+++ b/services/java/com/android/server/SystemServer.java
@@ -48,6 +48,7 @@
import android.content.res.Configuration;
import android.content.res.Resources.Theme;
import android.credentials.CredentialManager;
+import android.credentials.flags.Flags;
import android.database.sqlite.SQLiteCompatibilityWalFlags;
import android.database.sqlite.SQLiteGlobal;
import android.graphics.GraphicsStatsService;
@@ -269,6 +270,8 @@
"com.android.server.backup.BackupManagerService$Lifecycle";
private static final String APPWIDGET_SERVICE_CLASS =
"com.android.server.appwidget.AppWidgetService";
+ private static final String ARC_NETWORK_SERVICE_CLASS =
+ "com.android.server.arc.net.ArcNetworkService";
private static final String ARC_PERSISTENT_DATA_BLOCK_SERVICE_CLASS =
"com.android.server.arc.persistent_data_block.ArcPersistentDataBlockService";
private static final String ARC_SYSTEM_HEALTH_SERVICE =
@@ -2069,13 +2072,24 @@
if (context.getPackageManager().hasSystemFeature(
PackageManager.FEATURE_WIFI)) {
// Wifi Service must be started first for wifi-related services.
- t.traceBegin("StartWifi");
- mSystemServiceManager.startServiceFromJar(
- WIFI_SERVICE_CLASS, WIFI_APEX_SERVICE_JAR_PATH);
- t.traceEnd();
- t.traceBegin("StartWifiScanning");
- mSystemServiceManager.startServiceFromJar(
- WIFI_SCANNING_SERVICE_CLASS, WIFI_APEX_SERVICE_JAR_PATH);
+ if (!isArc) {
+ t.traceBegin("StartWifi");
+ mSystemServiceManager.startServiceFromJar(
+ WIFI_SERVICE_CLASS, WIFI_APEX_SERVICE_JAR_PATH);
+ t.traceEnd();
+ t.traceBegin("StartWifiScanning");
+ mSystemServiceManager.startServiceFromJar(
+ WIFI_SCANNING_SERVICE_CLASS, WIFI_APEX_SERVICE_JAR_PATH);
+ t.traceEnd();
+ }
+ }
+
+ // ARC - ArcNetworkService registers the ARC network stack and replaces the
+ // stock WiFi service in both ARC++ container and ARCVM. Always starts the ARC network
+ // stack regardless of whether FEATURE_WIFI is enabled/disabled (b/254755875).
+ if (isArc) {
+ t.traceBegin("StartArcNetworking");
+ mSystemServiceManager.startService(ARC_NETWORK_SERVICE_CLASS);
t.traceEnd();
}
@@ -2782,9 +2796,14 @@
DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_CREDENTIAL,
CredentialManager.DEVICE_CONFIG_ENABLE_CREDENTIAL_MANAGER, true);
if (credentialManagerEnabled) {
- t.traceBegin("StartCredentialManagerService");
- mSystemServiceManager.startService(CREDENTIAL_MANAGER_SERVICE_CLASS);
- t.traceEnd();
+ if(isWatch &&
+ !android.credentials.flags.Flags.wearCredentialManagerEnabled()) {
+ Slog.d(TAG, "CredentialManager disabled on wear.");
+ } else {
+ t.traceBegin("StartCredentialManagerService");
+ mSystemServiceManager.startService(CREDENTIAL_MANAGER_SERVICE_CLASS);
+ t.traceEnd();
+ }
} else {
Slog.d(TAG, "CredentialManager disabled.");
}
diff --git a/services/java/com/android/server/flags.aconfig b/services/java/com/android/server/flags.aconfig
new file mode 100644
index 0000000..4b578af
--- /dev/null
+++ b/services/java/com/android/server/flags.aconfig
@@ -0,0 +1,9 @@
+package: "android.server"
+
+flag {
+ namespace: "system_performance"
+ name: "telemetry_apis_service"
+ description: "Control service portion of telemetry APIs feature."
+ is_fixed_read_only: true
+ bug: "324153471"
+}
diff --git a/services/profcollect/src/com/android/server/profcollect/ProfcollectForwardingService.java b/services/profcollect/src/com/android/server/profcollect/ProfcollectForwardingService.java
index fb0fbe8..68038fa 100644
--- a/services/profcollect/src/com/android/server/profcollect/ProfcollectForwardingService.java
+++ b/services/profcollect/src/com/android/server/profcollect/ProfcollectForwardingService.java
@@ -49,7 +49,6 @@
import com.android.server.wm.ActivityMetricsLaunchObserverRegistry;
import com.android.server.wm.ActivityTaskManagerInternal;
-import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;
@@ -314,7 +313,7 @@
Log.w(LOG_TAG, "Couldn't get ArtManagerLocal");
return;
}
- aml.setBatchDexoptStartCallback(ForkJoinPool.commonPool(),
+ aml.setBatchDexoptStartCallback(Runnable::run,
(snapshot, reason, defaultPackages, builder, passedSignal) -> {
traceOnDex2oatStart();
});
@@ -327,7 +326,7 @@
// Sample for a fraction of dex2oat runs.
final int traceFrequency =
DeviceConfig.getInt(DeviceConfig.NAMESPACE_PROFCOLLECT_NATIVE_BOOT,
- "dex2oat_trace_freq", 10);
+ "dex2oat_trace_freq", 25);
int randomNum = ThreadLocalRandom.current().nextInt(100);
if (randomNum < traceFrequency) {
if (DEBUG) {
diff --git a/services/tests/BackgroundInstallControlServiceTests/host/Android.bp b/services/tests/BackgroundInstallControlServiceTests/host/Android.bp
index d479e52..682ed91 100644
--- a/services/tests/BackgroundInstallControlServiceTests/host/Android.bp
+++ b/services/tests/BackgroundInstallControlServiceTests/host/Android.bp
@@ -32,6 +32,7 @@
":BackgroundInstallControlServiceTestApp",
":BackgroundInstallControlMockApp1",
":BackgroundInstallControlMockApp2",
+ ":BackgroundInstallControlMockApp3",
],
test_suites: [
"general-tests",
diff --git a/services/tests/BackgroundInstallControlServiceTests/host/AndroidTest.xml b/services/tests/BackgroundInstallControlServiceTests/host/AndroidTest.xml
index 1e7a78a..a352851 100644
--- a/services/tests/BackgroundInstallControlServiceTests/host/AndroidTest.xml
+++ b/services/tests/BackgroundInstallControlServiceTests/host/AndroidTest.xml
@@ -34,6 +34,9 @@
<option name="push-file"
key="BackgroundInstallControlMockApp2.apk"
value="/data/local/tmp/BackgroundInstallControlMockApp2.apk" />
+ <option name="push-file"
+ key="BackgroundInstallControlMockApp3.apk"
+ value="/data/local/tmp/BackgroundInstallControlMockApp3.apk" />
</target_preparer>
<test class="com.android.compatibility.common.tradefed.testtype.JarHostTest" >
diff --git a/services/tests/BackgroundInstallControlServiceTests/host/src/com/android/server/pm/test/BackgroundInstallControlServiceHostTest.java b/services/tests/BackgroundInstallControlServiceTests/host/src/com/android/server/pm/test/BackgroundInstallControlServiceHostTest.java
index c99e712..5092a46 100644
--- a/services/tests/BackgroundInstallControlServiceTests/host/src/com/android/server/pm/test/BackgroundInstallControlServiceHostTest.java
+++ b/services/tests/BackgroundInstallControlServiceTests/host/src/com/android/server/pm/test/BackgroundInstallControlServiceHostTest.java
@@ -68,6 +68,20 @@
assertThat(getDevice().getAppPackageInfo(MOCK_PACKAGE_NAME_2)).isNull();
}
+ @Test
+ public void testRegisterCallback() throws Exception {
+ runDeviceTest(
+ "BackgroundInstallControlServiceTest",
+ "testRegisterBackgroundInstallControlCallback");
+ }
+
+ @Test
+ public void testUnregisterCallback() throws Exception {
+ runDeviceTest(
+ "BackgroundInstallControlServiceTest",
+ "testUnregisterBackgroundInstallControlCallback");
+ }
+
private void installPackage(String path) throws DeviceNotAvailableException {
String cmd = "pm install -t --force-queryable " + path;
CommandResult result = getDevice().executeShellV2Command(cmd);
diff --git a/services/tests/BackgroundInstallControlServiceTests/host/test-app/BackgroundInstallControlServiceTestApp/src/com/android/server/pm/test/app/BackgroundInstallControlServiceTest.java b/services/tests/BackgroundInstallControlServiceTests/host/test-app/BackgroundInstallControlServiceTestApp/src/com/android/server/pm/test/app/BackgroundInstallControlServiceTest.java
index b23f591..ac041f4 100644
--- a/services/tests/BackgroundInstallControlServiceTests/host/test-app/BackgroundInstallControlServiceTestApp/src/com/android/server/pm/test/app/BackgroundInstallControlServiceTest.java
+++ b/services/tests/BackgroundInstallControlServiceTests/host/test-app/BackgroundInstallControlServiceTestApp/src/com/android/server/pm/test/app/BackgroundInstallControlServiceTest.java
@@ -16,38 +16,59 @@
package com.android.server.pm.test.app;
-import static android.Manifest.permission.GET_BACKGROUND_INSTALLED_PACKAGES;
-
import static com.android.compatibility.common.util.SystemUtil.runShellCommand;
import static com.google.common.truth.Truth.assertThat;
+import android.app.PendingIntent;
+import android.content.BroadcastReceiver;
import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
import android.content.pm.IBackgroundInstallControlService;
import android.content.pm.PackageInfo;
+import android.content.pm.PackageInstaller;
import android.content.pm.PackageManager;
import android.content.pm.ParceledListSlice;
+import android.os.Bundle;
+import android.os.IRemoteCallback;
import android.os.Process;
import android.os.RemoteException;
import android.os.ServiceManager;
+import android.util.Pair;
+import androidx.annotation.NonNull;
+import androidx.test.platform.app.InstrumentationRegistry;
import androidx.test.runner.AndroidJUnit4;
import com.android.compatibility.common.util.ShellIdentityUtils;
+import com.android.compatibility.common.util.ThrowingRunnable;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
+import java.io.FileInputStream;
+import java.io.OutputStream;
+import java.util.ArrayList;
import java.util.Set;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Supplier;
import java.util.stream.Collectors;
@RunWith(AndroidJUnit4.class)
public class BackgroundInstallControlServiceTest {
private static final String TAG = "BackgroundInstallControlServiceTest";
+ private static final String ACTION_INSTALL_COMMIT =
+ "com.android.server.pm.test.app.BackgroundInstallControlServiceTest"
+ + ".ACTION_INSTALL_COMMIT";
private static final String MOCK_PACKAGE_NAME = "com.android.servicestests.apps.bicmockapp3";
+ private static final String TEST_DATA_DIR = "/data/local/tmp/";
+
+ private static final String MOCK_APK_FILE = "BackgroundInstallControlMockApp3.apk";
private IBackgroundInstallControlService mIBics;
@Before
@@ -74,10 +95,9 @@
PackageManager.MATCH_ALL, Process.myUserHandle()
.getIdentifier());
} catch (RemoteException e) {
- throw new RuntimeException(e);
+ throw e.rethrowFromSystemServer();
}
- },
- GET_BACKGROUND_INSTALLED_PACKAGES);
+ });
assertThat(slice).isNotNull();
var packageList = slice.getList();
@@ -94,4 +114,150 @@
.collect(Collectors.toSet());
assertThat(actualPackageNames).containsExactlyElementsIn(expectedPackageNames);
}
+
+ @Test
+ public void testRegisterBackgroundInstallControlCallback()
+ throws Exception {
+ String testPackageName = "test";
+ int testUserId = 1;
+ ArrayList<Pair<String, Integer>> sharedResource = new ArrayList<>();
+ IRemoteCallback testCallback =
+ new IRemoteCallback.Stub() {
+ private final ArrayList<Pair<String, Integer>> mArray = sharedResource;
+
+ @Override
+ public void sendResult(Bundle data) throws RemoteException {
+ mArray.add(new Pair(testPackageName, testUserId));
+ }
+ };
+ ShellIdentityUtils.invokeMethodWithShellPermissionsNoReturn(
+ mIBics,
+ (bics) -> {
+ try {
+ bics.registerBackgroundInstallCallback(testCallback);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ });
+ installPackage(TEST_DATA_DIR + MOCK_APK_FILE, MOCK_PACKAGE_NAME);
+
+ assertUntil(() -> sharedResource.size() == 1, 2000);
+ assertThat(sharedResource.get(0).first).isEqualTo(testPackageName);
+ assertThat(sharedResource.get(0).second).isEqualTo(testUserId);
+ }
+
+ @Test
+ public void testUnregisterBackgroundInstallControlCallback() {
+ String testValue = "test";
+ ArrayList<String> sharedResource = new ArrayList<>();
+ IRemoteCallback testCallback =
+ new IRemoteCallback.Stub() {
+ private final ArrayList<String> mArray = sharedResource;
+
+ @Override
+ public void sendResult(Bundle data) throws RemoteException {
+ mArray.add(testValue);
+ }
+ };
+ ShellIdentityUtils.invokeMethodWithShellPermissionsNoReturn(
+ mIBics,
+ (bics) -> {
+ try {
+ bics.registerBackgroundInstallCallback(testCallback);
+ bics.unregisterBackgroundInstallCallback(testCallback);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ });
+ installPackage(TEST_DATA_DIR + MOCK_APK_FILE, MOCK_PACKAGE_NAME);
+
+ assertUntil(sharedResource::isEmpty, 2000);
+ }
+
+ private static boolean installPackage(String apkPath, String packageName) {
+ Context context = InstrumentationRegistry.getInstrumentation().getContext();
+ final CountDownLatch installLatch = new CountDownLatch(1);
+ final BroadcastReceiver installReceiver =
+ new BroadcastReceiver() {
+ public void onReceive(Context context, Intent intent) {
+ int packageInstallStatus =
+ intent.getIntExtra(
+ PackageInstaller.EXTRA_STATUS,
+ PackageInstaller.STATUS_FAILURE_INVALID);
+ if (packageInstallStatus == PackageInstaller.STATUS_SUCCESS) {
+ installLatch.countDown();
+ }
+ }
+ };
+ final IntentFilter intentFilter = new IntentFilter(ACTION_INSTALL_COMMIT);
+ context.registerReceiver(installReceiver, intentFilter, Context.RECEIVER_EXPORTED);
+
+ PackageInstaller packageInstaller = context.getPackageManager().getPackageInstaller();
+ PackageInstaller.SessionParams params =
+ new PackageInstaller.SessionParams(
+ PackageInstaller.SessionParams.MODE_FULL_INSTALL);
+ params.setRequireUserAction(PackageInstaller.SessionParams.USER_ACTION_NOT_REQUIRED);
+ try {
+ int sessionId = packageInstaller.createSession(params);
+ PackageInstaller.Session session = packageInstaller.openSession(sessionId);
+ OutputStream out = session.openWrite(packageName, 0, -1);
+ FileInputStream fis = new FileInputStream(apkPath);
+ byte[] buffer = new byte[65536];
+ int size;
+ while ((size = fis.read(buffer)) != -1) {
+ out.write(buffer, 0, size);
+ }
+ session.fsync(out);
+ fis.close();
+ out.close();
+
+ runWithShellPermissionIdentity(
+ () -> {
+ session.commit(createPendingIntent(context).getIntentSender());
+ installLatch.await(5, TimeUnit.SECONDS);
+ });
+ return true;
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private static PendingIntent createPendingIntent(Context context) {
+ PendingIntent pendingIntent =
+ PendingIntent.getBroadcast(
+ context,
+ 1,
+ new Intent(ACTION_INSTALL_COMMIT)
+ .setPackage(
+ BackgroundInstallControlServiceTest.class.getPackageName()),
+ PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_MUTABLE);
+ return pendingIntent;
+ }
+
+ private static void runWithShellPermissionIdentity(@NonNull ThrowingRunnable command)
+ throws Exception {
+ InstrumentationRegistry.getInstrumentation()
+ .getUiAutomation()
+ .adoptShellPermissionIdentity();
+ try {
+ command.run();
+ } finally {
+ InstrumentationRegistry.getInstrumentation()
+ .getUiAutomation()
+ .dropShellPermissionIdentity();
+ }
+ }
+
+ private static void assertUntil(Supplier<Boolean> condition, int timeoutMs) {
+ long endTime = System.currentTimeMillis() + timeoutMs;
+ while (System.currentTimeMillis() <= endTime) {
+ if (condition.get()) return;
+ try {
+ Thread.sleep(10);
+ } catch (InterruptedException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ assertThat(condition.get()).isTrue();
+ }
}
\ No newline at end of file
diff --git a/services/tests/BackgroundInstallControlServiceTests/host/test-app/MockApp/Android.bp b/services/tests/BackgroundInstallControlServiceTests/host/test-app/MockApp/Android.bp
index 7804f4c..39b0ff7 100644
--- a/services/tests/BackgroundInstallControlServiceTests/host/test-app/MockApp/Android.bp
+++ b/services/tests/BackgroundInstallControlServiceTests/host/test-app/MockApp/Android.bp
@@ -50,3 +50,11 @@
"--rename-manifest-package com.android.servicestests.apps.bicmockapp2",
],
}
+
+android_test_helper_app {
+ name: "BackgroundInstallControlMockApp3",
+ defaults: ["bic-mock-app-defaults"],
+ aaptflags: [
+ "--rename-manifest-package com.android.servicestests.apps.bicmockapp3",
+ ],
+}
diff --git a/services/tests/InputMethodSystemServerTests/Android.bp b/services/tests/InputMethodSystemServerTests/Android.bp
index 897afbf..afd6dbd 100644
--- a/services/tests/InputMethodSystemServerTests/Android.bp
+++ b/services/tests/InputMethodSystemServerTests/Android.bp
@@ -13,6 +13,7 @@
// limitations under the License.
package {
+ default_team: "trendy_team_input_method_framework",
// See: http://go/android-license-faq
// A large-scale-change added 'default_applicable_licenses' to import
// all of the 'license_kinds' from "frameworks_base_license"
diff --git a/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/Android.bp b/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/Android.bp
index e1fd2b3..8a12dcd 100644
--- a/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/Android.bp
+++ b/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/Android.bp
@@ -13,6 +13,7 @@
// limitations under the License.
package {
+ default_team: "trendy_team_input_method_framework",
// See: http://go/android-license-faq
// A large-scale-change added 'default_applicable_licenses' to import
// all of the 'license_kinds' from "frameworks_base_license"
diff --git a/services/tests/PackageManagerComponentOverrideTests/Android.bp b/services/tests/PackageManagerComponentOverrideTests/Android.bp
index 00850a5..e3919a5 100644
--- a/services/tests/PackageManagerComponentOverrideTests/Android.bp
+++ b/services/tests/PackageManagerComponentOverrideTests/Android.bp
@@ -18,6 +18,7 @@
// and this is more representative of a real caller. It also uses Mockito extended, and this
// prevents converting the entire services test module.
package {
+ default_team: "trendy_team_framework_android_packages",
// See: http://go/android-license-faq
// A large-scale-change added 'default_applicable_licenses' to import
// all of the 'license_kinds' from "frameworks_base_license"
diff --git a/services/tests/PackageManagerServiceTests/appenumeration/Android.bp b/services/tests/PackageManagerServiceTests/appenumeration/Android.bp
index ad7af44..f15e533 100644
--- a/services/tests/PackageManagerServiceTests/appenumeration/Android.bp
+++ b/services/tests/PackageManagerServiceTests/appenumeration/Android.bp
@@ -13,6 +13,7 @@
// limitations under the License.
package {
+ default_team: "trendy_team_framework_android_packages",
// See: http://go/android-license-faq
// A large-scale-change added 'default_applicable_licenses' to import
// all of the 'license_kinds' from "frameworks_base_license"
diff --git a/services/tests/PackageManagerServiceTests/appenumeration/test-apps/target/Android.bp b/services/tests/PackageManagerServiceTests/appenumeration/test-apps/target/Android.bp
index 9921106..6da503d 100644
--- a/services/tests/PackageManagerServiceTests/appenumeration/test-apps/target/Android.bp
+++ b/services/tests/PackageManagerServiceTests/appenumeration/test-apps/target/Android.bp
@@ -13,6 +13,7 @@
// limitations under the License.
package {
+ default_team: "trendy_team_framework_android_packages",
// See: http://go/android-license-faq
// A large-scale-change added 'default_applicable_licenses' to import
// all of the 'license_kinds' from "frameworks_base_license"
diff --git a/services/tests/PackageManagerServiceTests/server/Android.bp b/services/tests/PackageManagerServiceTests/server/Android.bp
index f8accc3..4f27bc2 100644
--- a/services/tests/PackageManagerServiceTests/server/Android.bp
+++ b/services/tests/PackageManagerServiceTests/server/Android.bp
@@ -3,6 +3,7 @@
//########################################################################
package {
+ default_team: "trendy_team_framework_android_packages",
// See: http://go/android-license-faq
// A large-scale-change added 'default_applicable_licenses' to import
// all of the 'license_kinds' from "frameworks_base_license"
diff --git a/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/CrossProfileAppsServiceImplTest.java b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/CrossProfileAppsServiceImplTest.java
index 129efc6..005cad1 100644
--- a/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/CrossProfileAppsServiceImplTest.java
+++ b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/CrossProfileAppsServiceImplTest.java
@@ -46,6 +46,7 @@
import android.permission.PermissionCheckerManager;
import android.permission.PermissionManager;
import android.platform.test.annotations.Presubmit;
+import android.platform.test.flag.junit.SetFlagsRule;
import android.util.SparseArray;
import com.android.internal.util.FunctionalUtils.ThrowingRunnable;
@@ -55,6 +56,7 @@
import com.android.server.wm.ActivityTaskManagerInternal;
import org.junit.Before;
+import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
@@ -109,6 +111,8 @@
private IApplicationThread mIApplicationThread;
private SparseArray<Boolean> mUserEnabled = new SparseArray<>();
+ @Rule
+ public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
@Before
public void initCrossProfileAppsServiceImpl() {
@@ -123,8 +127,9 @@
mUserEnabled.put(PRIMARY_USER, true);
mUserEnabled.put(PROFILE_OF_PRIMARY_USER, true);
mUserEnabled.put(SECONDARY_USER, true);
+ mSetFlagsRule.enableFlags(android.multiuser.Flags.FLAG_ENABLE_HIDING_PROFILES);
- when(mUserManager.getEnabledProfileIds(anyInt())).thenAnswer(
+ when(mUserManager.getProfileIdsExcludingHidden(anyInt(), eq(true))).thenAnswer(
invocation -> {
List<Integer> users = new ArrayList<>();
final int targetUser = invocation.getArgument(0);
diff --git a/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageInstallerSessionTest.kt b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageInstallerSessionTest.kt
index 811b086..7aa2ff5 100644
--- a/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageInstallerSessionTest.kt
+++ b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageInstallerSessionTest.kt
@@ -22,7 +22,9 @@
import android.content.pm.PackageInstaller.SessionParams.PERMISSION_STATE_DENIED
import android.content.pm.PackageInstaller.SessionParams.PERMISSION_STATE_GRANTED
import android.content.pm.PackageManager
+import android.content.pm.verify.domain.DomainSet
import android.os.Parcel
+import android.os.Process
import android.platform.test.annotations.Presubmit
import android.util.AtomicFile
import android.util.Slog
@@ -173,7 +175,7 @@
/* stagingManager */ null,
/* sessionId */ sessionId,
/* userId */ 456,
- /* installerUid */ -1,
+ /* installerUid */ Process.myUid(),
/* installSource */ installSource,
/* sessionParams */ params,
/* createdMillis */ 0L,
@@ -183,8 +185,8 @@
/* files */ null,
/* checksums */ null,
/* prepared */ true,
- /* committed */ true,
- /* destroyed */ staged,
+ /* committed */ false,
+ /* destroyed */ false,
/* sealed */ false, // Setting to true would trigger some PM logic.
/* childSessionIds */ childSessionIds.toIntArray(),
/* parentSessionId */ parentSessionId,
@@ -192,7 +194,8 @@
/* isFailed */ false,
/* isApplied */ false,
/* stagedSessionErrorCode */ PackageManager.INSTALL_FAILED_VERIFICATION_FAILURE,
- /* stagedSessionErrorMessage */ "some error"
+ /* stagedSessionErrorMessage */ "some error",
+ /* preVerifiedDomains */ DomainSet(setOf("com.foo", "com.bar"))
)
}
@@ -332,6 +335,7 @@
assertThat(expected.parentSessionId).isEqualTo(actual.parentSessionId)
assertThat(expected.childSessionIds).asList()
.containsExactlyElementsIn(actual.childSessionIds.toList())
+ assertThat(expected.preVerifiedDomains).isEqualTo(actual.preVerifiedDomains)
}
private fun assertInstallSourcesEquivalent(expected: InstallSource, actual: InstallSource) {
diff --git a/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageManagerSettingsTests.java b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageManagerSettingsTests.java
index 6fff012..8ad557c 100644
--- a/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageManagerSettingsTests.java
+++ b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageManagerSettingsTests.java
@@ -58,10 +58,10 @@
import android.os.Process;
import android.os.UserHandle;
import android.platform.test.annotations.Presubmit;
-import android.platform.test.flag.junit.SetFlagsRule;
import android.platform.test.annotations.RequiresFlagsEnabled;
import android.platform.test.flag.junit.CheckFlagsRule;
import android.platform.test.flag.junit.DeviceFlagsValueProvider;
+import android.platform.test.flag.junit.SetFlagsRule;
import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.AtomicFile;
@@ -1043,6 +1043,27 @@
UserHandle.SYSTEM).getArchiveState()).isEqualTo(archiveState);
}
+ @RequiresFlagsEnabled(Flags.FLAG_ASL_IN_APK_APP_METADATA_SOURCE)
+ @Test
+ public void testWriteReadAppMetadataSource() {
+ Settings settings = makeSettings();
+ PackageSetting packageSetting = createPackageSetting(PACKAGE_NAME_1);
+ packageSetting.setAppId(Process.FIRST_APPLICATION_UID);
+ packageSetting.setPkg(PackageImpl.forTesting(PACKAGE_NAME_1).hideAsParsed()
+ .setUid(packageSetting.getAppId())
+ .hideAsFinal());
+
+ packageSetting.setAppMetadataSource(PackageManager.APP_METADATA_SOURCE_INSTALLER);
+ settings.mPackages.put(PACKAGE_NAME_1, packageSetting);
+
+ settings.writeLPr(computer, /*sync=*/true);
+ settings.mPackages.clear();
+
+ assertThat(settings.readLPw(computer, createFakeUsers()), is(true));
+ assertThat(settings.getPackageLPr(PACKAGE_NAME_1).getAppMetadataSource(),
+ is(PackageManager.APP_METADATA_SOURCE_INSTALLER));
+ }
+
@Test
public void testPackageRestrictionsDistractionFlagsDefault() {
final PackageSetting defaultSetting = createPackageSetting(PACKAGE_NAME_1);
diff --git a/services/tests/PackageManagerServiceTests/unit/Android.bp b/services/tests/PackageManagerServiceTests/unit/Android.bp
index 85059838..c93f482 100644
--- a/services/tests/PackageManagerServiceTests/unit/Android.bp
+++ b/services/tests/PackageManagerServiceTests/unit/Android.bp
@@ -13,6 +13,7 @@
// limitations under the License.
package {
+ default_team: "trendy_team_framework_android_packages",
// See: http://go/android-license-faq
// A large-scale-change added 'default_applicable_licenses' to import
// all of the 'license_kinds' from "frameworks_base_license"
diff --git a/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/parsing/parcelling/AndroidPackageTest.kt b/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/parsing/parcelling/AndroidPackageTest.kt
index ef9c62f..cfe701f 100644
--- a/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/parsing/parcelling/AndroidPackageTest.kt
+++ b/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/parsing/parcelling/AndroidPackageTest.kt
@@ -272,7 +272,8 @@
AndroidPackage::hasPreserveLegacyExternalStorage,
AndroidPackage::hasRequestForegroundServiceExemption,
AndroidPackage::hasRequestRawExternalStorageAccess,
- AndroidPackage::isUpdatableSystem
+ AndroidPackage::isUpdatableSystem,
+ AndroidPackage::getEmergencyInstaller
)
override fun extraParams() = listOf(
diff --git a/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/parsing/parcelling/ParsedActivityTest.kt b/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/parsing/parcelling/ParsedActivityTest.kt
index 2c8b1cd..349b831 100644
--- a/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/parsing/parcelling/ParsedActivityTest.kt
+++ b/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/parsing/parcelling/ParsedActivityTest.kt
@@ -54,7 +54,8 @@
ParsedActivity::getTheme,
ParsedActivity::getUiOptions,
ParsedActivity::isSupportsSizeChanges,
- ParsedActivity::getRequiredDisplayCategory
+ ParsedActivity::getRequiredDisplayCategory,
+ ParsedActivity::getRequireContentUriPermissionFromCaller
)
override fun mainComponentSubclassExtraParams() = listOf(
diff --git a/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/parsing/parcelling/ParsedProcessTest.kt b/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/parsing/parcelling/ParsedProcessTest.kt
index 93bdeae..d538f25 100644
--- a/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/parsing/parcelling/ParsedProcessTest.kt
+++ b/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/parsing/parcelling/ParsedProcessTest.kt
@@ -40,6 +40,7 @@
ParsedProcess::getGwpAsanMode,
ParsedProcess::getMemtagMode,
ParsedProcess::getNativeHeapZeroInitialized,
+ ParsedProcess::isUseEmbeddedDex,
)
override fun extraParams() = listOf(
diff --git a/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/verify/domain/DomainVerificationEnforcerTest.kt b/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/verify/domain/DomainVerificationEnforcerTest.kt
index d307608..b374af6 100644
--- a/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/verify/domain/DomainVerificationEnforcerTest.kt
+++ b/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/verify/domain/DomainVerificationEnforcerTest.kt
@@ -149,8 +149,8 @@
callingUidInt.set(it.callingUid)
callingUserIdInt.set(it.callingUserId)
service.proxy = it.proxy
- service.addPackage(visiblePkgState)
- service.addPackage(invisiblePkgState)
+ service.addPackage(visiblePkgState, null)
+ service.addPackage(invisiblePkgState, null)
service.block(it)
}
diff --git a/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/verify/domain/DomainVerificationManagerApiTest.kt b/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/verify/domain/DomainVerificationManagerApiTest.kt
index 5edf30a3..a8100af 100644
--- a/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/verify/domain/DomainVerificationManagerApiTest.kt
+++ b/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/verify/domain/DomainVerificationManagerApiTest.kt
@@ -566,7 +566,7 @@
}
private fun DomainVerificationService.addPackages(vararg pkgStates: PackageStateInternal) =
- pkgStates.forEach(::addPackage)
+ pkgStates.forEach {pkg: PackageStateInternal -> addPackage(pkg, null)}
private fun makeManager(service: DomainVerificationService, userId: Int) =
DomainVerificationManager(
diff --git a/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/verify/domain/DomainVerificationPackageTest.kt b/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/verify/domain/DomainVerificationPackageTest.kt
index 85f0125..e0407c1 100644
--- a/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/verify/domain/DomainVerificationPackageTest.kt
+++ b/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/verify/domain/DomainVerificationPackageTest.kt
@@ -21,6 +21,7 @@
import android.content.pm.Signature
import android.content.pm.SigningDetails
import android.content.pm.verify.domain.DomainOwner
+import android.content.pm.verify.domain.DomainSet
import android.content.pm.verify.domain.DomainVerificationInfo.STATE_MODIFIABLE_VERIFIED
import android.content.pm.verify.domain.DomainVerificationInfo.STATE_NO_RESPONSE
import android.content.pm.verify.domain.DomainVerificationInfo.STATE_SUCCESS
@@ -48,16 +49,16 @@
import com.android.server.testutils.spy
import com.android.server.testutils.whenever
import com.google.common.truth.Truth.assertThat
-import java.io.ByteArrayInputStream
-import java.io.ByteArrayOutputStream
-import java.security.PublicKey
-import java.util.UUID
import org.junit.Test
import org.mockito.ArgumentMatchers.any
import org.mockito.ArgumentMatchers.anyInt
import org.mockito.ArgumentMatchers.anyLong
import org.mockito.ArgumentMatchers.anyString
import org.mockito.Mockito.doReturn
+import java.io.ByteArrayInputStream
+import java.io.ByteArrayOutputStream
+import java.security.PublicKey
+import java.util.UUID
class DomainVerificationPackageTest {
@@ -88,7 +89,7 @@
@Test
fun addPackageFirstTime() {
val service = makeService(pkg1, pkg2)
- service.addPackage(pkg1)
+ service.addPackage(pkg1, null)
val info = service.getInfo(pkg1.packageName)
assertThat(info.packageName).isEqualTo(pkg1.packageName)
assertThat(info.identifier).isEqualTo(pkg1.domainSetId)
@@ -120,8 +121,8 @@
systemConfiguredPackageNames = ArraySet(setOf(pkg1.packageName, pkg2.packageName)),
pkg1, pkg2
)
- service.addPackage(pkg1)
- service.addPackage(pkg2)
+ service.addPackage(pkg1, null)
+ service.addPackage(pkg2, null)
service.getInfo(pkg1.packageName).apply {
assertThat(packageName).isEqualTo(pkg1.packageName)
@@ -198,7 +199,7 @@
val service = makeService(pkg1, pkg2)
val computer = mockComputer(pkg1, pkg2)
service.restoreSettings(computer, Xml.resolvePullParser(xml.byteInputStream()))
- service.addPackage(pkg1)
+ service.addPackage(pkg1, null)
val info = service.getInfo(pkg1.packageName)
assertThat(info.packageName).isEqualTo(pkg1.packageName)
assertThat(info.identifier).isEqualTo(pkg1.domainSetId)
@@ -248,7 +249,7 @@
val service = makeService(pkg1, pkg2)
val computer = mockComputer(pkg1, pkg2)
service.restoreSettings(computer, Xml.resolvePullParser(xml.byteInputStream()))
- service.addPackage(pkg1)
+ service.addPackage(pkg1, null)
val info = service.getInfo(pkg1.packageName)
assertThat(info.packageName).isEqualTo(pkg1.packageName)
assertThat(info.identifier).isEqualTo(pkg1.domainSetId)
@@ -307,7 +308,7 @@
service.readSettings(computer, Xml.resolvePullParser(it))
}
- service.addPackage(pkg1)
+ service.addPackage(pkg1, null)
assertAddPackageActivePendingRestoredState(service)
}
@@ -321,7 +322,7 @@
service.readSettings(computer, Xml.resolvePullParser(it))
}
- service.addPackage(pkg1)
+ service.addPackage(pkg1, null)
val userState = service.getUserState(pkg1.packageName)
assertThat(userState.packageName).isEqualTo(pkg1.packageName)
@@ -345,11 +346,55 @@
service.restoreSettings(computer, Xml.resolvePullParser(it))
}
- service.addPackage(pkg1)
+ service.addPackage(pkg1, null)
assertAddPackageActivePendingRestoredState(service, expectRestore = true)
}
+ @Test
+ fun addPackageWithPreVerifiedDomains() {
+ val service = makeService(pkg1)
+ val pkg1 = mockPkgState(
+ PKG_ONE,
+ UUID_ONE,
+ SIGNATURE_ONE,
+ autoVerifyDomains = listOf(DOMAIN_1, DOMAIN_2),
+ otherDomains = listOf(DOMAIN_3, DOMAIN_4))
+ service.addPackage(pkg1, DomainSet(setOf(DOMAIN_1, DOMAIN_3)))
+ val info = service.getInfo(pkg1.packageName)
+ assertThat(info.packageName).isEqualTo(pkg1.packageName)
+ assertThat(info.identifier).isEqualTo(pkg1.domainSetId)
+ // Test that DOMAIN_1 is pre-verified and DOMAIN_3 is ignored because autoVerify=false
+ assertThat(info.hostToStateMap).containsExactlyEntriesIn(mapOf(
+ DOMAIN_1 to STATE_MODIFIABLE_VERIFIED,
+ DOMAIN_2 to STATE_NO_RESPONSE,
+ ))
+
+ val userState = service.getUserState(pkg1.packageName)
+ assertThat(userState.packageName).isEqualTo(pkg1.packageName)
+ assertThat(userState.identifier).isEqualTo(pkg1.domainSetId)
+ assertThat(userState.isLinkHandlingAllowed).isEqualTo(true)
+ assertThat(userState.user.identifier).isEqualTo(USER_ID)
+ assertThat(userState.hostToStateMap).containsExactlyEntriesIn(mapOf(
+ DOMAIN_1 to DOMAIN_STATE_VERIFIED,
+ DOMAIN_2 to DOMAIN_STATE_NONE,
+ ))
+
+ assertThat(service.queryValidVerificationPackageNames())
+ .containsExactly(pkg1.packageName)
+
+ // Test that the pre-verified state can be overwritten to be disapproved
+ service.setDomainVerificationStatusInternal(
+ PKG_ONE,
+ DomainVerificationState.STATE_DENIED,
+ ArraySet(setOf(DOMAIN_1, DOMAIN_2)))
+ val infoUpdated = service.getInfo(pkg1.packageName)
+ assertThat(infoUpdated.hostToStateMap).containsExactlyEntriesIn(mapOf(
+ DOMAIN_1 to STATE_UNMODIFIABLE,
+ DOMAIN_2 to STATE_UNMODIFIABLE,
+ ))
+ }
+
/**
* Shared string that contains invalid [DOMAIN_3] and [DOMAIN_4] which should be stripped from
* the final state.
@@ -447,7 +492,7 @@
val map = mutableMapOf<String, PackageStateInternal>()
val service = makeService { map[it] }
- service.addPackage(pkgBefore)
+ service.addPackage(pkgBefore, null)
// Only insert the package after addPackage call to ensure the service doesn't access
// a live package inside the addPackage logic. It should only use the provided input.
@@ -482,7 +527,7 @@
map[pkgName] = pkgAfter
- service.migrateState(pkgBefore, pkgAfter)
+ service.migrateState(pkgBefore, pkgAfter, null)
assertThat(service.getInfo(pkgName).hostToStateMap).containsExactlyEntriesIn(mapOf(
DOMAIN_1 to STATE_UNMODIFIABLE,
@@ -503,7 +548,7 @@
val map = mutableMapOf<String, PackageStateInternal>()
val service = makeService { map[it] }
- service.addPackage(pkgBefore)
+ service.addPackage(pkgBefore, null)
// Only insert the package after addPackage call to ensure the service doesn't access
// a live package inside the addPackage logic. It should only use the provided input.
@@ -522,7 +567,7 @@
// Now remove the package because migrateState shouldn't use it either
map.remove(pkgName)
- service.migrateState(pkgBefore, pkgAfter)
+ service.migrateState(pkgBefore, pkgAfter, null)
map[pkgName] = pkgAfter
@@ -550,7 +595,7 @@
val map = mutableMapOf<String, PackageStateInternal>()
val service = makeService { map[it] }
- service.addPackage(pkgBefore)
+ service.addPackage(pkgBefore, null)
// Only insert the package after addPackage call to ensure the service doesn't access
// a live package inside the addPackage logic. It should only use the provided input.
@@ -571,7 +616,7 @@
// Now remove the package because migrateState shouldn't use it either
map.remove(pkgName)
- service.migrateState(pkgBefore, pkgAfter)
+ service.migrateState(pkgBefore, pkgAfter, null)
map[pkgName] = pkgAfter
@@ -596,7 +641,7 @@
val map = mutableMapOf<String, PackageStateInternal>()
val service = makeService { map[it] }
- service.addPackage(pkgBefore)
+ service.addPackage(pkgBefore, null)
// Only insert the package after addPackage call to ensure the service doesn't access
// a live package inside the addPackage logic. It should only use the provided input.
@@ -615,7 +660,7 @@
// Now remove the package because migrateState shouldn't use it either
map.remove(pkgName)
- service.migrateState(pkgBefore, pkgAfter)
+ service.migrateState(pkgBefore, pkgAfter, null)
map[pkgName] = pkgAfter
@@ -640,7 +685,7 @@
val map = mutableMapOf<String, PackageStateInternal>()
val service = makeService { map[it] }
- service.addPackage(pkgBefore)
+ service.addPackage(pkgBefore, null)
// Only insert the package after addPackage call to ensure the service doesn't access
// a live package inside the addPackage logic. It should only use the provided input.
@@ -667,7 +712,7 @@
// Now remove the package because migrateState shouldn't use it either
map.remove(pkgName)
- service.migrateState(pkgBefore, pkgAfter)
+ service.migrateState(pkgBefore, pkgAfter, null)
map[pkgName] = pkgAfter
@@ -685,6 +730,30 @@
}
@Test
+ fun migratePackageWithPreVerifiedDomains() {
+ val pkgName = PKG_ONE
+ val pkgBefore = mockPkgState(pkgName, UUID_ONE, SIGNATURE_ONE, emptyList())
+ val pkgAfter = mockPkgState(pkgName, UUID_TWO, SIGNATURE_TWO, listOf(DOMAIN_1, DOMAIN_2))
+
+ val map = mutableMapOf<String, PackageStateInternal>()
+ val service = makeService { map[it] }
+ service.addPackage(pkgBefore, null)
+ service.migrateState(pkgBefore, pkgAfter, DomainSet(setOf(DOMAIN_1, DOMAIN_3)))
+
+ map[pkgName] = pkgAfter
+
+ assertThat(service.getInfo(pkgName).hostToStateMap).containsExactlyEntriesIn(mapOf(
+ DOMAIN_1 to STATE_MODIFIABLE_VERIFIED,
+ DOMAIN_2 to STATE_NO_RESPONSE,
+ ))
+ assertThat(service.getUserState(pkgName).hostToStateMap).containsExactlyEntriesIn(mapOf(
+ DOMAIN_1 to DOMAIN_STATE_VERIFIED,
+ DOMAIN_2 to DOMAIN_STATE_NONE,
+ ))
+ assertThat(service.queryValidVerificationPackageNames()).containsExactly(pkgName)
+ }
+
+ @Test
fun backupAndRestore() {
// This test acts as a proxy for true user restore through PackageManager,
// as that's much harder to test for real.
@@ -694,8 +763,8 @@
listOf(DOMAIN_1, DOMAIN_2, DOMAIN_3))
val serviceBefore = makeService(pkg1, pkg2)
val computerBefore = mockComputer(pkg1, pkg2)
- serviceBefore.addPackage(pkg1)
- serviceBefore.addPackage(pkg2)
+ serviceBefore.addPackage(pkg1, null)
+ serviceBefore.addPackage(pkg2, null)
serviceBefore.setStatus(pkg1.domainSetId, setOf(DOMAIN_1), STATE_SUCCESS)
serviceBefore.setDomainVerificationLinkHandlingAllowed(pkg1.packageName, false, 10)
@@ -748,8 +817,8 @@
val serviceAfter = makeService(pkg1, pkg2)
val computerAfter = mockComputer(pkg1, pkg2)
- serviceAfter.addPackage(pkg1)
- serviceAfter.addPackage(pkg2)
+ serviceAfter.addPackage(pkg1, null)
+ serviceAfter.addPackage(pkg2, null)
// Check the state is default before the restoration applies
listOf(0, 10).forEach {
@@ -858,8 +927,8 @@
)
val service = makeService(pkg1, pkg2)
- service.addPackage(pkg1)
- service.addPackage(pkg2)
+ service.addPackage(pkg1, null)
+ service.addPackage(pkg2, null)
// Approve domain 1, 3, and 4 for package 2 for both users
USER_IDS.forEach {
diff --git a/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/verify/domain/DomainVerificationSettingsMutationTest.kt b/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/verify/domain/DomainVerificationSettingsMutationTest.kt
index a5c4f6c..9748307 100644
--- a/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/verify/domain/DomainVerificationSettingsMutationTest.kt
+++ b/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/verify/domain/DomainVerificationSettingsMutationTest.kt
@@ -106,7 +106,7 @@
fun service(name: String, block: DomainVerificationService.() -> Unit) =
Params(makeService, name) { service ->
service.proxy = proxy
- service.addPackage(mockPkgState())
+ service.addPackage(mockPkgState(), null)
service.block()
}
diff --git a/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/verify/domain/DomainVerificationUserSelectionOverrideTest.kt b/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/verify/domain/DomainVerificationUserSelectionOverrideTest.kt
index ae570a3..56ab841 100644
--- a/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/verify/domain/DomainVerificationUserSelectionOverrideTest.kt
+++ b/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/verify/domain/DomainVerificationUserSelectionOverrideTest.kt
@@ -97,8 +97,8 @@
}
}
})
- addPackage(pkg1)
- addPackage(pkg2)
+ addPackage(pkg1, null)
+ addPackage(pkg2, null)
// Starting state for all tests is to have domain 1 enabled for the first package
setDomainVerificationUserSelection(UUID_ONE, setOf(DOMAIN_ONE), true, USER_ID)
diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java b/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java
index 42bcb33..b142334 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java
@@ -72,6 +72,7 @@
import android.content.ContextWrapper;
import android.content.pm.PackageManager;
import android.content.pm.PackageManagerInternal;
+import android.content.pm.UserInfo;
import android.content.res.Resources;
import android.graphics.Insets;
import android.graphics.Rect;
@@ -102,7 +103,9 @@
import android.os.Process;
import android.os.RemoteException;
import android.os.SystemProperties;
+import android.os.UserManager;
import android.platform.test.flag.junit.SetFlagsRule;
+import android.provider.Settings;
import android.util.SparseArray;
import android.view.ContentRecordingSession;
import android.view.Display;
@@ -210,6 +213,10 @@
private int mPreferredHdrOutputType;
+ private Handler mPowerHandler;
+
+ private UserManager mUserManager;
+
private final DisplayManagerService.Injector mShortMockedInjector =
new DisplayManagerService.Injector() {
@Override
@@ -370,11 +377,14 @@
mContext = spy(new ContextWrapper(
ApplicationProvider.getApplicationContext().createDisplayContext(display)));
mResources = Mockito.spy(mContext.getResources());
+ mPowerHandler = new Handler(Looper.getMainLooper());
manageDisplaysPermission(/* granted= */ false);
when(mContext.getResources()).thenReturn(mResources);
+ mUserManager = Mockito.spy(mContext.getSystemService(UserManager.class));
VirtualDeviceManager vdm = new VirtualDeviceManager(mIVirtualDeviceManager, mContext);
when(mContext.getSystemService(VirtualDeviceManager.class)).thenReturn(vdm);
+ when(mContext.getSystemService(UserManager.class)).thenReturn(mUserManager);
// Disable binder caches in this process.
PropertyInvalidatedCache.disableForTestMode();
setUpDisplay();
@@ -2789,6 +2799,85 @@
assertThat(display.getDisplayOffloadSessionLocked()).isNull();
}
+ @Test
+ public void testOnUserSwitching_UpdatesBrightness() {
+ DisplayManagerService displayManager =
+ new DisplayManagerService(mContext, mShortMockedInjector);
+ DisplayManagerInternal localService = displayManager.new LocalService();
+ DisplayManagerService.BinderService displayManagerBinderService =
+ displayManager.new BinderService();
+ registerDefaultDisplays(displayManager);
+ initDisplayPowerController(localService);
+
+ float brightness1 = 0.3f;
+ float brightness2 = 0.45f;
+
+ int userId1 = 123;
+ int userId2 = 456;
+ UserInfo userInfo1 = new UserInfo();
+ userInfo1.id = userId1;
+ UserInfo userInfo2 = new UserInfo();
+ userInfo2.id = userId2;
+ when(mUserManager.getUserSerialNumber(userId1)).thenReturn(12345);
+ when(mUserManager.getUserSerialNumber(userId2)).thenReturn(45678);
+ final SystemService.TargetUser from = new SystemService.TargetUser(userInfo1);
+ final SystemService.TargetUser to = new SystemService.TargetUser(userInfo2);
+
+ // The same brightness will be restored for a user only if auto-brightness is off,
+ // otherwise the current lux will be used to determine the brightness.
+ Settings.System.putInt(mContext.getContentResolver(),
+ Settings.System.SCREEN_BRIGHTNESS_MODE,
+ Settings.System.SCREEN_BRIGHTNESS_MODE_MANUAL);
+
+ displayManager.onUserSwitching(to, from);
+ waitForIdleHandler(mPowerHandler);
+ displayManagerBinderService.setBrightness(Display.DEFAULT_DISPLAY, brightness1);
+ displayManager.onUserSwitching(from, to);
+ waitForIdleHandler(mPowerHandler);
+ displayManagerBinderService.setBrightness(Display.DEFAULT_DISPLAY, brightness2);
+
+ displayManager.onUserSwitching(to, from);
+ waitForIdleHandler(mPowerHandler);
+ assertEquals(brightness1,
+ displayManagerBinderService.getBrightness(Display.DEFAULT_DISPLAY),
+ FLOAT_TOLERANCE);
+
+ displayManager.onUserSwitching(from, to);
+ waitForIdleHandler(mPowerHandler);
+ assertEquals(brightness2,
+ displayManagerBinderService.getBrightness(Display.DEFAULT_DISPLAY),
+ FLOAT_TOLERANCE);
+ }
+
+ @Test
+ public void testOnUserSwitching_brightnessForNewUserIsDefault() {
+ DisplayManagerService displayManager =
+ new DisplayManagerService(mContext, mShortMockedInjector);
+ DisplayManagerInternal localService = displayManager.new LocalService();
+ DisplayManagerService.BinderService displayManagerBinderService =
+ displayManager.new BinderService();
+ registerDefaultDisplays(displayManager);
+ initDisplayPowerController(localService);
+
+ int userId1 = 123;
+ int userId2 = 456;
+ UserInfo userInfo1 = new UserInfo();
+ userInfo1.id = userId1;
+ UserInfo userInfo2 = new UserInfo();
+ userInfo2.id = userId2;
+ when(mUserManager.getUserSerialNumber(userId1)).thenReturn(12345);
+ when(mUserManager.getUserSerialNumber(userId2)).thenReturn(45678);
+ final SystemService.TargetUser from = new SystemService.TargetUser(userInfo1);
+ final SystemService.TargetUser to = new SystemService.TargetUser(userInfo2);
+
+ displayManager.onUserSwitching(from, to);
+ waitForIdleHandler(mPowerHandler);
+ assertEquals(displayManagerBinderService.getDisplayInfo(Display.DEFAULT_DISPLAY)
+ .brightnessDefault,
+ displayManagerBinderService.getBrightness(Display.DEFAULT_DISPLAY),
+ FLOAT_TOLERANCE);
+ }
+
private void initDisplayPowerController(DisplayManagerInternal localService) {
localService.initPowerManagement(new DisplayManagerInternal.DisplayPowerCallbacks() {
@Override
@@ -2820,7 +2909,7 @@
public void releaseSuspendBlocker(String id) {
}
- }, new Handler(Looper.getMainLooper()), mSensorManager);
+ }, mPowerHandler, mSensorManager);
}
private void testDisplayInfoFrameRateOverrideModeCompat(boolean compatChangeEnabled) {
diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java b/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java
index 88a9758..57b8632 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java
@@ -227,18 +227,14 @@
advanceTime(1);
// two times, one for unfinished business and one for proximity
- verify(mHolder.wakelockController, times(2)).acquireWakelock(
+ verify(mHolder.wakelockController).acquireWakelock(
WakelockController.WAKE_LOCK_UNFINISHED_BUSINESS);
verify(mHolder.wakelockController).acquireWakelock(
WakelockController.WAKE_LOCK_PROXIMITY_DEBOUNCE);
mHolder.dpc.stop();
advanceTime(1);
- // two times, one for unfinished business and one for proximity
- verify(mHolder.wakelockController, times(2)).acquireWakelock(
- WakelockController.WAKE_LOCK_UNFINISHED_BUSINESS);
- verify(mHolder.wakelockController).acquireWakelock(
- WakelockController.WAKE_LOCK_PROXIMITY_DEBOUNCE);
+ verify(mHolder.wakelockController).releaseAll();
}
@Test
@@ -515,9 +511,8 @@
verify(mHolder.animator).animateTo(eq(leadBrightness), anyFloat(),
eq(BRIGHTNESS_RAMP_RATE_FAST_INCREASE), eq(false));
- // One triggered by handleBrightnessModeChange, another triggered by setBrightnessToFollow
- verify(followerDpc.hbmController, times(2)).onAmbientLuxChange(ambientLux);
- verify(followerDpc.animator, times(2)).animateTo(eq(followerBrightness), anyFloat(),
+ verify(followerDpc.hbmController).onAmbientLuxChange(ambientLux);
+ verify(followerDpc.animator).animateTo(eq(followerBrightness), anyFloat(),
eq(BRIGHTNESS_RAMP_RATE_FAST_INCREASE), eq(false));
when(mHolder.displayPowerState.getScreenBrightness()).thenReturn(leadBrightness);
@@ -811,7 +806,7 @@
DisplayPowerRequest dpr = new DisplayPowerRequest();
mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
advanceTime(1); // Run updatePowerState
- verify(mHolder.displayPowerState, times(2)).setScreenState(anyInt());
+ verify(mHolder.displayPowerState).setScreenState(anyInt());
mHolder = createDisplayPowerController(42, UNIQUE_ID);
@@ -1024,15 +1019,14 @@
mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
advanceTime(1); // Run updatePowerState
- // One triggered by the test, the other by handleBrightnessModeChange
- verify(mHolder.automaticBrightnessController, times(2)).configure(
+ verify(mHolder.automaticBrightnessController).configure(
AutomaticBrightnessController.AUTO_BRIGHTNESS_DISABLED,
/* configuration= */ null, PowerManager.BRIGHTNESS_INVALID_FLOAT,
/* userChangedBrightness= */ false, /* adjustment= */ 0,
/* userChangedAutoBrightnessAdjustment= */ false, DisplayPowerRequest.POLICY_BRIGHT,
/* shouldResetShortTermModel= */ false
);
- verify(mHolder.hbmController, times(2))
+ verify(mHolder.hbmController)
.setAutoBrightnessEnabled(AutomaticBrightnessController.AUTO_BRIGHTNESS_DISABLED);
}
@@ -1098,8 +1092,7 @@
mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
advanceTime(1); // Run updatePowerState
- // One triggered by the test, the other by handleBrightnessModeChange
- verify(mHolder.automaticBrightnessController, times(2)).configure(
+ verify(mHolder.automaticBrightnessController).configure(
AutomaticBrightnessController.AUTO_BRIGHTNESS_DISABLED,
/* configuration= */ null, PowerManager.BRIGHTNESS_INVALID_FLOAT,
/* userChangedBrightness= */ false, /* adjustment= */ 0,
@@ -1136,8 +1129,7 @@
DisplayPowerRequest dpr = new DisplayPowerRequest();
mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
advanceTime(1); // Run updatePowerState
- // One triggered by handleBrightnessModeChange, another triggered by onDisplayChanged
- verify(mHolder.animator, times(2)).animateTo(eq(newBrightness), anyFloat(), anyFloat(),
+ verify(mHolder.animator).animateTo(eq(newBrightness), anyFloat(), anyFloat(),
eq(false));
}
@@ -1210,8 +1202,9 @@
when(mHolder.hbmController.getCurrentBrightnessMax()).thenReturn(clampedBrightness);
mHolder.dpc.setBrightness(PowerManager.BRIGHTNESS_MAX);
+ mHolder.dpc.setBrightness(0.8f, /* userSerial= */ 123);
- verify(mHolder.brightnessSetting).setBrightness(clampedBrightness);
+ verify(mHolder.brightnessSetting, times(2)).setBrightness(clampedBrightness);
}
@Test
@@ -1564,9 +1557,7 @@
mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
advanceTime(1); // Run updatePowerState
- // One triggered by handleBrightnessModeChange, another triggered by
- // setBrightnessFromOffload
- verify(mHolder.animator, times(2)).animateTo(eq(brightness), anyFloat(),
+ verify(mHolder.animator).animateTo(eq(brightness), anyFloat(),
eq(BRIGHTNESS_RAMP_RATE_FAST_INCREASE), eq(false));
}
@@ -1580,9 +1571,7 @@
mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
advanceTime(1); // Run updatePowerState
- // One triggered by handleBrightnessModeChange, another triggered by requestPowerState
- verify(mHolder.automaticBrightnessController, times(2))
- .switchMode(AUTO_BRIGHTNESS_MODE_DOZE);
+ verify(mHolder.automaticBrightnessController).switchMode(AUTO_BRIGHTNESS_MODE_DOZE);
// Back to default mode
when(mHolder.displayPowerState.getScreenState()).thenReturn(Display.STATE_ON);
@@ -1620,6 +1609,62 @@
.switchMode(AUTO_BRIGHTNESS_MODE_DOZE);
}
+ @Test
+ public void testOnSwitchUserUpdatesBrightness() {
+ int userSerial = 12345;
+ float brightness = 0.65f;
+
+ mHolder.dpc.onSwitchUser(/* newUserId= */ 15, userSerial, brightness);
+ advanceTime(1);
+
+ verify(mHolder.brightnessSetting).setUserSerial(userSerial);
+ verify(mHolder.brightnessSetting).setBrightness(brightness);
+ }
+
+ @Test
+ public void testOnSwitchUserDoesNotAddUserDataPoint() {
+ Settings.System.putInt(mContext.getContentResolver(),
+ Settings.System.SCREEN_BRIGHTNESS_MODE,
+ Settings.System.SCREEN_BRIGHTNESS_MODE_AUTOMATIC);
+ int userSerial = 12345;
+ float brightness = 0.65f;
+ when(mHolder.automaticBrightnessController.hasValidAmbientLux()).thenReturn(true);
+ when(mHolder.automaticBrightnessController.convertToAdjustedNits(brightness))
+ .thenReturn(500f);
+ when(mHolder.displayPowerState.getScreenState()).thenReturn(Display.STATE_ON);
+ DisplayPowerRequest dpr = new DisplayPowerRequest();
+ mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
+ advanceTime(1); // Run updatePowerState
+ ArgumentCaptor<BrightnessSetting.BrightnessSettingListener> listenerCaptor =
+ ArgumentCaptor.forClass(BrightnessSetting.BrightnessSettingListener.class);
+ verify(mHolder.brightnessSetting).registerListener(listenerCaptor.capture());
+ BrightnessSetting.BrightnessSettingListener listener = listenerCaptor.getValue();
+
+ mHolder.dpc.onSwitchUser(/* newUserId= */ 15, userSerial, brightness);
+ when(mHolder.brightnessSetting.getBrightness()).thenReturn(brightness);
+ listener.onBrightnessChanged(brightness);
+ advanceTime(1); // Send messages, run updatePowerState
+
+ verify(mHolder.automaticBrightnessController, never()).configure(
+ /* state= */ anyInt(),
+ /* configuration= */ any(),
+ eq(brightness),
+ /* userChangedBrightness= */ eq(true),
+ /* adjustment= */ anyFloat(),
+ /* userChangedAutoBrightnessAdjustment= */ anyBoolean(),
+ /* displayPolicy= */ anyInt(),
+ /* shouldResetShortTermModel= */ anyBoolean());
+ verify(mBrightnessTrackerMock, never()).notifyBrightnessChanged(
+ /* brightness= */ anyFloat(),
+ /* userInitiated= */ eq(true),
+ /* powerBrightnessFactor= */ anyFloat(),
+ /* wasShortTermModelActive= */ anyBoolean(),
+ /* isDefaultBrightnessConfig= */ anyBoolean(),
+ /* uniqueDisplayId= */ any(),
+ /* luxValues */ any(),
+ /* luxTimestamps= */ any());
+ }
+
/**
* Creates a mock and registers it to {@link LocalServices}.
*/
diff --git a/services/tests/displayservicetests/src/com/android/server/display/LocalDisplayAdapterTest.java b/services/tests/displayservicetests/src/com/android/server/display/LocalDisplayAdapterTest.java
index c92ce25..b99ecf3 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/LocalDisplayAdapterTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/LocalDisplayAdapterTest.java
@@ -178,8 +178,8 @@
.thenReturn(mockArray);
when(mMockedResources.obtainTypedArray(R.array.config_displayCutoutSideOverrideArray))
.thenReturn(mockArray);
- when(mMockedResources.getStringArray(R.array.config_mainBuiltInDisplayCutoutSideOverride))
- .thenReturn(new String[]{});
+ when(mMockedResources.getIntArray(R.array.config_mainBuiltInDisplayCutoutSideOverride))
+ .thenReturn(new int[]{});
when(mMockedResources.obtainTypedArray(R.array.config_waterfallCutoutArray))
.thenReturn(mockArray);
when(mMockedResources.obtainTypedArray(R.array.config_roundedCornerRadiusArray))
diff --git a/services/tests/displayservicetests/src/com/android/server/display/brightness/DisplayBrightnessControllerTest.java b/services/tests/displayservicetests/src/com/android/server/display/brightness/DisplayBrightnessControllerTest.java
index 2d0c3fd..289d54b 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/brightness/DisplayBrightnessControllerTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/brightness/DisplayBrightnessControllerTest.java
@@ -19,7 +19,6 @@
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
-import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.clearInvocations;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
@@ -252,7 +251,6 @@
0.0f);
verify(mBrightnessChangeExecutor).execute(mOnBrightnessChangeRunnable);
verify(mBrightnessSetting).setBrightness(brightnessValue);
- verify(mBrightnessSetting).setUserSerial(anyInt());
// Does nothing if the value is invalid
mDisplayBrightnessController.updateScreenBrightnessSetting(Float.NaN);
diff --git a/services/tests/displayservicetests/src/com/android/server/display/brightness/strategy/AutomaticBrightnessStrategyTest.java b/services/tests/displayservicetests/src/com/android/server/display/brightness/strategy/AutomaticBrightnessStrategyTest.java
index 78ec2ff3..5408e11 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/brightness/strategy/AutomaticBrightnessStrategyTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/brightness/strategy/AutomaticBrightnessStrategyTest.java
@@ -104,7 +104,7 @@
int policy = DisplayManagerInternal.DisplayPowerRequest.POLICY_BRIGHT;
float lastUserSetBrightness = 0.2f;
boolean userSetBrightnessChanged = true;
- mAutomaticBrightnessStrategy.updatePendingAutoBrightnessAdjustments(true);
+ mAutomaticBrightnessStrategy.updatePendingAutoBrightnessAdjustments();
mAutomaticBrightnessStrategy.setAutoBrightnessState(targetDisplayState,
allowAutoBrightnessWhileDozing, brightnessReason, policy, lastUserSetBrightness,
userSetBrightnessChanged);
@@ -127,7 +127,7 @@
int policy = DisplayManagerInternal.DisplayPowerRequest.POLICY_OFF;
float lastUserSetBrightness = 0.2f;
boolean userSetBrightnessChanged = true;
- mAutomaticBrightnessStrategy.updatePendingAutoBrightnessAdjustments(true);
+ mAutomaticBrightnessStrategy.updatePendingAutoBrightnessAdjustments();
mAutomaticBrightnessStrategy.setAutoBrightnessState(targetDisplayState,
allowAutoBrightnessWhileDozing, brightnessReason, policy, lastUserSetBrightness,
userSetBrightnessChanged);
@@ -150,7 +150,7 @@
int policy = DisplayManagerInternal.DisplayPowerRequest.POLICY_DOZE;
float lastUserSetBrightness = 0.2f;
boolean userSetBrightnessChanged = true;
- mAutomaticBrightnessStrategy.updatePendingAutoBrightnessAdjustments(true);
+ mAutomaticBrightnessStrategy.updatePendingAutoBrightnessAdjustments();
mAutomaticBrightnessStrategy.setAutoBrightnessState(targetDisplayState,
allowAutoBrightnessWhileDozing, brightnessReason, policy, lastUserSetBrightness,
userSetBrightnessChanged);
@@ -173,7 +173,7 @@
int policy = DisplayManagerInternal.DisplayPowerRequest.POLICY_BRIGHT;
float lastUserSetBrightness = 0.2f;
boolean userSetBrightnessChanged = true;
- mAutomaticBrightnessStrategy.updatePendingAutoBrightnessAdjustments(true);
+ mAutomaticBrightnessStrategy.updatePendingAutoBrightnessAdjustments();
mAutomaticBrightnessStrategy.setAutoBrightnessState(targetDisplayState,
allowAutoBrightnessWhileDozing, brightnessReason, policy, lastUserSetBrightness,
userSetBrightnessChanged);
@@ -196,7 +196,7 @@
int policy = DisplayManagerInternal.DisplayPowerRequest.POLICY_BRIGHT;
float lastUserSetBrightness = 0.2f;
boolean userSetBrightnessChanged = true;
- mAutomaticBrightnessStrategy.updatePendingAutoBrightnessAdjustments(true);
+ mAutomaticBrightnessStrategy.updatePendingAutoBrightnessAdjustments();
mAutomaticBrightnessStrategy.setAutoBrightnessState(targetDisplayState,
allowAutoBrightnessWhileDozing, brightnessReason, policy, lastUserSetBrightness,
userSetBrightnessChanged);
@@ -221,7 +221,7 @@
boolean userSetBrightnessChanged = true;
Settings.System.putFloat(mContext.getContentResolver(),
Settings.System.SCREEN_AUTO_BRIGHTNESS_ADJ, 0.4f);
- mAutomaticBrightnessStrategy.updatePendingAutoBrightnessAdjustments(false);
+ mAutomaticBrightnessStrategy.updatePendingAutoBrightnessAdjustments();
mAutomaticBrightnessStrategy.setAutoBrightnessState(targetDisplayState,
allowAutoBrightnessWhileDozing, brightnessReason, policy, lastUserSetBrightness,
userSetBrightnessChanged);
@@ -247,7 +247,7 @@
float pendingBrightnessAdjustment = 0.1f;
Settings.System.putFloat(mContext.getContentResolver(),
Settings.System.SCREEN_AUTO_BRIGHTNESS_ADJ, pendingBrightnessAdjustment);
- mAutomaticBrightnessStrategy.updatePendingAutoBrightnessAdjustments(false);
+ mAutomaticBrightnessStrategy.updatePendingAutoBrightnessAdjustments();
mAutomaticBrightnessStrategy.setAutoBrightnessState(targetDisplayState,
allowAutoBrightnessWhileDozing, brightnessReason, policy, lastUserSetBrightness,
userSetBrightnessChanged);
@@ -411,7 +411,7 @@
private void setPendingAutoBrightnessAdjustment(float pendingAutoBrightnessAdjustment) {
Settings.System.putFloat(mContext.getContentResolver(),
Settings.System.SCREEN_AUTO_BRIGHTNESS_ADJ, pendingAutoBrightnessAdjustment);
- mAutomaticBrightnessStrategy.updatePendingAutoBrightnessAdjustments(false);
+ mAutomaticBrightnessStrategy.updatePendingAutoBrightnessAdjustments();
}
private void setTemporaryAutoBrightnessAdjustment(float temporaryAutoBrightnessAdjustment) {
diff --git a/services/tests/mockingservicestests/Android.bp b/services/tests/mockingservicestests/Android.bp
index d928306..9f97551 100644
--- a/services/tests/mockingservicestests/Android.bp
+++ b/services/tests/mockingservicestests/Android.bp
@@ -73,6 +73,7 @@
"testng",
"compatibility-device-util-axt",
"flag-junit",
+ "am_flags_lib",
],
libs: [
diff --git a/services/tests/mockingservicestests/src/com/android/server/am/BaseBroadcastQueueTest.java b/services/tests/mockingservicestests/src/com/android/server/am/BaseBroadcastQueueTest.java
index f875f65..fb47aa8 100644
--- a/services/tests/mockingservicestests/src/com/android/server/am/BaseBroadcastQueueTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/am/BaseBroadcastQueueTest.java
@@ -38,6 +38,8 @@
import android.os.HandlerThread;
import android.os.TestLooperManager;
import android.os.UserHandle;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
import android.provider.Settings;
import android.util.SparseArray;
@@ -93,6 +95,9 @@
.spyStatic(ProcessList.class)
.build();
+ @Rule
+ public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
+
final BroadcastQueue[] mBroadcastQueues = new BroadcastQueue[1];
@Mock
diff --git a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java
index e7aaed4..3f6117b 100644
--- a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java
@@ -75,6 +75,8 @@
import android.os.PowerExemptionManager;
import android.os.SystemClock;
import android.os.UserHandle;
+import android.platform.test.annotations.RequiresFlagsEnabled;
+import android.util.ArrayMap;
import android.util.Log;
import android.util.Pair;
import android.util.proto.ProtoOutputStream;
@@ -135,6 +137,17 @@
ProcessStartBehavior.SUCCESS);
/**
+ * Map of processes to behaviors indicating how the new processes should behave as needed
+ * by the tests.
+ */
+ private ArrayMap<String, ProcessBehavior> mNewProcessBehaviors = new ArrayMap<>();
+
+ /**
+ * Map of processes to behaviors indicating how the new process starts should result in.
+ */
+ private ArrayMap<String, ProcessStartBehavior> mNewProcessStartBehaviors = new ArrayMap<>();
+
+ /**
* Collection of all active processes during current test run.
*/
private List<ProcessRecord> mActiveProcesses = new ArrayList<>();
@@ -161,15 +174,17 @@
Log.v(TAG, "Intercepting startProcessLocked() for "
+ Arrays.toString(invocation.getArguments()));
assertHealth();
- final ProcessStartBehavior behavior = mNextProcessStartBehavior
- .getAndSet(ProcessStartBehavior.SUCCESS);
+ final String processName = invocation.getArgument(0);
+ final ProcessStartBehavior behavior = mNewProcessStartBehaviors.getOrDefault(
+ processName, mNextProcessStartBehavior.getAndSet(ProcessStartBehavior.SUCCESS));
if (behavior == ProcessStartBehavior.FAIL_NULL) {
return null;
}
- final String processName = invocation.getArgument(0);
final ApplicationInfo ai = invocation.getArgument(1);
+ final ProcessBehavior processBehavior = mNewProcessBehaviors.getOrDefault(
+ processName, ProcessBehavior.NORMAL);
final ProcessRecord res = makeActiveProcessRecord(ai, processName,
- ProcessBehavior.NORMAL, UnaryOperator.identity());
+ processBehavior, UnaryOperator.identity());
final ProcessRecord deliverRes;
switch (behavior) {
case SUCCESS_PREDECESSOR:
@@ -236,10 +251,6 @@
}).when(mAms).registerUidObserver(any(), anyInt(),
eq(ActivityManager.PROCESS_STATE_TOP), any());
- mConstants.TIMEOUT = 200;
- mConstants.ALLOW_BG_ACTIVITY_START_TIMEOUT = 0;
- mConstants.PENDING_COLD_START_CHECK_INTERVAL_MILLIS = 500;
-
final BroadcastHistory emptyHistory = new BroadcastHistory(mConstants) {
public void addBroadcastToHistoryLocked(BroadcastRecord original) {
// Ignored
@@ -259,6 +270,12 @@
mBroadcastQueues[0] = mQueue;
mQueue.start(mContext.getContentResolver());
+
+ // Set the constants after invoking BroadcastQueue.start() to ensure they don't
+ // get overridden by the defaults.
+ mConstants.TIMEOUT = 200;
+ mConstants.ALLOW_BG_ACTIVITY_START_TIMEOUT = 0;
+ mConstants.PENDING_COLD_START_CHECK_INTERVAL_MILLIS = 500;
}
@After
@@ -272,6 +289,8 @@
assertEquals(app.toShortString(), ProcessList.SCHED_GROUP_UNDEFINED,
mQueue.getPreferredSchedulingGroupLocked(app));
}
+ mNewProcessBehaviors.clear();
+ mNewProcessStartBehaviors.clear();
}
@Override
@@ -953,6 +972,40 @@
}
/**
+ * Verify that we handle manifest receivers in a process that always
+ * responds with {@link DeadObjectException} even after restarting.
+ */
+ @Test
+ @RequiresFlagsEnabled(Flags.FLAG_AVOID_REPEATED_BCAST_RE_ENQUEUES)
+ public void testRepeatedDead_Manifest() throws Exception {
+ final ProcessRecord callerApp = makeActiveProcessRecord(PACKAGE_RED);
+ mNewProcessBehaviors.put(PACKAGE_GREEN, ProcessBehavior.DEAD);
+
+ final Intent airplane = new Intent(Intent.ACTION_AIRPLANE_MODE_CHANGED);
+ enqueueBroadcast(makeBroadcastRecord(airplane, callerApp, List.of(
+ withPriority(makeManifestReceiver(PACKAGE_GREEN, CLASS_GREEN), 10),
+ withPriority(makeManifestReceiver(PACKAGE_BLUE, CLASS_BLUE), 0))));
+ final Intent timezone = new Intent(Intent.ACTION_TIMEZONE_CHANGED);
+ enqueueBroadcast(makeBroadcastRecord(timezone, callerApp,
+ List.of(makeManifestReceiver(PACKAGE_YELLOW, CLASS_YELLOW))));
+ waitForIdle();
+
+ final ProcessRecord receiverGreenApp = mAms.getProcessRecordLocked(PACKAGE_GREEN,
+ getUidForPackage(PACKAGE_GREEN));
+ // Modern queue always kills the target process when broadcast delivery fails, where as
+ // the legacy queue leaves the process killing task to AMS
+ if (mImpl == Impl.MODERN) {
+ assertNull(receiverGreenApp);
+ }
+ final ProcessRecord receiverBlueApp = mAms.getProcessRecordLocked(PACKAGE_BLUE,
+ getUidForPackage(PACKAGE_BLUE));
+ verifyScheduleReceiver(receiverBlueApp, airplane);
+ final ProcessRecord receiverYellowApp = mAms.getProcessRecordLocked(PACKAGE_YELLOW,
+ getUidForPackage(PACKAGE_YELLOW));
+ verifyScheduleReceiver(receiverYellowApp, timezone);
+ }
+
+ /**
* Verify that we handle the system failing to start a process.
*/
@Test
@@ -1140,6 +1193,49 @@
}
/**
+ * Verify that when BroadcastQueue doesn't get notified when a process gets killed repeatedly,
+ * it doesn't get stuck.
+ */
+ @Test
+ @RequiresFlagsEnabled(Flags.FLAG_AVOID_REPEATED_BCAST_RE_ENQUEUES)
+ public void testRepeatedKillWithoutNotify() throws Exception {
+ // Legacy queue does not handle repeated kills that don't get notified.
+ Assume.assumeTrue(mImpl == Impl.MODERN);
+
+ final ProcessRecord callerApp = makeActiveProcessRecord(PACKAGE_RED);
+ final ProcessRecord receiverBlueApp = makeActiveProcessRecord(PACKAGE_BLUE);
+
+ mNewProcessStartBehaviors.put(PACKAGE_GREEN, ProcessStartBehavior.KILLED_WITHOUT_NOTIFY);
+
+ final Intent airplane = new Intent(Intent.ACTION_AIRPLANE_MODE_CHANGED);
+ enqueueBroadcast(makeBroadcastRecord(airplane, callerApp, List.of(
+ withPriority(makeManifestReceiver(PACKAGE_GREEN, CLASS_GREEN), 10),
+ withPriority(makeRegisteredReceiver(receiverBlueApp), 5),
+ withPriority(makeManifestReceiver(PACKAGE_YELLOW, CLASS_YELLOW), 0))));
+
+ final Intent timezone = new Intent(Intent.ACTION_TIMEZONE_CHANGED);
+ enqueueBroadcast(makeBroadcastRecord(timezone, callerApp,
+ List.of(makeManifestReceiver(PACKAGE_ORANGE, CLASS_ORANGE))));
+
+ waitForIdle();
+ final ProcessRecord receiverGreenApp = mAms.getProcessRecordLocked(PACKAGE_GREEN,
+ getUidForPackage(PACKAGE_GREEN));
+ final ProcessRecord receiverYellowApp = mAms.getProcessRecordLocked(PACKAGE_YELLOW,
+ getUidForPackage(PACKAGE_YELLOW));
+ final ProcessRecord receiverOrangeApp = mAms.getProcessRecordLocked(PACKAGE_ORANGE,
+ getUidForPackage(PACKAGE_ORANGE));
+
+ // Modern queue always kills the target process when broadcast delivery fails, where as
+ // the legacy queue leaves the process killing task to AMS
+ if (mImpl == Impl.MODERN) {
+ assertNull(receiverGreenApp);
+ }
+ verifyScheduleRegisteredReceiver(times(1), receiverBlueApp, airplane);
+ verifyScheduleReceiver(times(1), receiverYellowApp, airplane);
+ verifyScheduleReceiver(times(1), receiverOrangeApp, timezone);
+ }
+
+ /**
* Verify that a broadcast sent to a frozen app, which gets killed as part of unfreezing
* process due to pending sync binder transactions, is delivered as expected.
*/
diff --git a/services/tests/mockingservicestests/src/com/android/server/job/JobSchedulerServiceTest.java b/services/tests/mockingservicestests/src/com/android/server/job/JobSchedulerServiceTest.java
index eaf0ffd..fab7610 100644
--- a/services/tests/mockingservicestests/src/com/android/server/job/JobSchedulerServiceTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/job/JobSchedulerServiceTest.java
@@ -29,6 +29,8 @@
import static com.android.server.job.JobSchedulerService.RARE_INDEX;
import static com.android.server.job.JobSchedulerService.sElapsedRealtimeClock;
import static com.android.server.job.JobSchedulerService.sUptimeMillisClock;
+import static com.android.server.job.Flags.FLAG_BATCH_ACTIVE_BUCKET_JOBS;
+import static com.android.server.job.Flags.FLAG_BATCH_CONNECTIVITY_JOBS_PER_NETWORK;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
@@ -42,6 +44,7 @@
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.app.ActivityManager;
@@ -55,17 +58,23 @@
import android.app.usage.UsageStatsManagerInternal;
import android.content.ComponentName;
import android.content.Context;
+import android.content.Intent;
import android.content.PermissionChecker;
import android.content.pm.PackageManager;
import android.content.pm.PackageManagerInternal;
import android.content.res.Resources;
import android.net.ConnectivityManager;
+import android.net.Network;
+import android.net.NetworkCapabilities;
import android.net.NetworkPolicyManager;
+import android.os.BatteryManager;
import android.os.BatteryManagerInternal;
+import android.os.BatteryManagerInternal.ChargingPolicyChangeListener;
import android.os.Looper;
import android.os.RemoteException;
import android.os.ServiceManager;
import android.os.SystemClock;
+import android.platform.test.flag.junit.SetFlagsRule;
import com.android.server.AppStateTracker;
import com.android.server.AppStateTrackerImpl;
@@ -82,7 +91,10 @@
import org.junit.After;
import org.junit.Before;
+import org.junit.Rule;
import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+import org.mockito.ArgumentMatchers;
import org.mockito.Mock;
import org.mockito.MockitoSession;
import org.mockito.quality.Strictness;
@@ -101,10 +113,17 @@
@Mock
private ActivityManagerInternal mActivityMangerInternal;
@Mock
+ private BatteryManagerInternal mBatteryManagerInternal;
+ @Mock
private Context mContext;
@Mock
private PackageManagerInternal mPackageManagerInternal;
+ @Rule
+ public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+
+ private ChargingPolicyChangeListener mChargingPolicyChangeListener;
+
private class TestJobSchedulerService extends JobSchedulerService {
TestJobSchedulerService(Context context) {
super(context);
@@ -128,7 +147,7 @@
.when(() -> LocalServices.getService(ActivityManagerInternal.class));
doReturn(mock(AppStandbyInternal.class))
.when(() -> LocalServices.getService(AppStandbyInternal.class));
- doReturn(mock(BatteryManagerInternal.class))
+ doReturn(mBatteryManagerInternal)
.when(() -> LocalServices.getService(BatteryManagerInternal.class));
doReturn(mPackageManagerInternal)
.when(() -> LocalServices.getService(PackageManagerInternal.class));
@@ -177,8 +196,17 @@
// Called by DeviceIdlenessTracker
when(mContext.getSystemService(UiModeManager.class)).thenReturn(mock(UiModeManager.class));
+ setChargingPolicy(Integer.MIN_VALUE);
+
+ ArgumentCaptor<ChargingPolicyChangeListener> chargingPolicyChangeListenerCaptor =
+ ArgumentCaptor.forClass(ChargingPolicyChangeListener.class);
+
mService = new TestJobSchedulerService(mContext);
mService.waitOnAsyncLoadingForTesting();
+
+ verify(mBatteryManagerInternal).registerChargingPolicyChangeListener(
+ chargingPolicyChangeListenerCaptor.capture());
+ mChargingPolicyChangeListener = chargingPolicyChangeListenerCaptor.getValue();
}
@After
@@ -1709,6 +1737,383 @@
assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed());
}
+ @Test
+ public void testBatteryStateTrackerRegistersForImportantIntents() {
+ verify(mContext).registerReceiver(any(), ArgumentMatchers.argThat(filter -> true
+ && filter.hasAction(BatteryManager.ACTION_CHARGING)
+ && filter.hasAction(BatteryManager.ACTION_DISCHARGING)
+ && filter.hasAction(Intent.ACTION_BATTERY_LEVEL_CHANGED)
+ && filter.hasAction(Intent.ACTION_BATTERY_LOW)
+ && filter.hasAction(Intent.ACTION_BATTERY_OKAY)
+ && filter.hasAction(Intent.ACTION_POWER_CONNECTED)
+ && filter.hasAction(Intent.ACTION_POWER_DISCONNECTED)));
+ }
+
+ @Test
+ public void testIsCharging_standardChargingIntent() {
+ JobSchedulerService.BatteryStateTracker tracker = mService.mBatteryStateTracker;
+
+ Intent chargingIntent = new Intent(BatteryManager.ACTION_CHARGING);
+ Intent dischargingIntent = new Intent(BatteryManager.ACTION_DISCHARGING);
+ tracker.onReceive(mContext, dischargingIntent);
+ assertFalse(tracker.isCharging());
+ assertFalse(mService.isBatteryCharging());
+
+ tracker.onReceive(mContext, chargingIntent);
+ assertTrue(tracker.isCharging());
+ assertTrue(mService.isBatteryCharging());
+
+ tracker.onReceive(mContext, dischargingIntent);
+ assertFalse(tracker.isCharging());
+ assertFalse(mService.isBatteryCharging());
+ }
+
+ @Test
+ public void testIsCharging_adaptiveCharging_batteryTooLow() {
+ JobSchedulerService.BatteryStateTracker tracker = mService.mBatteryStateTracker;
+
+ tracker.onReceive(mContext, new Intent(BatteryManager.ACTION_DISCHARGING));
+ assertFalse(tracker.isCharging());
+ assertFalse(mService.isBatteryCharging());
+
+ setBatteryLevel(15);
+ setChargingPolicy(BatteryManager.CHARGING_POLICY_ADAPTIVE_AC);
+ assertFalse(tracker.isCharging());
+ assertFalse(mService.isBatteryCharging());
+
+ tracker.onReceive(mContext, new Intent(Intent.ACTION_POWER_CONNECTED));
+
+ setBatteryLevel(70);
+ assertTrue(tracker.isCharging());
+ assertTrue(mService.isBatteryCharging());
+ }
+
+ @Test
+ public void testIsCharging_adaptiveCharging_chargeBelowThreshold() {
+ JobSchedulerService.BatteryStateTracker tracker = mService.mBatteryStateTracker;
+
+ setChargingPolicy(BatteryManager.CHARGING_POLICY_ADAPTIVE_AC);
+ tracker.onReceive(mContext, new Intent(Intent.ACTION_POWER_CONNECTED));
+ setBatteryLevel(5);
+
+ tracker.onReceive(mContext, new Intent(BatteryManager.ACTION_CHARGING));
+ assertTrue(tracker.isCharging());
+ assertTrue(mService.isBatteryCharging());
+
+ for (int level = 5; level < 80; ++level) {
+ setBatteryLevel(level);
+ assertTrue(tracker.isCharging());
+ assertTrue(mService.isBatteryCharging());
+ }
+ }
+
+ @Test
+ public void testIsCharging_adaptiveCharging_dischargeAboveThreshold() {
+ JobSchedulerService.BatteryStateTracker tracker = mService.mBatteryStateTracker;
+
+ setChargingPolicy(BatteryManager.CHARGING_POLICY_ADAPTIVE_AC);
+ tracker.onReceive(mContext, new Intent(Intent.ACTION_POWER_CONNECTED));
+ setBatteryLevel(80);
+
+ tracker.onReceive(mContext, new Intent(BatteryManager.ACTION_DISCHARGING));
+ assertTrue(tracker.isCharging());
+ assertTrue(mService.isBatteryCharging());
+
+ for (int level = 80; level > 60; --level) {
+ setBatteryLevel(level);
+ assertEquals(level >= 70, tracker.isCharging());
+ assertEquals(level >= 70, mService.isBatteryCharging());
+ }
+ }
+
+ @Test
+ public void testIsCharging_adaptiveCharging_notPluggedIn() {
+ JobSchedulerService.BatteryStateTracker tracker = mService.mBatteryStateTracker;
+
+ tracker.onReceive(mContext, new Intent(Intent.ACTION_POWER_DISCONNECTED));
+ tracker.onReceive(mContext, new Intent(BatteryManager.ACTION_DISCHARGING));
+ assertFalse(tracker.isCharging());
+ assertFalse(mService.isBatteryCharging());
+
+ setBatteryLevel(15);
+ setChargingPolicy(BatteryManager.CHARGING_POLICY_ADAPTIVE_AC);
+ assertFalse(tracker.isCharging());
+ assertFalse(mService.isBatteryCharging());
+
+ setBatteryLevel(50);
+ setChargingPolicy(BatteryManager.CHARGING_POLICY_ADAPTIVE_AC);
+ assertFalse(tracker.isCharging());
+ assertFalse(mService.isBatteryCharging());
+
+ setBatteryLevel(70);
+ assertFalse(tracker.isCharging());
+ assertFalse(mService.isBatteryCharging());
+
+ setBatteryLevel(95);
+ assertFalse(tracker.isCharging());
+ assertFalse(mService.isBatteryCharging());
+
+ setBatteryLevel(100);
+ assertFalse(tracker.isCharging());
+ assertFalse(mService.isBatteryCharging());
+ }
+
+ /** Tests that rare job batching works as expected. */
+ @Test
+ public void testConnectivityJobBatching() {
+ mSetFlagsRule.enableFlags(FLAG_BATCH_CONNECTIVITY_JOBS_PER_NETWORK);
+
+ spyOn(mService);
+ doReturn(false).when(mService).evaluateControllerStatesLocked(any());
+ doNothing().when(mService).noteJobsPending(any());
+ doReturn(true).when(mService).isReadyToBeExecutedLocked(any(), anyBoolean());
+ ConnectivityController connectivityController = mService.getConnectivityController();
+ spyOn(connectivityController);
+ advanceElapsedClock(24 * HOUR_IN_MILLIS);
+
+ JobSchedulerService.MaybeReadyJobQueueFunctor maybeQueueFunctor =
+ mService.new MaybeReadyJobQueueFunctor();
+ mService.mConstants.CONN_TRANSPORT_BATCH_THRESHOLD.clear();
+ mService.mConstants.CONN_TRANSPORT_BATCH_THRESHOLD
+ .put(NetworkCapabilities.TRANSPORT_CELLULAR, 5);
+ mService.mConstants.CONN_TRANSPORT_BATCH_THRESHOLD
+ .put(NetworkCapabilities.TRANSPORT_WIFI, 2);
+ mService.mConstants.CONN_MAX_CONNECTIVITY_JOB_BATCH_DELAY_MS = HOUR_IN_MILLIS;
+
+ final Network network = mock(Network.class);
+
+ // Not enough connectivity jobs to run.
+ mService.getPendingJobQueue().clear();
+ maybeQueueFunctor.reset();
+ NetworkCapabilities capabilities = new NetworkCapabilities.Builder()
+ .addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR)
+ .build();
+ doReturn(capabilities).when(connectivityController).getNetworkCapabilities(network);
+ doReturn(false).when(connectivityController).isNetworkInStateForJobRunLocked(any());
+ for (int i = 0; i < 4; ++i) {
+ JobStatus job = createJobStatus(
+ "testConnectivityJobBatching",
+ createJobInfo().setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY));
+ job.setStandbyBucket(ACTIVE_INDEX);
+ job.network = network;
+
+ maybeQueueFunctor.accept(job);
+ assertNull(maybeQueueFunctor.mBatches.get(null));
+ assertEquals(i + 1, maybeQueueFunctor.mBatches.get(network).size());
+ assertEquals(i + 1, maybeQueueFunctor.runnableJobs.size());
+ assertEquals(sElapsedRealtimeClock.millis(), job.getFirstForceBatchedTimeElapsed());
+ }
+ maybeQueueFunctor.postProcessLocked();
+ assertEquals(0, mService.getPendingJobQueue().size());
+
+ // Not enough connectivity jobs to run, but the network is already active
+ mService.getPendingJobQueue().clear();
+ maybeQueueFunctor.reset();
+ doReturn(capabilities).when(connectivityController).getNetworkCapabilities(network);
+ doReturn(true).when(connectivityController).isNetworkInStateForJobRunLocked(any());
+ for (int i = 0; i < 4; ++i) {
+ JobStatus job = createJobStatus(
+ "testConnectivityJobBatching",
+ createJobInfo().setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY));
+ job.setStandbyBucket(ACTIVE_INDEX);
+ job.network = network;
+
+ maybeQueueFunctor.accept(job);
+ assertNull(maybeQueueFunctor.mBatches.get(null));
+ assertEquals(i + 1, maybeQueueFunctor.mBatches.get(network).size());
+ assertEquals(i + 1, maybeQueueFunctor.runnableJobs.size());
+ assertEquals(0, job.getFirstForceBatchedTimeElapsed());
+ }
+ maybeQueueFunctor.postProcessLocked();
+ assertEquals(4, mService.getPendingJobQueue().size());
+
+ // Enough connectivity jobs to run.
+ mService.getPendingJobQueue().clear();
+ maybeQueueFunctor.reset();
+ capabilities = new NetworkCapabilities.Builder()
+ .addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
+ .build();
+ doReturn(capabilities).when(connectivityController).getNetworkCapabilities(network);
+ doReturn(false).when(connectivityController).isNetworkInStateForJobRunLocked(any());
+ for (int i = 0; i < 3; ++i) {
+ JobStatus job = createJobStatus(
+ "testConnectivityJobBatching",
+ createJobInfo().setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY));
+ job.setStandbyBucket(ACTIVE_INDEX);
+ job.network = network;
+
+ maybeQueueFunctor.accept(job);
+ assertEquals(i + 1, maybeQueueFunctor.mBatches.get(network).size());
+ assertEquals(i + 1, maybeQueueFunctor.runnableJobs.size());
+ assertEquals(sElapsedRealtimeClock.millis(), job.getFirstForceBatchedTimeElapsed());
+ }
+ maybeQueueFunctor.postProcessLocked();
+ assertEquals(3, mService.getPendingJobQueue().size());
+
+ // Not enough connectivity jobs to run, but a non-batched job saves the day.
+ mService.getPendingJobQueue().clear();
+ maybeQueueFunctor.reset();
+ JobStatus runningJob = createJobStatus(
+ "testConnectivityJobBatching",
+ createJobInfo().setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY));
+ runningJob.network = network;
+ doReturn(true).when(mService).isCurrentlyRunningLocked(runningJob);
+ capabilities = new NetworkCapabilities.Builder()
+ .addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR)
+ .build();
+ doReturn(capabilities).when(connectivityController).getNetworkCapabilities(network);
+ for (int i = 0; i < 3; ++i) {
+ JobStatus job = createJobStatus(
+ "testConnectivityJobBatching",
+ createJobInfo().setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY));
+ job.setStandbyBucket(ACTIVE_INDEX);
+ job.network = network;
+
+ maybeQueueFunctor.accept(job);
+ assertEquals(i + 1, maybeQueueFunctor.mBatches.get(network).size());
+ assertEquals(i + 1, maybeQueueFunctor.runnableJobs.size());
+ assertEquals(sElapsedRealtimeClock.millis(), job.getFirstForceBatchedTimeElapsed());
+ }
+ maybeQueueFunctor.accept(runningJob);
+ maybeQueueFunctor.postProcessLocked();
+ assertEquals(3, mService.getPendingJobQueue().size());
+
+ // Not enough connectivity jobs to run, but an old connectivity job saves the day.
+ mService.getPendingJobQueue().clear();
+ maybeQueueFunctor.reset();
+ JobStatus oldConnJob = createJobStatus("testConnectivityJobBatching",
+ createJobInfo().setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY));
+ oldConnJob.network = network;
+ final long oldBatchTime = sElapsedRealtimeClock.millis()
+ - 2 * mService.mConstants.CONN_MAX_CONNECTIVITY_JOB_BATCH_DELAY_MS;
+ oldConnJob.setFirstForceBatchedTimeElapsed(oldBatchTime);
+ for (int i = 0; i < 2; ++i) {
+ JobStatus job = createJobStatus(
+ "testConnectivityJobBatching",
+ createJobInfo().setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY));
+ job.setStandbyBucket(ACTIVE_INDEX);
+ job.network = network;
+
+ maybeQueueFunctor.accept(job);
+ assertEquals(i + 1, maybeQueueFunctor.mBatches.get(network).size());
+ assertEquals(i + 1, maybeQueueFunctor.runnableJobs.size());
+ assertEquals(sElapsedRealtimeClock.millis(), job.getFirstForceBatchedTimeElapsed());
+ }
+ maybeQueueFunctor.accept(oldConnJob);
+ assertEquals(oldBatchTime, oldConnJob.getFirstForceBatchedTimeElapsed());
+ maybeQueueFunctor.postProcessLocked();
+ assertEquals(3, mService.getPendingJobQueue().size());
+
+ // Transport type doesn't have a set threshold. One job should be the default threshold.
+ mService.getPendingJobQueue().clear();
+ maybeQueueFunctor.reset();
+ capabilities = new NetworkCapabilities.Builder()
+ .addTransportType(NetworkCapabilities.TRANSPORT_ETHERNET)
+ .build();
+ doReturn(capabilities).when(connectivityController).getNetworkCapabilities(network);
+ JobStatus job = createJobStatus(
+ "testConnectivityJobBatching",
+ createJobInfo().setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY));
+ job.setStandbyBucket(ACTIVE_INDEX);
+ job.network = network;
+ maybeQueueFunctor.accept(job);
+ assertEquals(1, maybeQueueFunctor.mBatches.get(network).size());
+ assertEquals(1, maybeQueueFunctor.runnableJobs.size());
+ assertEquals(sElapsedRealtimeClock.millis(), job.getFirstForceBatchedTimeElapsed());
+ maybeQueueFunctor.postProcessLocked();
+ assertEquals(1, mService.getPendingJobQueue().size());
+ }
+
+ /** Tests that active job batching works as expected. */
+ @Test
+ public void testActiveJobBatching_activeBatchingEnabled() {
+ mSetFlagsRule.enableFlags(FLAG_BATCH_ACTIVE_BUCKET_JOBS);
+
+ spyOn(mService);
+ doReturn(false).when(mService).evaluateControllerStatesLocked(any());
+ doNothing().when(mService).noteJobsPending(any());
+ doReturn(true).when(mService).isReadyToBeExecutedLocked(any(), anyBoolean());
+ advanceElapsedClock(24 * HOUR_IN_MILLIS);
+
+ JobSchedulerService.MaybeReadyJobQueueFunctor maybeQueueFunctor =
+ mService.new MaybeReadyJobQueueFunctor();
+ mService.mConstants.MIN_READY_CPU_ONLY_JOBS_COUNT = 5;
+ mService.mConstants.MAX_CPU_ONLY_JOB_BATCH_DELAY_MS = HOUR_IN_MILLIS;
+
+ // Not enough ACTIVE jobs to run.
+ mService.getPendingJobQueue().clear();
+ maybeQueueFunctor.reset();
+ for (int i = 0; i < mService.mConstants.MIN_READY_CPU_ONLY_JOBS_COUNT / 2; ++i) {
+ JobStatus job = createJobStatus("testActiveJobBatching", createJobInfo());
+ job.setStandbyBucket(ACTIVE_INDEX);
+
+ maybeQueueFunctor.accept(job);
+ assertEquals(i + 1, maybeQueueFunctor.mBatches.get(null).size());
+ assertEquals(i + 1, maybeQueueFunctor.runnableJobs.size());
+ assertEquals(sElapsedRealtimeClock.millis(), job.getFirstForceBatchedTimeElapsed());
+ }
+ maybeQueueFunctor.postProcessLocked();
+ assertEquals(0, mService.getPendingJobQueue().size());
+
+ // Enough ACTIVE jobs to run.
+ mService.getPendingJobQueue().clear();
+ maybeQueueFunctor.reset();
+ for (int i = 0; i < mService.mConstants.MIN_READY_CPU_ONLY_JOBS_COUNT; ++i) {
+ JobStatus job = createJobStatus("testActiveJobBatching", createJobInfo());
+ job.setStandbyBucket(ACTIVE_INDEX);
+
+ maybeQueueFunctor.accept(job);
+ assertEquals(i + 1, maybeQueueFunctor.mBatches.get(null).size());
+ assertEquals(i + 1, maybeQueueFunctor.runnableJobs.size());
+ assertEquals(sElapsedRealtimeClock.millis(), job.getFirstForceBatchedTimeElapsed());
+ }
+ maybeQueueFunctor.postProcessLocked();
+ assertEquals(5, mService.getPendingJobQueue().size());
+
+ // Not enough ACTIVE jobs to run, but a non-batched job saves the day.
+ mService.getPendingJobQueue().clear();
+ maybeQueueFunctor.reset();
+ JobStatus expeditedJob = createJobStatus("testActiveJobBatching",
+ createJobInfo().setExpedited(true));
+ spyOn(expeditedJob);
+ when(expeditedJob.shouldTreatAsExpeditedJob()).thenReturn(true);
+ expeditedJob.setStandbyBucket(RARE_INDEX);
+ for (int i = 0; i < mService.mConstants.MIN_READY_CPU_ONLY_JOBS_COUNT / 2; ++i) {
+ JobStatus job = createJobStatus("testActiveJobBatching", createJobInfo());
+ job.setStandbyBucket(ACTIVE_INDEX);
+
+ maybeQueueFunctor.accept(job);
+ assertEquals(i + 1, maybeQueueFunctor.mBatches.get(null).size());
+ assertEquals(i + 1, maybeQueueFunctor.runnableJobs.size());
+ assertEquals(sElapsedRealtimeClock.millis(), job.getFirstForceBatchedTimeElapsed());
+ }
+ maybeQueueFunctor.accept(expeditedJob);
+ maybeQueueFunctor.postProcessLocked();
+ assertEquals(3, mService.getPendingJobQueue().size());
+
+ // Not enough ACTIVE jobs to run, but an old ACTIVE job saves the day.
+ mService.getPendingJobQueue().clear();
+ maybeQueueFunctor.reset();
+ JobStatus oldActiveJob = createJobStatus("testActiveJobBatching", createJobInfo());
+ oldActiveJob.setStandbyBucket(ACTIVE_INDEX);
+ final long oldBatchTime = sElapsedRealtimeClock.millis()
+ - 2 * mService.mConstants.MAX_CPU_ONLY_JOB_BATCH_DELAY_MS;
+ oldActiveJob.setFirstForceBatchedTimeElapsed(oldBatchTime);
+ for (int i = 0; i < mService.mConstants.MIN_READY_CPU_ONLY_JOBS_COUNT / 2; ++i) {
+ JobStatus job = createJobStatus("testActiveJobBatching", createJobInfo());
+ job.setStandbyBucket(ACTIVE_INDEX);
+
+ maybeQueueFunctor.accept(job);
+ assertEquals(i + 1, maybeQueueFunctor.mBatches.get(null).size());
+ assertEquals(i + 1, maybeQueueFunctor.runnableJobs.size());
+ assertEquals(sElapsedRealtimeClock.millis(), job.getFirstForceBatchedTimeElapsed());
+ }
+ maybeQueueFunctor.accept(oldActiveJob);
+ assertEquals(oldBatchTime, oldActiveJob.getFirstForceBatchedTimeElapsed());
+ maybeQueueFunctor.postProcessLocked();
+ assertEquals(3, mService.getPendingJobQueue().size());
+ }
+
/** Tests that rare job batching works as expected. */
@Test
public void testRareJobBatching() {
@@ -1723,17 +2128,15 @@
mService.mConstants.MIN_READY_NON_ACTIVE_JOBS_COUNT = 5;
mService.mConstants.MAX_NON_ACTIVE_JOB_BATCH_DELAY_MS = HOUR_IN_MILLIS;
- JobStatus job = createJobStatus(
- "testRareJobBatching",
- createJobInfo().setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY));
- job.setStandbyBucket(RARE_INDEX);
-
// Not enough RARE jobs to run.
mService.getPendingJobQueue().clear();
maybeQueueFunctor.reset();
for (int i = 0; i < mService.mConstants.MIN_READY_NON_ACTIVE_JOBS_COUNT / 2; ++i) {
+ JobStatus job = createJobStatus("testRareJobBatching", createJobInfo());
+ job.setStandbyBucket(RARE_INDEX);
+
maybeQueueFunctor.accept(job);
- assertEquals(i + 1, maybeQueueFunctor.forceBatchedCount);
+ assertEquals(i + 1, maybeQueueFunctor.mBatches.get(null).size());
assertEquals(i + 1, maybeQueueFunctor.runnableJobs.size());
assertEquals(sElapsedRealtimeClock.millis(), job.getFirstForceBatchedTimeElapsed());
}
@@ -1744,8 +2147,11 @@
mService.getPendingJobQueue().clear();
maybeQueueFunctor.reset();
for (int i = 0; i < mService.mConstants.MIN_READY_NON_ACTIVE_JOBS_COUNT; ++i) {
+ JobStatus job = createJobStatus("testRareJobBatching", createJobInfo());
+ job.setStandbyBucket(RARE_INDEX);
+
maybeQueueFunctor.accept(job);
- assertEquals(i + 1, maybeQueueFunctor.forceBatchedCount);
+ assertEquals(i + 1, maybeQueueFunctor.mBatches.get(null).size());
assertEquals(i + 1, maybeQueueFunctor.runnableJobs.size());
assertEquals(sElapsedRealtimeClock.millis(), job.getFirstForceBatchedTimeElapsed());
}
@@ -1753,15 +2159,17 @@
assertEquals(5, mService.getPendingJobQueue().size());
// Not enough RARE jobs to run, but a non-batched job saves the day.
+ mSetFlagsRule.disableFlags(FLAG_BATCH_ACTIVE_BUCKET_JOBS);
mService.getPendingJobQueue().clear();
maybeQueueFunctor.reset();
- JobStatus activeJob = createJobStatus(
- "testRareJobBatching",
- createJobInfo().setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY));
+ JobStatus activeJob = createJobStatus("testRareJobBatching", createJobInfo());
activeJob.setStandbyBucket(ACTIVE_INDEX);
for (int i = 0; i < mService.mConstants.MIN_READY_NON_ACTIVE_JOBS_COUNT / 2; ++i) {
+ JobStatus job = createJobStatus("testRareJobBatching", createJobInfo());
+ job.setStandbyBucket(RARE_INDEX);
+
maybeQueueFunctor.accept(job);
- assertEquals(i + 1, maybeQueueFunctor.forceBatchedCount);
+ assertEquals(i + 1, maybeQueueFunctor.mBatches.get(null).size());
assertEquals(i + 1, maybeQueueFunctor.runnableJobs.size());
assertEquals(sElapsedRealtimeClock.millis(), job.getFirstForceBatchedTimeElapsed());
}
@@ -1778,8 +2186,11 @@
- 2 * mService.mConstants.MAX_NON_ACTIVE_JOB_BATCH_DELAY_MS;
oldRareJob.setFirstForceBatchedTimeElapsed(oldBatchTime);
for (int i = 0; i < mService.mConstants.MIN_READY_NON_ACTIVE_JOBS_COUNT / 2; ++i) {
+ JobStatus job = createJobStatus("testRareJobBatching", createJobInfo());
+ job.setStandbyBucket(RARE_INDEX);
+
maybeQueueFunctor.accept(job);
- assertEquals(i + 1, maybeQueueFunctor.forceBatchedCount);
+ assertEquals(i + 1, maybeQueueFunctor.mBatches.get(null).size());
assertEquals(i + 1, maybeQueueFunctor.runnableJobs.size());
assertEquals(sElapsedRealtimeClock.millis(), job.getFirstForceBatchedTimeElapsed());
}
@@ -1986,4 +2397,17 @@
assertFalse(mService.getPendingJobQueue().contains(job2b));
assertEquals(JobScheduler.PENDING_JOB_REASON_USER, mService.getPendingJobReason(job2b));
}
+
+ private void setBatteryLevel(int level) {
+ doReturn(level).when(mBatteryManagerInternal).getBatteryLevel();
+ mService.mBatteryStateTracker
+ .onReceive(mContext, new Intent(Intent.ACTION_BATTERY_LEVEL_CHANGED));
+ }
+
+ private void setChargingPolicy(int policy) {
+ doReturn(policy).when(mBatteryManagerInternal).getChargingPolicy();
+ if (mChargingPolicyChangeListener != null) {
+ mChargingPolicyChangeListener.onChargingPolicyChanged(policy);
+ }
+ }
}
diff --git a/services/tests/mockingservicestests/src/com/android/server/job/controllers/BatteryControllerTest.java b/services/tests/mockingservicestests/src/com/android/server/job/controllers/BatteryControllerTest.java
index d4ef647..ea937de 100644
--- a/services/tests/mockingservicestests/src/com/android/server/job/controllers/BatteryControllerTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/job/controllers/BatteryControllerTest.java
@@ -25,14 +25,11 @@
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
-import static org.mockito.Mockito.verify;
import android.app.AppGlobals;
import android.app.job.JobInfo;
-import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
-import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.PackageManagerInternal;
import android.os.BatteryManagerInternal;
@@ -49,8 +46,6 @@
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
-import org.mockito.ArgumentCaptor;
-import org.mockito.ArgumentMatchers;
import org.mockito.Mock;
import org.mockito.MockitoSession;
import org.mockito.quality.Strictness;
@@ -63,7 +58,6 @@
private BatteryController mBatteryController;
private FlexibilityController mFlexibilityController;
- private BroadcastReceiver mPowerReceiver;
private JobSchedulerService.Constants mConstants = new JobSchedulerService.Constants();
private int mSourceUid;
@@ -99,10 +93,6 @@
.when(() -> LocalServices.getService(PackageManagerInternal.class));
// Initialize real objects.
- // Capture the listeners.
- ArgumentCaptor<BroadcastReceiver> receiverCaptor =
- ArgumentCaptor.forClass(BroadcastReceiver.class);
-
when(mContext.getPackageManager()).thenReturn(mPackageManager);
when(mPackageManager.hasSystemFeature(
PackageManager.FEATURE_AUTOMOTIVE)).thenReturn(false);
@@ -111,11 +101,6 @@
mBatteryController = new BatteryController(mJobSchedulerService, mFlexibilityController);
mBatteryController.startTrackingLocked();
- verify(mContext).registerReceiver(receiverCaptor.capture(),
- ArgumentMatchers.argThat(filter ->
- filter.hasAction(Intent.ACTION_POWER_CONNECTED)
- && filter.hasAction(Intent.ACTION_POWER_DISCONNECTED)));
- mPowerReceiver = receiverCaptor.getValue();
try {
mSourceUid = AppGlobals.getPackageManager().getPackageUid(SOURCE_PACKAGE, 0, 0);
// Need to do this since we're using a mock JS and not a real object.
@@ -159,9 +144,11 @@
}
private void setPowerConnected(boolean connected) {
- Intent intent = new Intent(
- connected ? Intent.ACTION_POWER_CONNECTED : Intent.ACTION_POWER_DISCONNECTED);
- mPowerReceiver.onReceive(mContext, intent);
+ doReturn(connected).when(mJobSchedulerService).isPowerConnected();
+ synchronized (mBatteryController.mLock) {
+ mBatteryController.onBatteryStateChangedLocked();
+ }
+ waitForNonDelayedMessagesProcessed();
}
private void setUidBias(int uid, int bias) {
diff --git a/services/tests/mockingservicestests/src/com/android/server/job/controllers/ConnectivityControllerTest.java b/services/tests/mockingservicestests/src/com/android/server/job/controllers/ConnectivityControllerTest.java
index 4958f1c..f27d0c2 100644
--- a/services/tests/mockingservicestests/src/com/android/server/job/controllers/ConnectivityControllerTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/job/controllers/ConnectivityControllerTest.java
@@ -36,6 +36,7 @@
import static com.android.dx.mockito.inline.extended.ExtendedMockito.mock;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.spy;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.when;
+import static com.android.server.job.Flags.FLAG_BATCH_CONNECTIVITY_JOBS_PER_NETWORK;
import static com.android.server.job.Flags.FLAG_RELAX_PREFETCH_CONNECTIVITY_CONSTRAINT_ONLY_ON_CHARGER;
import static com.android.server.job.JobSchedulerService.FREQUENT_INDEX;
import static com.android.server.job.JobSchedulerService.RARE_INDEX;
@@ -69,6 +70,7 @@
import android.content.pm.PackageManagerInternal;
import android.net.ConnectivityManager;
import android.net.ConnectivityManager.NetworkCallback;
+import android.net.ConnectivityManager.OnNetworkActiveListener;
import android.net.Network;
import android.net.NetworkCapabilities;
import android.net.NetworkPolicyManager;
@@ -102,6 +104,7 @@
import org.mockito.stubbing.Answer;
import java.time.Clock;
+import java.time.Duration;
import java.time.ZoneOffset;
import java.util.Set;
@@ -1650,6 +1653,141 @@
assertEquals((81920 + 4096) * SECOND_IN_MILLIS, controller.getEstimatedTransferTimeMs(job));
}
+ @Test
+ public void testIsNetworkInStateForJobRunLocked_JobStatus() {
+ mSetFlagsRule.enableFlags(FLAG_BATCH_CONNECTIVITY_JOBS_PER_NETWORK);
+
+ final ConnectivityController controller = new ConnectivityController(mService,
+ mFlexibilityController);
+
+ // Null network
+ final JobStatus expeditedJob =
+ spy(createJobStatus(createJob().setExpedited(true), UID_RED));
+ doReturn(true).when(expeditedJob).shouldTreatAsExpeditedJob();
+ final JobStatus highProcJob = spy(createJobStatus(createJob(), UID_BLUE));
+ doReturn(ActivityManager.PROCESS_STATE_BOUND_FOREGROUND_SERVICE)
+ .when(mService).getUidProcState(eq(UID_BLUE));
+ doReturn(ActivityManager.PROCESS_STATE_CACHED_EMPTY)
+ .when(mService).getUidProcState(eq(UID_RED));
+ final JobStatus regJob = createJobStatus(createJob(), UID_RED);
+ final JobStatus uiJob = spy(createJobStatus(
+ createJob().setUserInitiated(true).setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY),
+ UID_RED));
+ doReturn(true).when(uiJob).shouldTreatAsUserInitiatedJob();
+ assertFalse(controller.isNetworkInStateForJobRunLocked(expeditedJob));
+ assertFalse(controller.isNetworkInStateForJobRunLocked(highProcJob));
+ assertFalse(controller.isNetworkInStateForJobRunLocked(regJob));
+ assertFalse(controller.isNetworkInStateForJobRunLocked(uiJob));
+
+ // Privileged jobs are exempted
+ expeditedJob.network = mock(Network.class);
+ highProcJob.network = mock(Network.class);
+ regJob.network = mock(Network.class);
+ uiJob.network = mock(Network.class);
+ assertTrue(controller.isNetworkInStateForJobRunLocked(expeditedJob));
+ assertTrue(controller.isNetworkInStateForJobRunLocked(highProcJob));
+ assertFalse(controller.isNetworkInStateForJobRunLocked(regJob));
+ assertTrue(controller.isNetworkInStateForJobRunLocked(uiJob));
+
+ mSetFlagsRule.disableFlags(FLAG_BATCH_CONNECTIVITY_JOBS_PER_NETWORK);
+ assertTrue(controller.isNetworkInStateForJobRunLocked(expeditedJob));
+ assertTrue(controller.isNetworkInStateForJobRunLocked(highProcJob));
+ assertTrue(controller.isNetworkInStateForJobRunLocked(regJob));
+ assertTrue(controller.isNetworkInStateForJobRunLocked(uiJob));
+ }
+
+ @Test
+ public void testIsNetworkInStateForJobRunLocked_Network() {
+ mSetFlagsRule.disableFlags(FLAG_BATCH_CONNECTIVITY_JOBS_PER_NETWORK);
+
+ final ArgumentCaptor<NetworkCallback> allNetworkCallbackCaptor =
+ ArgumentCaptor.forClass(NetworkCallback.class);
+ doNothing().when(mConnManager)
+ .registerNetworkCallback(any(), allNetworkCallbackCaptor.capture());
+ final ArgumentCaptor<OnNetworkActiveListener> onNetworkActiveListenerCaptor =
+ ArgumentCaptor.forClass(OnNetworkActiveListener.class);
+ doNothing().when(mConnManager).addDefaultNetworkActiveListener(
+ onNetworkActiveListenerCaptor.capture());
+ final ArgumentCaptor<NetworkCallback> systemDefaultNetworkCallbackCaptor =
+ ArgumentCaptor.forClass(NetworkCallback.class);
+ doNothing().when(mConnManager).registerSystemDefaultNetworkCallback(
+ systemDefaultNetworkCallbackCaptor.capture(), any());
+
+ final ConnectivityController controller = new ConnectivityController(mService,
+ mFlexibilityController);
+
+ assertTrue(controller.isNetworkInStateForJobRunLocked(mock(Network.class)));
+
+ mSetFlagsRule.enableFlags(FLAG_BATCH_CONNECTIVITY_JOBS_PER_NETWORK);
+
+ // Unknown network
+ assertFalse(controller.isNetworkInStateForJobRunLocked(mock(Network.class)));
+
+ final Network systemDefaultNetwork = mock(Network.class);
+ final Network otherNetwork = mock(Network.class);
+
+ controller.startTrackingLocked();
+
+ final NetworkCallback allNetworkCallback = allNetworkCallbackCaptor.getValue();
+ final OnNetworkActiveListener onNetworkActiveListener =
+ onNetworkActiveListenerCaptor.getValue();
+ final NetworkCallback systemDefaultNetworkCallback =
+ systemDefaultNetworkCallbackCaptor.getValue();
+
+ // No capabilities set
+ allNetworkCallback.onAvailable(systemDefaultNetwork);
+ allNetworkCallback.onAvailable(otherNetwork);
+ assertFalse(controller.isNetworkInStateForJobRunLocked(systemDefaultNetwork));
+ assertFalse(controller.isNetworkInStateForJobRunLocked(otherNetwork));
+
+ // Capabilities set, but never active.
+ allNetworkCallback.onCapabilitiesChanged(
+ systemDefaultNetwork, mock(NetworkCapabilities.class));
+ allNetworkCallback.onCapabilitiesChanged(otherNetwork, mock(NetworkCapabilities.class));
+ assertFalse(controller.isNetworkInStateForJobRunLocked(systemDefaultNetwork));
+ assertFalse(controller.isNetworkInStateForJobRunLocked(otherNetwork));
+
+ // Mark system default network as active before identifying system default network.
+ onNetworkActiveListener.onNetworkActive();
+ assertFalse(controller.isNetworkInStateForJobRunLocked(systemDefaultNetwork));
+ assertFalse(controller.isNetworkInStateForJobRunLocked(otherNetwork));
+
+ // Identify system default network and mark as active.
+ systemDefaultNetworkCallback.onAvailable(systemDefaultNetwork);
+ onNetworkActiveListener.onNetworkActive();
+ assertTrue(controller.isNetworkInStateForJobRunLocked(systemDefaultNetwork));
+ assertFalse(controller.isNetworkInStateForJobRunLocked(otherNetwork));
+
+ advanceElapsedClock(controller.getCcConfig().NETWORK_ACTIVATION_EXPIRATION_MS - 1);
+ assertTrue(controller.isNetworkInStateForJobRunLocked(systemDefaultNetwork));
+ assertFalse(controller.isNetworkInStateForJobRunLocked(otherNetwork));
+
+ // Network stays active beyond expected timeout.
+ advanceElapsedClock(2);
+ doReturn(true).when(mConnManager).isDefaultNetworkActive();
+ assertTrue(controller.isNetworkInStateForJobRunLocked(systemDefaultNetwork));
+ assertFalse(controller.isNetworkInStateForJobRunLocked(otherNetwork));
+
+ // Network becomes inactive after expected timeout.
+ advanceElapsedClock(controller.getCcConfig().NETWORK_ACTIVATION_EXPIRATION_MS);
+ doReturn(false).when(mConnManager).isDefaultNetworkActive();
+ assertFalse(controller.isNetworkInStateForJobRunLocked(systemDefaultNetwork));
+ assertFalse(controller.isNetworkInStateForJobRunLocked(otherNetwork));
+
+ // Other network hasn't received a signal for a long time. System default network has
+ // been active within the max wait time.
+ advanceElapsedClock(controller.getCcConfig().NETWORK_ACTIVATION_MAX_WAIT_TIME_MS
+ - controller.getCcConfig().NETWORK_ACTIVATION_EXPIRATION_MS);
+ doReturn(false).when(mConnManager).isDefaultNetworkActive();
+ assertFalse(controller.isNetworkInStateForJobRunLocked(systemDefaultNetwork));
+ assertTrue(controller.isNetworkInStateForJobRunLocked(otherNetwork));
+ }
+
+ private void advanceElapsedClock(long incrementMs) {
+ JobSchedulerService.sElapsedRealtimeClock = Clock.offset(
+ JobSchedulerService.sElapsedRealtimeClock, Duration.ofMillis(incrementMs));
+ }
+
private void answerNetwork(@NonNull NetworkCallback generalCallback,
@Nullable NetworkCallback uidCallback, @Nullable Network lastNetwork,
@Nullable Network net, @Nullable NetworkCapabilities caps) {
diff --git a/services/tests/mockingservicestests/src/com/android/server/location/altitude/AltitudeConverterTest.java b/services/tests/mockingservicestests/src/com/android/server/location/altitude/AltitudeConverterTest.java
index 9a143d5..1e81951 100644
--- a/services/tests/mockingservicestests/src/com/android/server/location/altitude/AltitudeConverterTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/location/altitude/AltitudeConverterTest.java
@@ -189,7 +189,7 @@
assertThat(response.geoidHeightMeters).isWithin(2).of(-5.0622);
assertThat(response.geoidHeightErrorMeters).isGreaterThan(0f);
assertThat(response.geoidHeightErrorMeters).isLessThan(1f);
- assertThat(response.expirationDistanceMeters).isWithin(1).of(-6.33);
+ assertThat(response.expirationDistanceMeters).isWithin(1).of(120490);
assertThat(response.additionalGeoidHeightErrorMeters).isGreaterThan(0f);
assertThat(response.additionalGeoidHeightErrorMeters).isLessThan(1f);
assertThat(response.success).isTrue();
diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/BackgroundInstallControlCallbackHelperTest.java b/services/tests/mockingservicestests/src/com/android/server/pm/BackgroundInstallControlCallbackHelperTest.java
new file mode 100644
index 0000000..574f369
--- /dev/null
+++ b/services/tests/mockingservicestests/src/com/android/server/pm/BackgroundInstallControlCallbackHelperTest.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2024 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.pm;
+
+import static com.android.server.pm.BackgroundInstallControlCallbackHelper.FLAGGED_PACKAGE_NAME_KEY;
+import static com.android.server.pm.BackgroundInstallControlCallbackHelper.FLAGGED_USER_ID_KEY;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.Mockito.after;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+
+import android.os.Bundle;
+import android.os.IRemoteCallback;
+import android.os.RemoteException;
+import android.platform.test.annotations.Presubmit;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.ArgumentCaptor;
+
+/** Unit tests for {@link BackgroundInstallControlCallbackHelper} */
+@Presubmit
+@RunWith(JUnit4.class)
+public class BackgroundInstallControlCallbackHelperTest {
+
+ private final IRemoteCallback mCallback =
+ spy(
+ new IRemoteCallback.Stub() {
+ @Override
+ public void sendResult(Bundle extras) {}
+ });
+
+ private BackgroundInstallControlCallbackHelper mCallbackHelper;
+
+ @Before
+ public void setup() {
+ mCallbackHelper = new BackgroundInstallControlCallbackHelper();
+ }
+
+ @Test
+ public void registerBackgroundInstallControlCallback_registers_successfully() {
+ mCallbackHelper.registerBackgroundInstallCallback(mCallback);
+
+ synchronized (mCallbackHelper.mCallbacks) {
+ assertEquals(1, mCallbackHelper.mCallbacks.getRegisteredCallbackCount());
+ assertEquals(mCallback, mCallbackHelper.mCallbacks.getRegisteredCallbackItem(0));
+ }
+ }
+
+ @Test
+ public void unregisterBackgroundInstallControlCallback_unregisters_successfully() {
+ synchronized (mCallbackHelper.mCallbacks) {
+ mCallbackHelper.mCallbacks.register(mCallback);
+ }
+
+ mCallbackHelper.unregisterBackgroundInstallCallback(mCallback);
+
+ synchronized (mCallbackHelper.mCallbacks) {
+ assertEquals(0, mCallbackHelper.mCallbacks.getRegisteredCallbackCount());
+ }
+ }
+
+ @Test
+ public void notifyAllCallbacks_broadcastsToCallbacks()
+ throws RemoteException {
+ String testPackageName = "testname";
+ int testUserId = 1;
+ mCallbackHelper.registerBackgroundInstallCallback(mCallback);
+
+ mCallbackHelper.notifyAllCallbacks(testUserId, testPackageName);
+
+ ArgumentCaptor<Bundle> bundleCaptor = ArgumentCaptor.forClass(Bundle.class);
+ verify(mCallback, after(1000).times(1)).sendResult(bundleCaptor.capture());
+ Bundle receivedBundle = bundleCaptor.getValue();
+ assertEquals(testPackageName, receivedBundle.getString(FLAGGED_PACKAGE_NAME_KEY));
+ assertEquals(testUserId, receivedBundle.getInt(FLAGGED_USER_ID_KEY));
+ }
+}
diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/PackageArchiverTest.java b/services/tests/mockingservicestests/src/com/android/server/pm/PackageArchiverTest.java
index bf00b75..4535ece 100644
--- a/services/tests/mockingservicestests/src/com/android/server/pm/PackageArchiverTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/pm/PackageArchiverTest.java
@@ -270,7 +270,7 @@
assertThat(value.getStringExtra(PackageInstaller.EXTRA_PACKAGE_NAME)).isEqualTo(PACKAGE);
assertThat(value.getIntExtra(PackageInstaller.EXTRA_STATUS, 0)).isEqualTo(
PackageInstaller.STATUS_FAILURE);
- assertThat(value.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE)).isEqualTo(
+ assertThat(value.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE)).contains(
String.format("Package %s not found.", PACKAGE));
}
diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/StagingManagerTest.java b/services/tests/mockingservicestests/src/com/android/server/pm/StagingManagerTest.java
index d2547a3..6f9b8df 100644
--- a/services/tests/mockingservicestests/src/com/android/server/pm/StagingManagerTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/pm/StagingManagerTest.java
@@ -755,7 +755,8 @@
/* isFailed */ false,
/* isApplied */false,
/* stagedSessionErrorCode */ PackageManager.INSTALL_UNKNOWN,
- /* stagedSessionErrorMessage */ "no error");
+ /* stagedSessionErrorMessage */ "no error",
+ /* preVerifiedDomains */ null);
StagingManager.StagedSession stagedSession = spy(session.mStagedSession);
doReturn(packageName).when(stagedSession).getPackageName();
diff --git a/services/tests/servicestests/Android.bp b/services/tests/servicestests/Android.bp
index ad6e2c6..3743483 100644
--- a/services/tests/servicestests/Android.bp
+++ b/services/tests/servicestests/Android.bp
@@ -154,6 +154,7 @@
"androidx.annotation_annotation",
"androidx.test.rules",
"services.core",
+ "flag-junit",
],
srcs: [
"src/com/android/server/uri/**/*.java",
diff --git a/services/tests/servicestests/res/xml/usertypes_test_profile.xml b/services/tests/servicestests/res/xml/usertypes_test_profile.xml
index 6537d47..f9c6794 100644
--- a/services/tests/servicestests/res/xml/usertypes_test_profile.xml
+++ b/services/tests/servicestests/res/xml/usertypes_test_profile.xml
@@ -47,6 +47,7 @@
deleteAppWithParent='false'
alwaysVisible='true'
crossProfileContentSharingStrategy='0'
+ itemsRestrictedOnHomeScreen='true'
/>
</profile-type>
<profile-type name='custom.test.1' max-allowed-per-parent='14' />
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityWindowManagerTest.java b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityWindowManagerTest.java
index 2dfabd0..b12d6da 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityWindowManagerTest.java
+++ b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityWindowManagerTest.java
@@ -893,13 +893,13 @@
@Test
public void getTokenLocked_windowIsRegistered_shouldReturnToken() {
- final IBinder token = mA11yWindowManager.getTokenLocked(HOST_WINDOW_ID);
+ final IBinder token = mA11yWindowManager.getLeashTokenLocked(HOST_WINDOW_ID);
assertEquals(token, mMockHostToken);
}
@Test
public void getTokenLocked_windowIsNotRegistered_shouldReturnNull() {
- final IBinder token = mA11yWindowManager.getTokenLocked(OTHER_WINDOW_ID);
+ final IBinder token = mA11yWindowManager.getLeashTokenLocked(OTHER_WINDOW_ID);
assertNull(token);
}
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/magnification/FullScreenMagnificationControllerTest.java b/services/tests/servicestests/src/com/android/server/accessibility/magnification/FullScreenMagnificationControllerTest.java
index 52726ca..b224773 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/magnification/FullScreenMagnificationControllerTest.java
+++ b/services/tests/servicestests/src/com/android/server/accessibility/magnification/FullScreenMagnificationControllerTest.java
@@ -42,6 +42,7 @@
import static org.mockito.hamcrest.MockitoHamcrest.argThat;
import android.accessibilityservice.MagnificationConfig;
+import android.animation.TimeAnimator;
import android.animation.ValueAnimator;
import android.content.BroadcastReceiver;
import android.content.Context;
@@ -52,11 +53,15 @@
import android.hardware.display.DisplayManagerInternal;
import android.os.Looper;
import android.os.UserHandle;
+import android.platform.test.annotations.RequiresFlagsEnabled;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
import android.provider.Settings;
import android.test.mock.MockContentResolver;
import android.view.DisplayInfo;
import android.view.MagnificationSpec;
import android.view.accessibility.MagnificationAnimationCallback;
+import android.widget.Scroller;
import androidx.test.InstrumentationRegistry;
import androidx.test.runner.AndroidJUnit4;
@@ -65,6 +70,7 @@
import com.android.internal.util.test.FakeSettingsProvider;
import com.android.server.LocalServices;
import com.android.server.accessibility.AccessibilityTraceManager;
+import com.android.server.accessibility.Flags;
import com.android.server.accessibility.test.MessageCapturingHandler;
import com.android.server.wm.WindowManagerInternal;
import com.android.server.wm.WindowManagerInternal.MagnificationCallbacks;
@@ -74,6 +80,7 @@
import org.hamcrest.TypeSafeMatcher;
import org.junit.After;
import org.junit.Before;
+import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
@@ -104,6 +111,9 @@
static final int INVALID_DISPLAY = 2;
private static final int CURRENT_USER_ID = UserHandle.USER_SYSTEM;
+ @Rule
+ public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
+
final FullScreenMagnificationController.ControllerContext mMockControllerCtx =
mock(FullScreenMagnificationController.ControllerContext.class);
final Context mMockContext = mock(Context.class);
@@ -118,6 +128,7 @@
private MagnificationScaleProvider mScaleProvider;
private MockContentResolver mResolver;
private final MagnificationThumbnail mMockThumbnail = mock(MagnificationThumbnail.class);
+ private final Scroller mMockScroller = mock(Scroller.class);
private final ArgumentCaptor<MagnificationConfig> mConfigCaptor = ArgumentCaptor.forClass(
MagnificationConfig.class);
@@ -126,6 +137,8 @@
ValueAnimator.AnimatorUpdateListener mTargetAnimationListener;
ValueAnimator.AnimatorListener mStateListener;
+ private final TimeAnimator mMockTimeAnimator = mock(TimeAnimator.class);
+
FullScreenMagnificationController mFullScreenMagnificationController;
public DisplayManagerInternal mDisplayManagerInternalMock = mock(DisplayManagerInternal.class);
@@ -134,7 +147,8 @@
@Before
public void setUp() {
- Looper looper = InstrumentationRegistry.getContext().getMainLooper();
+ Context realContext = InstrumentationRegistry.getContext();
+ Looper looper = realContext.getMainLooper();
// Pretending ID of the Thread associated with looper as main thread ID in controller
when(mMockContext.getMainLooper()).thenReturn(looper);
when(mMockControllerCtx.getContext()).thenReturn(mMockContext);
@@ -168,7 +182,9 @@
mRequestObserver,
mScaleProvider,
() -> mMockThumbnail,
- ConcurrentUtils.DIRECT_EXECUTOR);
+ ConcurrentUtils.DIRECT_EXECUTOR,
+ () -> mMockScroller,
+ () -> mMockTimeAnimator);
}
@After
@@ -428,7 +444,7 @@
mTargetAnimationListener.onAnimationUpdate(mMockValueAnimator);
mStateListener.onAnimationEnd(mMockValueAnimator);
verify(mMockWindowManager).setMagnificationSpec(eq(displayId), argThat(closeTo(endSpec)));
- verify(mAnimationCallback).onResult(true);
+ verify(mAnimationCallback).onResult(eq(true), any());
}
@Test
@@ -451,7 +467,7 @@
mMessageCapturingHandler.sendAllMessages();
verify(mMockValueAnimator, never()).start();
- verify(mAnimationCallback).onResult(true);
+ verify(mAnimationCallback).onResult(eq(true), any());
}
@Test
@@ -653,6 +669,85 @@
}
@Test
+ @RequiresFlagsEnabled(Flags.FLAG_FULLSCREEN_FLING_GESTURE)
+ public void testStartFling_whileMagnifying_flings() throws InterruptedException {
+ for (int i = 0; i < DISPLAY_COUNT; i++) {
+ startFling_whileMagnifying_flings(i);
+ resetMockWindowManager();
+ }
+ }
+
+ private void startFling_whileMagnifying_flings(int displayId) throws InterruptedException {
+ register(displayId);
+ PointF startCenter = INITIAL_MAGNIFICATION_BOUNDS_CENTER;
+ float scale = 2.0f;
+ // First zoom in
+ assertTrue(mFullScreenMagnificationController
+ .setScaleAndCenter(displayId, scale, startCenter.x, startCenter.y, false,
+ SERVICE_ID_1));
+ mMessageCapturingHandler.sendAllMessages();
+
+ PointF newCenter = INITIAL_BOUNDS_LOWER_RIGHT_2X_CENTER;
+ PointF newOffsets = computeOffsets(INITIAL_MAGNIFICATION_BOUNDS, newCenter, scale);
+ mFullScreenMagnificationController.startFling(displayId,
+ /* xPixelsPerSecond= */ 400f,
+ /* yPixelsPerSecond= */ 100f,
+ SERVICE_ID_1
+ );
+ mMessageCapturingHandler.sendAllMessages();
+
+ verify(mMockTimeAnimator).start();
+ verify(mMockScroller).fling(
+ /* startX= */ eq((int) newOffsets.x / 2),
+ /* startY= */ eq((int) newOffsets.y / 2),
+ /* velocityX= */ eq(400),
+ /* velocityY= */ eq(100),
+ /* minX= */ anyInt(),
+ /* minY= */ anyInt(),
+ /* maxX= */ anyInt(),
+ /* maxY= */ anyInt()
+ );
+ }
+
+ @Test
+ @RequiresFlagsEnabled(Flags.FLAG_FULLSCREEN_FLING_GESTURE)
+ public void testStopFling_whileMagnifyingAndFlinging_stops() throws InterruptedException {
+ for (int i = 0; i < DISPLAY_COUNT; i++) {
+ stopFling_whileMagnifyingAndFlinging_stops(i);
+ resetMockWindowManager();
+ }
+ }
+
+ private void stopFling_whileMagnifyingAndFlinging_stops(int displayId)
+ throws InterruptedException {
+ register(displayId);
+ PointF startCenter = INITIAL_MAGNIFICATION_BOUNDS_CENTER;
+ float scale = 2.0f;
+ PointF startOffsets = computeOffsets(INITIAL_MAGNIFICATION_BOUNDS, startCenter, scale);
+ // First zoom in
+ assertTrue(mFullScreenMagnificationController
+ .setScaleAndCenter(displayId, scale, startCenter.x, startCenter.y, false,
+ SERVICE_ID_1));
+ mMessageCapturingHandler.sendAllMessages();
+
+ mFullScreenMagnificationController.startFling(displayId,
+ /* xPixelsPerSecond= */ 400f,
+ /* yPixelsPerSecond= */ 100f,
+ SERVICE_ID_1
+ );
+ mMessageCapturingHandler.sendAllMessages();
+
+ when(mMockTimeAnimator.isRunning()).thenReturn(true);
+
+ mFullScreenMagnificationController.cancelFling(displayId, SERVICE_ID_1);
+ mMessageCapturingHandler.sendAllMessages();
+
+ verify(mMockTimeAnimator).cancel();
+ // Can't verify forceFinished() because it's final
+// verify(mMockScroller).forceFinished(eq(true));
+ }
+
+ @Test
public void testGetIdOfLastServiceToChange_returnsCorrectValue() {
for (int i = 0; i < DISPLAY_COUNT; i++) {
getIdOfLastServiceToChange_returnsCorrectValue(i);
@@ -736,7 +831,7 @@
verify(mRequestObserver, never()).onFullScreenMagnificationChanged(eq(displayId),
any(Region.class), any(MagnificationConfig.class));
- verify(mAnimationCallback).onResult(true);
+ verify(mAnimationCallback).onResult(eq(true), any());
}
@Test
@@ -772,7 +867,7 @@
mMessageCapturingHandler.sendAllMessages();
// Verify expected actions.
- verify(mAnimationCallback).onResult(false);
+ verify(mAnimationCallback).onResult(eq(false), any());
verify(mMockValueAnimator).start();
verify(mMockValueAnimator).cancel();
@@ -782,7 +877,7 @@
mStateListener.onAnimationEnd(mMockValueAnimator);
checkActivatedAndMagnifying(/* activated= */ false, /* magnifying= */ false, displayId);
- verify(lastAnimationCallback).onResult(true);
+ verify(lastAnimationCallback).onResult(eq(true), any());
}
@Test
@@ -1379,6 +1474,8 @@
private void resetMockWindowManager() {
Mockito.reset(mMockWindowManager);
Mockito.reset(mMockThumbnail);
+ Mockito.reset(mMockScroller);
+ Mockito.reset(mMockTimeAnimator);
initMockWindowManager();
}
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandlerTest.java b/services/tests/servicestests/src/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandlerTest.java
index 71d64cf..8c0d44c 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandlerTest.java
+++ b/services/tests/servicestests/src/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandlerTest.java
@@ -31,6 +31,7 @@
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.junit.Assume.assumeTrue;
+import static org.mockito.AdditionalMatchers.gt;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Matchers.any;
@@ -40,10 +41,13 @@
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
+import android.animation.TimeAnimator;
import android.animation.ValueAnimator;
import android.annotation.NonNull;
import android.content.pm.PackageManager;
@@ -65,6 +69,7 @@
import android.view.InputDevice;
import android.view.MotionEvent;
import android.view.ViewConfiguration;
+import android.widget.Scroller;
import androidx.test.filters.FlakyTest;
import androidx.test.runner.AndroidJUnit4;
@@ -79,6 +84,8 @@
import com.android.server.testutils.TestHandler;
import com.android.server.wm.WindowManagerInternal;
+import com.google.common.truth.Truth;
+
import org.junit.After;
import org.junit.Before;
import org.junit.Ignore;
@@ -186,6 +193,8 @@
@Rule
public final TestableContext mContext = new TestableContext(getInstrumentation().getContext());
+ private final Scroller mMockScroller = spy(new Scroller(mContext));
+
private OffsettableClock mClock;
private FullScreenMagnificationGestureHandler mMgh;
private TestHandler mHandler;
@@ -218,18 +227,21 @@
Settings.Secure.putFloatForUser(mContext.getContentResolver(),
Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_SCALE, 2.0f,
UserHandle.USER_SYSTEM);
- mFullScreenMagnificationController = new FullScreenMagnificationController(
- mockController,
- new Object(),
- mMagnificationInfoChangedCallback,
- new MagnificationScaleProvider(mContext),
- () -> null,
- ConcurrentUtils.DIRECT_EXECUTOR) {
- @Override
- public boolean magnificationRegionContains(int displayId, float x, float y) {
- return true;
- }
- };
+ mFullScreenMagnificationController =
+ new FullScreenMagnificationController(
+ mockController,
+ new Object(),
+ mMagnificationInfoChangedCallback,
+ new MagnificationScaleProvider(mContext),
+ () -> null,
+ ConcurrentUtils.DIRECT_EXECUTOR,
+ () -> mMockScroller,
+ TimeAnimator::new) {
+ @Override
+ public boolean magnificationRegionContains(int displayId, float x, float y) {
+ return true;
+ }
+ };
doAnswer((Answer<Void>) invocationOnMock -> {
Object[] args = invocationOnMock.getArguments();
@@ -263,11 +275,20 @@
@NonNull
private FullScreenMagnificationGestureHandler newInstance(boolean detectSingleFingerTripleTap,
boolean detectTwoFingerTripleTap, boolean detectShortcutTrigger) {
- FullScreenMagnificationGestureHandler h = new FullScreenMagnificationGestureHandler(
- mContext, mFullScreenMagnificationController, mMockTraceManager, mMockCallback,
- detectSingleFingerTripleTap, detectTwoFingerTripleTap, detectShortcutTrigger,
- mWindowMagnificationPromptController, DISPLAY_0,
- mMockFullScreenMagnificationVibrationHelper, mMockMagnificationLogger);
+ FullScreenMagnificationGestureHandler h =
+ new FullScreenMagnificationGestureHandler(
+ mContext,
+ mFullScreenMagnificationController,
+ mMockTraceManager,
+ mMockCallback,
+ detectSingleFingerTripleTap,
+ detectTwoFingerTripleTap,
+ detectShortcutTrigger,
+ mWindowMagnificationPromptController,
+ DISPLAY_0,
+ mMockFullScreenMagnificationVibrationHelper,
+ mMockMagnificationLogger,
+ ViewConfiguration.get(mContext));
if (isWatch()) {
h.setSinglePanningEnabled(true);
} else {
@@ -724,7 +745,7 @@
//The minimum movement to transit to panningState.
final float sWipeMinDistance = ViewConfiguration.get(mContext).getScaledTouchSlop();
pointer2.offset(sWipeMinDistance + 1, 0);
- send(pointerEvent(ACTION_MOVE, new PointF[] {pointer1, pointer2}, 1));
+ send(pointerEvent(ACTION_MOVE, new PointF[] {pointer1, pointer2}, 0));
fastForward(ViewConfiguration.getTapTimeout());
assertIn(STATE_PANNING);
@@ -743,7 +764,7 @@
//The minimum movement to transit to panningState.
final float sWipeMinDistance = ViewConfiguration.get(mContext).getScaledTouchSlop();
pointer2.offset(sWipeMinDistance + 1, 0);
- send(pointerEvent(ACTION_MOVE, new PointF[] {pointer1, pointer2}, 1));
+ send(pointerEvent(ACTION_MOVE, new PointF[] {pointer1, pointer2}, 0));
fastForward(ViewConfiguration.getTapTimeout());
assertIn(STATE_PANNING);
@@ -762,7 +783,7 @@
//The minimum movement to transit to panningState.
final float sWipeMinDistance = ViewConfiguration.get(mContext).getScaledTouchSlop();
pointer2.offset(sWipeMinDistance + 1, 0);
- send(pointerEvent(ACTION_MOVE, new PointF[] {pointer1, pointer2}, 1));
+ send(pointerEvent(ACTION_MOVE, new PointF[] {pointer1, pointer2}, 0));
assertIn(STATE_PANNING);
returnToNormalFrom(STATE_PANNING);
@@ -780,7 +801,7 @@
//The minimum movement to transit to panningState.
final float sWipeMinDistance = ViewConfiguration.get(mContext).getScaledTouchSlop();
pointer2.offset(sWipeMinDistance + 1, 0);
- send(pointerEvent(ACTION_MOVE, new PointF[] {pointer1, pointer2}, 1));
+ send(pointerEvent(ACTION_MOVE, new PointF[] {pointer1, pointer2}, 0));
assertIn(STATE_PANNING);
returnToNormalFrom(STATE_PANNING);
@@ -972,6 +993,198 @@
}
@Test
+ public void singleFinger_testScrollAfterMagnified_startsFling() {
+ assumeTrue(mMgh.mIsSinglePanningEnabled);
+ goFromStateIdleTo(STATE_ACTIVATED);
+
+ swipeAndHold();
+ fastForward(20);
+ swipe(DEFAULT_POINT, new PointF(DEFAULT_X * 2, DEFAULT_Y * 2), /* durationMs= */ 20);
+
+ verify(mMockScroller).fling(
+ /* startX= */ anyInt(),
+ /* startY= */ anyInt(),
+ // The system fling velocity is configurable and hard to test across devices, so as
+ // long as there is some fling velocity, we are happy.
+ /* velocityX= */ gt(1000),
+ /* velocityY= */ gt(1000),
+ /* minX= */ anyInt(),
+ /* minY= */ anyInt(),
+ /* maxX= */ anyInt(),
+ /* maxY= */ anyInt()
+ );
+ }
+
+ @Test
+ @RequiresFlagsDisabled(Flags.FLAG_FULLSCREEN_FLING_GESTURE)
+ public void testTwoFingerPanDiagonalAfterMagnified_doesNotFlingXY()
+ throws InterruptedException {
+ goFromStateIdleTo(STATE_ACTIVATED);
+ PointF pointer1 = DEFAULT_POINT;
+ PointF pointer2 = new PointF(DEFAULT_X * 1.5f, DEFAULT_Y);
+
+ send(downEvent());
+ send(pointerEvent(ACTION_POINTER_DOWN, new PointF[]{pointer1, pointer2}, 1));
+
+ // first move triggers the panning state
+ pointer1.offset(100, 100);
+ pointer2.offset(100, 100);
+ fastForward(20);
+ send(pointerEvent(ACTION_MOVE, new PointF[]{pointer1, pointer2}, 0));
+
+ // second move actually pans
+ pointer1.offset(100, 100);
+ pointer2.offset(100, 100);
+ fastForward(20);
+ send(pointerEvent(ACTION_MOVE, new PointF[]{pointer1, pointer2}, 0));
+ pointer1.offset(100, 100);
+ pointer2.offset(100, 100);
+ fastForward(20);
+ send(pointerEvent(ACTION_MOVE, new PointF[]{pointer1, pointer2}, 0));
+
+ assertIn(STATE_PANNING);
+ mHandler.timeAdvance();
+ returnToNormalFrom(STATE_PANNING);
+
+ mHandler.timeAdvance();
+
+ verifyNoMoreInteractions(mMockScroller);
+ }
+
+ @Test
+ @RequiresFlagsEnabled(Flags.FLAG_FULLSCREEN_FLING_GESTURE)
+ public void testTwoFingerPanDiagonalAfterMagnified_startsFlingXY()
+ throws InterruptedException {
+ goFromStateIdleTo(STATE_ACTIVATED);
+ PointF pointer1 = DEFAULT_POINT;
+ PointF pointer2 = new PointF(DEFAULT_X * 1.5f, DEFAULT_Y);
+
+ send(downEvent());
+ send(pointerEvent(ACTION_POINTER_DOWN, new PointF[] {pointer1, pointer2}, 1));
+
+ // first move triggers the panning state
+ pointer1.offset(100, 100);
+ pointer2.offset(100, 100);
+ fastForward(20);
+ send(pointerEvent(ACTION_MOVE, new PointF[] {pointer1, pointer2}, 0));
+
+ // second move actually pans
+ pointer1.offset(100, 100);
+ pointer2.offset(100, 100);
+ fastForward(20);
+ send(pointerEvent(ACTION_MOVE, new PointF[] {pointer1, pointer2}, 0));
+ pointer1.offset(100, 100);
+ pointer2.offset(100, 100);
+ fastForward(20);
+ send(pointerEvent(ACTION_MOVE, new PointF[] {pointer1, pointer2}, 0));
+
+ assertIn(STATE_PANNING);
+ mHandler.timeAdvance();
+ returnToNormalFrom(STATE_PANNING);
+
+ mHandler.timeAdvance();
+
+ verify(mMockScroller).fling(
+ /* startX= */ anyInt(),
+ /* startY= */ anyInt(),
+ // The system fling velocity is configurable and hard to test across devices, so as
+ // long as there is some fling velocity, we are happy.
+ /* velocityX= */ gt(1000),
+ /* velocityY= */ gt(1000),
+ /* minX= */ anyInt(),
+ /* minY= */ anyInt(),
+ /* maxX= */ anyInt(),
+ /* maxY= */ anyInt()
+ );
+ }
+
+ @Test
+ @RequiresFlagsEnabled(Flags.FLAG_FULLSCREEN_FLING_GESTURE)
+ public void testTwoFingerPanRightAfterMagnified_startsFlingXOnly()
+ throws InterruptedException {
+ goFromStateIdleTo(STATE_ACTIVATED);
+ PointF pointer1 = DEFAULT_POINT;
+ PointF pointer2 = new PointF(DEFAULT_X * 1.5f, DEFAULT_Y);
+
+ send(downEvent());
+ send(pointerEvent(ACTION_POINTER_DOWN, new PointF[] {pointer1, pointer2}, 1));
+
+ // first move triggers the panning state
+ pointer1.offset(100, 0);
+ pointer2.offset(100, 0);
+ fastForward(20);
+ send(pointerEvent(ACTION_MOVE, new PointF[] {pointer1, pointer2}, 0));
+
+ // second move actually pans
+ pointer1.offset(100, 0);
+ pointer2.offset(100, 0);
+ fastForward(20);
+ send(pointerEvent(ACTION_MOVE, new PointF[] {pointer1, pointer2}, 0));
+ pointer1.offset(100, 0);
+ pointer2.offset(100, 0);
+ fastForward(20);
+ send(pointerEvent(ACTION_MOVE, new PointF[] {pointer1, pointer2}, 0));
+
+ assertIn(STATE_PANNING);
+ mHandler.timeAdvance();
+ returnToNormalFrom(STATE_PANNING);
+
+ mHandler.timeAdvance();
+
+ verify(mMockScroller).fling(
+ /* startX= */ anyInt(),
+ /* startY= */ anyInt(),
+ // The system fling velocity is configurable and hard to test across devices, so as
+ // long as there is some fling velocity, we are happy.
+ /* velocityX= */ gt(100),
+ /* velocityY= */ eq(0),
+ /* minX= */ anyInt(),
+ /* minY= */ anyInt(),
+ /* maxX= */ anyInt(),
+ /* maxY= */ anyInt()
+ );
+ }
+
+ @Test
+ @RequiresFlagsEnabled(Flags.FLAG_FULLSCREEN_FLING_GESTURE)
+ public void testDownEvent_cancelsFling()
+ throws InterruptedException {
+ goFromStateIdleTo(STATE_ACTIVATED);
+ PointF pointer1 = DEFAULT_POINT;
+ PointF pointer2 = new PointF(DEFAULT_X * 1.5f, DEFAULT_Y);
+
+ send(downEvent());
+ send(pointerEvent(ACTION_POINTER_DOWN, new PointF[] {pointer1, pointer2}, 1));
+
+ // first move triggers the panning state
+ pointer1.offset(100, 0);
+ pointer2.offset(100, 0);
+ fastForward(20);
+ send(pointerEvent(ACTION_MOVE, new PointF[] {pointer1, pointer2}, 0));
+
+ // second move actually pans
+ pointer1.offset(100, 0);
+ pointer2.offset(100, 0);
+ fastForward(20);
+ send(pointerEvent(ACTION_MOVE, new PointF[] {pointer1, pointer2}, 0));
+ pointer1.offset(100, 0);
+ pointer2.offset(100, 0);
+ fastForward(20);
+ send(pointerEvent(ACTION_MOVE, new PointF[] {pointer1, pointer2}, 0));
+
+ assertIn(STATE_PANNING);
+ mHandler.timeAdvance();
+ returnToNormalFrom(STATE_PANNING);
+
+ mHandler.timeAdvance();
+
+ send(downEvent());
+ mHandler.timeAdvance();
+
+ verify(mMockScroller).forceFinished(eq(true));
+ }
+
+ @Test
public void testShortcutTriggered_invokeShowWindowPromptAction() {
goFromStateIdleTo(STATE_SHORTCUT_TRIGGERED);
@@ -1397,8 +1610,11 @@
send(upEvent());
}
- private void swipe(PointF start, PointF end) {
- swipeAndHold(start, end);
+ private void swipe(PointF start, PointF end, int durationMs) {
+ var mid = new PointF(start.x + (end.x - start.x) / 2f, start.y + (end.y - start.y) / 2f);
+ swipeAndHold(start, mid);
+ fastForward(durationMs);
+ send(moveEvent(end.x - start.x / 10f, end.y - start.y / 10f));
send(upEvent(end.x, end.y));
}
@@ -1491,9 +1707,18 @@
private MotionEvent pointerEvent(int action, float x, float y) {
- return pointerEvent(action, new PointF[] {DEFAULT_POINT, new PointF(x, y)}, 1);
+ return pointerEvent(action, new PointF[] {DEFAULT_POINT, new PointF(x, y)},
+ (action == ACTION_POINTER_UP || action == ACTION_POINTER_DOWN) ? 1 : 0);
}
+ /**
+ * Create a pointer event simulating the given pointer positions.
+ *
+ * @param action the action
+ * @param pointersPosition positions of the pointers
+ * @param changedIndex the index of the pointer associated with the ACTION_POINTER_UP or
+ * ACTION_POINTER_DOWN action. Must be 0 for all other actions.
+ */
private MotionEvent pointerEvent(int action, PointF[] pointersPosition, int changedIndex) {
final MotionEvent.PointerProperties[] PointerPropertiesArray =
new MotionEvent.PointerProperties[pointersPosition.length];
@@ -1513,12 +1738,12 @@
pointerCoordsArray[i] = pointerCoords;
}
- action += (changedIndex << ACTION_POINTER_INDEX_SHIFT);
+ var actionWithPointer = action | (changedIndex << ACTION_POINTER_INDEX_SHIFT);
- return MotionEvent.obtain(
+ var event = MotionEvent.obtain(
/* downTime */ mClock.now(),
/* eventTime */ mClock.now(),
- /* action */ action,
+ /* action */ actionWithPointer,
/* pointerCount */ pointersPosition.length,
/* pointerProperties */ PointerPropertiesArray,
/* pointerCoords */ pointerCoordsArray,
@@ -1530,6 +1755,14 @@
/* edgeFlags */ 0,
/* source */ InputDevice.SOURCE_TOUCHSCREEN,
/* flags */ 0);
+
+ Truth.assertThat(event.getActionIndex()).isEqualTo(changedIndex);
+ Truth.assertThat(event.getActionMasked()).isEqualTo(action);
+ if (action == ACTION_DOWN) {
+ Truth.assertThat(changedIndex).isEqualTo(0);
+ }
+
+ return event;
}
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/magnification/MagnificationControllerTest.java b/services/tests/servicestests/src/com/android/server/accessibility/magnification/MagnificationControllerTest.java
index 28d07f9..cd904eb 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/magnification/MagnificationControllerTest.java
+++ b/services/tests/servicestests/src/com/android/server/accessibility/magnification/MagnificationControllerTest.java
@@ -42,6 +42,7 @@
import static org.mockito.Mockito.when;
import android.accessibilityservice.MagnificationConfig;
+import android.animation.TimeAnimator;
import android.animation.ValueAnimator;
import android.content.Context;
import android.content.pm.PackageManager;
@@ -59,6 +60,7 @@
import android.view.DisplayInfo;
import android.view.accessibility.IRemoteMagnificationAnimationCallback;
import android.view.accessibility.MagnificationAnimationCallback;
+import android.widget.Scroller;
import androidx.annotation.NonNull;
import androidx.test.InstrumentationRegistry;
@@ -119,6 +121,8 @@
@Mock
private ValueAnimator mValueAnimator;
@Mock
+ private TimeAnimator mTimeAnimator;
+ @Mock
private MessageCapturingHandler mMessageCapturingHandler;
private FullScreenMagnificationController mScreenMagnificationController;
@@ -195,14 +199,17 @@
LocalServices.removeServiceForTest(DisplayManagerInternal.class);
LocalServices.addService(DisplayManagerInternal.class, mDisplayManagerInternal);
- mScreenMagnificationController = spy(new FullScreenMagnificationController(
- mControllerCtx,
- new Object(),
- mScreenMagnificationInfoChangedCallbackDelegate,
- mScaleProvider,
- () -> null,
- ConcurrentUtils.DIRECT_EXECUTOR
- ));
+ mScreenMagnificationController =
+ spy(
+ new FullScreenMagnificationController(
+ mControllerCtx,
+ new Object(),
+ mScreenMagnificationInfoChangedCallbackDelegate,
+ mScaleProvider,
+ () -> null,
+ ConcurrentUtils.DIRECT_EXECUTOR,
+ () -> new Scroller(mContext),
+ () -> mTimeAnimator));
mScreenMagnificationController.register(TEST_DISPLAY);
mMagnificationConnectionManager = spy(
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/BiometricServiceTest.java b/services/tests/servicestests/src/com/android/server/biometrics/BiometricServiceTest.java
index 408442b..35ad55c 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/BiometricServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/BiometricServiceTest.java
@@ -74,7 +74,9 @@
import android.hardware.fingerprint.FingerprintManager;
import android.hardware.keymaster.HardwareAuthenticatorType;
import android.os.Binder;
+import android.os.Handler;
import android.os.IBinder;
+import android.os.Looper;
import android.os.RemoteException;
import android.os.UserManager;
import android.platform.test.annotations.Presubmit;
@@ -83,6 +85,8 @@
import android.security.KeyStore;
import android.security.authorization.IKeystoreAuthorization;
import android.service.gatekeeper.IGateKeeperService;
+import android.testing.AndroidTestingRunner;
+import android.testing.TestableLooper;
import android.view.Display;
import android.view.DisplayInfo;
import android.view.WindowManager;
@@ -100,6 +104,7 @@
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
+import org.junit.runner.RunWith;
import org.mockito.AdditionalMatchers;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
@@ -110,6 +115,8 @@
@Presubmit
@SmallTest
+@RunWith(AndroidTestingRunner.class)
+@TestableLooper.RunWithLooper()
public class BiometricServiceTest {
@Rule
@@ -171,6 +178,8 @@
private UserManager mUserManager;
@Mock
private BiometricCameraManager mBiometricCameraManager;
+ @Mock
+ private BiometricHandlerProvider mBiometricHandlerProvider;
@Mock
private IKeystoreAuthorization mKeystoreAuthService;
@@ -235,6 +244,14 @@
when(mInjector.getGateKeeperService()).thenReturn(mGateKeeperService);
when(mGateKeeperService.getSecureUserId(anyInt())).thenReturn(42L);
+ if (com.android.server.biometrics.Flags.deHidl()) {
+ when(mBiometricHandlerProvider.getBiometricCallbackHandler()).thenReturn(
+ new Handler(TestableLooper.get(this).getLooper()));
+ } else {
+ when(mBiometricHandlerProvider.getBiometricCallbackHandler()).thenReturn(
+ new Handler(Looper.getMainLooper()));
+ }
+
final String[] config = {
"0:2:15", // ID0:Fingerprint:Strong
"1:8:15", // ID1:Face:Strong
@@ -312,7 +329,7 @@
when(mTrustManager.isDeviceSecure(anyInt(), anyInt()))
.thenReturn(false);
- mBiometricService = new BiometricService(mContext, mInjector);
+ mBiometricService = new BiometricService(mContext, mInjector, mBiometricHandlerProvider);
mBiometricService.onStart();
invokeAuthenticate(mBiometricService.mImpl, mReceiver1, false /* requireConfirmation */,
@@ -333,7 +350,7 @@
when(mTrustManager.isDeviceSecure(anyInt(), anyInt()))
.thenReturn(true);
- mBiometricService = new BiometricService(mContext, mInjector);
+ mBiometricService = new BiometricService(mContext, mInjector, mBiometricHandlerProvider);
mBiometricService.onStart();
invokeAuthenticate(mBiometricService.mImpl, mReceiver1, false /* requireConfirmation */,
@@ -360,7 +377,7 @@
@Test
public void testAuthenticate_withoutHardware_returnsErrorHardwareNotPresent() throws
Exception {
- mBiometricService = new BiometricService(mContext, mInjector);
+ mBiometricService = new BiometricService(mContext, mInjector, mBiometricHandlerProvider);
mBiometricService.onStart();
invokeAuthenticate(mBiometricService.mImpl, mReceiver1, false /* requireConfirmation */,
@@ -377,7 +394,7 @@
public void testAuthenticate_withoutEnrolled_returnsErrorNoBiometrics() throws Exception {
when(mFingerprintAuthenticator.isHardwareDetected(any())).thenReturn(true);
- mBiometricService = new BiometricService(mContext, mInjector);
+ mBiometricService = new BiometricService(mContext, mInjector, mBiometricHandlerProvider);
mBiometricService.onStart();
mBiometricService.mImpl.registerAuthenticator(0 /* id */,
TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG,
@@ -451,7 +468,7 @@
when(mFingerprintAuthenticator.hasEnrolledTemplates(anyInt(), any())).thenReturn(true);
when(mFingerprintAuthenticator.isHardwareDetected(any())).thenReturn(false);
- mBiometricService = new BiometricService(mContext, mInjector);
+ mBiometricService = new BiometricService(mContext, mInjector, mBiometricHandlerProvider);
mBiometricService.onStart();
mBiometricService.mImpl.registerAuthenticator(0 /* id */,
TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG,
@@ -1374,7 +1391,7 @@
@Test
public void testCanAuthenticate_onlyCredentialRequested() throws Exception {
- mBiometricService = new BiometricService(mContext, mInjector);
+ mBiometricService = new BiometricService(mContext, mInjector, mBiometricHandlerProvider);
mBiometricService.onStart();
// Credential requested but not set up
@@ -1428,7 +1445,7 @@
@Test
public void testCanAuthenticate_whenNoBiometricSensor() throws Exception {
- mBiometricService = new BiometricService(mContext, mInjector);
+ mBiometricService = new BiometricService(mContext, mInjector, mBiometricHandlerProvider);
mBiometricService.onStart();
// When only biometric is requested
@@ -1515,7 +1532,7 @@
@Test
public void testRegisterAuthenticator_updatesStrengths() throws Exception {
- mBiometricService = new BiometricService(mContext, mInjector);
+ mBiometricService = new BiometricService(mContext, mInjector, mBiometricHandlerProvider);
mBiometricService.onStart();
verify(mBiometricService.mBiometricStrengthController).startListening();
@@ -1533,7 +1550,7 @@
@Test
public void testWithDowngradedAuthenticator() throws Exception {
- mBiometricService = new BiometricService(mContext, mInjector);
+ mBiometricService = new BiometricService(mContext, mInjector, mBiometricHandlerProvider);
mBiometricService.onStart();
final int testId = 0;
@@ -1639,7 +1656,7 @@
@Test(expected = IllegalStateException.class)
public void testRegistrationWithDuplicateId_throwsIllegalStateException() throws Exception {
- mBiometricService = new BiometricService(mContext, mInjector);
+ mBiometricService = new BiometricService(mContext, mInjector, mBiometricHandlerProvider);
mBiometricService.onStart();
mBiometricService.mImpl.registerAuthenticator(
@@ -1653,7 +1670,7 @@
@Test(expected = IllegalArgumentException.class)
public void testRegistrationWithNullAuthenticator_throwsIllegalArgumentException()
throws Exception {
- mBiometricService = new BiometricService(mContext, mInjector);
+ mBiometricService = new BiometricService(mContext, mInjector, mBiometricHandlerProvider);
mBiometricService.onStart();
mBiometricService.mImpl.registerAuthenticator(
@@ -1665,7 +1682,7 @@
@Test
public void testRegistrationHappyPath_isOk() throws Exception {
// This is being tested in many of the other cases, but here's the base case.
- mBiometricService = new BiometricService(mContext, mInjector);
+ mBiometricService = new BiometricService(mContext, mInjector, mBiometricHandlerProvider);
mBiometricService.onStart();
for (String s : mInjector.getConfiguration(null)) {
@@ -1751,7 +1768,7 @@
final IBiometricEnabledOnKeyguardCallback callback =
mock(IBiometricEnabledOnKeyguardCallback.class);
- mBiometricService = new BiometricService(mContext, mInjector);
+ mBiometricService = new BiometricService(mContext, mInjector, mBiometricHandlerProvider);
when(mUserManager.getAliveUsers()).thenReturn(aliveUsers);
when(mBiometricService.mSettingObserver.getEnabledOnKeyguard(userInfo1.id))
@@ -1775,7 +1792,7 @@
throws RemoteException {
mSetFlagsRule.disableFlags(Flags.FLAG_LAST_AUTHENTICATION_TIME);
- mBiometricService = new BiometricService(mContext, mInjector);
+ mBiometricService = new BiometricService(mContext, mInjector, mBiometricHandlerProvider);
mBiometricService.mImpl.getLastAuthenticationTime(0, Authenticators.BIOMETRIC_STRONG);
}
@@ -1799,7 +1816,7 @@
when(mKeystoreAuthService.getLastAuthTime(eq(secureUserId), eq(hardwareAuthenticators)))
.thenReturn(expectedResult);
- mBiometricService = new BiometricService(mContext, mInjector);
+ mBiometricService = new BiometricService(mContext, mInjector, mBiometricHandlerProvider);
final long result = mBiometricService.mImpl.getLastAuthenticationTime(userId,
Authenticators.BIOMETRIC_STRONG | Authenticators.DEVICE_CREDENTIAL);
@@ -1822,7 +1839,7 @@
// TODO: Reconcile the registration strength with the injector
private void setupAuthForOnly(int modality, int strength, boolean enrolled) throws Exception {
- mBiometricService = new BiometricService(mContext, mInjector);
+ mBiometricService = new BiometricService(mContext, mInjector, mBiometricHandlerProvider);
mBiometricService.onStart();
when(mBiometricService.mSettingObserver.getEnabledForApps(anyInt())).thenReturn(true);
@@ -1855,7 +1872,7 @@
// TODO: Reduce duplicated code, currently we cannot start the BiometricService in setUp() for
// all tests.
private void setupAuthForMultiple(int[] modalities, int[] strengths) throws RemoteException {
- mBiometricService = new BiometricService(mContext, mInjector);
+ mBiometricService = new BiometricService(mContext, mInjector, mBiometricHandlerProvider);
mBiometricService.onStart();
when(mBiometricService.mSettingObserver.getEnabledForApps(anyInt())).thenReturn(true);
@@ -1993,8 +2010,12 @@
return requestWrapper.eligibleSensors.get(0).getCookie();
}
- private static void waitForIdle() {
- InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+ private void waitForIdle() {
+ if (com.android.server.biometrics.Flags.deHidl()) {
+ TestableLooper.get(this).processAllMessages();
+ } else {
+ InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+ }
}
private byte[] generateRandomHAT() {
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/FaceProviderTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/FaceProviderTest.java
index 7648bd17..9eca93e 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/FaceProviderTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/FaceProviderTest.java
@@ -24,19 +24,28 @@
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
+import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.content.Context;
import android.content.res.Resources;
+import android.hardware.biometrics.IBiometricSensorReceiver;
import android.hardware.biometrics.common.CommonProps;
import android.hardware.biometrics.face.IFace;
import android.hardware.biometrics.face.ISession;
import android.hardware.biometrics.face.SensorProps;
+import android.hardware.face.FaceAuthenticateOptions;
import android.hardware.face.HidlFaceSensorConfig;
import android.os.Handler;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.Message;
import android.os.RemoteException;
import android.os.UserManager;
import android.os.test.TestLooper;
@@ -49,18 +58,23 @@
import androidx.test.filters.SmallTest;
import com.android.internal.R;
+import com.android.server.biometrics.BiometricHandlerProvider;
import com.android.server.biometrics.Flags;
import com.android.server.biometrics.log.BiometricContext;
+import com.android.server.biometrics.sensors.AuthSessionCoordinator;
import com.android.server.biometrics.sensors.AuthenticationStateListeners;
import com.android.server.biometrics.sensors.BaseClientMonitor;
import com.android.server.biometrics.sensors.BiometricScheduler;
import com.android.server.biometrics.sensors.BiometricStateCallback;
+import com.android.server.biometrics.sensors.ClientMonitorCallback;
+import com.android.server.biometrics.sensors.ClientMonitorCallbackConverter;
import com.android.server.biometrics.sensors.HalClientMonitor;
import com.android.server.biometrics.sensors.LockoutResetDispatcher;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
+import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
@@ -92,6 +106,14 @@
private BiometricStateCallback mBiometricStateCallback;
@Mock
private AuthenticationStateListeners mAuthenticationStateListeners;
+ @Mock
+ private BiometricHandlerProvider mBiometricHandlerProvider;
+ @Mock
+ private Handler mBiometricCallbackHandler;
+ @Mock
+ private BiometricScheduler<IFace, ISession> mScheduler;
+ @Mock
+ AuthSessionCoordinator mAuthSessionCoordinator;
private final TestLooper mLooper = new TestLooper();
private SensorProps[] mSensorProps;
@@ -109,6 +131,16 @@
when(mContext.getResources()).thenReturn(mResources);
when(mResources.getFraction(R.fraction.config_biometricNotificationFrrThreshold, 1, 1))
.thenReturn(FRR_THRESHOLD);
+ when(mBiometricHandlerProvider.getBiometricCallbackHandler()).thenReturn(
+ mBiometricCallbackHandler);
+ when(mBiometricContext.getAuthSessionCoordinator()).thenReturn(mAuthSessionCoordinator);
+ if (Flags.deHidl()) {
+ when(mBiometricHandlerProvider.getFaceHandler()).thenReturn(new Handler(
+ mLooper.getLooper()));
+ } else {
+ when(mBiometricHandlerProvider.getFaceHandler()).thenReturn(new Handler(
+ Looper.getMainLooper()));
+ }
final SensorProps sensor1 = new SensorProps();
sensor1.commonProps = new CommonProps();
@@ -123,7 +155,7 @@
mFaceProvider = new FaceProvider(mContext, mBiometricStateCallback,
mAuthenticationStateListeners, mSensorProps, TAG, mLockoutResetDispatcher,
- mBiometricContext, mDaemon, new Handler(mLooper.getLooper()),
+ mBiometricContext, mDaemon, mBiometricHandlerProvider,
false /* resetLockoutRequiresChallenge */, false /* testHalEnabled */);
}
@@ -159,8 +191,7 @@
mFaceProvider = new FaceProvider(mContext,
mBiometricStateCallback, mAuthenticationStateListeners, hidlFaceSensorConfig, TAG,
mLockoutResetDispatcher, mBiometricContext, mDaemon,
- new Handler(mLooper.getLooper()),
- true /* resetLockoutRequiresChallenge */,
+ mBiometricHandlerProvider, true /* resetLockoutRequiresChallenge */,
true /* testHalEnabled */);
assertThat(mFaceProvider.mFaceSensors.get(faceId)
@@ -215,6 +246,54 @@
}
}
+ @Test
+ @RequiresFlagsEnabled(Flags.FLAG_DE_HIDL)
+ public void testAuthenticateCallbackHandler() {
+ waitForIdle();
+
+ mFaceProvider.mFaceSensors.get(0).setScheduler(mScheduler);
+ mFaceProvider.scheduleAuthenticate(mock(IBinder.class), 0 /* operationId */,
+ 0 /* cookie */, new ClientMonitorCallbackConverter(
+ new IBiometricSensorReceiver.Default()),
+ new FaceAuthenticateOptions.Builder()
+ .setSensorId(0)
+ .build(),
+ false /* restricted */, 1 /* statsClient */,
+ true /* allowBackgroundAuthentication */);
+
+ waitForIdle();
+
+ ArgumentCaptor<ClientMonitorCallback> callbackArgumentCaptor = ArgumentCaptor.forClass(
+ ClientMonitorCallback.class);
+ ArgumentCaptor<BaseClientMonitor> clientMonitorArgumentCaptor = ArgumentCaptor.forClass(
+ BaseClientMonitor.class);
+ ArgumentCaptor<Message> messageCaptor = ArgumentCaptor.forClass(
+ Message.class);
+
+ verify(mScheduler).scheduleClientMonitor(clientMonitorArgumentCaptor.capture(),
+ callbackArgumentCaptor.capture());
+
+ BaseClientMonitor client = clientMonitorArgumentCaptor.getValue();
+ ClientMonitorCallback callback = callbackArgumentCaptor.getValue();
+ callback.onClientStarted(client);
+
+ verify(mBiometricCallbackHandler).sendMessageAtTime(messageCaptor.capture(), anyLong());
+
+ messageCaptor.getValue().getCallback().run();
+
+ verify(mAuthSessionCoordinator).authStartedFor(anyInt(), anyInt(), anyLong());
+
+ callback.onClientFinished(client, true /* success */);
+
+ verify(mBiometricCallbackHandler, times(2)).sendMessageAtTime(
+ messageCaptor.capture(), anyLong());
+
+ messageCaptor.getValue().getCallback().run();
+
+ verify(mAuthSessionCoordinator).authEndedFor(anyInt(), anyInt(), anyInt(), anyLong(),
+ anyBoolean());
+ }
+
private void waitForIdle() {
if (Flags.deHidl()) {
mLooper.dispatchAll();
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClientTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClientTest.java
index f96d9e8..7a77392 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClientTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClientTest.java
@@ -18,6 +18,7 @@
import static android.adaptiveauth.Flags.FLAG_REPORT_BIOMETRIC_AUTH_ATTEMPTS;
import static android.hardware.biometrics.BiometricConstants.BIOMETRIC_ERROR_CANCELED;
+import static android.hardware.biometrics.BiometricFingerprintConstants.FINGERPRINT_ACQUIRED_START;
import static com.android.systemui.shared.Flags.FLAG_SIDEFPS_CONTROLLER_REFACTOR;
@@ -471,6 +472,7 @@
verify(mUdfpsOverlayController).hideUdfpsOverlay(anyInt());
verify(mSideFpsController).hide(anyInt());
+ verify(mHal, times(2)).setIgnoreDisplayTouches(false);
}
@Test
@@ -517,6 +519,16 @@
}
@Test
+ public void testAuthenticationStateListeners_onAuthenticationAcquired()
+ throws RemoteException {
+ final FingerprintAuthenticationClient client = createClient();
+ client.start(mCallback);
+ client.onAcquired(FINGERPRINT_ACQUIRED_START, 0);
+
+ verify(mAuthenticationStateListeners).onAuthenticationAcquired(any(), anyInt(), anyInt());
+ }
+
+ @Test
public void testAuthenticationStateListeners_onAuthenticationSucceeded()
throws RemoteException {
mSetFlagsRule.enableFlags(FLAG_REPORT_BIOMETRIC_AUTH_ATTEMPTS);
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintEnrollClientTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintEnrollClientTest.java
index 951c9393..3ee54f5 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintEnrollClientTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintEnrollClientTest.java
@@ -26,6 +26,7 @@
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.same;
+import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -327,6 +328,7 @@
verify(mUdfpsOverlayController).hideUdfpsOverlay(anyInt());
verify(mSideFpsController).hide(anyInt());
+ verify(mHal, times(2)).setIgnoreDisplayTouches(false);
}
@Test
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintProviderTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintProviderTest.java
index 258be57..0a35037 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintProviderTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintProviderTest.java
@@ -24,21 +24,31 @@
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.anyInt;
import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.content.Context;
import android.content.res.Resources;
import android.content.res.TypedArray;
+import android.hardware.biometrics.IBiometricSensorReceiver;
import android.hardware.biometrics.common.CommonProps;
import android.hardware.biometrics.fingerprint.IFingerprint;
import android.hardware.biometrics.fingerprint.ISession;
import android.hardware.biometrics.fingerprint.SensorLocation;
import android.hardware.biometrics.fingerprint.SensorProps;
+import android.hardware.fingerprint.FingerprintAuthenticateOptions;
import android.hardware.fingerprint.HidlFingerprintSensorConfig;
import android.os.Handler;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.Message;
import android.os.RemoteException;
import android.os.UserManager;
import android.os.test.TestLooper;
@@ -50,11 +60,16 @@
import androidx.test.InstrumentationRegistry;
import androidx.test.filters.SmallTest;
+import com.android.server.biometrics.BiometricHandlerProvider;
import com.android.server.biometrics.Flags;
import com.android.server.biometrics.log.BiometricContext;
+import com.android.server.biometrics.sensors.AuthSessionCoordinator;
import com.android.server.biometrics.sensors.AuthenticationStateListeners;
+import com.android.server.biometrics.sensors.BaseClientMonitor;
import com.android.server.biometrics.sensors.BiometricScheduler;
import com.android.server.biometrics.sensors.BiometricStateCallback;
+import com.android.server.biometrics.sensors.ClientMonitorCallback;
+import com.android.server.biometrics.sensors.ClientMonitorCallbackConverter;
import com.android.server.biometrics.sensors.HalClientMonitor;
import com.android.server.biometrics.sensors.LockoutResetDispatcher;
import com.android.server.biometrics.sensors.fingerprint.GestureAvailabilityDispatcher;
@@ -62,6 +77,7 @@
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
+import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
@@ -93,6 +109,14 @@
private BiometricStateCallback mBiometricStateCallback;
@Mock
private BiometricContext mBiometricContext;
+ @Mock
+ private BiometricHandlerProvider mBiometricHandlerProvider;
+ @Mock
+ private Handler mBiometricCallbackHandler;
+ @Mock
+ private AuthSessionCoordinator mAuthSessionCoordinator;
+ @Mock
+ private BiometricScheduler<IFingerprint, ISession> mScheduler;
private final TestLooper mLooper = new TestLooper();
@@ -109,6 +133,16 @@
when(mContext.getSystemService(Context.USER_SERVICE)).thenReturn(mUserManager);
when(mUserManager.getAliveUsers()).thenReturn(new ArrayList<>());
when(mDaemon.createSession(anyInt(), anyInt(), any())).thenReturn(mock(ISession.class));
+ when(mBiometricContext.getAuthSessionCoordinator()).thenReturn(mAuthSessionCoordinator);
+ when(mBiometricHandlerProvider.getBiometricCallbackHandler()).thenReturn(
+ mBiometricCallbackHandler);
+ if (Flags.deHidl()) {
+ when(mBiometricHandlerProvider.getFingerprintHandler()).thenReturn(
+ new Handler(mLooper.getLooper()));
+ } else {
+ when(mBiometricHandlerProvider.getFingerprintHandler()).thenReturn(
+ new Handler(Looper.getMainLooper()));
+ }
final SensorProps sensor1 = new SensorProps();
sensor1.commonProps = new CommonProps();
@@ -126,9 +160,8 @@
mFingerprintProvider = new FingerprintProvider(mContext,
mBiometricStateCallback, mAuthenticationStateListeners, mSensorProps, TAG,
mLockoutResetDispatcher, mGestureAvailabilityDispatcher, mBiometricContext,
- mDaemon, new Handler(mLooper.getLooper()),
- false /* resetLockoutRequiresHardwareAuthToken */,
- true /* testHalEnabled */);
+ mDaemon, mBiometricHandlerProvider,
+ false /* resetLockoutRequiresHardwareAuthToken */, true /* testHalEnabled */);
}
@Test
@@ -160,7 +193,7 @@
mBiometricStateCallback, mAuthenticationStateListeners,
hidlFingerprintSensorConfigs, TAG, mLockoutResetDispatcher,
mGestureAvailabilityDispatcher, mBiometricContext, mDaemon,
- new Handler(mLooper.getLooper()),
+ mBiometricHandlerProvider,
false /* resetLockoutRequiresHardwareAuthToken */,
true /* testHalEnabled */);
@@ -218,6 +251,56 @@
}
}
+ @Test
+ @RequiresFlagsEnabled(Flags.FLAG_DE_HIDL)
+ public void testScheduleAuthenticate() {
+ waitForIdle();
+
+ mFingerprintProvider.mFingerprintSensors.get(0).setScheduler(mScheduler);
+ mFingerprintProvider.scheduleAuthenticate(mock(IBinder.class), 0 /* operationId */,
+ 0 /* cookie */, new ClientMonitorCallbackConverter(
+ new IBiometricSensorReceiver.Default()),
+ new FingerprintAuthenticateOptions.Builder()
+ .setSensorId(0)
+ .build(),
+ false /* restricted */, 1 /* statsClient */,
+ true /* allowBackgroundAuthentication */);
+
+ waitForIdle();
+
+ ArgumentCaptor<ClientMonitorCallback> callbackArgumentCaptor = ArgumentCaptor.forClass(
+ ClientMonitorCallback.class);
+ ArgumentCaptor<BaseClientMonitor> clientMonitorArgumentCaptor = ArgumentCaptor.forClass(
+ BaseClientMonitor.class);
+ ArgumentCaptor<Message> messageCaptor = ArgumentCaptor.forClass(
+ Message.class);
+
+ verify(mScheduler).scheduleClientMonitor(clientMonitorArgumentCaptor.capture(),
+ callbackArgumentCaptor.capture());
+
+ BaseClientMonitor client = clientMonitorArgumentCaptor.getValue();
+ ClientMonitorCallback callback = callbackArgumentCaptor.getValue();
+ callback.onClientStarted(client);
+
+ verify(mBiometricStateCallback).onClientStarted(eq(client));
+ verify(mBiometricCallbackHandler).sendMessageAtTime(messageCaptor.capture(), anyLong());
+
+ messageCaptor.getValue().getCallback().run();
+
+ verify(mAuthSessionCoordinator).authStartedFor(anyInt(), anyInt(), anyLong());
+
+ callback.onClientFinished(client, true /* success */);
+
+ verify(mBiometricStateCallback).onClientFinished(eq(client), eq(true /* success */));
+ verify(mBiometricCallbackHandler, times(2)).sendMessageAtTime(
+ messageCaptor.capture(), anyLong());
+
+ messageCaptor.getValue().getCallback().run();
+
+ verify(mAuthSessionCoordinator).authEndedFor(anyInt(), anyInt(), anyInt(), anyLong(),
+ anyBoolean());
+ }
+
private void waitForIdle() {
if (Flags.deHidl()) {
mLooper.dispatchAll();
diff --git a/services/tests/servicestests/src/com/android/server/companion/virtual/InputControllerTest.java b/services/tests/servicestests/src/com/android/server/companion/virtual/InputControllerTest.java
index 5943832..07e6ab2 100644
--- a/services/tests/servicestests/src/com/android/server/companion/virtual/InputControllerTest.java
+++ b/services/tests/servicestests/src/com/android/server/companion/virtual/InputControllerTest.java
@@ -19,11 +19,10 @@
import static com.google.common.truth.Truth.assertWithMessage;
import static org.junit.Assert.assertThrows;
-import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.startsWith;
-import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.verify;
import android.hardware.display.DisplayManagerInternal;
@@ -86,9 +85,8 @@
LocalServices.removeServiceForTest(InputManagerInternal.class);
LocalServices.addService(InputManagerInternal.class, mInputManagerInternalMock);
- final DisplayInfo displayInfo = new DisplayInfo();
- displayInfo.uniqueId = "uniqueId";
- doReturn(displayInfo).when(mDisplayManagerInternalMock).getDisplayInfo(anyInt());
+ setUpDisplay(1 /* displayId */);
+ setUpDisplay(2 /* displayId */);
LocalServices.removeServiceForTest(DisplayManagerInternal.class);
LocalServices.addService(DisplayManagerInternal.class, mDisplayManagerInternalMock);
@@ -100,6 +98,16 @@
threadVerifier);
}
+ void setUpDisplay(int displayId) {
+ final String uniqueId = "uniqueId:" + displayId;
+ doAnswer((inv) -> {
+ final DisplayInfo displayInfo = new DisplayInfo();
+ displayInfo.uniqueId = uniqueId;
+ return displayInfo;
+ }).when(mDisplayManagerInternalMock).getDisplayInfo(eq(displayId));
+ mInputManagerMockHelper.addDisplayIdMapping(uniqueId, displayId);
+ }
+
@After
public void tearDown() {
mInputManagerMockHelper.tearDown();
diff --git a/services/tests/servicestests/src/com/android/server/companion/virtual/InputManagerMockHelper.java b/services/tests/servicestests/src/com/android/server/companion/virtual/InputManagerMockHelper.java
index 3722247..74e854e4 100644
--- a/services/tests/servicestests/src/com/android/server/companion/virtual/InputManagerMockHelper.java
+++ b/services/tests/servicestests/src/com/android/server/companion/virtual/InputManagerMockHelper.java
@@ -20,7 +20,6 @@
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.notNull;
import static org.mockito.Mockito.doAnswer;
-import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.when;
import android.hardware.input.IInputDevicesChangedListener;
@@ -28,12 +27,15 @@
import android.hardware.input.InputManagerGlobal;
import android.os.RemoteException;
import android.testing.TestableLooper;
+import android.view.Display;
import android.view.InputDevice;
import org.mockito.invocation.InvocationOnMock;
import java.util.ArrayList;
+import java.util.HashMap;
import java.util.List;
+import java.util.Map;
import java.util.Objects;
import java.util.stream.IntStream;
@@ -49,6 +51,10 @@
private final InputManagerGlobal.TestSession mInputManagerGlobalSession;
private final List<InputDevice> mDevices = new ArrayList<>();
private IInputDevicesChangedListener mDevicesChangedListener;
+ private final Map<String /* uniqueId */, Integer /* displayId */> mDisplayIdMapping =
+ new HashMap<>();
+ private final Map<String /* phys */, String /* uniqueId */> mUniqueIdAssociation =
+ new HashMap<>();
InputManagerMockHelper(TestableLooper testableLooper,
InputController.NativeWrapper nativeWrapperMock, IInputManager iInputManagerMock)
@@ -73,8 +79,10 @@
when(mIInputManagerMock.getInputDeviceIds()).thenReturn(new int[0]);
doAnswer(inv -> mDevices.get(inv.getArgument(0)))
.when(mIInputManagerMock).getInputDevice(anyInt());
- doNothing().when(mIInputManagerMock).addUniqueIdAssociation(anyString(), anyString());
- doNothing().when(mIInputManagerMock).removeUniqueIdAssociation(anyString());
+ doAnswer(inv -> mUniqueIdAssociation.put(inv.getArgument(0), inv.getArgument(1))).when(
+ mIInputManagerMock).addUniqueIdAssociation(anyString(), anyString());
+ doAnswer(inv -> mUniqueIdAssociation.remove(inv.getArgument(0))).when(
+ mIInputManagerMock).removeUniqueIdAssociation(anyString());
// Set a new instance of InputManager for testing that uses the IInputManager mock as the
// interface to the server.
@@ -87,17 +95,25 @@
}
}
+ public void addDisplayIdMapping(String uniqueId, int displayId) {
+ mDisplayIdMapping.put(uniqueId, displayId);
+ }
+
private long handleNativeOpenInputDevice(InvocationOnMock inv) {
Objects.requireNonNull(mDevicesChangedListener,
"InputController did not register an InputDevicesChangedListener.");
+ final String phys = inv.getArgument(3);
final InputDevice device = new InputDevice.Builder()
.setId(mDevices.size())
.setName(inv.getArgument(0))
.setVendorId(inv.getArgument(1))
.setProductId(inv.getArgument(2))
- .setDescriptor(inv.getArgument(3))
+ .setDescriptor(phys)
.setExternal(true)
+ .setAssociatedDisplayId(
+ mDisplayIdMapping.getOrDefault(mUniqueIdAssociation.get(phys),
+ Display.INVALID_DISPLAY))
.build();
mDevices.add(device);
diff --git a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java
index 5442af8..157e893 100644
--- a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java
@@ -39,6 +39,7 @@
import static org.mockito.ArgumentMatchers.nullable;
import static org.mockito.Mockito.argThat;
import static org.mockito.Mockito.clearInvocations;
+import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
@@ -2015,6 +2016,13 @@
eq(virtualDevice), any(), any())).thenReturn(displayId);
virtualDevice.createVirtualDisplay(VIRTUAL_DISPLAY_CONFIG, mVirtualDisplayCallback,
NONBLOCKED_APP_PACKAGE_NAME);
+ final String uniqueId = UNIQUE_ID + displayId;
+ doAnswer(inv -> {
+ final DisplayInfo displayInfo = new DisplayInfo();
+ displayInfo.uniqueId = uniqueId;
+ return displayInfo;
+ }).when(mDisplayManagerInternalMock).getDisplayInfo(eq(displayId));
+ mInputManagerMockHelper.addDisplayIdMapping(uniqueId, displayId);
}
private ComponentName getPermissionDialogComponent() {
diff --git a/services/tests/servicestests/src/com/android/server/devicestate/DeviceStateManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/devicestate/DeviceStateManagerServiceTest.java
index 7dcfc88..fa39364 100644
--- a/services/tests/servicestests/src/com/android/server/devicestate/DeviceStateManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/devicestate/DeviceStateManagerServiceTest.java
@@ -30,6 +30,7 @@
import static org.testng.Assert.assertNull;
import static org.testng.Assert.assertThrows;
+import android.annotation.NonNull;
import android.hardware.devicestate.DeviceStateInfo;
import android.hardware.devicestate.DeviceStateRequest;
import android.hardware.devicestate.IDeviceStateManagerCallback;
@@ -52,6 +53,7 @@
import org.junit.Test;
import org.junit.runner.RunWith;
+import java.io.PrintWriter;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Optional;
@@ -959,6 +961,10 @@
}
onComplete.run();
}
+
+ @Override
+ public void dump(@NonNull PrintWriter writer, @Nullable String[] args) {
+ }
}
private static final class TestDeviceStateProvider implements DeviceStateProvider {
@@ -1001,6 +1007,10 @@
public void setState(int identifier) {
mListener.onStateChanged(identifier);
}
+
+ @Override
+ public void dump(@NonNull PrintWriter writer, @Nullable String[] args) {
+ }
}
private static final class TestDeviceStateManagerCallback extends
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/BaseAbsoluteVolumeBehaviorTest.java b/services/tests/servicestests/src/com/android/server/hdmi/BaseAbsoluteVolumeBehaviorTest.java
index e7da26e..98789ac 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/BaseAbsoluteVolumeBehaviorTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/BaseAbsoluteVolumeBehaviorTest.java
@@ -519,11 +519,12 @@
eq(AudioManager.ADJUST_MUTE), anyInt());
clearInvocations(mAudioManager);
- // New volume only: sets volume only
+ // New volume only: sets both volume and mute.
+ // Volume changes can affect mute status; we need to set mute afterwards to undo this.
receiveReportAudioStatus(32, true);
verify(mAudioManager).setStreamVolume(eq(AudioManager.STREAM_MUSIC), eq(8),
anyInt());
- verify(mAudioManager, never()).adjustStreamVolume(eq(AudioManager.STREAM_MUSIC),
+ verify(mAudioManager).adjustStreamVolume(eq(AudioManager.STREAM_MUSIC),
eq(AudioManager.ADJUST_MUTE), anyInt());
clearInvocations(mAudioManager);
@@ -536,17 +537,17 @@
clearInvocations(mAudioManager);
// Repeat of earlier message: sets neither volume nor mute
- // Exception: On TV, volume is set to ensure that UI is shown
+ // Exception: On TV, mute is set to ensure that UI is shown
receiveReportAudioStatus(32, false);
+ verify(mAudioManager, never()).setStreamVolume(eq(AudioManager.STREAM_MUSIC),
+ eq(32), anyInt());
if (getDeviceType() == HdmiDeviceInfo.DEVICE_TV) {
- verify(mAudioManager).setStreamVolume(eq(AudioManager.STREAM_MUSIC), eq(8),
- anyInt());
+ verify(mAudioManager).adjustStreamVolume(eq(AudioManager.STREAM_MUSIC),
+ eq(AudioManager.ADJUST_UNMUTE), anyInt());
} else {
- verify(mAudioManager, never()).setStreamVolume(eq(AudioManager.STREAM_MUSIC), eq(8),
- anyInt());
+ verify(mAudioManager, never()).adjustStreamVolume(eq(AudioManager.STREAM_MUSIC),
+ eq(AudioManager.ADJUST_UNMUTE), anyInt());
}
- verify(mAudioManager, never()).adjustStreamVolume(eq(AudioManager.STREAM_MUSIC),
- eq(AudioManager.ADJUST_UNMUTE), anyInt());
clearInvocations(mAudioManager);
// Volume not within range [0, 100]: sets neither volume nor mute
@@ -570,7 +571,8 @@
receiveReportAudioStatus(32, false);
verify(mAudioManager).setStreamVolume(eq(AudioManager.STREAM_MUSIC), eq(8),
anyInt());
- verify(mAudioManager, never()).adjustStreamVolume(eq(AudioManager.STREAM_MUSIC),
+ // Update mute status because we updated volume
+ verify(mAudioManager).adjustStreamVolume(eq(AudioManager.STREAM_MUSIC),
eq(AudioManager.ADJUST_UNMUTE), anyInt());
}
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/BaseTvToAudioSystemAvbTest.java b/services/tests/servicestests/src/com/android/server/hdmi/BaseTvToAudioSystemAvbTest.java
index a410702..9ad2652 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/BaseTvToAudioSystemAvbTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/BaseTvToAudioSystemAvbTest.java
@@ -174,11 +174,12 @@
eq(AudioManager.ADJUST_MUTE), anyInt());
clearInvocations(mAudioManager);
- // New volume only: sets volume only
+ // New volume only: sets both volume and mute.
+ // Volume changes can affect mute status; we need to set mute afterwards to undo this.
receiveReportAudioStatus(32, true);
verify(mAudioManager).setStreamVolume(eq(AudioManager.STREAM_MUSIC), eq(8),
anyInt());
- verify(mAudioManager, never()).adjustStreamVolume(eq(AudioManager.STREAM_MUSIC),
+ verify(mAudioManager).adjustStreamVolume(eq(AudioManager.STREAM_MUSIC),
eq(AudioManager.ADJUST_MUTE), anyInt());
clearInvocations(mAudioManager);
@@ -190,11 +191,11 @@
eq(AudioManager.ADJUST_UNMUTE), anyInt());
clearInvocations(mAudioManager);
- // Repeat of earlier message: sets volume only (to ensure volume UI is shown)
+ // Repeat of earlier message: sets mute only (to ensure volume UI is shown)
receiveReportAudioStatus(32, false);
- verify(mAudioManager).setStreamVolume(eq(AudioManager.STREAM_MUSIC), eq(8),
+ verify(mAudioManager, never()).setStreamVolume(eq(AudioManager.STREAM_MUSIC), eq(8),
anyInt());
- verify(mAudioManager, never()).adjustStreamVolume(eq(AudioManager.STREAM_MUSIC),
+ verify(mAudioManager).adjustStreamVolume(eq(AudioManager.STREAM_MUSIC),
eq(AudioManager.ADJUST_UNMUTE), anyInt());
clearInvocations(mAudioManager);
@@ -392,18 +393,18 @@
INITIAL_SYSTEM_AUDIO_DEVICE_STATUS.getMute()));
mTestLooper.dispatchAll();
- // HdmiControlService calls setStreamVolume to trigger volume UI
- verify(mAudioManager).setStreamVolume(
+ // HdmiControlService calls adjustStreamVolume to trigger volume UI
+ verify(mAudioManager).adjustStreamVolume(
+ eq(AudioManager.STREAM_MUSIC),
+ eq(AudioManager.ADJUST_UNMUTE),
+ eq(AudioManager.FLAG_ABSOLUTE_VOLUME | AudioManager.FLAG_SHOW_UI));
+ // setStreamVolume is not called because volume didn't change,
+ // and adjustStreamVolume is sufficient to show volume UI
+ verify(mAudioManager, never()).setStreamVolume(
eq(AudioManager.STREAM_MUSIC),
// Volume level is rescaled to the max volume of STREAM_MUSIC
eq(INITIAL_SYSTEM_AUDIO_DEVICE_STATUS.getVolume()
* STREAM_MUSIC_MAX_VOLUME / AudioStatus.MAX_VOLUME),
eq(AudioManager.FLAG_ABSOLUTE_VOLUME | AudioManager.FLAG_SHOW_UI));
- // adjustStreamVolume is not called because mute status didn't change,
- // and setStreamVolume is sufficient to show volume UI
- verify(mAudioManager, never()).adjustStreamVolume(
- eq(AudioManager.STREAM_MUSIC),
- eq(AudioManager.ADJUST_UNMUTE),
- eq(AudioManager.FLAG_ABSOLUTE_VOLUME | AudioManager.FLAG_SHOW_UI));
}
}
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 99fa30c..1d3dacc 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiControlServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiControlServiceTest.java
@@ -169,14 +169,14 @@
.setEarcSupported(true)
.build();
mHdmiPortInfo[3] =
- new HdmiPortInfo.Builder(4, HdmiPortInfo.PORT_INPUT, 0x3000)
+ new HdmiPortInfo.Builder(4, HdmiPortInfo.PORT_OUTPUT, 0x3000)
.setCecSupported(true)
.setMhlSupported(false)
.setArcSupported(false)
.setEarcSupported(false)
.build();
mHdmiPortInfo[4] =
- new HdmiPortInfo.Builder(4, HdmiPortInfo.PORT_OUTPUT, 0x3000)
+ new HdmiPortInfo.Builder(5, HdmiPortInfo.PORT_OUTPUT, 0x3000)
.setCecSupported(true)
.setMhlSupported(false)
.setArcSupported(false)
@@ -841,6 +841,65 @@
}
@Test
+ public void onHotPlugIn_CecDisabledOnTv_CecNotAvailable() {
+ HdmiControlStatusCallback hdmiControlStatusCallback = new HdmiControlStatusCallback();
+ mHdmiControlServiceSpy.addHdmiControlStatusChangeListener(hdmiControlStatusCallback);
+ mTestLooper.dispatchAll();
+
+ mHdmiControlServiceSpy.setPowerStatus(HdmiControlManager.POWER_STATUS_ON);
+ mHdmiControlServiceSpy.playback().removeAction(DevicePowerStatusAction.class);
+ mNativeWrapper.clearResultMessages();
+ mTestLooper.dispatchAll();
+
+ mHdmiControlServiceSpy.onHotplug(4, true);
+ mTestLooper.dispatchAll();
+
+ HdmiCecMessage giveDevicePowerStatus =
+ HdmiCecMessageBuilder.buildGiveDevicePowerStatus(
+ mHdmiControlServiceSpy.playback().getDeviceInfo().getLogicalAddress(),
+ Constants.ADDR_TV);
+ assertThat(mNativeWrapper.getResultMessages()).contains(giveDevicePowerStatus);
+ // Wait for DevicePowerStatusAction to finish.
+ mTestLooper.moveTimeForward(HdmiConfig.TIMEOUT_MS);
+ mTestLooper.dispatchAll();
+ mTestLooper.moveTimeForward(HdmiConfig.TIMEOUT_MS);
+ mTestLooper.dispatchAll();
+
+ assertThat(hdmiControlStatusCallback.mCecEnabled).isTrue();
+ assertThat(hdmiControlStatusCallback.mCecAvailable).isFalse();
+ }
+
+ @Test
+ public void onHotPlugIn_CecEnabledOnTv_CecAvailable() {
+ HdmiControlStatusCallback hdmiControlStatusCallback = new HdmiControlStatusCallback();
+ mHdmiControlServiceSpy.addHdmiControlStatusChangeListener(hdmiControlStatusCallback);
+ mTestLooper.dispatchAll();
+
+ mHdmiControlServiceSpy.setPowerStatus(HdmiControlManager.POWER_STATUS_ON);
+ mHdmiControlServiceSpy.playback().removeAction(DevicePowerStatusAction.class);
+ mNativeWrapper.clearResultMessages();
+ mTestLooper.dispatchAll();
+
+ mHdmiControlServiceSpy.onHotplug(4, true);
+ mTestLooper.dispatchAll();
+
+ HdmiCecMessage giveDevicePowerStatus =
+ HdmiCecMessageBuilder.buildGiveDevicePowerStatus(
+ mHdmiControlServiceSpy.playback().getDeviceInfo().getLogicalAddress(),
+ Constants.ADDR_TV);
+ HdmiCecMessage reportPowerStatus =
+ HdmiCecMessageBuilder.buildReportPowerStatus(
+ Constants.ADDR_TV,
+ mHdmiControlServiceSpy.playback().getDeviceInfo().getLogicalAddress(),
+ HdmiControlManager.POWER_STATUS_ON);
+ assertThat(mNativeWrapper.getResultMessages()).contains(giveDevicePowerStatus);
+ mNativeWrapper.onCecMessage(reportPowerStatus);
+ mTestLooper.dispatchAll();
+
+ assertThat(hdmiControlStatusCallback.mCecEnabled).isTrue();
+ assertThat(hdmiControlStatusCallback.mCecAvailable).isTrue();
+ }
+ @Test
public void handleCecCommand_errorParameter_returnsAbortInvalidOperand() {
// Validity ERROR_PARAMETER. Taken from HdmiCecMessageValidatorTest#isValid_menuStatus
HdmiCecMessage message = HdmiUtils.buildMessage("80:8D:03");
diff --git a/services/tests/servicestests/src/com/android/server/job/PendingJobQueueTest.java b/services/tests/servicestests/src/com/android/server/job/PendingJobQueueTest.java
index 213e05e..2b07d33 100644
--- a/services/tests/servicestests/src/com/android/server/job/PendingJobQueueTest.java
+++ b/services/tests/servicestests/src/com/android/server/job/PendingJobQueueTest.java
@@ -18,6 +18,7 @@
import static android.app.job.JobInfo.NETWORK_TYPE_ANY;
import static android.app.job.JobInfo.NETWORK_TYPE_NONE;
+
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
@@ -39,8 +40,6 @@
import org.junit.Test;
-import java.util.ArrayList;
-import java.util.List;
import java.util.Random;
public class PendingJobQueueTest {
@@ -68,7 +67,7 @@
@Test
public void testAdd() {
- List<JobStatus> jobs = new ArrayList<>();
+ ArraySet<JobStatus> jobs = new ArraySet<>();
jobs.add(createJobStatus("testAdd", createJobInfo(1), 1));
jobs.add(createJobStatus("testAdd", createJobInfo(2), 2));
jobs.add(createJobStatus("testAdd", createJobInfo(3).setExpedited(true), 3));
@@ -77,7 +76,7 @@
PendingJobQueue jobQueue = new PendingJobQueue();
for (int i = 0; i < jobs.size(); ++i) {
- jobQueue.add(jobs.get(i));
+ jobQueue.add(jobs.valueAt(i));
assertEquals(i + 1, jobQueue.size());
}
@@ -90,7 +89,7 @@
@Test
public void testAddAll() {
- List<JobStatus> jobs = new ArrayList<>();
+ ArraySet<JobStatus> jobs = new ArraySet<>();
jobs.add(createJobStatus("testAddAll", createJobInfo(1), 1));
jobs.add(createJobStatus("testAddAll", createJobInfo(2), 2));
jobs.add(createJobStatus("testAddAll", createJobInfo(3).setExpedited(true), 3));
@@ -110,7 +109,7 @@
@Test
public void testClear() {
- List<JobStatus> jobs = new ArrayList<>();
+ ArraySet<JobStatus> jobs = new ArraySet<>();
jobs.add(createJobStatus("testClear", createJobInfo(1), 1));
jobs.add(createJobStatus("testClear", createJobInfo(2), 2));
jobs.add(createJobStatus("testClear", createJobInfo(3).setExpedited(true), 3));
@@ -179,7 +178,7 @@
@Test
public void testRemove() {
- List<JobStatus> jobs = new ArrayList<>();
+ ArraySet<JobStatus> jobs = new ArraySet<>();
jobs.add(createJobStatus("testRemove", createJobInfo(1), 1));
jobs.add(createJobStatus("testRemove", createJobInfo(2), 2));
jobs.add(createJobStatus("testRemove", createJobInfo(3).setExpedited(true), 3));
@@ -192,8 +191,8 @@
ArraySet<JobStatus> removed = new ArraySet<>();
JobStatus job;
for (int i = 0; i < jobs.size(); ++i) {
- jobQueue.remove(jobs.get(i));
- removed.add(jobs.get(i));
+ jobQueue.remove(jobs.valueAt(i));
+ removed.add(jobs.valueAt(i));
assertEquals(jobs.size() - i - 1, jobQueue.size());
@@ -209,7 +208,7 @@
@Test
public void testRemove_duringIteration() {
- List<JobStatus> jobs = new ArrayList<>();
+ ArraySet<JobStatus> jobs = new ArraySet<>();
jobs.add(createJobStatus("testRemove", createJobInfo(1), 1));
jobs.add(createJobStatus("testRemove", createJobInfo(2), 2));
jobs.add(createJobStatus("testRemove", createJobInfo(3).setExpedited(true), 3));
@@ -234,7 +233,7 @@
@Test
public void testRemove_outOfOrder() {
- List<JobStatus> jobs = new ArrayList<>();
+ ArraySet<JobStatus> jobs = new ArraySet<>();
JobStatus job1 = createJobStatus("testRemove", createJobInfo(1), 1);
JobStatus job2 = createJobStatus("testRemove", createJobInfo(2), 1);
JobStatus job3 = createJobStatus("testRemove", createJobInfo(3).setExpedited(true), 1);
@@ -269,8 +268,8 @@
Log.d(TAG, testJobToString(job));
}
for (int i = 0; i < jobs.size(); ++i) {
- jobQueue.remove(jobs.get(i));
- removed.add(jobs.get(i));
+ jobQueue.remove(jobs.valueAt(i));
+ removed.add(jobs.valueAt(i));
assertEquals(jobs.size() - i - 1, jobQueue.size());
@@ -294,8 +293,8 @@
removed.clear();
for (int i = 0; i < jobs.size(); ++i) {
- jobQueue.remove(jobs.get(i));
- removed.add(jobs.get(i));
+ jobQueue.remove(jobs.valueAt(i));
+ removed.add(jobs.valueAt(i));
assertEquals(jobs.size() - i - 1, jobQueue.size());
@@ -319,8 +318,8 @@
removed.clear();
for (int i = 0; i < jobs.size(); ++i) {
- jobQueue.remove(jobs.get(i));
- removed.add(jobs.get(i));
+ jobQueue.remove(jobs.valueAt(i));
+ removed.add(jobs.valueAt(i));
assertEquals(jobs.size() - i - 1, jobQueue.size());
diff --git a/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsServiceTests.java b/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsServiceTests.java
index 5081198..7053597 100644
--- a/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsServiceTests.java
+++ b/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsServiceTests.java
@@ -39,7 +39,6 @@
import static org.mockito.Mockito.when;
import android.app.PropertyInvalidatedCache;
-import android.os.IBinder;
import android.os.RemoteException;
import android.platform.test.annotations.Presubmit;
import android.platform.test.flag.junit.SetFlagsRule;
@@ -49,8 +48,8 @@
import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;
-import com.android.internal.widget.ILockSettingsStateListener;
import com.android.internal.widget.LockPatternUtils;
+import com.android.internal.widget.LockSettingsStateListener;
import com.android.internal.widget.LockscreenCredential;
import com.android.internal.widget.VerifyCredentialResponse;
@@ -412,7 +411,7 @@
mSetFlagsRule.enableFlags(FLAG_REPORT_PRIMARY_AUTH_ATTEMPTS);
final LockscreenCredential password = newPassword("password");
setCredential(PRIMARY_USER_ID, password);
- final ILockSettingsStateListener listener = mockLockSettingsStateListener();
+ final LockSettingsStateListener listener = mock(LockSettingsStateListener.class);
mLocalService.registerLockSettingsStateListener(listener);
assertEquals(VerifyCredentialResponse.RESPONSE_OK,
@@ -429,7 +428,7 @@
final LockscreenCredential password = newPassword("password");
setCredential(PRIMARY_USER_ID, password);
final LockscreenCredential badPassword = newPassword("badPassword");
- final ILockSettingsStateListener listener = mockLockSettingsStateListener();
+ final LockSettingsStateListener listener = mock(LockSettingsStateListener.class);
mLocalService.registerLockSettingsStateListener(listener);
assertEquals(VerifyCredentialResponse.RESPONSE_ERROR,
@@ -445,7 +444,7 @@
final LockscreenCredential password = newPassword("password");
setCredential(PRIMARY_USER_ID, password);
final LockscreenCredential badPassword = newPassword("badPassword");
- final ILockSettingsStateListener listener = mockLockSettingsStateListener();
+ final LockSettingsStateListener listener = mock(LockSettingsStateListener.class);
mLocalService.registerLockSettingsStateListener(listener);
assertEquals(VerifyCredentialResponse.RESPONSE_OK,
@@ -599,12 +598,4 @@
assertNotEquals(0, mGateKeeperService.getSecureUserId(userId));
}
}
-
- private ILockSettingsStateListener mockLockSettingsStateListener() {
- ILockSettingsStateListener listener = mock(ILockSettingsStateListener.Stub.class);
- IBinder binder = mock(IBinder.class);
- when(binder.isBinderAlive()).thenReturn(true);
- when(listener.asBinder()).thenReturn(binder);
- return listener;
- }
}
diff --git a/services/tests/servicestests/src/com/android/server/net/watchlist/FileHashCacheTests.java b/services/tests/servicestests/src/com/android/server/net/watchlist/FileHashCacheTests.java
new file mode 100644
index 0000000..5df7a5e
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/net/watchlist/FileHashCacheTests.java
@@ -0,0 +1,157 @@
+/*
+ * Copyright (C) 2024 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.net.watchlist;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import android.annotation.NonNull;
+import android.os.FileUtils;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.os.SystemClock;
+import android.system.Os;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.internal.util.HexDump;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.File;
+import java.io.IOException;
+
+/**
+ * atest frameworks-services -c com.android.server.net.watchlist.FileHashCacheTests
+ */
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class FileHashCacheTests {
+
+ private static final String APK_A = "A.apk";
+ private static final String APK_B = "B.apk";
+ private static final String APK_A_CONTENT = "AAA";
+ private static final String APK_A_ALT_CONTENT = "AAA_ALT";
+ private static final String APK_B_CONTENT = "BBB";
+
+ private static final String PERSIST_FILE_NAME_FOR_TEST = "file_hash_cache";
+
+ // Sha256 of "AAA"
+ private static final String APK_A_CONTENT_HASH =
+ "CB1AD2119D8FAFB69566510EE712661F9F14B83385006EF92AEC47F523A38358";
+ // Sha256 of "AAA_ALT"
+ private static final String APK_A_ALT_CONTENT_HASH =
+ "2AB726E3C5B316F4C7507BFCCC3861F0473523D572E0C62BA21601C20693AEF0";
+ // Sha256 of "BBB"
+ private static final String APK_B_CONTENT_HASH =
+ "DCDB704109A454784B81229D2B05F368692E758BFA33CB61D04C1B93791B0273";
+
+ @Before
+ public void setUp() throws Exception {
+ final File persistFile = getFile(PERSIST_FILE_NAME_FOR_TEST);
+ persistFile.delete();
+ FileHashCache.sPersistFileName = persistFile.getAbsolutePath();
+ getFile(APK_A).delete();
+ getFile(APK_B).delete();
+ FileHashCache.sSaveDeferredDelayMillis = 0;
+ }
+
+ @After
+ public void tearDown() {
+ }
+
+ @Test
+ public void testFileHashCache_generic() throws Exception {
+ final File apkA = getFile(APK_A);
+ final File apkB = getFile(APK_B);
+
+ Looper.prepare();
+ FileHashCache fileHashCache = new FileHashCache(new InlineHandler());
+
+ assertFalse(getFile(PERSIST_FILE_NAME_FOR_TEST).exists());
+
+ // No hash for non-existing files.
+ assertNull("Found existing entry in the cache",
+ fileHashCache.getSha256HashFromCache(apkA));
+ assertNull("Found existing entry in the cache",
+ fileHashCache.getSha256HashFromCache(apkB));
+ try {
+ fileHashCache.getSha256Hash(apkA);
+ fail("Not reached");
+ } catch (IOException e) { }
+ try {
+ fileHashCache.getSha256Hash(apkB);
+ fail("Not reached");
+ } catch (IOException e) { }
+
+ assertFalse(getFile(PERSIST_FILE_NAME_FOR_TEST).exists());
+ FileUtils.stringToFile(apkA, APK_A_CONTENT);
+ FileUtils.stringToFile(apkB, APK_B_CONTENT);
+
+ assertEquals(APK_A_CONTENT_HASH, HexDump.toHexString(fileHashCache.getSha256Hash(apkA)));
+ assertTrue(getFile(PERSIST_FILE_NAME_FOR_TEST).exists());
+ assertEquals(APK_B_CONTENT_HASH, HexDump.toHexString(fileHashCache.getSha256Hash(apkB)));
+ assertEquals(APK_A_CONTENT_HASH,
+ HexDump.toHexString(fileHashCache.getSha256HashFromCache(apkA)));
+ assertEquals(APK_B_CONTENT_HASH,
+ HexDump.toHexString(fileHashCache.getSha256HashFromCache(apkB)));
+
+ // Recreate handler. It should read persistent state.
+ fileHashCache = new FileHashCache(new InlineHandler());
+ assertEquals(APK_A_CONTENT_HASH,
+ HexDump.toHexString(fileHashCache.getSha256HashFromCache(apkA)));
+ assertEquals(APK_B_CONTENT_HASH,
+ HexDump.toHexString(fileHashCache.getSha256HashFromCache(apkB)));
+
+ // Modify one APK. Cache entry should be invalidated. Make sure that FS timestamp resolution
+ // allows us to detect update.
+ final long before = Os.stat(apkA.getAbsolutePath()).st_ctime;
+ do {
+ FileUtils.stringToFile(apkA, APK_A_ALT_CONTENT);
+ } while (android.system.Os.stat(apkA.getAbsolutePath()).st_ctime == before);
+
+ assertNull("Found stale entry in the cache", fileHashCache.getSha256HashFromCache(apkA));
+ assertEquals(APK_A_ALT_CONTENT_HASH,
+ HexDump.toHexString(fileHashCache.getSha256Hash(apkA)));
+ }
+
+ // Helper handler that executes tasks inline in context of current thread if time is good for
+ // this.
+ private static class InlineHandler extends Handler {
+ @Override
+ public boolean sendMessageAtTime(Message msg, long uptimeMillis) {
+ if (SystemClock.uptimeMillis() >= uptimeMillis && getLooper().isCurrentThread()) {
+ dispatchMessage(msg);
+ return true;
+ }
+ return super.sendMessageAtTime(msg, uptimeMillis);
+ }
+ }
+
+ private File getFile(@NonNull String name) {
+ return new File(InstrumentationRegistry.getContext().getFilesDir(), name);
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/pm/BackgroundInstallControlServiceTest.java b/services/tests/servicestests/src/com/android/server/pm/BackgroundInstallControlServiceTest.java
index 8656f60..bf87e3a 100644
--- a/services/tests/servicestests/src/com/android/server/pm/BackgroundInstallControlServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/pm/BackgroundInstallControlServiceTest.java
@@ -111,6 +111,8 @@
private UsageStatsManagerInternal mUsageStatsManagerInternal;
@Mock
private PermissionManagerServiceInternal mPermissionManager;
+ @Mock
+ private BackgroundInstallControlCallbackHelper mCallbackHelper;
@Captor
private ArgumentCaptor<PackageManagerInternal.PackageListObserver> mPackageListObserverCaptor;
@@ -982,5 +984,11 @@
public File getDiskFile() {
return mFile;
}
+
+
+ @Override
+ public BackgroundInstallControlCallbackHelper getBackgroundInstallControlCallbackHelper() {
+ return mCallbackHelper;
+ }
}
}
diff --git a/services/tests/servicestests/src/com/android/server/pm/CrossProfileAppsServiceImplTest.java b/services/tests/servicestests/src/com/android/server/pm/CrossProfileAppsServiceImplTest.java
index c81fbb4..cee7387 100644
--- a/services/tests/servicestests/src/com/android/server/pm/CrossProfileAppsServiceImplTest.java
+++ b/services/tests/servicestests/src/com/android/server/pm/CrossProfileAppsServiceImplTest.java
@@ -46,6 +46,7 @@
import android.permission.PermissionCheckerManager;
import android.permission.PermissionManager;
import android.platform.test.annotations.Presubmit;
+import android.platform.test.flag.junit.SetFlagsRule;
import android.util.SparseArray;
import com.android.internal.util.FunctionalUtils.ThrowingRunnable;
@@ -55,6 +56,7 @@
import com.android.server.wm.ActivityTaskManagerInternal;
import org.junit.Before;
+import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
@@ -109,6 +111,8 @@
private IApplicationThread mIApplicationThread;
private SparseArray<Boolean> mUserEnabled = new SparseArray<>();
+ @Rule
+ public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
@Before
public void initCrossProfileAppsServiceImpl() {
@@ -123,8 +127,9 @@
mUserEnabled.put(PRIMARY_USER, true);
mUserEnabled.put(PROFILE_OF_PRIMARY_USER, true);
mUserEnabled.put(SECONDARY_USER, true);
+ mSetFlagsRule.enableFlags(android.multiuser.Flags.FLAG_ENABLE_HIDING_PROFILES);
- when(mUserManager.getEnabledProfileIds(anyInt())).thenAnswer(
+ when(mUserManager.getProfileIdsExcludingHidden(anyInt(), eq(true))).thenAnswer(
invocation -> {
List<Integer> users = new ArrayList<>();
final int targetUser = invocation.getArgument(0);
diff --git a/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceCreateProfileTest.java b/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceCreateProfileTest.java
index 39cc653..6decf43 100644
--- a/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceCreateProfileTest.java
+++ b/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceCreateProfileTest.java
@@ -114,16 +114,19 @@
final String userType1 = USER_TYPE_PROFILE_MANAGED;
// System user should still have no userType1 profile so getProfileIds should be empty.
- int[] users = mUserManagerService.getProfileIds(UserHandle.USER_SYSTEM, userType1, false);
+ int[] users = mUserManagerService.getProfileIds(UserHandle.USER_SYSTEM, userType1,
+ false, /* excludeHidden */ false);
assertEquals("System user should have no managed profiles", 0, users.length);
// Secondary user should have one userType1 profile, so return just that.
- users = mUserManagerService.getProfileIds(secondaryUser.id, userType1, false);
+ users = mUserManagerService.getProfileIds(secondaryUser.id, userType1,
+ false, /* excludeHidden */ false);
assertEquals("Wrong number of profiles", 1, users.length);
assertEquals("Wrong profile id", profile.id, users[0]);
// The profile itself is a userType1 profile, so it should return just itself.
- users = mUserManagerService.getProfileIds(profile.id, userType1, false);
+ users = mUserManagerService.getProfileIds(profile.id, userType1, false, /* excludeHidden */
+ false);
assertEquals("Wrong number of profiles", 1, users.length);
assertEquals("Wrong profile id", profile.id, users[0]);
}
diff --git a/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceUserPropertiesTest.java b/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceUserPropertiesTest.java
index 8d8dc9c..f1d3ba9 100644
--- a/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceUserPropertiesTest.java
+++ b/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceUserPropertiesTest.java
@@ -23,7 +23,6 @@
import android.content.pm.UserProperties;
import android.os.Parcel;
import android.platform.test.annotations.Presubmit;
-import android.platform.test.flag.junit.SetFlagsRule;
import android.util.Xml;
import androidx.test.filters.MediumTest;
@@ -32,7 +31,6 @@
import com.android.modules.utils.TypedXmlPullParser;
import com.android.modules.utils.TypedXmlSerializer;
-import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -54,13 +52,10 @@
@RunWith(AndroidJUnit4.class)
@MediumTest
public class UserManagerServiceUserPropertiesTest {
- @Rule
- public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
/** Test that UserProperties can properly read the xml information that it writes. */
@Test
public void testWriteReadXml() throws Exception {
- mSetFlagsRule.enableFlags(android.multiuser.Flags.FLAG_SUPPORT_HIDING_PROFILES);
final UserProperties defaultProps = new UserProperties.Builder()
.setShowInLauncher(21)
.setStartWithParent(false)
@@ -79,6 +74,7 @@
.setAlwaysVisible(false)
.setCrossProfileContentSharingStrategy(0)
.setProfileApiVisibility(34)
+ .setItemsRestrictedOnHomeScreen(false)
.build();
final UserProperties actualProps = new UserProperties(defaultProps);
actualProps.setShowInLauncher(14);
@@ -97,6 +93,7 @@
actualProps.setAlwaysVisible(true);
actualProps.setCrossProfileContentSharingStrategy(1);
actualProps.setProfileApiVisibility(36);
+ actualProps.setItemsRestrictedOnHomeScreen(true);
// Write the properties to xml.
final ByteArrayOutputStream baos = new ByteArrayOutputStream();
@@ -121,7 +118,6 @@
/** Tests parcelling an object in which all properties are present. */
@Test
public void testParcelUnparcel() throws Exception {
- mSetFlagsRule.enableFlags(android.multiuser.Flags.FLAG_SUPPORT_HIDING_PROFILES);
final UserProperties originalProps = new UserProperties.Builder()
.setShowInLauncher(2145)
.build();
@@ -132,7 +128,6 @@
/** Tests copying a UserProperties object varying permissions. */
@Test
public void testCopyLacksPermissions() throws Exception {
- mSetFlagsRule.enableFlags(android.multiuser.Flags.FLAG_SUPPORT_HIDING_PROFILES);
final UserProperties defaultProps = new UserProperties.Builder()
.setShowInLauncher(2145)
.setStartWithParent(true)
@@ -144,6 +139,7 @@
.setAllowStoppingUserWithDelayedLocking(false)
.setAlwaysVisible(true)
.setProfileApiVisibility(110)
+ .setItemsRestrictedOnHomeScreen(false)
.build();
final UserProperties orig = new UserProperties(defaultProps);
orig.setShowInLauncher(2841);
@@ -154,6 +150,7 @@
orig.setAuthAlwaysRequiredToDisableQuietMode(true);
orig.setAllowStoppingUserWithDelayedLocking(true);
orig.setAlwaysVisible(false);
+ orig.setItemsRestrictedOnHomeScreen(true);
// Test every permission level. (Currently, it's linear so it's easy.)
for (int permLevel = 0; permLevel < 4; permLevel++) {
@@ -200,6 +197,8 @@
assertEqualGetterOrThrows(orig::getAlwaysVisible, copy::getAlwaysVisible, exposeAll);
assertEqualGetterOrThrows(orig::getAllowStoppingUserWithDelayedLocking,
copy::getAllowStoppingUserWithDelayedLocking, exposeAll);
+ assertEqualGetterOrThrows(orig::areItemsRestrictedOnHomeScreen,
+ copy::areItemsRestrictedOnHomeScreen, exposeAll);
// Items requiring hasManagePermission - put them here using hasManagePermission.
assertEqualGetterOrThrows(orig::getShowInSettings, copy::getShowInSettings,
@@ -283,5 +282,7 @@
assertThat(expected.getCrossProfileContentSharingStrategy())
.isEqualTo(actual.getCrossProfileContentSharingStrategy());
assertThat(expected.getProfileApiVisibility()).isEqualTo(actual.getProfileApiVisibility());
+ assertThat(expected.areItemsRestrictedOnHomeScreen())
+ .isEqualTo(actual.areItemsRestrictedOnHomeScreen());
}
}
diff --git a/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceUserTypeTest.java b/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceUserTypeTest.java
index 1ee604e..3047bcf 100644
--- a/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceUserTypeTest.java
+++ b/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceUserTypeTest.java
@@ -41,7 +41,6 @@
import android.os.Bundle;
import android.os.UserManager;
import android.platform.test.annotations.Presubmit;
-import android.platform.test.flag.junit.SetFlagsRule;
import android.util.ArrayMap;
import androidx.test.InstrumentationRegistry;
@@ -51,7 +50,6 @@
import com.android.frameworks.servicestests.R;
import org.junit.Before;
-import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -73,11 +71,8 @@
public void setup() {
mResources = InstrumentationRegistry.getTargetContext().getResources();
}
- @Rule
- public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
@Test
public void testUserTypeBuilder_createUserType() {
- mSetFlagsRule.enableFlags(android.multiuser.Flags.FLAG_SUPPORT_HIDING_PROFILES);
final Bundle restrictions = makeRestrictionsBundle("r1", "r2");
final Bundle systemSettings = makeSettingsBundle("s1", "s2");
final Bundle secureSettings = makeSettingsBundle("secure_s1", "secure_s2");
@@ -102,7 +97,8 @@
.setDeleteAppWithParent(true)
.setAlwaysVisible(true)
.setCrossProfileContentSharingStrategy(1)
- .setProfileApiVisibility(34);
+ .setProfileApiVisibility(34)
+ .setItemsRestrictedOnHomeScreen(true);
final UserTypeDetails type = new UserTypeDetails.Builder()
.setName("a.name")
@@ -186,6 +182,7 @@
assertEquals(1, type.getDefaultUserPropertiesReference()
.getCrossProfileContentSharingStrategy());
assertEquals(34, type.getDefaultUserPropertiesReference().getProfileApiVisibility());
+ assertTrue(type.getDefaultUserPropertiesReference().areItemsRestrictedOnHomeScreen());
assertEquals(23, type.getBadgeLabel(0));
assertEquals(24, type.getBadgeLabel(1));
@@ -205,7 +202,6 @@
@Test
public void testUserTypeBuilder_defaults() {
- mSetFlagsRule.enableFlags(android.multiuser.Flags.FLAG_SUPPORT_HIDING_PROFILES);
UserTypeDetails type = new UserTypeDetails.Builder()
.setName("name") // Required (no default allowed)
.setBaseType(FLAG_FULL) // Required (no default allowed)
@@ -319,7 +315,6 @@
/** Tests {@link UserTypeFactory#customizeBuilders} for a reasonable xml file. */
@Test
public void testUserTypeFactoryCustomize_profile() throws Exception {
- mSetFlagsRule.enableFlags(android.multiuser.Flags.FLAG_SUPPORT_HIDING_PROFILES);
final String userTypeAosp1 = "android.test.1"; // Profile user that is not customized
final String userTypeAosp2 = "android.test.2"; // Profile user that is customized
final String userTypeOem1 = "custom.test.1"; // Custom-defined profile
@@ -343,7 +338,8 @@
.setDeleteAppWithParent(true)
.setAlwaysVisible(false)
.setCrossProfileContentSharingStrategy(1)
- .setProfileApiVisibility(36);
+ .setProfileApiVisibility(36)
+ .setItemsRestrictedOnHomeScreen(false);
final ArrayMap<String, UserTypeDetails.Builder> builders = new ArrayMap<>();
builders.put(userTypeAosp1, new UserTypeDetails.Builder()
@@ -395,6 +391,7 @@
assertEquals(1, aospType.getDefaultUserPropertiesReference()
.getCrossProfileContentSharingStrategy());
assertEquals(36, aospType.getDefaultUserPropertiesReference().getProfileApiVisibility());
+ assertFalse(aospType.getDefaultUserPropertiesReference().areItemsRestrictedOnHomeScreen());
// userTypeAosp2 should be modified.
aospType = builders.get(userTypeAosp2).createUserTypeDetails();
@@ -452,6 +449,7 @@
assertEquals(0, aospType.getDefaultUserPropertiesReference()
.getCrossProfileContentSharingStrategy());
assertEquals(36, aospType.getDefaultUserPropertiesReference().getProfileApiVisibility());
+ assertTrue(aospType.getDefaultUserPropertiesReference().areItemsRestrictedOnHomeScreen());
// userTypeOem1 should be created.
UserTypeDetails.Builder customType = builders.get(userTypeOem1);
diff --git a/services/tests/servicestests/src/com/android/server/pm/UserManagerTest.java b/services/tests/servicestests/src/com/android/server/pm/UserManagerTest.java
index db561c4..df2069e 100644
--- a/services/tests/servicestests/src/com/android/server/pm/UserManagerTest.java
+++ b/services/tests/servicestests/src/com/android/server/pm/UserManagerTest.java
@@ -39,7 +39,6 @@
import android.os.UserManager;
import android.platform.test.annotations.Postsubmit;
import android.platform.test.annotations.RequiresFlagsEnabled;
-import android.platform.test.flag.junit.SetFlagsRule;
import android.provider.Settings;
import android.test.suitebuilder.annotation.LargeTest;
import android.test.suitebuilder.annotation.MediumTest;
@@ -56,7 +55,6 @@
import org.junit.After;
import org.junit.Before;
-import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -99,8 +97,6 @@
private UserSwitchWaiter mUserSwitchWaiter;
private UserRemovalWaiter mUserRemovalWaiter;
private int mOriginalCurrentUserId;
- @Rule
- public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
@Before
public void setUp() throws Exception {
@@ -172,7 +168,6 @@
@Test
public void testCloneUser() throws Exception {
- mSetFlagsRule.enableFlags(android.multiuser.Flags.FLAG_SUPPORT_HIDING_PROFILES);
assumeCloneEnabled();
UserHandle mainUser = mUserManager.getMainUser();
assumeTrue("Main user is null", mainUser != null);
@@ -229,7 +224,8 @@
.isEqualTo(cloneUserProperties.getCrossProfileContentSharingStrategy());
assertThrows(SecurityException.class, cloneUserProperties::getDeleteAppWithParent);
assertThrows(SecurityException.class, cloneUserProperties::getAlwaysVisible);
- assertThrows(SecurityException.class, cloneUserProperties::getProfileApiVisibility);
+ assertThat(typeProps.getProfileApiVisibility()).isEqualTo(
+ cloneUserProperties.getProfileApiVisibility());
compareDrawables(mUserManager.getUserBadge(),
Resources.getSystem().getDrawable(userTypeDetails.getBadgePlain()));
@@ -311,7 +307,6 @@
@Test
public void testPrivateProfile() throws Exception {
- mSetFlagsRule.enableFlags(android.multiuser.Flags.FLAG_SUPPORT_HIDING_PROFILES);
UserHandle mainUser = mUserManager.getMainUser();
assumeTrue("Main user is null", mainUser != null);
// Get the default properties for private profile user type.
@@ -353,8 +348,10 @@
assertThrows(SecurityException.class, privateProfileUserProperties::getDeleteAppWithParent);
assertThrows(SecurityException.class,
privateProfileUserProperties::getAllowStoppingUserWithDelayedLocking);
+ assertThat(typeProps.getProfileApiVisibility()).isEqualTo(
+ privateProfileUserProperties.getProfileApiVisibility());
assertThrows(SecurityException.class,
- privateProfileUserProperties::getProfileApiVisibility);
+ privateProfileUserProperties::areItemsRestrictedOnHomeScreen);
compareDrawables(mUserManager.getUserBadge(),
Resources.getSystem().getDrawable(userTypeDetails.getBadgePlain()));
diff --git a/services/tests/servicestests/src/com/android/server/pm/dex/PackageDynamicCodeLoadingTests.java b/services/tests/servicestests/src/com/android/server/pm/dex/PackageDynamicCodeLoadingTests.java
index e075379..c0ea157 100644
--- a/services/tests/servicestests/src/com/android/server/pm/dex/PackageDynamicCodeLoadingTests.java
+++ b/services/tests/servicestests/src/com/android/server/pm/dex/PackageDynamicCodeLoadingTests.java
@@ -106,13 +106,13 @@
}
@Test
- public void testRecord_changeUserForFile_throws() {
+ public void testRecord_changeUserForFile_ignored() {
Entry entry1 = new Entry("owning.package1", "/path/file1", 'D', 10, "loading.package1");
Entry entry2 = new Entry("owning.package1", "/path/file1", 'D', 20, "loading.package1");
PackageDynamicCodeLoading info = makePackageDcl(entry1);
- assertThrows(() -> record(info, entry2));
+ assertThat(record(info, entry2)).isFalse();
assertHasEntries(info, entry1);
}
diff --git a/services/tests/servicestests/src/com/android/server/power/hint/HintManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/power/hint/HintManagerServiceTest.java
index ffb3bce..4ab9d3e 100644
--- a/services/tests/servicestests/src/com/android/server/power/hint/HintManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/power/hint/HintManagerServiceTest.java
@@ -90,10 +90,12 @@
private static final long[] DURATIONS_ZERO = new long[] {};
private static final long[] TIMESTAMPS_ZERO = new long[] {};
private static final long[] TIMESTAMPS_TWO = new long[] {1L, 2L};
- private static final WorkDuration[] WORK_DURATIONS_THREE = new WorkDuration[] {
+ private static final WorkDuration[] WORK_DURATIONS_FIVE = new WorkDuration[] {
new WorkDuration(1L, 11L, 8L, 4L, 1L),
new WorkDuration(2L, 13L, 8L, 6L, 2L),
new WorkDuration(3L, 333333333L, 8L, 333333333L, 3L),
+ new WorkDuration(2L, 13L, 0L, 6L, 2L),
+ new WorkDuration(2L, 13L, 8L, 0L, 2L),
};
@Mock private Context mContext;
@@ -609,9 +611,9 @@
.createHintSession(token, SESSION_TIDS_A, DEFAULT_TARGET_DURATION);
a.updateTargetWorkDuration(100L);
- a.reportActualWorkDuration2(WORK_DURATIONS_THREE);
+ a.reportActualWorkDuration2(WORK_DURATIONS_FIVE);
verify(mNativeWrapperMock, times(1)).halReportActualWorkDuration(anyLong(),
- eq(WORK_DURATIONS_THREE));
+ eq(WORK_DURATIONS_FIVE));
assertThrows(IllegalArgumentException.class, () -> {
a.reportActualWorkDuration2(new WorkDuration[] {});
@@ -627,7 +629,7 @@
});
assertThrows(IllegalArgumentException.class, () -> {
- a.reportActualWorkDuration2(new WorkDuration[] {new WorkDuration(1L, 11L, 0L, 4L, 1L)});
+ a.reportActualWorkDuration2(new WorkDuration[] {new WorkDuration(1L, 11L, 0L, 0L, 1L)});
});
assertThrows(IllegalArgumentException.class, () -> {
@@ -648,7 +650,7 @@
latch.await();
assertFalse(service.mUidObserver.isUidForeground(a.mUid));
- a.reportActualWorkDuration2(WORK_DURATIONS_THREE);
+ a.reportActualWorkDuration2(WORK_DURATIONS_FIVE);
verify(mNativeWrapperMock, never()).halReportActualWorkDuration(anyLong(), any(), any());
}
}
diff --git a/services/tests/servicestests/src/com/android/server/systemconfig/SystemConfigTest.java b/services/tests/servicestests/src/com/android/server/systemconfig/SystemConfigTest.java
index aca96ad..eddff9ab 100644
--- a/services/tests/servicestests/src/com/android/server/systemconfig/SystemConfigTest.java
+++ b/services/tests/servicestests/src/com/android/server/systemconfig/SystemConfigTest.java
@@ -21,8 +21,13 @@
import static org.junit.Assert.assertEquals;
import static org.testng.Assert.expectThrows;
+import android.content.pm.Signature;
+import android.content.pm.SignedPackage;
import android.os.Build;
import android.platform.test.annotations.Presubmit;
+import android.platform.test.annotations.RequiresFlagsEnabled;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.Log;
@@ -68,6 +73,8 @@
@Rule public TemporaryFolder mTemporaryFolder = new TemporaryFolder();
+ @Rule public CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
+
@Before
public void setUp() throws Exception {
mSysConfig = new SystemConfigTestClass();
@@ -696,6 +703,44 @@
assertThat(mSysConfig.getSystemAppUpdateOwnerPackageName("com.foo")).isNull();
}
+ /**
+ * Tests that SystemConfig::getEnhancedConfirmationTrustedInstallers correctly parses a list of
+ * SignedPackage objects.
+ */
+ @Test
+ @RequiresFlagsEnabled(
+ android.permission.flags.Flags.FLAG_ENHANCED_CONFIRMATION_MODE_APIS_ENABLED)
+ public void getEnhancedConfirmationTrustedInstallers_returnsTrustedInstallers()
+ throws IOException {
+ String pkgName = "com.example.app";
+ String certificateDigestStr = "E9:7A:BC:2C:D1:CA:8D:58:6A:57:0B:8C:F8:60:AA:D2:"
+ + "8D:13:30:2A:FB:C9:00:2C:5D:53:B2:6C:09:A4:85:A0";
+
+ byte[] certificateDigest = new Signature(certificateDigestStr.replace(":", ""))
+ .toByteArray();
+ String contents = "<config>"
+ + "<" + "enhanced-confirmation-trusted-installer" + " "
+ + "package=\"" + pkgName + "\""
+ + " sha256-cert-digest=\"" + certificateDigestStr + "\""
+ + "/>"
+ + "</config>";
+
+ final File folder = createTempSubfolder("folder");
+ createTempFile(folder, "enhanced-confirmation.xml", contents);
+ readPermissions(folder, /* Grant all permission flags */ ~0);
+
+ ArraySet<SignedPackage> actualTrustedInstallers =
+ mSysConfig.getEnhancedConfirmationTrustedInstallers();
+
+ assertThat(actualTrustedInstallers.size()).isEqualTo(1);
+ SignedPackage actual = actualTrustedInstallers.stream().findFirst().orElseThrow();
+ SignedPackage expected = new SignedPackage(pkgName, certificateDigest);
+
+ assertThat(actual.getCertificateDigest()).isEqualTo(expected.getCertificateDigest());
+ assertThat(actual.getPkgName()).isEqualTo(expected.getPkgName());
+ assertThat(actual).isEqualTo(expected);
+ }
+
private void parseSharedLibraries(String contents) throws IOException {
File folder = createTempSubfolder("permissions_folder");
createTempFile(folder, "permissions.xml", contents);
diff --git a/services/tests/servicestests/src/com/android/server/uri/UriGrantsManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/uri/UriGrantsManagerServiceTest.java
index 3218586..24abc18 100644
--- a/services/tests/servicestests/src/com/android/server/uri/UriGrantsManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/uri/UriGrantsManagerServiceTest.java
@@ -16,6 +16,8 @@
package com.android.server.uri;
+import static android.platform.test.flag.junit.SetFlagsRule.DefaultInitValueType.NULL_DEFAULT;
+
import static com.android.server.uri.UriGrantsMockContext.FLAG_PERSISTABLE;
import static com.android.server.uri.UriGrantsMockContext.FLAG_PREFIX;
import static com.android.server.uri.UriGrantsMockContext.FLAG_READ;
@@ -57,22 +59,49 @@
import android.net.Uri;
import android.os.Process;
import android.os.UserHandle;
+import android.platform.test.flag.junit.FlagsParameterization;
+import android.platform.test.flag.junit.SetFlagsRule;
import android.platform.test.ravenwood.RavenwoodRule;
import android.util.ArraySet;
-import androidx.test.InstrumentationRegistry;
-
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
import java.util.Arrays;
+import java.util.List;
import java.util.Set;
+@RunWith(Parameterized.class)
public class UriGrantsManagerServiceTest {
@Rule
public final RavenwoodRule mRavenwood = new RavenwoodRule();
+ /**
+ * Why this class needs to test all combinations of
+ * {@link android.security.Flags#FLAG_CONTENT_URI_PERMISSION_APIS}:
+ *
+ * <p>Although tests in this class don't directly query the flag, its value
+ * is needed for {@link UriGrantsManagerInternal#checkGrantUriPermissionFromIntent}. This is
+ * particularly important for host side tests (Ravenwood), which cannot read flag values from
+ * the device and must have them set explicitly.
+ */
+ @Parameters(name = "{0}")
+ public static List<FlagsParameterization> getFlags() {
+ return FlagsParameterization.allCombinationsOf(
+ android.security.Flags.FLAG_CONTENT_URI_PERMISSION_APIS);
+ }
+
+ public UriGrantsManagerServiceTest(FlagsParameterization flags) {
+ mSetFlagsRule = new SetFlagsRule(NULL_DEFAULT, flags);
+ }
+
+ @Rule
+ public final SetFlagsRule mSetFlagsRule;
+
private UriGrantsMockContext mContext;
private UriGrantsManagerInternal mService;
diff --git a/services/tests/servicestests/utils/com/android/server/testutils/StubTransaction.java b/services/tests/servicestests/utils/com/android/server/testutils/StubTransaction.java
index b8dcecd..e27bb4c 100644
--- a/services/tests/servicestests/utils/com/android/server/testutils/StubTransaction.java
+++ b/services/tests/servicestests/utils/com/android/server/testutils/StubTransaction.java
@@ -320,4 +320,10 @@
mWindowInfosReportedListeners.add(listener);
return this;
}
+
+ @Override
+ public SurfaceControl.Transaction setCanOccludePresentation(SurfaceControl sc,
+ boolean canOccludePresentation) {
+ return this;
+ }
}
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/GroupHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/GroupHelperTest.java
index 516fb4a..5eb76e3 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/GroupHelperTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/GroupHelperTest.java
@@ -15,37 +15,56 @@
*/
package com.android.server.notification;
+import static android.app.Notification.COLOR_DEFAULT;
import static android.app.Notification.FLAG_AUTO_CANCEL;
import static android.app.Notification.FLAG_BUBBLE;
import static android.app.Notification.FLAG_CAN_COLORIZE;
import static android.app.Notification.FLAG_FOREGROUND_SERVICE;
import static android.app.Notification.FLAG_NO_CLEAR;
import static android.app.Notification.FLAG_ONGOING_EVENT;
+import static android.platform.test.flag.junit.SetFlagsRule.DefaultInitValueType.DEVICE_DEFAULT;
import static com.android.server.notification.GroupHelper.BASE_FLAGS;
+import static com.google.common.truth.Truth.assertThat;
+
import static junit.framework.Assert.assertEquals;
+import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Matchers.anyInt;
import static org.mockito.Matchers.anyString;
import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyZeroInteractions;
+import static org.mockito.Mockito.when;
import android.annotation.SuppressLint;
import android.app.Notification;
+import android.content.pm.PackageManager;
+import android.graphics.Color;
+import android.graphics.drawable.AdaptiveIconDrawable;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.Icon;
import android.os.UserHandle;
+import android.platform.test.annotations.EnableFlags;
+import android.platform.test.flag.junit.SetFlagsRule;
import android.service.notification.StatusBarNotification;
import android.test.suitebuilder.annotation.SmallTest;
import android.util.ArrayMap;
import androidx.test.runner.AndroidJUnit4;
+import com.android.internal.R;
import com.android.server.UiServiceTestCase;
+import com.android.server.notification.GroupHelper.NotificationAttributes;
import org.junit.Before;
+import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
@@ -59,23 +78,37 @@
@SuppressLint("GuardedBy") // It's ok for this test to access guarded methods from the class.
@RunWith(AndroidJUnit4.class)
public class GroupHelperTest extends UiServiceTestCase {
+ @Rule
+ public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(DEVICE_DEFAULT);
+
private @Mock GroupHelper.Callback mCallback;
+ private @Mock PackageManager mPackageManager;
private final static int AUTOGROUP_AT_COUNT = 7;
private GroupHelper mGroupHelper;
+ private @Mock Icon mSmallIcon;
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
- mGroupHelper = new GroupHelper(AUTOGROUP_AT_COUNT, mCallback);
+ mGroupHelper = new GroupHelper(getContext(), mPackageManager, AUTOGROUP_AT_COUNT,
+ mCallback);
+
+ NotificationRecord r = mock(NotificationRecord.class);
+ StatusBarNotification sbn = getSbn("package", 0, "0", UserHandle.SYSTEM);
+ when(r.getNotification()).thenReturn(sbn.getNotification());
+ when(r.getSbn()).thenReturn(sbn);
+ when(mSmallIcon.sameAs(mSmallIcon)).thenReturn(true);
}
private StatusBarNotification getSbn(String pkg, int id, String tag,
- UserHandle user, String groupKey) {
+ UserHandle user, String groupKey, Icon smallIcon, int iconColor) {
Notification.Builder nb = new Notification.Builder(getContext(), "test_channel_id")
.setContentTitle("A")
- .setWhen(1205);
+ .setWhen(1205)
+ .setSmallIcon(smallIcon)
+ .setColor(iconColor);
if (groupKey != null) {
nb.setGroup(groupKey);
}
@@ -84,23 +117,32 @@
}
private StatusBarNotification getSbn(String pkg, int id, String tag,
+ UserHandle user, String groupKey) {
+ return getSbn(pkg, id, tag, user, groupKey, mSmallIcon, Notification.COLOR_DEFAULT);
+ }
+
+ private StatusBarNotification getSbn(String pkg, int id, String tag,
UserHandle user) {
return getSbn(pkg, id, tag, user, null);
}
+ private NotificationAttributes getNotificationAttributes(int flags) {
+ return new NotificationAttributes(flags, mSmallIcon, COLOR_DEFAULT);
+ }
+
@Test
public void testGetAutogroupSummaryFlags_noChildren() {
- ArrayMap<String, Integer> children = new ArrayMap<>();
+ ArrayMap<String, NotificationAttributes> children = new ArrayMap<>();
assertEquals(BASE_FLAGS, mGroupHelper.getAutogroupSummaryFlags(children));
}
@Test
public void testGetAutogroupSummaryFlags_oneOngoing() {
- ArrayMap<String, Integer> children = new ArrayMap<>();
- children.put("a", 0);
- children.put("b", FLAG_ONGOING_EVENT);
- children.put("c", FLAG_BUBBLE);
+ ArrayMap<String, NotificationAttributes> children = new ArrayMap<>();
+ children.put("a", getNotificationAttributes(0));
+ children.put("b", getNotificationAttributes(FLAG_ONGOING_EVENT));
+ children.put("c", getNotificationAttributes(FLAG_BUBBLE));
assertEquals(FLAG_ONGOING_EVENT | BASE_FLAGS,
mGroupHelper.getAutogroupSummaryFlags(children));
@@ -108,10 +150,10 @@
@Test
public void testGetAutogroupSummaryFlags_oneOngoingNoClear() {
- ArrayMap<String, Integer> children = new ArrayMap<>();
- children.put("a", 0);
- children.put("b", FLAG_ONGOING_EVENT|FLAG_NO_CLEAR);
- children.put("c", FLAG_BUBBLE);
+ ArrayMap<String, NotificationAttributes> children = new ArrayMap<>();
+ children.put("a", getNotificationAttributes(0));
+ children.put("b", getNotificationAttributes(FLAG_ONGOING_EVENT | FLAG_NO_CLEAR));
+ children.put("c", getNotificationAttributes(FLAG_BUBBLE));
assertEquals(FLAG_NO_CLEAR | FLAG_ONGOING_EVENT | BASE_FLAGS,
mGroupHelper.getAutogroupSummaryFlags(children));
@@ -119,10 +161,10 @@
@Test
public void testGetAutogroupSummaryFlags_oneOngoingBubble() {
- ArrayMap<String, Integer> children = new ArrayMap<>();
- children.put("a", 0);
- children.put("b", FLAG_ONGOING_EVENT | FLAG_BUBBLE);
- children.put("c", FLAG_BUBBLE);
+ ArrayMap<String, NotificationAttributes> children = new ArrayMap<>();
+ children.put("a", getNotificationAttributes(0));
+ children.put("b", getNotificationAttributes(FLAG_ONGOING_EVENT | FLAG_BUBBLE));
+ children.put("c", getNotificationAttributes(FLAG_BUBBLE));
assertEquals(FLAG_ONGOING_EVENT | BASE_FLAGS,
mGroupHelper.getAutogroupSummaryFlags(children));
@@ -130,11 +172,11 @@
@Test
public void testGetAutogroupSummaryFlags_multipleOngoing() {
- ArrayMap<String, Integer> children = new ArrayMap<>();
- children.put("a", 0);
- children.put("b", FLAG_ONGOING_EVENT);
- children.put("c", FLAG_BUBBLE);
- children.put("d", FLAG_ONGOING_EVENT);
+ ArrayMap<String, NotificationAttributes> children = new ArrayMap<>();
+ children.put("a", getNotificationAttributes(0));
+ children.put("b", getNotificationAttributes(FLAG_ONGOING_EVENT));
+ children.put("c", getNotificationAttributes(FLAG_BUBBLE));
+ children.put("d", getNotificationAttributes(FLAG_ONGOING_EVENT));
assertEquals(FLAG_ONGOING_EVENT | BASE_FLAGS,
mGroupHelper.getAutogroupSummaryFlags(children));
@@ -142,10 +184,10 @@
@Test
public void testGetAutogroupSummaryFlags_oneAutoCancel() {
- ArrayMap<String, Integer> children = new ArrayMap<>();
- children.put("a", 0);
- children.put("b", FLAG_AUTO_CANCEL);
- children.put("c", FLAG_BUBBLE);
+ ArrayMap<String, NotificationAttributes> children = new ArrayMap<>();
+ children.put("a", getNotificationAttributes(0));
+ children.put("b", getNotificationAttributes(FLAG_AUTO_CANCEL));
+ children.put("c", getNotificationAttributes(FLAG_BUBBLE));
assertEquals(BASE_FLAGS,
mGroupHelper.getAutogroupSummaryFlags(children));
@@ -153,11 +195,11 @@
@Test
public void testGetAutogroupSummaryFlags_allAutoCancel() {
- ArrayMap<String, Integer> children = new ArrayMap<>();
- children.put("a", FLAG_AUTO_CANCEL);
- children.put("b", FLAG_AUTO_CANCEL | FLAG_CAN_COLORIZE);
- children.put("c", FLAG_AUTO_CANCEL);
- children.put("d", FLAG_AUTO_CANCEL | FLAG_FOREGROUND_SERVICE);
+ ArrayMap<String, NotificationAttributes> children = new ArrayMap<>();
+ children.put("a", getNotificationAttributes(FLAG_AUTO_CANCEL));
+ children.put("b", getNotificationAttributes(FLAG_AUTO_CANCEL | FLAG_CAN_COLORIZE));
+ children.put("c", getNotificationAttributes(FLAG_AUTO_CANCEL));
+ children.put("d", getNotificationAttributes(FLAG_AUTO_CANCEL | FLAG_FOREGROUND_SERVICE));
assertEquals(FLAG_AUTO_CANCEL | BASE_FLAGS,
mGroupHelper.getAutogroupSummaryFlags(children));
@@ -165,11 +207,12 @@
@Test
public void testGetAutogroupSummaryFlags_allAutoCancelOneOngoing() {
- ArrayMap<String, Integer> children = new ArrayMap<>();
- children.put("a", FLAG_AUTO_CANCEL);
- children.put("b", FLAG_AUTO_CANCEL | FLAG_CAN_COLORIZE);
- children.put("c", FLAG_AUTO_CANCEL);
- children.put("d", FLAG_AUTO_CANCEL | FLAG_FOREGROUND_SERVICE | FLAG_ONGOING_EVENT);
+ ArrayMap<String, NotificationAttributes> children = new ArrayMap<>();
+ children.put("a", getNotificationAttributes(FLAG_AUTO_CANCEL));
+ children.put("b", getNotificationAttributes(FLAG_AUTO_CANCEL | FLAG_CAN_COLORIZE));
+ children.put("c", getNotificationAttributes(FLAG_AUTO_CANCEL));
+ children.put("d", getNotificationAttributes(
+ FLAG_AUTO_CANCEL | FLAG_FOREGROUND_SERVICE | FLAG_ONGOING_EVENT));
assertEquals(FLAG_AUTO_CANCEL| FLAG_ONGOING_EVENT | BASE_FLAGS,
mGroupHelper.getAutogroupSummaryFlags(children));
@@ -230,11 +273,11 @@
getSbn(pkg, i, String.valueOf(i), UserHandle.SYSTEM), false);
}
verify(mCallback, times(1)).addAutoGroupSummary(
- anyInt(), eq(pkg), anyString(), eq(BASE_FLAGS));
+ anyInt(), eq(pkg), anyString(), eq(getNotificationAttributes(BASE_FLAGS)));
verify(mCallback, times(AUTOGROUP_AT_COUNT)).addAutoGroup(anyString());
verify(mCallback, never()).removeAutoGroup(anyString());
verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString());
- verify(mCallback, never()).updateAutogroupSummary(anyInt(), anyString(), anyInt());
+ verify(mCallback, never()).updateAutogroupSummary(anyInt(), anyString(), any());
}
@Test
@@ -248,11 +291,11 @@
mGroupHelper.onNotificationPosted(sbn, false);
}
verify(mCallback, times(1)).addAutoGroupSummary(anyInt(), eq(pkg), anyString(),
- eq(BASE_FLAGS | FLAG_ONGOING_EVENT));
+ eq(getNotificationAttributes(BASE_FLAGS | FLAG_ONGOING_EVENT)));
verify(mCallback, times(AUTOGROUP_AT_COUNT)).addAutoGroup(anyString());
verify(mCallback, never()).removeAutoGroup(anyString());
verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString());
- verify(mCallback, never()).updateAutogroupSummary(anyInt(), anyString(), anyInt());
+ verify(mCallback, never()).updateAutogroupSummary(anyInt(), anyString(), any());
}
@Test
@@ -266,11 +309,11 @@
mGroupHelper.onNotificationPosted(sbn, false);
}
verify(mCallback, times(1)).addAutoGroupSummary(
- anyInt(), eq(pkg), anyString(), eq(BASE_FLAGS));
+ anyInt(), eq(pkg), anyString(), eq(getNotificationAttributes(BASE_FLAGS)));
verify(mCallback, times(AUTOGROUP_AT_COUNT)).addAutoGroup(anyString());
verify(mCallback, never()).removeAutoGroup(anyString());
verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString());
- verify(mCallback, never()).updateAutogroupSummary(anyInt(), anyString(), anyInt());
+ verify(mCallback, never()).updateAutogroupSummary(anyInt(), anyString(), any());
}
@Test
@@ -282,11 +325,11 @@
mGroupHelper.onNotificationPosted(sbn, false);
}
verify(mCallback, times(1)).addAutoGroupSummary(anyInt(), eq(pkg), anyString(),
- eq(BASE_FLAGS | FLAG_AUTO_CANCEL));
+ eq(getNotificationAttributes(BASE_FLAGS | FLAG_AUTO_CANCEL)));
verify(mCallback, times(AUTOGROUP_AT_COUNT)).addAutoGroup(anyString());
verify(mCallback, never()).removeAutoGroup(anyString());
verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString());
- verify(mCallback, never()).updateAutogroupSummary(anyInt(), anyString(), anyInt());
+ verify(mCallback, never()).updateAutogroupSummary(anyInt(), anyString(), any());
}
@Test
@@ -301,11 +344,11 @@
mGroupHelper.onNotificationPosted(sbn, false);
}
verify(mCallback, times(1)).addAutoGroupSummary(anyInt(), eq(pkg), anyString(),
- eq(BASE_FLAGS | FLAG_AUTO_CANCEL | FLAG_NO_CLEAR));
+ eq(getNotificationAttributes(BASE_FLAGS | FLAG_AUTO_CANCEL | FLAG_NO_CLEAR)));
verify(mCallback, times(AUTOGROUP_AT_COUNT)).addAutoGroup(anyString());
verify(mCallback, never()).removeAutoGroup(anyString());
verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString());
- verify(mCallback, never()).updateAutogroupSummary(anyInt(), anyString(), anyInt());
+ verify(mCallback, never()).updateAutogroupSummary(anyInt(), anyString(), any());
}
@Test
@@ -329,8 +372,8 @@
mGroupHelper.onNotificationPosted(notifications.get(0), true);
// Summary should keep FLAG_ONGOING_EVENT if any child has it
- verify(mCallback).updateAutogroupSummary(
- anyInt(), anyString(), eq(BASE_FLAGS | FLAG_ONGOING_EVENT));
+ verify(mCallback).updateAutogroupSummary(anyInt(), anyString(),
+ eq(getNotificationAttributes(BASE_FLAGS | FLAG_ONGOING_EVENT)));
}
@Test
@@ -355,7 +398,8 @@
mGroupHelper.onNotificationRemoved(notifications.get(0));
// Summary is no longer ongoing
- verify(mCallback).updateAutogroupSummary(anyInt(), anyString(), eq(BASE_FLAGS));
+ verify(mCallback).updateAutogroupSummary(anyInt(), anyString(),
+ eq(getNotificationAttributes(BASE_FLAGS)));
}
@Test
@@ -378,8 +422,8 @@
mGroupHelper.onNotificationPosted(notifications.get(0), true);
// Summary is now ongoing
- verify(mCallback).updateAutogroupSummary(
- anyInt(), anyString(), eq(BASE_FLAGS | FLAG_ONGOING_EVENT));
+ verify(mCallback).updateAutogroupSummary(anyInt(), anyString(),
+ eq(getNotificationAttributes(BASE_FLAGS | FLAG_ONGOING_EVENT)));
}
@Test
@@ -403,8 +447,8 @@
mGroupHelper.onNotificationPosted(sbn, true);
// Summary is now ongoing
- verify(mCallback).updateAutogroupSummary(
- anyInt(), anyString(), eq(BASE_FLAGS | FLAG_ONGOING_EVENT));
+ verify(mCallback).updateAutogroupSummary(anyInt(), anyString(),
+ eq(getNotificationAttributes(BASE_FLAGS | FLAG_ONGOING_EVENT)));
}
@Test
@@ -430,7 +474,8 @@
mGroupHelper.onNotificationPosted(sbn, true);
// Summary is no longer ongoing
- verify(mCallback).updateAutogroupSummary(anyInt(), anyString(), eq(BASE_FLAGS));
+ verify(mCallback).updateAutogroupSummary(anyInt(), anyString(),
+ eq(getNotificationAttributes(BASE_FLAGS)));
}
@Test
@@ -455,7 +500,7 @@
mGroupHelper.onNotificationRemoved(notifications.get(1));
// Summary is still ongoing
- verify(mCallback, never()).updateAutogroupSummary(anyInt(), anyString(), anyInt());
+ verify(mCallback, never()).updateAutogroupSummary(anyInt(), anyString(), any());
}
@Test
@@ -479,7 +524,8 @@
mGroupHelper.onNotificationPosted(notifications.get(0), true);
// Summary should no longer be autocancelable
- verify(mCallback).updateAutogroupSummary(anyInt(), anyString(), eq(BASE_FLAGS));
+ verify(mCallback).updateAutogroupSummary(anyInt(), anyString(),
+ eq(getNotificationAttributes(BASE_FLAGS)));
}
@Test
@@ -505,8 +551,8 @@
mGroupHelper.onNotificationPosted(notifications.get(0), true);
// Summary should now autocancelable
- verify(mCallback).updateAutogroupSummary(
- anyInt(), anyString(), eq(BASE_FLAGS | FLAG_AUTO_CANCEL));
+ verify(mCallback).updateAutogroupSummary(anyInt(), anyString(),
+ eq(getNotificationAttributes(BASE_FLAGS | FLAG_AUTO_CANCEL)));
}
@Test
@@ -530,7 +576,7 @@
mGroupHelper.onNotificationPosted(sbn, true);
// Summary should be still be autocancelable
- verify(mCallback, never()).updateAutogroupSummary(anyInt(), anyString(), anyInt());
+ verify(mCallback, never()).updateAutogroupSummary(anyInt(), anyString(), any());
}
@Test
@@ -552,7 +598,7 @@
mGroupHelper.onNotificationRemoved(notifications.get(0));
// Summary should still be autocancelable
- verify(mCallback, never()).updateAutogroupSummary(anyInt(), anyString(), anyInt());
+ verify(mCallback, never()).updateAutogroupSummary(anyInt(), anyString(), any());
}
@Test
@@ -565,7 +611,7 @@
mGroupHelper.onNotificationPosted(sbn, false);
}
verify(mCallback, times(1)).addAutoGroupSummary(
- anyInt(), eq(pkg), anyString(), eq(BASE_FLAGS));
+ anyInt(), eq(pkg), anyString(), eq(getNotificationAttributes(BASE_FLAGS)));
verify(mCallback, times(AUTOGROUP_AT_COUNT)).addAutoGroup(anyString());
verify(mCallback, never()).removeAutoGroup(anyString());
verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString());
@@ -593,7 +639,7 @@
mGroupHelper.onNotificationPosted(sbn, false);
}
verify(mCallback, times(1)).addAutoGroupSummary(
- anyInt(), eq(pkg), anyString(), eq(BASE_FLAGS));
+ anyInt(), eq(pkg), anyString(), eq(getNotificationAttributes(BASE_FLAGS)));
verify(mCallback, times(AUTOGROUP_AT_COUNT)).addAutoGroup(anyString());
verify(mCallback, never()).removeAutoGroup(anyString());
verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString());
@@ -622,7 +668,7 @@
mGroupHelper.onNotificationPosted(sbn, false);
}
verify(mCallback, times(1)).addAutoGroupSummary(
- anyInt(), eq(pkg), anyString(), eq(BASE_FLAGS));
+ anyInt(), eq(pkg), anyString(), eq(getNotificationAttributes(BASE_FLAGS)));
verify(mCallback, times(AUTOGROUP_AT_COUNT)).addAutoGroup(anyString());
verify(mCallback, never()).removeAutoGroup(anyString());
verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString());
@@ -646,8 +692,213 @@
verify(mCallback, times(1)).addAutoGroup(sbn.getKey());
verify(mCallback, never()).removeAutoGroup(anyString());
verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString());
- verify(mCallback).updateAutogroupSummary(anyInt(), anyString(), eq(BASE_FLAGS));
- verify(mCallback, never()).addAutoGroupSummary(
- anyInt(), anyString(), anyString(), anyInt());
+ verify(mCallback).updateAutogroupSummary(anyInt(), anyString(),
+ eq(getNotificationAttributes(BASE_FLAGS)));
+ verify(mCallback, never()).addAutoGroupSummary(anyInt(), anyString(), anyString(), any());
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_AUTOGROUP_SUMMARY_ICON_UPDATE)
+ public void testAddSummary_sameIcon_sameColor() {
+ final String pkg = "package";
+ final Icon icon = mock(Icon.class);
+ when(icon.sameAs(icon)).thenReturn(true);
+ final int iconColor = Color.BLUE;
+ final NotificationAttributes attr = new NotificationAttributes(BASE_FLAGS, icon, iconColor);
+
+ // Add notifications with same icon and color
+ for (int i = 0; i < AUTOGROUP_AT_COUNT; i++) {
+ StatusBarNotification sbn = getSbn(pkg, i, String.valueOf(i), UserHandle.SYSTEM, null,
+ icon, iconColor);
+ mGroupHelper.onNotificationPosted(sbn, false);
+ }
+ // Check that the summary would have the same icon and color
+ verify(mCallback, times(1)).addAutoGroupSummary(
+ anyInt(), eq(pkg), anyString(), eq(attr));
+ verify(mCallback, times(AUTOGROUP_AT_COUNT)).addAutoGroup(anyString());
+ verify(mCallback, never()).removeAutoGroup(anyString());
+ verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString());
+
+ // After auto-grouping, add new notification with the same color
+ StatusBarNotification sbn = getSbn(pkg, AUTOGROUP_AT_COUNT,
+ String.valueOf(AUTOGROUP_AT_COUNT), UserHandle.SYSTEM, null, icon, iconColor);
+ mGroupHelper.onNotificationPosted(sbn, true);
+
+ // Check that the summary was updated
+ //NotificationAttributes newAttr = new NotificationAttributes(BASE_FLAGS, icon, iconColor);
+ verify(mCallback, times(1)).updateAutogroupSummary(anyInt(), anyString(), eq(attr));
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_AUTOGROUP_SUMMARY_ICON_UPDATE)
+ public void testAddSummary_diffIcon_diffColor() {
+ final String pkg = "package";
+ final Icon initialIcon = mock(Icon.class);
+ when(initialIcon.sameAs(initialIcon)).thenReturn(true);
+ final int initialIconColor = Color.BLUE;
+
+ // Spy GroupHelper for getMonochromeAppIcon
+ final Icon monochromeIcon = mock(Icon.class);
+ when(monochromeIcon.sameAs(monochromeIcon)).thenReturn(true);
+ GroupHelper groupHelper = spy(mGroupHelper);
+ doReturn(monochromeIcon).when(groupHelper).getMonochromeAppIcon(eq(pkg));
+
+ final NotificationAttributes initialAttr = new NotificationAttributes(BASE_FLAGS,
+ initialIcon, initialIconColor);
+
+ // Add notifications with same icon and color
+ for (int i = 0; i < AUTOGROUP_AT_COUNT; i++) {
+ StatusBarNotification sbn = getSbn(pkg, i, String.valueOf(i), UserHandle.SYSTEM, null,
+ initialIcon, initialIconColor);
+ groupHelper.onNotificationPosted(sbn, false);
+ }
+ // Check that the summary would have the same icon and color
+ verify(mCallback, times(1)).addAutoGroupSummary(
+ anyInt(), eq(pkg), anyString(), eq(initialAttr));
+ verify(mCallback, times(AUTOGROUP_AT_COUNT)).addAutoGroup(anyString());
+ verify(mCallback, never()).removeAutoGroup(anyString());
+ verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString());
+
+ // After auto-grouping, add new notification with a different color
+ final Icon newIcon = mock(Icon.class);
+ final int newIconColor = Color.YELLOW;
+ StatusBarNotification sbn = getSbn(pkg, AUTOGROUP_AT_COUNT,
+ String.valueOf(AUTOGROUP_AT_COUNT), UserHandle.SYSTEM, null, newIcon,
+ newIconColor);
+ groupHelper.onNotificationPosted(sbn, true);
+
+ // Summary should be updated to the default color and the icon to the monochrome icon
+ NotificationAttributes newAttr = new NotificationAttributes(BASE_FLAGS, monochromeIcon,
+ COLOR_DEFAULT);
+ verify(mCallback, times(1)).updateAutogroupSummary(anyInt(), anyString(), eq(newAttr));
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_AUTOGROUP_SUMMARY_ICON_UPDATE)
+ public void testAutoGrouped_diffIcon_diffColor_removeChild_updateTo_sameIcon_sameColor() {
+ final String pkg = "package";
+ final Icon initialIcon = mock(Icon.class);
+ when(initialIcon.sameAs(initialIcon)).thenReturn(true);
+ final int initialIconColor = Color.BLUE;
+ final NotificationAttributes initialAttr = new NotificationAttributes(
+ GroupHelper.FLAG_INVALID, initialIcon, initialIconColor);
+
+ // Add AUTOGROUP_AT_COUNT-1 notifications with same icon and color
+ ArrayList<StatusBarNotification> notifications = new ArrayList<>();
+ for (int i = 0; i < AUTOGROUP_AT_COUNT - 1; i++) {
+ StatusBarNotification sbn = getSbn(pkg, i, String.valueOf(i), UserHandle.SYSTEM, null,
+ initialIcon, initialIconColor);
+ notifications.add(sbn);
+ }
+ // And an additional notification with different icon and color
+ final int lastIdx = AUTOGROUP_AT_COUNT - 1;
+ StatusBarNotification newSbn = getSbn(pkg, lastIdx,
+ String.valueOf(lastIdx), UserHandle.SYSTEM, null, mock(Icon.class),
+ Color.YELLOW);
+ notifications.add(newSbn);
+ for (StatusBarNotification sbn: notifications) {
+ mGroupHelper.onNotificationPosted(sbn, false);
+ }
+
+ // Remove last notification (the only one with different icon and color)
+ mGroupHelper.onNotificationRemoved(notifications.get(lastIdx));
+
+ // Summary should be updated to the common icon and color
+ verify(mCallback, times(1)).updateAutogroupSummary(anyInt(), anyString(), eq(initialAttr));
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_AUTOGROUP_SUMMARY_ICON_UPDATE)
+ public void testAutobundledSummaryIcon_sameIcon() {
+ final String pkg = "package";
+ final Icon icon = mock(Icon.class);
+ when(icon.sameAs(icon)).thenReturn(true);
+
+ // Create notifications with the same icon
+ List<NotificationAttributes> childrenAttr = new ArrayList<>();
+ for (int i = 0; i < AUTOGROUP_AT_COUNT; i++) {
+ childrenAttr.add(new NotificationAttributes(0, icon, COLOR_DEFAULT));
+ }
+
+ //Check that the generated summary icon is the same as the child notifications'
+ Icon summaryIcon = mGroupHelper.getAutobundledSummaryIconAndColor(pkg, childrenAttr).icon;
+ assertThat(summaryIcon).isEqualTo(icon);
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_AUTOGROUP_SUMMARY_ICON_UPDATE)
+ public void testAutobundledSummaryIcon_diffIcon() {
+ final String pkg = "package";
+ // Spy GroupHelper for getMonochromeAppIcon
+ final Icon monochromeIcon = mock(Icon.class);
+ GroupHelper groupHelper = spy(mGroupHelper);
+ doReturn(monochromeIcon).when(groupHelper).getMonochromeAppIcon(eq(pkg));
+
+ // Create notifications with different icons
+ List<NotificationAttributes> childrenAttr = new ArrayList<>();
+ for (int i = 0; i < AUTOGROUP_AT_COUNT; i++) {
+ childrenAttr.add(new NotificationAttributes(0, mock(Icon.class), COLOR_DEFAULT));
+ }
+
+ // Check that the generated summary icon is the monochrome icon
+ Icon summaryIcon = groupHelper.getAutobundledSummaryIconAndColor(pkg, childrenAttr).icon;
+ assertThat(summaryIcon).isEqualTo(monochromeIcon);
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_AUTOGROUP_SUMMARY_ICON_UPDATE)
+ public void testAutobundledSummaryIconColor_sameColor() {
+ final String pkg = "package";
+ final int iconColor = Color.BLUE;
+ // Create notifications with the same icon color
+ List<NotificationAttributes> childrenAttr = new ArrayList<>();
+ for (int i = 0; i < AUTOGROUP_AT_COUNT; i++) {
+ childrenAttr.add(new NotificationAttributes(0, mock(Icon.class), iconColor));
+ }
+
+ // Check that the generated summary icon color is the same as the child notifications'
+ int summaryIconColor = mGroupHelper.getAutobundledSummaryIconAndColor(pkg,
+ childrenAttr).iconColor;
+ assertThat(summaryIconColor).isEqualTo(iconColor);
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_AUTOGROUP_SUMMARY_ICON_UPDATE)
+ public void testAutobundledSummaryIconColor_diffColor() {
+ final String pkg = "package";
+ // Create notifications with different icon colors
+ List<NotificationAttributes> childrenAttr = new ArrayList<>();
+ for (int i = 0; i < AUTOGROUP_AT_COUNT; i++) {
+ childrenAttr.add(new NotificationAttributes(0, mock(Icon.class), i));
+ }
+
+ // Check that the generated summary icon color is the default color
+ int summaryIconColor = mGroupHelper.getAutobundledSummaryIconAndColor(pkg,
+ childrenAttr).iconColor;
+ assertThat(summaryIconColor).isEqualTo(Notification.COLOR_DEFAULT);
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_AUTOGROUP_SUMMARY_ICON_UPDATE)
+ public void testMonochromeAppIcon_adaptiveIconExists() throws Exception {
+ final String pkg = "testPackage";
+ final int monochromeIconResId = 1234;
+ AdaptiveIconDrawable adaptiveIcon = mock(AdaptiveIconDrawable.class);
+ Drawable monochromeIcon = mock(Drawable.class);
+ when(mPackageManager.getApplicationIcon(pkg)).thenReturn(adaptiveIcon);
+ when(adaptiveIcon.getMonochrome()).thenReturn(monochromeIcon);
+ when(adaptiveIcon.getSourceDrawableResId()).thenReturn(monochromeIconResId);
+ assertThat(mGroupHelper.getMonochromeAppIcon(pkg).getResId())
+ .isEqualTo(monochromeIconResId);
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_AUTOGROUP_SUMMARY_ICON_UPDATE)
+ public void testMonochromeAppIcon_adaptiveIconMissing_fallback() throws Exception {
+ final String pkg = "testPackage";
+ final int fallbackIconResId = R.drawable.ic_notification_summary_auto;
+ when(mPackageManager.getApplicationIcon(pkg)).thenReturn(mock(Drawable.class));
+ assertThat(mGroupHelper.getMonochromeAppIcon(pkg).getResId())
+ .isEqualTo(fallbackIconResId);
}
}
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
index ef0ac33..77be01c 100755
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
@@ -279,6 +279,7 @@
import com.android.server.job.JobSchedulerInternal;
import com.android.server.lights.LightsManager;
import com.android.server.lights.LogicalLight;
+import com.android.server.notification.GroupHelper.NotificationAttributes;
import com.android.server.notification.NotificationManagerService.NotificationAssistants;
import com.android.server.notification.NotificationManagerService.NotificationListeners;
import com.android.server.notification.NotificationManagerService.PostNotificationTracker;
@@ -322,6 +323,7 @@
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
+import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
@@ -658,7 +660,8 @@
// NOTE: Prefer using the @EnableFlag annotation where possible. Do not add any android.app
// flags here.
mSetFlagsRule.disableFlags(Flags.FLAG_REFACTOR_ATTENTION_HELPER,
- Flags.FLAG_POLITE_NOTIFICATIONS);
+ Flags.FLAG_POLITE_NOTIFICATIONS, Flags.FLAG_AUTOGROUP_SUMMARY_ICON_UPDATE);
+
initNMS();
}
@@ -1254,6 +1257,11 @@
verify(mAlarmManager).setExactAndAllowWhileIdle(anyInt(), anyLong(), captor.capture());
assertEquals(PackageManagerService.PLATFORM_PACKAGE_NAME,
captor.getValue().getIntent().getPackage());
+
+ mService.cancelScheduledTimeoutLocked(r);
+ verify(mAlarmManager).cancel(captor.capture());
+ assertEquals(PackageManagerService.PLATFORM_PACKAGE_NAME,
+ captor.getValue().getIntent().getPackage());
}
@Test
@@ -2327,8 +2335,9 @@
mService.mAutobundledSummaries.put(0, new ArrayMap<>());
mService.mAutobundledSummaries.get(0).put("pkg", summary.getKey());
- mService.updateAutobundledSummaryFlags(
- 0, "pkg", GroupHelper.BASE_FLAGS | FLAG_ONGOING_EVENT, false);
+ mService.updateAutobundledSummaryLocked(0, "pkg",
+ new NotificationAttributes(GroupHelper.BASE_FLAGS | FLAG_ONGOING_EVENT,
+ mock(Icon.class), 0), false);
waitForIdle();
assertTrue(summary.getSbn().isOngoing());
@@ -2345,7 +2354,9 @@
mService.mAutobundledSummaries.get(0).put("pkg", summary.getKey());
mService.mSummaryByGroupKey.put("pkg", summary);
- mService.updateAutobundledSummaryFlags(0, "pkg", GroupHelper.BASE_FLAGS, false);
+ mService.updateAutobundledSummaryLocked(0, "pkg",
+ new NotificationAttributes(GroupHelper.BASE_FLAGS,
+ mock(Icon.class), 0), false);
waitForIdle();
assertFalse(summary.getSbn().isOngoing());
@@ -3422,8 +3433,8 @@
when(mPermissionHelper.hasPermission(mUid)).thenReturn(true);
when(mPermissionHelper.isPermissionFixed(PKG, temp.getUserId())).thenReturn(true);
- NotificationRecord r = mService.createAutoGroupSummary(
- temp.getUserId(), temp.getSbn().getPackageName(), temp.getKey(), 0);
+ NotificationRecord r = mService.createAutoGroupSummary(temp.getUserId(),
+ temp.getSbn().getPackageName(), temp.getKey(), 0, mock(Icon.class), 0);
assertThat(r.isImportanceFixed()).isTrue();
}
@@ -4111,7 +4122,8 @@
when(mListener.enabledAndUserMatches(anyInt())).thenReturn(false);
when(mListeners.checkServiceTokenLocked(any())).thenReturn(mListener);
- mService.snoozeNotificationInt(r.getKey(), 1000, null, mListener);
+ mService.snoozeNotificationInt(Binder.getCallingUid(), mock(INotificationListener.class),
+ r.getKey(), 1000, null);
verify(mWorkerHandler, never()).post(
any(NotificationManagerService.SnoozeNotificationRunnable.class));
@@ -4129,13 +4141,118 @@
when(mListener.enabledAndUserMatches(anyInt())).thenReturn(true);
when(mListeners.checkServiceTokenLocked(any())).thenReturn(mListener);
- mService.snoozeNotificationInt(r2.getKey(), 1000, null, mListener);
+ mService.snoozeNotificationInt(Binder.getCallingUid(), mock(INotificationListener.class),
+ r2.getKey(), 1000, null);
verify(mWorkerHandler).post(
any(NotificationManagerService.SnoozeNotificationRunnable.class));
}
@Test
+ public void snoozeNotificationInt_rapidSnooze_new() {
+ mSetFlagsRule.enableFlags(android.view.contentprotection.flags.Flags
+ .FLAG_RAPID_CLEAR_NOTIFICATIONS_BY_LISTENER_APP_OP_ENABLED);
+
+ // Create recent notification.
+ final NotificationRecord nr1 = generateNotificationRecord(mTestNotificationChannel,
+ System.currentTimeMillis());
+ mService.addNotification(nr1);
+
+ mListener = mock(ManagedServices.ManagedServiceInfo.class);
+ mListener.component = new ComponentName(PKG, PKG);
+ when(mListener.enabledAndUserMatches(anyInt())).thenReturn(true);
+ when(mListeners.checkServiceTokenLocked(any())).thenReturn(mListener);
+
+ mService.snoozeNotificationInt(Binder.getCallingUid(), mock(INotificationListener.class),
+ nr1.getKey(), 1000, null);
+
+ verify(mWorkerHandler).post(
+ any(NotificationManagerService.SnoozeNotificationRunnable.class));
+ // Ensure cancel event is logged.
+ verify(mAppOpsManager).noteOpNoThrow(
+ AppOpsManager.OP_RAPID_CLEAR_NOTIFICATIONS_BY_LISTENER, mUid, PKG, null,
+ null);
+ }
+
+ @Test
+ public void snoozeNotificationInt_rapidSnooze_old() {
+ mSetFlagsRule.enableFlags(android.view.contentprotection.flags.Flags
+ .FLAG_RAPID_CLEAR_NOTIFICATIONS_BY_LISTENER_APP_OP_ENABLED);
+
+ // Create old notification.
+ final NotificationRecord nr1 = generateNotificationRecord(mTestNotificationChannel,
+ System.currentTimeMillis() - 60000);
+ mService.addNotification(nr1);
+
+ mListener = mock(ManagedServices.ManagedServiceInfo.class);
+ mListener.component = new ComponentName(PKG, PKG);
+ when(mListener.enabledAndUserMatches(anyInt())).thenReturn(true);
+ when(mListeners.checkServiceTokenLocked(any())).thenReturn(mListener);
+
+ mService.snoozeNotificationInt(Binder.getCallingUid(), mock(INotificationListener.class),
+ nr1.getKey(), 1000, null);
+
+ verify(mWorkerHandler).post(
+ any(NotificationManagerService.SnoozeNotificationRunnable.class));
+ // Ensure cancel event is not logged.
+ verify(mAppOpsManager, never()).noteOpNoThrow(
+ eq(AppOpsManager.OP_RAPID_CLEAR_NOTIFICATIONS_BY_LISTENER), anyInt(), anyString(),
+ any(), any());
+ }
+
+ @Test
+ public void snoozeNotificationInt_rapidSnooze_new_flagDisabled() {
+ mSetFlagsRule.disableFlags(android.view.contentprotection.flags.Flags
+ .FLAG_RAPID_CLEAR_NOTIFICATIONS_BY_LISTENER_APP_OP_ENABLED);
+
+ // Create recent notification.
+ final NotificationRecord nr1 = generateNotificationRecord(mTestNotificationChannel,
+ System.currentTimeMillis());
+ mService.addNotification(nr1);
+
+ mListener = mock(ManagedServices.ManagedServiceInfo.class);
+ mListener.component = new ComponentName(PKG, PKG);
+ when(mListener.enabledAndUserMatches(anyInt())).thenReturn(true);
+ when(mListeners.checkServiceTokenLocked(any())).thenReturn(mListener);
+
+ mService.snoozeNotificationInt(Binder.getCallingUid(), mock(INotificationListener.class),
+ nr1.getKey(), 1000, null);
+
+ verify(mWorkerHandler).post(
+ any(NotificationManagerService.SnoozeNotificationRunnable.class));
+ // Ensure cancel event is not logged.
+ verify(mAppOpsManager, never()).noteOpNoThrow(
+ eq(AppOpsManager.OP_RAPID_CLEAR_NOTIFICATIONS_BY_LISTENER), anyInt(), anyString(),
+ any(), any());
+ }
+
+ @Test
+ public void snoozeNotificationInt_rapidSnooze_old_flagDisabled() {
+ mSetFlagsRule.disableFlags(android.view.contentprotection.flags.Flags
+ .FLAG_RAPID_CLEAR_NOTIFICATIONS_BY_LISTENER_APP_OP_ENABLED);
+
+ // Create old notification.
+ final NotificationRecord nr1 = generateNotificationRecord(mTestNotificationChannel,
+ System.currentTimeMillis() - 60000);
+ mService.addNotification(nr1);
+
+ mListener = mock(ManagedServices.ManagedServiceInfo.class);
+ mListener.component = new ComponentName(PKG, PKG);
+ when(mListener.enabledAndUserMatches(anyInt())).thenReturn(true);
+ when(mListeners.checkServiceTokenLocked(any())).thenReturn(mListener);
+
+ mService.snoozeNotificationInt(Binder.getCallingUid(), mock(INotificationListener.class),
+ nr1.getKey(), 1000, null);
+
+ verify(mWorkerHandler).post(
+ any(NotificationManagerService.SnoozeNotificationRunnable.class));
+ // Ensure cancel event is not logged.
+ verify(mAppOpsManager, never()).noteOpNoThrow(
+ eq(AppOpsManager.OP_RAPID_CLEAR_NOTIFICATIONS_BY_LISTENER), anyInt(), anyString(),
+ any(), any());
+ }
+
+ @Test
public void testSnoozeRunnable_tooManySnoozed_singleNotification() {
final NotificationRecord notification = generateNotificationRecord(
mTestNotificationChannel, 1, null, true);
@@ -9736,8 +9853,11 @@
throws RemoteException {
IRingtonePlayer mockPlayer = mock(IRingtonePlayer.class);
when(mAudioManager.getRingtonePlayer()).thenReturn(mockPlayer);
- // Set up volume to be above 0 for the sound to actually play
+ // Set up volume to be above 0, and for AudioManager to signal playback should happen,
+ // for the sound to actually play
when(mAudioManager.getStreamVolume(anyInt())).thenReturn(10);
+ when(mAudioManager.shouldNotificationSoundPlay(any(android.media.AudioAttributes.class)))
+ .thenReturn(true);
setUpPrefsForBubbles(PKG, mUid,
true /* global */,
@@ -11681,7 +11801,7 @@
// style + self managed call - bypasses block
when(mTelecomManager.isInSelfManagedCall(
- r.getSbn().getPackageName(), r.getUser())).thenReturn(true);
+ r.getSbn().getPackageName(), r.getUser(), true)).thenReturn(true);
assertThat(mService.checkDisqualifyingFeatures(r.getUserId(), r.getUid(),
r.getSbn().getId(), r.getSbn().getTag(), r, false, false)).isTrue();
@@ -11764,7 +11884,7 @@
// style + self managed call - bypasses block
mService.clearNotifications();
reset(mUsageStats);
- when(mTelecomManager.isInSelfManagedCall(r.getSbn().getPackageName(), r.getUser()))
+ when(mTelecomManager.isInSelfManagedCall(r.getSbn().getPackageName(), r.getUser(), true))
.thenReturn(true);
mService.addEnqueuedNotification(r);
@@ -11957,7 +12077,7 @@
// add summary
mService.addNotification(mService.createAutoGroupSummary(nr1.getUserId(),
nr1.getSbn().getPackageName(), nr1.getKey(),
- GroupHelper.BASE_FLAGS | FLAG_ONGOING_EVENT));
+ GroupHelper.BASE_FLAGS | FLAG_ONGOING_EVENT, mock(Icon.class), 0));
// cancel both children
mBinderService.cancelNotificationWithTag(PKG, PKG, nr0.getSbn().getTag(),
@@ -11984,8 +12104,9 @@
// add notifications + summary for USER_SYSTEM
mService.addNotification(nr0);
mService.addNotification(nr1);
- mService.addNotification(mService.createAutoGroupSummary(nr1.getUserId(),
- nr1.getSbn().getPackageName(), nr1.getKey(), GroupHelper.BASE_FLAGS));
+ mService.addNotification(
+ mService.createAutoGroupSummary(nr1.getUserId(), nr1.getSbn().getPackageName(),
+ nr1.getKey(), GroupHelper.BASE_FLAGS, mock(Icon.class), 0));
// add notifications + summary for USER_ALL
NotificationRecord nr0_all =
@@ -11995,8 +12116,10 @@
mService.addNotification(nr0_all);
mService.addNotification(nr1_all);
- mService.addNotification(mService.createAutoGroupSummary(nr0_all.getUserId(),
- nr0_all.getSbn().getPackageName(), nr0_all.getKey(), GroupHelper.BASE_FLAGS));
+ mService.addNotification(
+ mService.createAutoGroupSummary(nr0_all.getUserId(),
+ nr0_all.getSbn().getPackageName(),
+ nr0_all.getKey(), GroupHelper.BASE_FLAGS, mock(Icon.class), 0));
// cancel both children for USER_ALL
mBinderService.cancelNotificationWithTag(PKG, PKG, nr0_all.getSbn().getTag(),
@@ -12849,6 +12972,35 @@
}
@Test
+ public void fixNotification_customAllowlistToken()
+ throws Exception {
+ Notification n = new Notification.Builder(mContext, "test")
+ .build();
+ try {
+ Field allowlistToken = Class.forName("android.app.Notification").
+ getDeclaredField("mAllowlistToken");
+ allowlistToken.setAccessible(true);
+ allowlistToken.set(n, new Binder());
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+
+ mService.fixNotification(n, PKG, "tag", 9, 0, mUid, NOT_FOREGROUND_SERVICE, true);
+
+ IBinder actual = null;
+ try {
+ Field allowlistToken = Class.forName("android.app.Notification").
+ getDeclaredField("mAllowlistToken");
+ allowlistToken.setAccessible(true);
+ actual = (IBinder) allowlistToken.get(n);
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+
+ assertTrue(mService.ALLOWLIST_TOKEN == actual);
+ }
+
+ @Test
public void testCancelAllNotifications_IgnoreUserInitiatedJob() throws Exception {
when(mJsi.isNotificationAssociatedWithAnyUserInitiatedJobs(anyInt(), anyInt(), anyString()))
.thenReturn(true);
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationRecordTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationRecordTest.java
index 670d097..130a8ca 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationRecordTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationRecordTest.java
@@ -66,6 +66,7 @@
import android.os.Build;
import android.os.Bundle;
import android.os.UserHandle;
+import android.os.VibrationEffect;
import android.os.Vibrator;
import android.platform.test.flag.junit.SetFlagsRule;
import android.provider.Settings;
@@ -114,11 +115,13 @@
NotificationManager.IMPORTANCE_UNSPECIFIED);
private android.os.UserHandle mUser = UserHandle.of(ActivityManager.getCurrentUser());
- private static final long[] CUSTOM_VIBRATION = new long[] {
+ private static final long[] CUSTOM_NOTIFICATION_VIBRATION = new long[] {
300, 400, 300, 400, 300, 400, 300, 400, 300, 400, 300, 400,
300, 400, 300, 400, 300, 400, 300, 400, 300, 400, 300, 400,
300, 400, 300, 400, 300, 400, 300, 400, 300, 400, 300, 400 };
- private static final long[] CUSTOM_CHANNEL_VIBRATION = new long[] {300, 400, 300, 400 };
+ private static final long[] CUSTOM_CHANNEL_VIBRATION_PATTERN = new long[] {300, 400, 300, 400 };
+ private static final VibrationEffect CUSTOM_CHANNEL_VIBRATION_EFFECT =
+ VibrationEffect.createPredefined(VibrationEffect.EFFECT_CLICK);
private static final Uri CUSTOM_SOUND = Settings.System.DEFAULT_ALARM_ALERT_URI;
private static final AudioAttributes CUSTOM_ATTRIBUTES = new AudioAttributes.Builder()
.setContentType(AudioAttributes.CONTENT_TYPE_UNKNOWN)
@@ -135,6 +138,7 @@
MockitoAnnotations.initMocks(this);
when(mMockContext.getSystemService(eq(Vibrator.class))).thenReturn(mVibrator);
+ when(mVibrator.areVibrationFeaturesSupported(any())).thenReturn(true);
final Resources res = mContext.getResources();
when(mMockContext.getResources()).thenReturn(res);
when(mMockContext.getPackageManager()).thenReturn(mPm);
@@ -168,8 +172,8 @@
if (defaultVibration) {
defaults |= Notification.DEFAULT_VIBRATE;
} else {
- builder.setVibrate(CUSTOM_VIBRATION);
- channel.setVibrationPattern(CUSTOM_CHANNEL_VIBRATION);
+ builder.setVibrate(CUSTOM_NOTIFICATION_VIBRATION);
+ channel.setVibrationPattern(CUSTOM_CHANNEL_VIBRATION_PATTERN);
}
}
if (lights) {
@@ -217,26 +221,39 @@
return new StatusBarNotification(mPkg, mPkg, id1, tag1, uid, uid, n, mUser, null, uid);
}
+ private StatusBarNotification getNotification(
+ long[] channelVibrationPattern,
+ VibrationEffect channelVibrationEffect,
+ boolean insistent) {
+ if (channelVibrationPattern != null) {
+ channel.setVibrationPattern(channelVibrationPattern);
+ } else if (channelVibrationEffect != null) {
+ channel.setVibrationEffect(channelVibrationEffect);
+ }
- private StatusBarNotification getInsistentNotification(boolean defaultVibration) {
final Builder builder = new Builder(mMockContext)
.setContentTitle("foo")
.setSmallIcon(android.R.drawable.sym_def_app_icon)
- .setPriority(Notification.PRIORITY_HIGH);
- int defaults = 0;
- if (defaultVibration) {
- defaults |= Notification.DEFAULT_VIBRATE;
- } else {
- builder.setVibrate(CUSTOM_VIBRATION);
- channel.setVibrationPattern(CUSTOM_CHANNEL_VIBRATION);
- }
- builder.setDefaults(defaults);
- builder.setFlag(Notification.FLAG_INSISTENT, true);
+ .setPriority(Notification.PRIORITY_HIGH)
+ .setVibrate(CUSTOM_NOTIFICATION_VIBRATION)
+ .setFlag(Notification.FLAG_INSISTENT, insistent);
Notification n = builder.build();
return new StatusBarNotification(mPkg, mPkg, id1, tag1, uid, uid, n, mUser, null, uid);
}
+ private StatusBarNotification getNotification(
+ VibrationEffect channelVibrationEffect, boolean insistent) {
+ return getNotification(
+ /* channelVibrationPattern= */ null, channelVibrationEffect, insistent);
+ }
+
+ private StatusBarNotification getNotification(
+ long[] channelVibrationPattern, boolean insistent) {
+ return getNotification(
+ channelVibrationPattern, /* channelVibrationEffect= */ null, insistent);
+ }
+
private StatusBarNotification getMessagingStyleNotification() {
return getMessagingStyleNotification(mPkg);
}
@@ -344,7 +361,7 @@
NotificationRecord record = new NotificationRecord(mMockContext, sbn, defaultChannel);
assertEquals(VibratorHelper.createWaveformVibration(
- CUSTOM_VIBRATION, /* insistent= */ false), record.getVibration());
+ CUSTOM_NOTIFICATION_VIBRATION, /* insistent= */ false), record.getVibration());
}
@Test
@@ -358,30 +375,137 @@
NotificationRecord record = new NotificationRecord(mMockContext, sbn, defaultChannel);
assertNotEquals(VibratorHelper.createWaveformVibration(
- CUSTOM_VIBRATION, /* insistent= */ false), record.getVibration());
+ CUSTOM_NOTIFICATION_VIBRATION, /* insistent= */ false), record.getVibration());
}
@Test
- public void testVibration_custom_upgradeUsesChannel() {
+ public void testVibration_customPattern_nonInsistent_usesCustomPattern() {
channel.enableVibration(true);
- // post upgrade, custom vibration.
- StatusBarNotification sbn = getNotification(PKG_O, false /* noisy */,
- false /* defaultSound */, true /* buzzy */, false /* defaultBuzz */,
- false /* lights */, false /* defaultLights */, null /* group */);
+ StatusBarNotification sbn = getNotification(
+ CUSTOM_CHANNEL_VIBRATION_PATTERN, /* insistent= */ false);
NotificationRecord record = new NotificationRecord(mMockContext, sbn, channel);
assertEquals(VibratorHelper.createWaveformVibration(
- CUSTOM_CHANNEL_VIBRATION, /* insistent= */ false), record.getVibration());
+ CUSTOM_CHANNEL_VIBRATION_PATTERN, /* insistent= */ false), record.getVibration());
}
@Test
- public void testVibration_insistent_createsInsistentVibrationEffect() {
+ public void testVibration_customPattern_insistent_createsInsistentEffect() {
channel.enableVibration(true);
- StatusBarNotification sbn = getInsistentNotification(false /* defaultBuzz */);
+ StatusBarNotification sbn = getNotification(
+ CUSTOM_CHANNEL_VIBRATION_PATTERN, /* insistent= */ true);
NotificationRecord record = new NotificationRecord(mMockContext, sbn, channel);
assertEquals(VibratorHelper.createWaveformVibration(
- CUSTOM_CHANNEL_VIBRATION, /* insistent= */ true), record.getVibration());
+ CUSTOM_CHANNEL_VIBRATION_PATTERN, /* insistent= */ true), record.getVibration());
+ }
+
+ @Test
+ public void testVibration_customEffect_flagNotEnabled_usesDefaultEffect() {
+ mSetFlagsRule.disableFlags(Flags.FLAG_NOTIFICATION_CHANNEL_VIBRATION_EFFECT_API);
+ channel.enableVibration(true);
+ StatusBarNotification sbn = getNotification(
+ CUSTOM_CHANNEL_VIBRATION_EFFECT, /* insistent= */ false);
+
+ NotificationRecord record = new NotificationRecord(mMockContext, sbn, channel);
+ VibrationEffect effect = record.getVibration();
+ assertNotEquals(effect, CUSTOM_CHANNEL_VIBRATION_EFFECT);
+ assertNotNull(effect);
+ }
+
+ @Test
+ public void testVibration_customEffect_effectNotSupported_usesDefaultEffect() {
+ mSetFlagsRule.enableFlags(Flags.FLAG_NOTIFICATION_CHANNEL_VIBRATION_EFFECT_API);
+ when(mVibrator.areVibrationFeaturesSupported(any())).thenReturn(false);
+ StatusBarNotification sbn = getNotification(
+ CUSTOM_CHANNEL_VIBRATION_EFFECT, /* insistent= */ false);
+
+ NotificationRecord record = new NotificationRecord(mMockContext, sbn, channel);
+ VibrationEffect effect = record.getVibration();
+ assertNotEquals(effect, CUSTOM_CHANNEL_VIBRATION_EFFECT);
+ assertNotNull(effect);
+ }
+
+ @Test
+ public void testVibration_customNonRepeatingEffect_nonInsistent_usesCustomEffect() {
+ mSetFlagsRule.enableFlags(Flags.FLAG_NOTIFICATION_CHANNEL_VIBRATION_EFFECT_API);
+ StatusBarNotification sbn = getNotification(
+ CUSTOM_CHANNEL_VIBRATION_EFFECT, /* insistent= */ false);
+
+ NotificationRecord record = new NotificationRecord(mMockContext, sbn, channel);
+ assertEquals(CUSTOM_CHANNEL_VIBRATION_EFFECT, record.getVibration());
+ }
+
+ @Test
+ public void testVibration_customNonRepeatingEffect_insistent_createsInsistentEffect() {
+ mSetFlagsRule.enableFlags(Flags.FLAG_NOTIFICATION_CHANNEL_VIBRATION_EFFECT_API);
+ StatusBarNotification sbn = getNotification(
+ CUSTOM_CHANNEL_VIBRATION_EFFECT, /* insistent= */ true);
+
+ NotificationRecord record = new NotificationRecord(mMockContext, sbn, channel);
+ VibrationEffect repeatingEffect =
+ CUSTOM_CHANNEL_VIBRATION_EFFECT
+ .applyRepeatingIndefinitely(true, /* loopDelayMs= */ 0);
+ assertEquals(repeatingEffect, record.getVibration());
+ }
+
+ @Test
+ public void testVibration_customRepeatingEffect_nonInsistent_createsNonRepeatingCustomEffect() {
+ mSetFlagsRule.enableFlags(Flags.FLAG_NOTIFICATION_CHANNEL_VIBRATION_EFFECT_API);
+ VibrationEffect repeatingEffect =
+ CUSTOM_CHANNEL_VIBRATION_EFFECT
+ .applyRepeatingIndefinitely(true, /* loopDelayMs= */ 0);
+ StatusBarNotification sbn = getNotification(repeatingEffect, /* insistent= */ false);
+
+ NotificationRecord record = new NotificationRecord(mMockContext, sbn, channel);
+ assertEquals(CUSTOM_CHANNEL_VIBRATION_EFFECT, record.getVibration());
+ }
+
+ @Test
+ public void testVibration_customRepeatingEffect_insistent_usesCustomEffect() {
+ mSetFlagsRule.enableFlags(Flags.FLAG_NOTIFICATION_CHANNEL_VIBRATION_EFFECT_API);
+ VibrationEffect repeatingEffect =
+ CUSTOM_CHANNEL_VIBRATION_EFFECT
+ .applyRepeatingIndefinitely(true, /* loopDelayMs= */ 0);
+ StatusBarNotification sbn = getNotification(repeatingEffect, /* insistent= */ true);
+
+ NotificationRecord record = new NotificationRecord(mMockContext, sbn, channel);
+ assertEquals(repeatingEffect, record.getVibration());
+ }
+
+ @Test
+ public void testVibration_noCustomVibration_vibrationEnabled_usesDefaultVibration() {
+ channel.enableVibration(true);
+ StatusBarNotification sbn = getNotification(
+ /* channelVibrationPattern= */ null,
+ /* channelVibrationEffect= */ null,
+ /* insistent= */ false);
+
+ NotificationRecord record = new NotificationRecord(mMockContext, sbn, channel);
+ assertNotNull(record.getVibration());
+ }
+
+ @Test
+ public void testVibration_noCustomVibration_vibrationNotEnabled_usesNoVibration() {
+ channel.enableVibration(false);
+ StatusBarNotification sbn = getNotification(
+ /* channelVibrationPattern= */ null,
+ /* channelVibrationEffect= */ null,
+ /* insistent= */ false);
+
+ NotificationRecord record = new NotificationRecord(mMockContext, sbn, channel);
+ assertNull(record.getVibration());
+ }
+
+ @Test
+ public void testVibration_customVibration_vibrationNotEnabled_usesNoVibration() {
+ mSetFlagsRule.enableFlags(Flags.FLAG_NOTIFICATION_CHANNEL_VIBRATION_EFFECT_API);
+ StatusBarNotification sbn = getNotification(
+ CUSTOM_CHANNEL_VIBRATION_PATTERN, /* insistent= */ false);
+ channel.enableVibration(false);
+
+ NotificationRecord record = new NotificationRecord(mMockContext, sbn, channel);
+ assertNull(record.getVibration());
}
@Test
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java
index 5ddac03..8b55778 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java
@@ -391,6 +391,7 @@
assertEquals(expected.getSound(), actual.getSound());
assertEquals(expected.canBypassDnd(), actual.canBypassDnd());
assertTrue(Arrays.equals(expected.getVibrationPattern(), actual.getVibrationPattern()));
+ assertEquals(expected.getVibrationEffect(), actual.getVibrationEffect());
assertEquals(expected.getGroup(), actual.getGroup());
assertEquals(expected.getAudioAttributes(), actual.getAudioAttributes());
assertEquals(expected.getLightColor(), actual.getLightColor());
@@ -410,6 +411,7 @@
assertEquals(parent.getSound(), actual.getSound());
assertEquals(parent.canBypassDnd(), actual.canBypassDnd());
assertTrue(Arrays.equals(parent.getVibrationPattern(), actual.getVibrationPattern()));
+ assertEquals(parent.getVibrationEffect(), actual.getVibrationEffect());
assertEquals(parent.getGroup(), actual.getGroup());
assertEquals(parent.getAudioAttributes(), actual.getAudioAttributes());
assertEquals(parent.getLightColor(), actual.getLightColor());
diff --git a/services/tests/wmtests/src/com/android/server/wm/AssistDataRequesterTest.java b/services/tests/wmtests/src/com/android/server/wm/AssistDataRequesterTest.java
index 56c3ec0..c2a5824 100644
--- a/services/tests/wmtests/src/com/android/server/wm/AssistDataRequesterTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/AssistDataRequesterTest.java
@@ -146,7 +146,7 @@
private void setupMocks(boolean currentActivityAssistAllowed, boolean assistStructureAllowed,
boolean assistScreenshotAllowed) throws Exception {
- doReturn(currentActivityAssistAllowed).when(mAtm).isAssistDataAllowedOnCurrentActivity();
+ doReturn(currentActivityAssistAllowed).when(mAtm).isAssistDataAllowed();
doReturn(assistStructureAllowed ? MODE_ALLOWED : MODE_ERRORED).when(mAppOpsManager)
.noteOpNoThrow(eq(OP_ASSIST_STRUCTURE), anyInt(), anyString(), any(), any());
doReturn(assistScreenshotAllowed ? MODE_ALLOWED : MODE_ERRORED).when(mAppOpsManager)
diff --git a/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java
index c44be7b..c29547f 100644
--- a/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java
@@ -56,7 +56,6 @@
import android.os.RemoteCallback;
import android.os.RemoteException;
import android.platform.test.annotations.Presubmit;
-import android.platform.test.annotations.RequiresFlagsEnabled;
import android.util.ArraySet;
import android.view.WindowManager;
import android.window.BackAnimationAdapter;
@@ -632,8 +631,8 @@
}
@Test
- @RequiresFlagsEnabled(Flags.FLAG_EMBEDDED_ACTIVITY_BACK_NAV_FLAG)
public void testAdjacentFocusInActivityEmbedding() {
+ mSetFlagsRule.enableFlags(Flags.FLAG_EMBEDDED_ACTIVITY_BACK_NAV_FLAG);
Task task = createTask(mDefaultDisplay);
TaskFragment primary = createTaskFragmentWithActivity(task);
TaskFragment secondary = createTaskFragmentWithActivity(task);
@@ -645,7 +644,7 @@
doReturn(primary).when(windowState).getTaskFragment();
startBackNavigation();
- verify(mWm).moveFocusToAdjacentWindow(any(), anyInt());
+ verify(mWm).moveFocusToActivity(any());
}
/**
diff --git a/services/tests/wmtests/src/com/android/server/wm/ClientLifecycleManagerTests.java b/services/tests/wmtests/src/com/android/server/wm/ClientLifecycleManagerTests.java
index 46e14d51..5f18f84 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ClientLifecycleManagerTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ClientLifecycleManagerTests.java
@@ -208,6 +208,21 @@
}
@Test
+ public void testScheduleTransactionAndLifecycleItems_shouldDispatchImmediately()
+ throws RemoteException {
+ mSetFlagsRule.enableFlags(FLAG_BUNDLE_CLIENT_TRANSACTION_FLAG);
+ spyOn(mWms.mWindowPlacerLocked);
+ doReturn(true).when(mWms.mWindowPlacerLocked).isTraversalScheduled();
+
+ // Use non binder client to get non-recycled ClientTransaction.
+ mLifecycleManager.scheduleTransactionAndLifecycleItems(mNonBinderClient, mTransactionItem,
+ mLifecycleItem, true /* shouldDispatchImmediately */);
+
+ verify(mLifecycleManager).scheduleTransaction(any());
+ assertTrue(mLifecycleManager.mPendingTransactions.isEmpty());
+ }
+
+ @Test
public void testDispatchPendingTransactions() throws RemoteException {
mSetFlagsRule.enableFlags(FLAG_BUNDLE_CLIENT_TRANSACTION_FLAG);
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 95850ac..f63ff6e 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java
@@ -1003,7 +1003,13 @@
dc.computeScreenConfiguration(config, ROTATION_0);
dc.onRequestedOverrideConfigurationChanged(config);
assertEquals(Configuration.ORIENTATION_LANDSCAPE, config.orientation);
- assertEquals(Configuration.ORIENTATION_LANDSCAPE, dc.getNaturalOrientation());
+ assertEquals(Configuration.ORIENTATION_LANDSCAPE, dc.getNaturalConfigurationOrientation());
+ window.setOverrideOrientation(SCREEN_ORIENTATION_NOSENSOR);
+ assertEquals(Configuration.ORIENTATION_LANDSCAPE,
+ window.getRequestedConfigurationOrientation());
+ // Note that getNaturalOrientation is based on logical display size. So it is portrait if
+ // the display width equals to height.
+ assertEquals(Configuration.ORIENTATION_PORTRAIT, dc.getNaturalOrientation());
}
@Test
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 0c1a9c3..f42cdb8 100644
--- a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
@@ -845,6 +845,30 @@
}
@Test
+ public void testLetterboxDisplayedForWindowBelow() {
+ setUpDisplaySizeWithApp(1000, 2500);
+ prepareUnresizable(mActivity, SCREEN_ORIENTATION_LANDSCAPE);
+ // Prepare two windows, one base app window below the splash screen
+ final WindowState appWindow = addWindowToActivity(mActivity);
+ final WindowState startWindow = addWindowToActivity(mActivity, TYPE_APPLICATION_STARTING);
+ spyOn(appWindow);
+ // Base app window is letterboxed for display cutout and splash screen is fullscreen
+ doReturn(true).when(appWindow).isLetterboxedForDisplayCutout();
+
+ mActivity.mRootWindowContainer.performSurfacePlacement();
+
+ assertEquals(2, mActivity.mChildren.size());
+ // Splash screen is still the activity's main window
+ assertEquals(startWindow, mActivity.findMainWindow());
+ assertFalse(startWindow.isLetterboxedForDisplayCutout());
+
+ final Rect letterboxInnerBounds = new Rect();
+ mActivity.getLetterboxInnerBounds(letterboxInnerBounds);
+ // Letterboxed is still displayed for app window below splash screen
+ assertFalse(letterboxInnerBounds.isEmpty());
+ }
+
+ @Test
public void testLetterboxFullscreenBoundsAndNotImeAttachable() {
final int displayWidth = 2500;
setUpDisplaySizeWithApp(displayWidth, 1000);
@@ -4773,8 +4797,12 @@
}
private WindowState addWindowToActivity(ActivityRecord activity) {
+ return addWindowToActivity(activity, WindowManager.LayoutParams.TYPE_BASE_APPLICATION);
+ }
+
+ private WindowState addWindowToActivity(ActivityRecord activity, int type) {
final WindowManager.LayoutParams params = new WindowManager.LayoutParams();
- params.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
+ params.type = type;
params.setFitInsetsSides(0);
params.setFitInsetsTypes(0);
final TestWindowState w = new TestWindowState(
diff --git a/services/tests/wmtests/src/com/android/server/wm/SystemServicesTestRule.java b/services/tests/wmtests/src/com/android/server/wm/SystemServicesTestRule.java
index 90493d4..295b124 100644
--- a/services/tests/wmtests/src/com/android/server/wm/SystemServicesTestRule.java
+++ b/services/tests/wmtests/src/com/android/server/wm/SystemServicesTestRule.java
@@ -346,6 +346,7 @@
doReturn(true).when(amInternal).hasStartedUserState(anyInt());
doReturn(false).when(amInternal).shouldConfirmCredentials(anyInt());
doReturn(false).when(amInternal).isActivityStartsLoggingEnabled();
+ doReturn(true).when(amInternal).getThemeOverlayReadiness();
LocalServices.addService(ActivityManagerInternal.class, amInternal);
final ActivityManagerService amService =
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 a88285a..897a3da 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java
@@ -527,29 +527,11 @@
@Test
public void testOnActivityReparentedToTask_untrustedEmbed_notReported() {
- final int pid = Binder.getCallingPid();
- final int uid = Binder.getCallingUid();
- mTaskFragment.setTaskFragmentOrganizer(mOrganizer.getOrganizerToken(), uid,
- DEFAULT_TASK_FRAGMENT_ORGANIZER_PROCESS_NAME);
- mWindowOrganizerController.mLaunchTaskFragments.put(mFragmentToken, mTaskFragment);
- final Task task = createTask(mDisplayContent);
- task.addChild(mTaskFragment, POSITION_TOP);
- final ActivityRecord activity = createActivityRecord(task);
- // Flush EVENT_APPEARED.
- mController.dispatchPendingEvents();
-
- // Make sure the activity is embedded in untrusted mode.
- activity.info.applicationInfo.uid = uid + 1;
- doReturn(pid + 1).when(activity).getPid();
- task.effectiveUid = uid;
- doReturn(EMBEDDING_ALLOWED).when(task).isAllowedToEmbedActivity(activity, uid);
- doReturn(false).when(task).isAllowedToEmbedActivityInTrustedMode(activity, uid);
- doReturn(true).when(task).isAllowedToEmbedActivityInUntrustedMode(activity);
+ final ActivityRecord activity = setupUntrustedEmbeddingPipReparent();
+ doReturn(false).when(activity).isUntrustedEmbeddingStateSharingAllowed();
// Notify organizer if it was embedded before entered Pip.
// Create a temporary token since the activity doesn't belong to the same process.
- clearInvocations(mOrganizer);
- activity.mLastTaskFragmentOrganizerBeforePip = mIOrganizer;
mController.onActivityReparentedToTask(activity);
mController.dispatchPendingEvents();
@@ -558,6 +540,30 @@
}
@Test
+ public void testOnActivityReparentedToTask_untrustedEmbed_reportedWhenAppOptIn() {
+ final ActivityRecord activity = setupUntrustedEmbeddingPipReparent();
+ doReturn(true).when(activity).isUntrustedEmbeddingStateSharingAllowed();
+
+ // Notify organizer if it was embedded before entered Pip.
+ // Create a temporary token since the activity doesn't belong to the same process.
+ mController.onActivityReparentedToTask(activity);
+ mController.dispatchPendingEvents();
+
+ // Allow organizer to reparent activity in other process using the temporary token.
+ verify(mOrganizer).onTransactionReady(mTransactionCaptor.capture());
+ final TaskFragmentTransaction transaction = mTransactionCaptor.getValue();
+ final List<TaskFragmentTransaction.Change> changes = transaction.getChanges();
+ assertFalse(changes.isEmpty());
+ final TaskFragmentTransaction.Change change = changes.get(0);
+ assertEquals(TYPE_ACTIVITY_REPARENTED_TO_TASK, change.getType());
+ assertEquals(activity.getTask().mTaskId, change.getTaskId());
+ assertIntentsEqualForOrganizer(activity.intent, change.getActivityIntent());
+ assertNotEquals(activity.token, change.getActivityToken());
+ mTransaction.reparentActivityToTaskFragment(mFragmentToken, change.getActivityToken());
+ assertApplyTransactionAllowed(mTransaction);
+ }
+
+ @Test
public void testOnActivityReparentedToTask_trimReportedIntent() {
// Make sure the activity pid/uid is the same as the organizer caller.
final int pid = Binder.getCallingPid();
@@ -1868,6 +1874,34 @@
OP_TYPE_REORDER_TO_TOP_OF_TASK);
}
+ @NonNull
+ private ActivityRecord setupUntrustedEmbeddingPipReparent() {
+ final int pid = Binder.getCallingPid();
+ final int uid = Binder.getCallingUid();
+ mTaskFragment.setTaskFragmentOrganizer(mOrganizer.getOrganizerToken(), uid,
+ DEFAULT_TASK_FRAGMENT_ORGANIZER_PROCESS_NAME);
+ mWindowOrganizerController.mLaunchTaskFragments.put(mFragmentToken, mTaskFragment);
+ final Task task = createTask(mDisplayContent);
+ task.addChild(mTaskFragment, POSITION_TOP);
+ final ActivityRecord activity = createActivityRecord(task);
+
+ // Flush EVENT_APPEARED.
+ mController.dispatchPendingEvents();
+
+ // Make sure the activity is embedded in untrusted mode.
+ activity.info.applicationInfo.uid = uid + 1;
+ doReturn(pid + 1).when(activity).getPid();
+ task.effectiveUid = uid;
+ doReturn(EMBEDDING_ALLOWED).when(task).isAllowedToEmbedActivity(activity, uid);
+ doReturn(false).when(task).isAllowedToEmbedActivityInTrustedMode(activity, uid);
+ doReturn(true).when(task).isAllowedToEmbedActivityInUntrustedMode(activity);
+
+ clearInvocations(mOrganizer);
+ activity.mLastTaskFragmentOrganizerBeforePip = mIOrganizer;
+
+ return activity;
+ }
+
private void testApplyTransaction_reorder_failsIfNotSystemOrganizer_common(
@TaskFragmentOperation.OperationType int opType) {
final Task task = createTask(mDisplayContent);
diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java
index 98ca094..01bd96b 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java
@@ -867,24 +867,36 @@
assertEquals(winLeftTop, mDisplayContent.mCurrentFocus);
// Send request from a non-focused window with valid direction.
- assertFalse(mWm.moveFocusToAdjacentWindow(null, winLeftBottom.mClient, View.FOCUS_RIGHT));
+ assertFalse(mWm.moveFocusToAdjacentWindow(winLeftBottom, View.FOCUS_RIGHT));
// The focus should remain the same.
assertEquals(winLeftTop, mDisplayContent.mCurrentFocus);
// Send request from the focused window with valid direction.
- assertTrue(mWm.moveFocusToAdjacentWindow(null, winLeftTop.mClient, View.FOCUS_RIGHT));
+ assertTrue(mWm.moveFocusToAdjacentWindow(winLeftTop, View.FOCUS_RIGHT));
// The focus should change.
assertEquals(winRightTop, mDisplayContent.mCurrentFocus);
// Send request from the focused window with invalid direction.
- assertFalse(mWm.moveFocusToAdjacentWindow(null, winRightTop.mClient, View.FOCUS_UP));
+ assertFalse(mWm.moveFocusToAdjacentWindow(winRightTop, View.FOCUS_UP));
// The focus should remain the same.
assertEquals(winRightTop, mDisplayContent.mCurrentFocus);
// Send request from the focused window with valid direction.
- assertTrue(mWm.moveFocusToAdjacentWindow(null, winRightTop.mClient, View.FOCUS_BACKWARD));
+ assertTrue(mWm.moveFocusToAdjacentWindow(winRightTop, View.FOCUS_BACKWARD));
// The focus should change.
assertEquals(winLeftTop, mDisplayContent.mCurrentFocus);
+
+ if (Flags.embeddedActivityBackNavFlag()) {
+ // Send request to move the focus to top window from the left window.
+ assertTrue(mWm.moveFocusToTopEmbeddedWindow(winLeftTop));
+ // The focus should change.
+ assertEquals(winRightTop, mDisplayContent.mCurrentFocus);
+
+ // Send request to move the focus to top window from the right window.
+ assertFalse(mWm.moveFocusToTopEmbeddedWindow(winRightTop));
+ // The focus should NOT change.
+ assertEquals(winRightTop, mDisplayContent.mCurrentFocus);
+ }
}
private WindowState createAppWindow(ActivityRecord app, String name) {
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 74aabe1..aa9c0c8 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java
@@ -94,7 +94,6 @@
import com.android.server.wm.TaskOrganizerController.PendingTaskEvent;
import com.android.window.flags.Flags;
-import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
@@ -153,13 +152,6 @@
return createTask(mDisplayContent);
}
- @Before
- public void setUp() {
- // We defer callbacks since we need to adjust task surface visibility, but for these tests,
- // just run the callbacks synchronously
- mWm.mAtmService.mTaskOrganizerController.setDeferTaskOrgCallbacksConsumer((r) -> r.run());
- }
-
@Test
public void testAppearVanish() throws RemoteException {
final ITaskOrganizer organizer = registerMockOrganizer();
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 a0562aa..f02dd3f 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java
@@ -236,6 +236,26 @@
assertTrue(window.isOnScreen());
window.hide(false /* doAnimation */, false /* requestAnim */);
assertFalse(window.isOnScreen());
+
+ // Verifies that a window without animation can be hidden even if its parent is animating.
+ window.show(false /* doAnimation */, false /* requestAnim */);
+ assertTrue(window.isVisibleByPolicy());
+ window.getParent().startAnimation(mTransaction, mock(AnimationAdapter.class),
+ false /* hidden */, SurfaceAnimator.ANIMATION_TYPE_TOKEN_TRANSFORM);
+ window.mAttrs.windowAnimations = 0;
+ window.hide(true /* doAnimation */, true /* requestAnim */);
+ assertFalse(window.isSelfAnimating(0, SurfaceAnimator.ANIMATION_TYPE_WINDOW_ANIMATION));
+ assertFalse(window.isVisibleByPolicy());
+ assertFalse(window.isOnScreen());
+
+ // Verifies that a window with animation can be hidden after the hide animation is finished.
+ window.show(false /* doAnimation */, false /* requestAnim */);
+ window.mAttrs.windowAnimations = android.R.style.Animation_Dialog;
+ window.hide(true /* doAnimation */, true /* requestAnim */);
+ assertTrue(window.isSelfAnimating(0, SurfaceAnimator.ANIMATION_TYPE_WINDOW_ANIMATION));
+ assertTrue(window.isVisibleByPolicy());
+ window.cancelAnimation();
+ assertFalse(window.isVisibleByPolicy());
}
@Test
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 0ee78e3..28e03bf 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java
@@ -1791,7 +1791,6 @@
TestStartingWindowOrganizer(ActivityTaskManagerService service) {
mAtm = service;
mWMService = mAtm.mWindowManager;
- mAtm.mTaskOrganizerController.setDeferTaskOrgCallbacksConsumer(Runnable::run);
mAtm.mTaskOrganizerController.registerTaskOrganizer(this);
}
diff --git a/services/usage/Android.bp b/services/usage/Android.bp
index 867773d..2c1095a 100644
--- a/services/usage/Android.bp
+++ b/services/usage/Android.bp
@@ -18,5 +18,8 @@
name: "services.usage",
defaults: ["platform_service_defaults"],
srcs: [":services.usage-sources"],
- libs: ["services.core"],
+ libs: [
+ "services.core",
+ "service-art.stubs.system_server",
+ ],
}
diff --git a/services/usage/java/com/android/server/usage/StorageStatsService.java b/services/usage/java/com/android/server/usage/StorageStatsService.java
index 2445f51..44f4068 100644
--- a/services/usage/java/com/android/server/usage/StorageStatsService.java
+++ b/services/usage/java/com/android/server/usage/StorageStatsService.java
@@ -19,7 +19,9 @@
import static android.content.pm.PackageManager.PERMISSION_GRANTED;
import static com.android.internal.util.ArrayUtils.defeatNullable;
+import static com.android.server.pm.DexOptHelper.getArtManagerLocal;
import static com.android.server.pm.PackageManagerService.PLATFORM_PACKAGE_NAME;
+import static com.android.server.pm.PackageManagerServiceUtils.getPackageManagerLocal;
import static com.android.server.usage.StorageStatsManagerLocal.StorageStatsAugmenter;
import android.Manifest;
@@ -76,6 +78,9 @@
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.ArrayUtils;
import com.android.internal.util.Preconditions;
+import com.android.server.art.ArtManagerLocal;
+import com.android.server.art.model.ArtManagedFileStats;
+import com.android.server.pm.PackageManagerLocal.FilteredSnapshot;
import com.android.server.IoThread;
import com.android.server.LocalManagerRegistry;
import com.android.server.LocalServices;
@@ -449,7 +454,7 @@
}
if (Flags.getAppBytesByDataTypeApi()) {
computeAppStatsByDataTypes(
- stats, appInfo.sourceDir);
+ stats, appInfo.sourceDir, packageNames[i]);
}
}
} catch (NameNotFoundException e) {
@@ -592,6 +597,9 @@
res.codeBytes = stats.codeSize + stats.externalCodeSize;
res.dataBytes = stats.dataSize + stats.externalDataSize;
res.cacheBytes = stats.cacheSize + stats.externalCacheSize;
+ res.dexoptBytes = stats.dexoptSize;
+ res.curProfBytes = stats.curProfSize;
+ res.refProfBytes = stats.refProfSize;
res.apkBytes = stats.apkSize;
res.libBytes = stats.libSize;
res.dmBytes = stats.dmSize;
@@ -946,7 +954,7 @@
}
private void computeAppStatsByDataTypes(
- PackageStats stats, String sourceDirName) {
+ PackageStats stats, String sourceDirName, String packageName) {
// Get apk, lib, dm file sizes.
File srcDir = new File(sourceDirName);
@@ -958,5 +966,24 @@
stats.apkSize += getFileBytesInDir(srcDir, ".apk");
stats.dmSize += getFileBytesInDir(srcDir, ".dm");
stats.libSize += getDirBytes(new File(sourceDirName + "/lib/"));
+
+ // Get dexopt, current profle and reference profile sizes.
+ if (SystemProperties.getBoolean("dalvik.vm.features.art_managed_file_stats", false)) {
+ ArtManagedFileStats artManagedFileStats;
+ try (var snapshot = getPackageManagerLocal().withFilteredSnapshot()) {
+ artManagedFileStats =
+ getArtManagerLocal().getArtManagedFileStats(snapshot, packageName);
+ }
+
+ stats.dexoptSize +=
+ artManagedFileStats
+ .getTotalSizeBytesByType(ArtManagedFileStats.TYPE_DEXOPT_ARTIFACT);
+ stats.refProfSize +=
+ artManagedFileStats
+ .getTotalSizeBytesByType(ArtManagedFileStats.TYPE_REF_PROFILE);
+ stats.curProfSize +=
+ artManagedFileStats
+ .getTotalSizeBytesByType(ArtManagedFileStats.TYPE_CUR_PROFILE);
+ }
}
}
diff --git a/telecomm/OWNERS b/telecomm/OWNERS
index b57b7c7..bb2ac51 100644
--- a/telecomm/OWNERS
+++ b/telecomm/OWNERS
@@ -6,4 +6,5 @@
rgreenwalt@google.com
grantmenke@google.com
pmadapurmath@google.com
-tjstuart@google.com
\ No newline at end of file
+tjstuart@google.com
+huiwang@google.com
diff --git a/telecomm/java/android/telecom/Connection.java b/telecomm/java/android/telecom/Connection.java
index a2105b0..1df6cf7 100644
--- a/telecomm/java/android/telecom/Connection.java
+++ b/telecomm/java/android/telecom/Connection.java
@@ -21,6 +21,7 @@
import android.Manifest;
import android.annotation.CallbackExecutor;
import android.annotation.ElapsedRealtimeLong;
+import android.annotation.FlaggedApi;
import android.annotation.IntDef;
import android.annotation.IntRange;
import android.annotation.NonNull;
@@ -56,6 +57,7 @@
import com.android.internal.os.SomeArgs;
import com.android.internal.telecom.IVideoCallback;
import com.android.internal.telecom.IVideoProvider;
+import com.android.server.telecom.flags.Flags;
import java.io.FileInputStream;
import java.io.FileOutputStream;
@@ -1015,9 +1017,11 @@
/**
* Connection event used to communicate a {@link android.telephony.CallQuality} report from
* telephony to Telecom for relaying to
- * {@link DiagnosticCall#onCallQualityReceived(CallQuality)}.
+ * {@link CallDiagnostics#onCallQualityReceived(CallQuality)}.
* @hide
*/
+ @SystemApi
+ @FlaggedApi(Flags.FLAG_TELECOM_RESOLVE_HIDDEN_DEPENDENCIES)
public static final String EVENT_CALL_QUALITY_REPORT =
"android.telecom.event.CALL_QUALITY_REPORT";
@@ -1026,6 +1030,8 @@
* {@link android.telephony.CallQuality} data.
* @hide
*/
+ @SystemApi
+ @FlaggedApi(Flags.FLAG_TELECOM_RESOLVE_HIDDEN_DEPENDENCIES)
public static final String EXTRA_CALL_QUALITY_REPORT =
"android.telecom.extra.CALL_QUALITY_REPORT";
@@ -4019,9 +4025,12 @@
}
/**
+ * Retrieves the direction of this connection.
* @return The direction of the call.
* @hide
*/
+ @SystemApi
+ @FlaggedApi(Flags.FLAG_TELECOM_RESOLVE_HIDDEN_DEPENDENCIES)
public final @Call.Details.CallDirection int getCallDirection() {
return mCallDirection;
}
diff --git a/telecomm/java/android/telecom/ConnectionService.java b/telecomm/java/android/telecom/ConnectionService.java
index 536e458..01448c3 100755
--- a/telecomm/java/android/telecom/ConnectionService.java
+++ b/telecomm/java/android/telecom/ConnectionService.java
@@ -17,6 +17,7 @@
package android.telecom;
import android.annotation.CallbackExecutor;
+import android.annotation.FlaggedApi;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.RequiresPermission;
@@ -43,6 +44,7 @@
import com.android.internal.telecom.IConnectionService;
import com.android.internal.telecom.IConnectionServiceAdapter;
import com.android.internal.telecom.RemoteServiceCallback;
+import com.android.server.telecom.flags.Flags;
import java.util.ArrayList;
import java.util.Collection;
@@ -3235,27 +3237,27 @@
}
/**
- * Called after the {@link Connection} returned by
+ * Called by Telecom after the {@link Connection} returned by
* {@link #onCreateIncomingConnection(PhoneAccountHandle, ConnectionRequest)}
* or {@link #onCreateOutgoingConnection(PhoneAccountHandle, ConnectionRequest)} has been
* added to the {@link ConnectionService} and sent to Telecom.
*
- * @param connection the {@link Connection}.
- * @hide
+ * @param connection the {@link Connection} which was added to Telecom.
*/
- public void onCreateConnectionComplete(Connection connection) {
+ @FlaggedApi(Flags.FLAG_TELECOM_RESOLVE_HIDDEN_DEPENDENCIES)
+ public void onCreateConnectionComplete(@NonNull Connection connection) {
}
/**
- * Called after the {@link Conference} returned by
+ * Called by Telecom after the {@link Conference} returned by
* {@link #onCreateIncomingConference(PhoneAccountHandle, ConnectionRequest)}
* or {@link #onCreateOutgoingConference(PhoneAccountHandle, ConnectionRequest)} has been
* added to the {@link ConnectionService} and sent to Telecom.
*
- * @param conference the {@link Conference}.
- * @hide
+ * @param conference the {@link Conference} which was added to Telecom.
*/
- public void onCreateConferenceComplete(Conference conference) {
+ @FlaggedApi(Flags.FLAG_TELECOM_RESOLVE_HIDDEN_DEPENDENCIES)
+ public void onCreateConferenceComplete(@NonNull Conference conference) {
}
diff --git a/telecomm/java/android/telecom/DisconnectCause.java b/telecomm/java/android/telecom/DisconnectCause.java
index 331caa1..7ad26c9 100644
--- a/telecomm/java/android/telecom/DisconnectCause.java
+++ b/telecomm/java/android/telecom/DisconnectCause.java
@@ -16,7 +16,10 @@
package android.telecom;
+import android.annotation.FlaggedApi;
+import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.annotation.SystemApi;
import android.media.ToneGenerator;
import android.os.Parcel;
import android.os.Parcelable;
@@ -25,6 +28,8 @@
import android.telephony.ims.ImsReasonInfo;
import android.text.TextUtils;
+import com.android.server.telecom.flags.Flags;
+
import java.util.Objects;
/**
@@ -169,7 +174,9 @@
}
/**
- * Creates a new DisconnectCause instance.
+ * Creates a new DisconnectCause instance. This is used by Telephony to pass in extra debug
+ * info to Telecom regarding the disconnect cause.
+ *
* @param code The code for the disconnect cause.
* @param label The localized label to show to the user to explain the disconnect.
* @param description The localized description to show to the user to explain the disconnect.
@@ -180,7 +187,10 @@
* @param imsReasonInfo The relevant {@link ImsReasonInfo}, or {@code null} if not available.
* @hide
*/
- public DisconnectCause(int code, CharSequence label, CharSequence description, String reason,
+ @SystemApi
+ @FlaggedApi(Flags.FLAG_TELECOM_RESOLVE_HIDDEN_DEPENDENCIES)
+ public DisconnectCause(int code, @NonNull CharSequence label,
+ @NonNull CharSequence description, @NonNull String reason,
int toneToPlay, @Annotation.DisconnectCauses int telephonyDisconnectCause,
@Annotation.PreciseDisconnectCauses int telephonyPreciseDisconnectCause,
@Nullable ImsReasonInfo imsReasonInfo) {
@@ -241,28 +251,40 @@
}
/**
- * Returns the telephony {@link android.telephony.DisconnectCause} for the call.
+ * Returns the telephony {@link android.telephony.DisconnectCause} for the call. This is only
+ * used internally by Telecom for providing extra debug information from Telephony.
+ *
* @return The disconnect cause.
* @hide
*/
+ @SystemApi
+ @FlaggedApi(Flags.FLAG_TELECOM_RESOLVE_HIDDEN_DEPENDENCIES)
public @Annotation.DisconnectCauses int getTelephonyDisconnectCause() {
return mTelephonyDisconnectCause;
}
/**
- * Returns the telephony {@link android.telephony.PreciseDisconnectCause} for the call.
+ * Returns the telephony {@link android.telephony.PreciseDisconnectCause} for the call. This is
+ * only used internally by Telecom for providing extra debug information from Telephony.
+ *
* @return The precise disconnect cause.
* @hide
*/
+ @SystemApi
+ @FlaggedApi(Flags.FLAG_TELECOM_RESOLVE_HIDDEN_DEPENDENCIES)
public @Annotation.PreciseDisconnectCauses int getTelephonyPreciseDisconnectCause() {
return mTelephonyPreciseDisconnectCause;
}
/**
- * Returns the telephony {@link ImsReasonInfo} associated with the call disconnection.
+ * Returns the telephony {@link ImsReasonInfo} associated with the call disconnection. This is
+ * only used internally by Telecom for providing extra debug information from Telephony.
+ *
* @return The {@link ImsReasonInfo} or {@code null} if not known.
* @hide
*/
+ @SystemApi
+ @FlaggedApi(Flags.FLAG_TELECOM_RESOLVE_HIDDEN_DEPENDENCIES)
public @Nullable ImsReasonInfo getImsReasonInfo() {
return mImsReasonInfo;
}
diff --git a/telecomm/java/android/telecom/PhoneAccount.java b/telecomm/java/android/telecom/PhoneAccount.java
index b7706a9..6bdc43e 100644
--- a/telecomm/java/android/telecom/PhoneAccount.java
+++ b/telecomm/java/android/telecom/PhoneAccount.java
@@ -810,6 +810,9 @@
* If this setter method is never called or cleared using
* {@link #clearSimultaneousCallingRestriction()}, there is no restriction and all
* {@link PhoneAccount}s registered to Telecom by this package support simultaneous calling.
+ * If this setter is called and set as an empty Set, then this {@link PhoneAccount} does
+ * not support simultaneous calling with any other {@link PhoneAccount}s registered by the
+ * same application.
* <p>
* Note: Simultaneous calling restrictions can only be placed on {@link PhoneAccount}s that
* were registered by the same application. Simultaneous calling across applications is
diff --git a/telecomm/java/android/telecom/TelecomManager.java b/telecomm/java/android/telecom/TelecomManager.java
index 2c6e1e4..e62bd90 100644
--- a/telecomm/java/android/telecom/TelecomManager.java
+++ b/telecomm/java/android/telecom/TelecomManager.java
@@ -1359,8 +1359,9 @@
/**
* Returns a list of {@link PhoneAccountHandle}s which can be used to make and receive phone
- * calls. The returned list includes those accounts which have been explicitly enabled by
- * the user or enabled by other users but visible to the user.
+ * calls. The returned list includes those accounts which have been explicitly enabled.
+ * In contrast to {@link #getCallCapablePhoneAccounts}, this also includes accounts from
+ * the calling user's {@link android.os.UserManager#getUserProfiles} profile group.
*
* @see #EXTRA_PHONE_ACCOUNT_HANDLE
* @return A list of {@code PhoneAccountHandle} objects.
@@ -2752,17 +2753,23 @@
*
* @param packageName the package name of the app to check calls for.
* @param userHandle the user handle on which to check for calls.
+ * @param hasCrossUserAccess indicates if calls should be detected across all users.
* @return {@code true} if there are ongoing calls, {@code false} otherwise.
* @hide
*/
- @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE)
+ @SystemApi
+ @FlaggedApi(Flags.FLAG_TELECOM_RESOLVE_HIDDEN_DEPENDENCIES)
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE,
+ Manifest.permission.INTERACT_ACROSS_USERS
+ })
public boolean isInSelfManagedCall(@NonNull String packageName,
- @NonNull UserHandle userHandle) {
+ @NonNull UserHandle userHandle, boolean hasCrossUserAccess) {
ITelecomService service = getTelecomService();
if (service != null) {
try {
return service.isInSelfManagedCall(packageName, userHandle,
- mContext.getOpPackageName());
+ mContext.getOpPackageName(), hasCrossUserAccess);
} catch (RemoteException e) {
Log.e(TAG, "RemoteException isInSelfManagedCall: " + e);
e.rethrowFromSystemServer();
diff --git a/telecomm/java/com/android/internal/telecom/ITelecomService.aidl b/telecomm/java/com/android/internal/telecom/ITelecomService.aidl
index f1bfd22..412e827 100644
--- a/telecomm/java/com/android/internal/telecom/ITelecomService.aidl
+++ b/telecomm/java/com/android/internal/telecom/ITelecomService.aidl
@@ -395,7 +395,7 @@
* @see TelecomServiceImpl#isInSelfManagedCall
*/
boolean isInSelfManagedCall(String packageName, in UserHandle userHandle,
- String callingPackage);
+ String callingPackage, boolean hasCrossUserAccess);
/**
* @see TelecomServiceImpl#addCall
diff --git a/telephony/OWNERS b/telephony/OWNERS
index 287aa65..7607c64 100644
--- a/telephony/OWNERS
+++ b/telephony/OWNERS
@@ -15,4 +15,4 @@
per-file CarrierConfigManager.java=amruthr@google.com,tgunn@google.com,rgreenwalt@google.com,satk@google.com
#Domain Selection is jointly owned, add additional owners for domain selection specific files
-per-file TransportSelectorCallback.java,WwanSelectorCallback.java,DomainSelectionService.java,DomainSelectionService.aidl,DomainSelector.java,EmergencyRegResult.java,EmergencyRegResult.aidl,IDomainSelectionServiceController.aidl,IDomainSelector.aidl,ITransportSelectorCallback.aidl,ITransportSelectorResultCallback.aidl,IWwanSelectorCallback.aidl,IWwanSelectorResultCallback.aidl=hwangoo@google.com,forestchoi@google.com,avinashmp@google.com,mkoon@google.com,seheele@google.com,radhikaagrawal@google.com
+per-file TransportSelectorCallback.java,WwanSelectorCallback.java,DomainSelectionService.java,DomainSelectionService.aidl,DomainSelector.java,EmergencyRegResult.java,EmergencyRegResult.aidl,IDomainSelectionServiceController.aidl,IDomainSelector.aidl,ITransportSelectorCallback.aidl,ITransportSelectorResultCallback.aidl,IWwanSelectorCallback.aidl,IWwanSelectorResultCallback.aidl=hwangoo@google.com,forestchoi@google.com,avinashmp@google.com,mkoon@google.com,seheele@google.com,radhikaagrawal@google.com,jdyou@google.com
diff --git a/telephony/java/android/service/euicc/EuiccService.java b/telephony/java/android/service/euicc/EuiccService.java
index b59e855..5af2c34 100644
--- a/telephony/java/android/service/euicc/EuiccService.java
+++ b/telephony/java/android/service/euicc/EuiccService.java
@@ -19,6 +19,7 @@
import android.Manifest;
import android.annotation.CallSuper;
+import android.annotation.FlaggedApi;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
@@ -38,6 +39,8 @@
import android.text.TextUtils;
import android.util.Log;
+import com.android.internal.telephony.flags.Flags;
+
import java.io.PrintWriter;
import java.io.StringWriter;
import java.lang.annotation.Retention;
@@ -759,6 +762,20 @@
public abstract int onRetainSubscriptionsForFactoryReset(int slotId);
/**
+ * Return the available memory in bytes of the eUICC.
+ *
+ * @param slotId ID of the SIM slot being queried.
+ * @return the available memory in bytes.
+ * @see android.telephony.euicc.EuiccManager#getAvailableMemoryInBytes
+ */
+ @FlaggedApi(Flags.FLAG_ESIM_AVAILABLE_MEMORY)
+ public long onGetAvailableMemoryInBytes(int slotId) {
+ // stub implementation, LPA needs to implement this
+ throw new UnsupportedOperationException("The connected LPA does not implement"
+ + "EuiccService#onGetAvailableMemoryInBytes(int)");
+ }
+
+ /**
* Dump to a provided printWriter.
*/
public void dump(@NonNull PrintWriter printWriter) {
@@ -834,6 +851,22 @@
}
@Override
+ @FlaggedApi(Flags.FLAG_ESIM_AVAILABLE_MEMORY)
+ public void getAvailableMemoryInBytes(
+ int slotId, IGetAvailableMemoryInBytesCallback callback) {
+ mExecutor.execute(
+ () -> {
+ long availableMemoryInBytes =
+ EuiccService.this.onGetAvailableMemoryInBytes(slotId);
+ try {
+ callback.onSuccess(availableMemoryInBytes);
+ } catch (RemoteException e) {
+ // Can't communicate with the phone process; ignore.
+ }
+ });
+ }
+
+ @Override
public void startOtaIfNecessary(
int slotId, IOtaStatusChangedCallback statusChangedCallback) {
mExecutor.execute(new Runnable() {
diff --git a/telephony/java/android/service/euicc/IEuiccService.aidl b/telephony/java/android/service/euicc/IEuiccService.aidl
index f8d5ae9..0f8c72b 100644
--- a/telephony/java/android/service/euicc/IEuiccService.aidl
+++ b/telephony/java/android/service/euicc/IEuiccService.aidl
@@ -19,6 +19,7 @@
import android.service.euicc.IDeleteSubscriptionCallback;
import android.service.euicc.IDownloadSubscriptionCallback;
import android.service.euicc.IEraseSubscriptionsCallback;
+import android.service.euicc.IGetAvailableMemoryInBytesCallback;
import android.service.euicc.IGetDefaultDownloadableSubscriptionListCallback;
import android.service.euicc.IGetDownloadableSubscriptionMetadataCallback;
import android.service.euicc.IGetEidCallback;
@@ -60,4 +61,5 @@
void retainSubscriptionsForFactoryReset(
int slotId, in IRetainSubscriptionsForFactoryResetCallback callback);
void dump(in IEuiccServiceDumpResultCallback callback);
-}
\ No newline at end of file
+ void getAvailableMemoryInBytes(int slotId, in IGetAvailableMemoryInBytesCallback callback);
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/animation/ActivityLaunchAnimatorKosmos.kt b/telephony/java/android/service/euicc/IGetAvailableMemoryInBytesCallback.aidl
similarity index 71%
copy from packages/SystemUI/tests/utils/src/com/android/systemui/animation/ActivityLaunchAnimatorKosmos.kt
copy to telephony/java/android/service/euicc/IGetAvailableMemoryInBytesCallback.aidl
index 128f58b..bd6d19b 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/animation/ActivityLaunchAnimatorKosmos.kt
+++ b/telephony/java/android/service/euicc/IGetAvailableMemoryInBytesCallback.aidl
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2023 The Android Open Source Project
+ * Copyright (C) 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,8 +14,9 @@
* limitations under the License.
*/
-package com.android.systemui.animation
+package android.service.euicc;
-import com.android.systemui.kosmos.Kosmos
-
-val Kosmos.activityLaunchAnimator by Kosmos.Fixture { ActivityLaunchAnimator() }
+/** @hide */
+oneway interface IGetAvailableMemoryInBytesCallback {
+ void onSuccess(long availableMemoryInBytes);
+}
diff --git a/telephony/java/android/telephony/CarrierConfigManager.java b/telephony/java/android/telephony/CarrierConfigManager.java
index e5a94c3..f7793f3 100644
--- a/telephony/java/android/telephony/CarrierConfigManager.java
+++ b/telephony/java/android/telephony/CarrierConfigManager.java
@@ -7517,8 +7517,8 @@
*
* The default value for this key is
* {{@link AccessNetworkConstants.AccessNetworkType#EUTRAN},
- * @hide
*/
+ @FlaggedApi(Flags.FLAG_USE_OEM_DOMAIN_SELECTION_SERVICE)
public static final String
KEY_EMERGENCY_OVER_IMS_SUPPORTED_3GPP_NETWORK_TYPES_INT_ARRAY = KEY_PREFIX
+ "emergency_over_ims_supported_3gpp_network_types_int_array";
@@ -7535,8 +7535,8 @@
*
* The default value for this key is
* {{@link AccessNetworkConstants.AccessNetworkType#EUTRAN},
- * @hide
*/
+ @FlaggedApi(Flags.FLAG_USE_OEM_DOMAIN_SELECTION_SERVICE)
public static final String
KEY_EMERGENCY_OVER_IMS_ROAMING_SUPPORTED_3GPP_NETWORK_TYPES_INT_ARRAY = KEY_PREFIX
+ "emergency_over_ims_roaming_supported_3gpp_network_types_int_array";
@@ -7555,8 +7555,8 @@
* The default value for this key is
* {{@link AccessNetworkConstants.AccessNetworkType#UTRAN},
* {@link AccessNetworkConstants.AccessNetworkType#GERAN}}.
- * @hide
*/
+ @FlaggedApi(Flags.FLAG_USE_OEM_DOMAIN_SELECTION_SERVICE)
public static final String KEY_EMERGENCY_OVER_CS_SUPPORTED_ACCESS_NETWORK_TYPES_INT_ARRAY =
KEY_PREFIX + "emergency_over_cs_supported_access_network_types_int_array";
@@ -7574,8 +7574,8 @@
* The default value for this key is
* {{@link AccessNetworkConstants.AccessNetworkType#UTRAN},
* {@link AccessNetworkConstants.AccessNetworkType#GERAN}}.
- * @hide
*/
+ @FlaggedApi(Flags.FLAG_USE_OEM_DOMAIN_SELECTION_SERVICE)
public static final String
KEY_EMERGENCY_OVER_CS_ROAMING_SUPPORTED_ACCESS_NETWORK_TYPES_INT_ARRAY = KEY_PREFIX
+ "emergency_over_cs_roaming_supported_access_network_types_int_array";
@@ -7590,20 +7590,20 @@
/**
* Circuit switched domain.
- * @hide
*/
+ @FlaggedApi(Flags.FLAG_USE_OEM_DOMAIN_SELECTION_SERVICE)
public static final int DOMAIN_CS = 1;
/**
* Packet switched domain over 3GPP networks.
- * @hide
*/
+ @FlaggedApi(Flags.FLAG_USE_OEM_DOMAIN_SELECTION_SERVICE)
public static final int DOMAIN_PS_3GPP = 2;
/**
* Packet switched domain over non-3GPP networks such as Wi-Fi.
- * @hide
*/
+ @FlaggedApi(Flags.FLAG_USE_OEM_DOMAIN_SELECTION_SERVICE)
public static final int DOMAIN_PS_NON_3GPP = 3;
/**
@@ -7620,8 +7620,8 @@
* {{@link #DOMAIN_PS_3GPP},
* {@link #DOMAIN_CS},
* {@link #DOMAIN_PS_NON_3GPP}}.
- * @hide
*/
+ @FlaggedApi(Flags.FLAG_USE_OEM_DOMAIN_SELECTION_SERVICE)
public static final String KEY_EMERGENCY_DOMAIN_PREFERENCE_INT_ARRAY =
KEY_PREFIX + "emergency_domain_preference_int_array";
@@ -7639,18 +7639,22 @@
* {{@link #DOMAIN_PS_3GPP},
* {@link #DOMAIN_CS},
* {@link #DOMAIN_PS_NON_3GPP}}.
- * @hide
*/
+ @FlaggedApi(Flags.FLAG_USE_OEM_DOMAIN_SELECTION_SERVICE)
public static final String KEY_EMERGENCY_DOMAIN_PREFERENCE_ROAMING_INT_ARRAY =
KEY_PREFIX + "emergency_domain_preference_roaming_int_array";
/**
- * Specifies if emergency call shall be attempted on IMS, if PS is attached even though IMS
- * is not registered and normal calls fallback to the CS networks.
+ * Specifies whether the emergency call shall be preferred over IMS or not
+ * irrespective of IMS registration status.
+ * If the value of the config is {@code true} then emergency calls shall prefer IMS
+ * when device is combined-attached in LTE network and IMS is not registered.
+ * If the value of the config is {@code false} then emergency calls use CS domain
+ * in the same scenario.
*
* The default value for this key is {@code false}.
- * @hide
*/
+ @FlaggedApi(Flags.FLAG_USE_OEM_DOMAIN_SELECTION_SERVICE)
public static final String KEY_PREFER_IMS_EMERGENCY_WHEN_VOICE_CALLS_ON_CS_BOOL =
KEY_PREFIX + "prefer_ims_emergency_when_voice_calls_on_cs_bool";
@@ -7667,32 +7671,39 @@
* If {@link ImsWfc#KEY_EMERGENCY_CALL_OVER_EMERGENCY_PDN_BOOL} is {@code true},
* VoWi-Fi emergency call shall be attempted if Wi-Fi network is connected.
* Otherwise, it shall be attempted if IMS is registered over Wi-Fi.
- * @hide
*/
+ @FlaggedApi(Flags.FLAG_USE_OEM_DOMAIN_SELECTION_SERVICE)
public static final int VOWIFI_REQUIRES_NONE = 0;
/**
* VoWi-Fi emergency call shall be attempted on IMS over Wi-Fi if Wi-Fi network is connected
* and Wi-Fi calling setting is enabled. This value is applicable if the value of
* {@link ImsWfc#KEY_EMERGENCY_CALL_OVER_EMERGENCY_PDN_BOOL} is {@code true}.
- * @hide
*/
+ @FlaggedApi(Flags.FLAG_USE_OEM_DOMAIN_SELECTION_SERVICE)
public static final int VOWIFI_REQUIRES_SETTING_ENABLED = 1;
/**
* VoWi-Fi emergency call shall be attempted on IMS over Wi-Fi if Wi-Fi network is connected
- * and Wi-Fi calling is activated successfully. This value is applicable if the value of
+ * and Wi-Fi calling is activated successfully. The device shall have the valid
+ * Entitlement ID if the user activates VoWi-Fi emergency calling successfully.
+ * This value is applicable if the value of
* {@link ImsWfc#KEY_EMERGENCY_CALL_OVER_EMERGENCY_PDN_BOOL} is {@code true}.
- * @hide
*/
+ @FlaggedApi(Flags.FLAG_USE_OEM_DOMAIN_SELECTION_SERVICE)
public static final int VOWIFI_REQUIRES_VALID_EID = 2;
/**
* Specifies the condition when emergency call shall be attempted on IMS over Wi-Fi.
*
- * The default value for this key is {@code #VOWIFI_REQUIRES_NONE}.
- * @hide
+ * <p>Possible values are,
+ * {@link #VOWIFI_REQUIRES_NONE}
+ * {@link #VOWIFI_REQUIRES_SETTING_ENABLED}
+ * {@link #VOWIFI_REQUIRES_VALID_EID}
+ *
+ * The default value for this key is {@link #VOWIFI_REQUIRES_NONE}.
*/
+ @FlaggedApi(Flags.FLAG_USE_OEM_DOMAIN_SELECTION_SERVICE)
public static final String KEY_EMERGENCY_VOWIFI_REQUIRES_CONDITION_INT =
KEY_PREFIX + "emergency_vowifi_requires_condition_int";
@@ -7703,8 +7714,8 @@
* {@link #KEY_EMERGENCY_DOMAIN_PREFERENCE_ROAMING_INT_ARRAY}.
*
* The default value for this key is 1.
- * @hide
*/
+ @FlaggedApi(Flags.FLAG_USE_OEM_DOMAIN_SELECTION_SERVICE)
public static final String KEY_MAXIMUM_NUMBER_OF_EMERGENCY_TRIES_OVER_VOWIFI_INT =
KEY_PREFIX + "maximum_number_of_emergency_tries_over_vowifi_int";
@@ -7712,14 +7723,14 @@
* Emergency scan timer to wait for scan results from radio before attempting the call
* over Wi-Fi. On timer expiry, if emergency call on Wi-Fi is allowed and possible,
* telephony shall cancel the scan and place the call on Wi-Fi. If emergency call on Wi-Fi
- * is not possible, then domain seleciton continues to wait for the scan result from the
+ * is not possible, then domain selection continues to wait for the scan result from the
* radio. If an emergency scan result is received before the timer expires, the timer shall
* be stopped and no dialing over Wi-Fi will be tried. If this value is set to 0, then
* the timer is never started and domain selection waits for the scan result from the radio.
*
* The default value for the timer is 10 seconds.
- * @hide
*/
+ @FlaggedApi(Flags.FLAG_USE_OEM_DOMAIN_SELECTION_SERVICE)
public static final String KEY_EMERGENCY_SCAN_TIMER_SEC_INT =
KEY_PREFIX + "emergency_scan_timer_sec_int";
@@ -7737,8 +7748,8 @@
* started.
*
* The default value for the timer is {@link #REDIAL_TIMER_DISABLED}.
- * @hide
*/
+ @FlaggedApi(Flags.FLAG_USE_OEM_DOMAIN_SELECTION_SERVICE)
public static final String KEY_MAXIMUM_CELLULAR_SEARCH_TIMER_SEC_INT =
KEY_PREFIX + "maximum_cellular_search_timer_sec_int";
@@ -7753,21 +7764,21 @@
/**
* No specific preference given to the modem. Modem can return an emergency
* capable network either with limited service or full service.
- * @hide
*/
+ @FlaggedApi(Flags.FLAG_USE_OEM_DOMAIN_SELECTION_SERVICE)
public static final int SCAN_TYPE_NO_PREFERENCE = 0;
/**
* Modem will attempt to camp on a network with full service only.
- * @hide
*/
+ @FlaggedApi(Flags.FLAG_USE_OEM_DOMAIN_SELECTION_SERVICE)
public static final int SCAN_TYPE_FULL_SERVICE = 1;
/**
* Telephony shall attempt full service scan first.
* If a full service network is not found, telephony shall attempt a limited service scan.
- * @hide
*/
+ @FlaggedApi(Flags.FLAG_USE_OEM_DOMAIN_SELECTION_SERVICE)
public static final int SCAN_TYPE_FULL_SERVICE_FOLLOWED_BY_LIMITED_SERVICE = 2;
/**
@@ -7779,8 +7790,8 @@
* {@link #SCAN_TYPE_FULL_SERVICE_FOLLOWED_BY_LIMITED_SERVICE}
*
* The default value for this key is {@link #SCAN_TYPE_NO_PREFERENCE}.
- * @hide
*/
+ @FlaggedApi(Flags.FLAG_USE_OEM_DOMAIN_SELECTION_SERVICE)
public static final String KEY_EMERGENCY_NETWORK_SCAN_TYPE_INT =
KEY_PREFIX + "emergency_network_scan_type_int";
@@ -7791,8 +7802,8 @@
* If this value is set to 0, the timer shall be disabled.
*
* The default value for this key is 0.
- * @hide
*/
+ @FlaggedApi(Flags.FLAG_USE_OEM_DOMAIN_SELECTION_SERVICE)
public static final String KEY_EMERGENCY_CALL_SETUP_TIMER_ON_CURRENT_NETWORK_SEC_INT =
KEY_PREFIX + "emergency_call_setup_timer_on_current_network_sec_int";
@@ -7801,8 +7812,8 @@
* This is applicable only for the case PS is in service.
*
* The default value for this key is {@code false}.
- * @hide
*/
+ @FlaggedApi(Flags.FLAG_USE_OEM_DOMAIN_SELECTION_SERVICE)
public static final String KEY_EMERGENCY_REQUIRES_IMS_REGISTRATION_BOOL =
KEY_PREFIX + "emergency_requires_ims_registration_bool";
@@ -7811,8 +7822,8 @@
* over NR. If not, CS will be preferred.
*
* The default value for this key is {@code false}.
- * @hide
*/
+ @FlaggedApi(Flags.FLAG_USE_OEM_DOMAIN_SELECTION_SERVICE)
public static final String KEY_EMERGENCY_LTE_PREFERRED_AFTER_NR_FAILED_BOOL =
KEY_PREFIX + "emergency_lte_preferred_after_nr_failed_bool";
@@ -7820,8 +7831,8 @@
* Specifies the numbers to be dialed over CDMA network in case of dialing over CS network.
*
* The default value for this key is an empty string array.
- * @hide
*/
+ @FlaggedApi(Flags.FLAG_USE_OEM_DOMAIN_SELECTION_SERVICE)
public static final String KEY_EMERGENCY_CDMA_PREFERRED_NUMBERS_STRING_ARRAY =
KEY_PREFIX + "emergency_cdma_preferred_numbers_string_array";
@@ -7830,8 +7841,8 @@
* only when VoLTE is enabled.
*
* The default value for this key is {@code false}.
- * @hide
*/
+ @FlaggedApi(Flags.FLAG_USE_OEM_DOMAIN_SELECTION_SERVICE)
public static final String KEY_EMERGENCY_REQUIRES_VOLTE_ENABLED_BOOL =
KEY_PREFIX + "emergency_requires_volte_enabled_bool";
@@ -7842,8 +7853,8 @@
* @see #KEY_CROSS_STACK_REDIAL_TIMER_SEC_INT
* @see #KEY_QUICK_CROSS_STACK_REDIAL_TIMER_SEC_INT
* @see #KEY_MAXIMUM_CELLULAR_SEARCH_TIMER_SEC_INT
- * @hide
*/
+ @FlaggedApi(Flags.FLAG_USE_OEM_DOMAIN_SELECTION_SERVICE)
public static final int REDIAL_TIMER_DISABLED = 0;
/**
@@ -7855,8 +7866,8 @@
* This value should be greater than the value of {@link #KEY_EMERGENCY_SCAN_TIMER_SEC_INT}.
*
* The default value for the timer is 120 seconds.
- * @hide
*/
+ @FlaggedApi(Flags.FLAG_USE_OEM_DOMAIN_SELECTION_SERVICE)
public static final String KEY_CROSS_STACK_REDIAL_TIMER_SEC_INT =
KEY_PREFIX + "cross_stack_redial_timer_sec_int";
@@ -7871,8 +7882,8 @@
* in the roaming networks and non-domestic networks.
*
* The default value for the timer is {@link #REDIAL_TIMER_DISABLED}.
- * @hide
*/
+ @FlaggedApi(Flags.FLAG_USE_OEM_DOMAIN_SELECTION_SERVICE)
public static final String KEY_QUICK_CROSS_STACK_REDIAL_TIMER_SEC_INT =
KEY_PREFIX + "quick_cross_stack_redial_timer_sec_int";
@@ -7881,11 +7892,24 @@
* the device is registered to the network.
*
* The default value is {@code true}.
- * @hide
*/
+ @FlaggedApi(Flags.FLAG_USE_OEM_DOMAIN_SELECTION_SERVICE)
public static final String KEY_START_QUICK_CROSS_STACK_REDIAL_TIMER_WHEN_REGISTERED_BOOL =
KEY_PREFIX + "start_quick_cross_stack_redial_timer_when_registered_bool";
+ /**
+ * Indicates whether limited service only scanning will be requested after VoLTE fails.
+ * This value is applicable if the value of
+ * {@link #KEY_EMERGENCY_NETWORK_SCAN_TYPE_INT} is any of {@link #SCAN_TYPE_NO_PREFERENCE}
+ * or {@link #SCAN_TYPE_FULL_SERVICE_FOLLOWED_BY_LIMITED_SERVICE}.
+ *
+ * The default value is {@code false}.
+ */
+ @FlaggedApi(Flags.FLAG_USE_OEM_DOMAIN_SELECTION_SERVICE)
+ public static final String
+ KEY_SCAN_LIMITED_SERVICE_AFTER_VOLTE_FAILURE_BOOL =
+ KEY_PREFIX + "scan_limited_service_after_volte_failure_bool";
+
private static PersistableBundle getDefaults() {
PersistableBundle defaults = new PersistableBundle();
defaults.putBoolean(KEY_RETRY_EMERGENCY_ON_IMS_PDN_BOOL, false);
@@ -7957,6 +7981,7 @@
defaults.putInt(KEY_QUICK_CROSS_STACK_REDIAL_TIMER_SEC_INT, REDIAL_TIMER_DISABLED);
defaults.putBoolean(KEY_START_QUICK_CROSS_STACK_REDIAL_TIMER_WHEN_REGISTERED_BOOL,
true);
+ defaults.putBoolean(KEY_SCAN_LIMITED_SERVICE_AFTER_VOLTE_FAILURE_BOOL, false);
return defaults;
}
@@ -9692,6 +9717,19 @@
"remove_satellite_plmn_in_manual_network_scan_bool";
/**
+ * Determine whether to override roaming Wi-Fi Calling preference when device is connected to
+ * non-terrestrial network.
+ * {@code true} - roaming preference cannot be changed by user independently.
+ * Roaming preference is overridden to
+ * {@link com.android.ims.ImsConfig.WfcModeFeatureValueConstants#WIFI_PREFERRED}
+ * {@code false} - roaming preference can be changed by user independently and is not
+ * overridden when device is connected to non-terrestrial network.
+ * @hide
+ */
+ public static final String KEY_OVERRIDE_WFC_ROAMING_MODE_WHILE_USING_NTN_BOOL =
+ "override_wfc_roaming_mode_while_using_ntn_bool";
+
+ /**
* An integer key holds the time interval for refreshing or re-querying the satellite
* entitlement status from the entitlement server to ensure it is the latest.
*
@@ -10817,6 +10855,7 @@
sDefaults.putInt(KEY_PARAMETERS_USED_FOR_NTN_LTE_SIGNAL_BAR_INT,
CellSignalStrengthLte.USE_RSRP);
sDefaults.putBoolean(KEY_REMOVE_SATELLITE_PLMN_IN_MANUAL_NETWORK_SCAN_BOOL, true);
+ sDefaults.putBoolean(KEY_OVERRIDE_WFC_ROAMING_MODE_WHILE_USING_NTN_BOOL, true);
sDefaults.putInt(KEY_SATELLITE_ENTITLEMENT_STATUS_REFRESH_DAYS_INT, 30);
sDefaults.putBoolean(KEY_SATELLITE_ENTITLEMENT_SUPPORTED_BOOL, false);
sDefaults.putBoolean(KEY_DISABLE_DUN_APN_WHILE_ROAMING_WITH_PRESET_APN_BOOL, false);
diff --git a/telephony/java/android/telephony/CarrierRestrictionRules.java b/telephony/java/android/telephony/CarrierRestrictionRules.java
index 2b0d626..d5db612 100644
--- a/telephony/java/android/telephony/CarrierRestrictionRules.java
+++ b/telephony/java/android/telephony/CarrierRestrictionRules.java
@@ -174,6 +174,7 @@
private int mMultiSimPolicy;
@CarrierRestrictionStatus
private int mCarrierRestrictionStatus;
+ private boolean mUseCarrierLockInfo;
private CarrierRestrictionRules() {
mAllowedCarriers = new ArrayList<CarrierIdentifier>();
@@ -183,6 +184,7 @@
mCarrierRestrictionDefault = CARRIER_RESTRICTION_DEFAULT_NOT_ALLOWED;
mMultiSimPolicy = MULTISIM_POLICY_NONE;
mCarrierRestrictionStatus = TelephonyManager.CARRIER_RESTRICTION_STATUS_UNKNOWN;
+ mUseCarrierLockInfo = false;
}
private CarrierRestrictionRules(Parcel in) {
@@ -198,6 +200,7 @@
if (Flags.carrierRestrictionRulesEnhancement()) {
in.readTypedList(mAllowedCarrierInfo, CarrierInfo.CREATOR);
in.readTypedList(mExcludedCarrierInfo, CarrierInfo.CREATOR);
+ mUseCarrierLockInfo = in.readBoolean();
}
}
@@ -213,6 +216,14 @@
* Indicates if all carriers are allowed
*/
public boolean isAllCarriersAllowed() {
+ if (Flags.carrierRestrictionStatus() && mCarrierRestrictionStatus
+ == TelephonyManager.CARRIER_RESTRICTION_STATUS_NOT_RESTRICTED) {
+ return true;
+ }
+ if (Flags.carrierRestrictionRulesEnhancement() && mUseCarrierLockInfo) {
+ return (mAllowedCarrierInfo.isEmpty() && mExcludedCarrierInfo.isEmpty()
+ && mCarrierRestrictionDefault == CARRIER_RESTRICTION_DEFAULT_ALLOWED);
+ }
return (mAllowedCarriers.isEmpty() && mExcludedCarriers.isEmpty()
&& mCarrierRestrictionDefault == CARRIER_RESTRICTION_DEFAULT_ALLOWED);
}
@@ -419,6 +430,7 @@
if (Flags.carrierRestrictionRulesEnhancement()) {
out.writeTypedList(mAllowedCarrierInfo);
out.writeTypedList(mExcludedCarrierInfo);
+ out.writeBoolean(mUseCarrierLockInfo);
}
}
@@ -451,7 +463,8 @@
public String toString() {
return "CarrierRestrictionRules(allowed:" + mAllowedCarriers + ", excluded:"
+ mExcludedCarriers + ", default:" + mCarrierRestrictionDefault
- + ", multisim policy:" + mMultiSimPolicy + getCarrierInfoList() + ")";
+ + ", MultiSim policy:" + mMultiSimPolicy + getCarrierInfoList() +
+ " mIsCarrierLockInfoSupported = " + mUseCarrierLockInfo + ")";
}
private String getCarrierInfoList() {
@@ -490,6 +503,7 @@
TelephonyManager.CARRIER_RESTRICTION_STATUS_NOT_RESTRICTED;
mRules.mAllowedCarrierInfo.clear();
mRules.mExcludedCarrierInfo.clear();
+ mRules.mUseCarrierLockInfo = false;
}
return this;
}
@@ -572,5 +586,16 @@
mRules.mExcludedCarrierInfo = new ArrayList<CarrierInfo>(excludedCarrierInfo);
return this;
}
+
+ /**
+ * set whether the HAL radio supports the advanced carrier lock features or not.
+ *
+ * @param carrierLockInfoSupported advanced carrierInfo changes supported or not
+ * @hide
+ */
+ public @NonNull Builder setCarrierLockInfoFeature(boolean carrierLockInfoSupported) {
+ mRules.mUseCarrierLockInfo = carrierLockInfoSupported;
+ return this;
+ }
}
}
diff --git a/telephony/java/android/telephony/DomainSelectionService.java b/telephony/java/android/telephony/DomainSelectionService.java
index abcce5f..3c11da5 100644
--- a/telephony/java/android/telephony/DomainSelectionService.java
+++ b/telephony/java/android/telephony/DomainSelectionService.java
@@ -16,12 +16,14 @@
package android.telephony;
+import android.annotation.FlaggedApi;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
-import android.annotation.SuppressLint;
+import android.annotation.SystemApi;
import android.app.Service;
import android.content.Intent;
+import android.net.Uri;
import android.os.Build;
import android.os.CancellationSignal;
import android.os.IBinder;
@@ -40,6 +42,7 @@
import com.android.internal.telephony.ITransportSelectorResultCallback;
import com.android.internal.telephony.IWwanSelectorCallback;
import com.android.internal.telephony.IWwanSelectorResultCallback;
+import com.android.internal.telephony.flags.Flags;
import com.android.internal.telephony.util.TelephonyUtils;
import com.android.telephony.Rlog;
@@ -55,13 +58,39 @@
import java.util.function.Consumer;
/**
- * Main domain selection implementation for various telephony features.
- *
- * The telephony framework will bind to the {@link DomainSelectionService}.
+ * Base domain selection implementation.
+ * <p>
+ * Services that extend {@link DomainSelectionService} must register the service in their
+ * AndroidManifest.xml to be detected by the framework.
+ * <p>
+ * 1) The application must declare that they use the
+ * android.permission.BIND_DOMAIN_SELECTION_SERVICE permission.
+ * <p>
+ * 2) The DomainSelectionService definition in the manifest must follow this format:
+ * <pre>
+ * {@code
+ * ...
+ * <service android:name=".EgDomainSelectionService"
+ * android:permission="android.permission.BIND_DOMAIN_SELECTION_SERVICE" >
+ * <intent-filter>
+ * <action android:name="android.telephony.DomainSelectionService" />
+ * </intent-filter>
+ * </service>
+ * ...
+ * }
+ * </pre>
+ * <p>
+ * The ComponentName corresponding to this DomainSelectionService component MUST also be set
+ * as the system domain selection implementation in order to be bound.
+ * The system domain selection implementation is set in the device overlay for
+ * {@code config_domain_selection_service_component_name}
+ * in {@code packages/services/Telephony/res/values/config.xml}.
*
* @hide
*/
-public class DomainSelectionService extends Service {
+@SystemApi
+@FlaggedApi(Flags.FLAG_USE_OEM_DOMAIN_SELECTION_SERVICE)
+public abstract class DomainSelectionService extends Service {
private static final String LOG_TAG = "DomainSelectionService";
@@ -78,16 +107,13 @@
@IntDef(prefix = "SELECTOR_TYPE_",
value = {
SELECTOR_TYPE_CALLING,
- SELECTOR_TYPE_SMS,
- SELECTOR_TYPE_UT})
+ SELECTOR_TYPE_SMS})
public @interface SelectorType {}
/** Indicates the domain selector type for calling. */
public static final int SELECTOR_TYPE_CALLING = 1;
/** Indicates the domain selector type for sms. */
public static final int SELECTOR_TYPE_SMS = 2;
- /** Indicates the domain selector type for supplementary services. */
- public static final int SELECTOR_TYPE_UT = 3;
/** Indicates that the modem can scan for emergency service as per modem’s implementation. */
public static final int SCAN_TYPE_NO_PREFERENCE = 0;
@@ -110,54 +136,55 @@
/**
* Contains attributes required to determine the domain for a telephony service.
*/
+ @FlaggedApi(Flags.FLAG_USE_OEM_DOMAIN_SELECTION_SERVICE)
public static final class SelectionAttributes implements Parcelable {
private static final String TAG = "SelectionAttributes";
- private int mSlotId;
+ private int mSlotIndex;
private int mSubId;
private @Nullable String mCallId;
- private @Nullable String mNumber;
+ private @Nullable Uri mAddress;
private @SelectorType int mSelectorType;
private boolean mIsVideoCall;
private boolean mIsEmergency;
+ private boolean mIsTestEmergencyNumber;
private boolean mIsExitedFromAirplaneMode;
- //private @Nullable UtAttributes mUtAttributes;
private @Nullable ImsReasonInfo mImsReasonInfo;
private @PreciseDisconnectCauses int mCause;
- private @Nullable EmergencyRegResult mEmergencyRegResult;
+ private @Nullable EmergencyRegistrationResult mEmergencyRegistrationResult;
/**
- * @param slotId The slot identifier.
- * @param subId The subscription identifier.
+ * @param slotIndex The logical slot index.
+ * @param subscriptionId The subscription identifier.
* @param callId The call identifier.
- * @param number The dialed number.
+ * @param address The dialed address.
* @param selectorType Indicates the requested domain selector type.
* @param video Indicates it's a video call.
* @param emergency Indicates it's emergency service.
+ * @param isTest Indicates it's a test emergency number.
* @param exited {@code true} if the request caused the device to move out of airplane mode.
* @param imsReasonInfo The reason why the last PS attempt failed.
* @param cause The reason why the last CS attempt failed.
* @param regResult The current registration result for emergency services.
*/
- private SelectionAttributes(int slotId, int subId, @Nullable String callId,
- @Nullable String number, @SelectorType int selectorType,
- boolean video, boolean emergency, boolean exited,
- /*UtAttributes attr,*/
+ private SelectionAttributes(int slotIndex, int subscriptionId, @Nullable String callId,
+ @Nullable Uri address, @SelectorType int selectorType,
+ boolean video, boolean emergency, boolean isTest, boolean exited,
@Nullable ImsReasonInfo imsReasonInfo, @PreciseDisconnectCauses int cause,
- @Nullable EmergencyRegResult regResult) {
- mSlotId = slotId;
- mSubId = subId;
+ @Nullable EmergencyRegistrationResult regResult) {
+ mSlotIndex = slotIndex;
+ mSubId = subscriptionId;
mCallId = callId;
- mNumber = number;
+ mAddress = address;
mSelectorType = selectorType;
mIsVideoCall = video;
mIsEmergency = emergency;
+ mIsTestEmergencyNumber = isTest;
mIsExitedFromAirplaneMode = exited;
- //mUtAttributes = attr;
mImsReasonInfo = imsReasonInfo;
mCause = cause;
- mEmergencyRegResult = regResult;
+ mEmergencyRegistrationResult = regResult;
}
/**
@@ -167,17 +194,17 @@
* @hide
*/
public SelectionAttributes(@NonNull SelectionAttributes s) {
- mSlotId = s.mSlotId;
+ mSlotIndex = s.mSlotIndex;
mSubId = s.mSubId;
mCallId = s.mCallId;
- mNumber = s.mNumber;
+ mAddress = s.mAddress;
mSelectorType = s.mSelectorType;
mIsEmergency = s.mIsEmergency;
+ mIsTestEmergencyNumber = s.mIsTestEmergencyNumber;
mIsExitedFromAirplaneMode = s.mIsExitedFromAirplaneMode;
- //mUtAttributes = s.mUtAttributes;
mImsReasonInfo = s.mImsReasonInfo;
mCause = s.mCause;
- mEmergencyRegResult = s.mEmergencyRegResult;
+ mEmergencyRegistrationResult = s.mEmergencyRegistrationResult;
}
/**
@@ -188,16 +215,16 @@
}
/**
- * @return The slot identifier.
+ * @return The logical slot index.
*/
- public int getSlotId() {
- return mSlotId;
+ public int getSlotIndex() {
+ return mSlotIndex;
}
/**
* @return The subscription identifier.
*/
- public int getSubId() {
+ public int getSubscriptionId() {
return mSubId;
}
@@ -209,10 +236,10 @@
}
/**
- * @return The dialed number.
+ * @return The dialed address.
*/
- public @Nullable String getNumber() {
- return mNumber;
+ public @Nullable Uri getAddress() {
+ return mAddress;
}
/**
@@ -237,18 +264,19 @@
}
/**
+ * @return {@code true} if the dialed number is a test emergency number.
+ */
+ public boolean isTestEmergencyNumber() {
+ return mIsTestEmergencyNumber;
+ }
+
+ /**
* @return {@code true} if the request caused the device to move out of airplane mode.
*/
public boolean isExitedFromAirplaneMode() {
return mIsExitedFromAirplaneMode;
}
- /*
- public @Nullable UtAttributes getUtAttributes();
- return mUtAttributes;
- }
- */
-
/**
* @return The PS disconnect cause if trying over PS resulted in a failure and
* reselection is required.
@@ -268,23 +296,24 @@
/**
* @return The current registration state of cellular network.
*/
- public @Nullable EmergencyRegResult getEmergencyRegResult() {
- return mEmergencyRegResult;
+ public @Nullable EmergencyRegistrationResult getEmergencyRegistrationResult() {
+ return mEmergencyRegistrationResult;
}
@Override
public @NonNull String toString() {
- return "{ slotId=" + mSlotId
+ return "{ slotIndex=" + mSlotIndex
+ ", subId=" + mSubId
+ ", callId=" + mCallId
- + ", number=" + (Build.IS_DEBUGGABLE ? mNumber : "***")
+ + ", address=" + (Build.IS_DEBUGGABLE ? mAddress : "***")
+ ", type=" + mSelectorType
+ ", videoCall=" + mIsVideoCall
+ ", emergency=" + mIsEmergency
+ + ", isTest=" + mIsTestEmergencyNumber
+ ", airplaneMode=" + mIsExitedFromAirplaneMode
+ ", reasonInfo=" + mImsReasonInfo
+ ", cause=" + mCause
- + ", regResult=" + mEmergencyRegResult
+ + ", regResult=" + mEmergencyRegistrationResult
+ " }";
}
@@ -293,23 +322,24 @@
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
SelectionAttributes that = (SelectionAttributes) o;
- return mSlotId == that.mSlotId && mSubId == that.mSubId
+ return mSlotIndex == that.mSlotIndex && mSubId == that.mSubId
&& TextUtils.equals(mCallId, that.mCallId)
- && TextUtils.equals(mNumber, that.mNumber)
+ && equalsHandlesNulls(mAddress, that.mAddress)
&& mSelectorType == that.mSelectorType && mIsVideoCall == that.mIsVideoCall
&& mIsEmergency == that.mIsEmergency
+ && mIsTestEmergencyNumber == that.mIsTestEmergencyNumber
&& mIsExitedFromAirplaneMode == that.mIsExitedFromAirplaneMode
- //&& equalsHandlesNulls(mUtAttributes, that.mUtAttributes)
&& equalsHandlesNulls(mImsReasonInfo, that.mImsReasonInfo)
&& mCause == that.mCause
- && equalsHandlesNulls(mEmergencyRegResult, that.mEmergencyRegResult);
+ && equalsHandlesNulls(mEmergencyRegistrationResult,
+ that.mEmergencyRegistrationResult);
}
@Override
public int hashCode() {
- return Objects.hash(mCallId, mNumber, mImsReasonInfo,
- mIsVideoCall, mIsEmergency, mIsExitedFromAirplaneMode, mEmergencyRegResult,
- mSlotId, mSubId, mSelectorType, mCause);
+ return Objects.hash(mCallId, mAddress, mImsReasonInfo,
+ mIsVideoCall, mIsEmergency, mIsTestEmergencyNumber, mIsExitedFromAirplaneMode,
+ mEmergencyRegistrationResult, mSlotIndex, mSubId, mSelectorType, mCause);
}
@Override
@@ -319,35 +349,37 @@
@Override
public void writeToParcel(@NonNull Parcel out, int flags) {
- out.writeInt(mSlotId);
+ out.writeInt(mSlotIndex);
out.writeInt(mSubId);
out.writeString8(mCallId);
- out.writeString8(mNumber);
+ out.writeParcelable(mAddress, 0);
out.writeInt(mSelectorType);
out.writeBoolean(mIsVideoCall);
out.writeBoolean(mIsEmergency);
+ out.writeBoolean(mIsTestEmergencyNumber);
out.writeBoolean(mIsExitedFromAirplaneMode);
- //out.writeParcelable(mUtAttributes, 0);
out.writeParcelable(mImsReasonInfo, 0);
out.writeInt(mCause);
- out.writeParcelable(mEmergencyRegResult, 0);
+ out.writeParcelable(mEmergencyRegistrationResult, 0);
}
private void readFromParcel(@NonNull Parcel in) {
- mSlotId = in.readInt();
+ mSlotIndex = in.readInt();
mSubId = in.readInt();
mCallId = in.readString8();
- mNumber = in.readString8();
+ mAddress = in.readParcelable(Uri.class.getClassLoader(),
+ android.net.Uri.class);
mSelectorType = in.readInt();
mIsVideoCall = in.readBoolean();
mIsEmergency = in.readBoolean();
+ mIsTestEmergencyNumber = in.readBoolean();
mIsExitedFromAirplaneMode = in.readBoolean();
- //mUtAttributes = s.mUtAttributes;
mImsReasonInfo = in.readParcelable(ImsReasonInfo.class.getClassLoader(),
android.telephony.ims.ImsReasonInfo.class);
mCause = in.readInt();
- mEmergencyRegResult = in.readParcelable(EmergencyRegResult.class.getClassLoader(),
- EmergencyRegResult.class);
+ mEmergencyRegistrationResult = in.readParcelable(
+ EmergencyRegistrationResult.class.getClassLoader(),
+ EmergencyRegistrationResult.class);
}
public static final @NonNull Creator<SelectionAttributes> CREATOR =
@@ -370,26 +402,27 @@
/**
* Builder class creating a new instance.
*/
+ @FlaggedApi(Flags.FLAG_USE_OEM_DOMAIN_SELECTION_SERVICE)
public static final class Builder {
- private final int mSlotId;
+ private final int mSlotIndex;
private final int mSubId;
private @Nullable String mCallId;
- private @Nullable String mNumber;
+ private @Nullable Uri mAddress;
private final @SelectorType int mSelectorType;
private boolean mIsVideoCall;
private boolean mIsEmergency;
+ private boolean mIsTestEmergencyNumber;
private boolean mIsExitedFromAirplaneMode;
- //private @Nullable UtAttributes mUtAttributes;
private @Nullable ImsReasonInfo mImsReasonInfo;
private @PreciseDisconnectCauses int mCause;
- private @Nullable EmergencyRegResult mEmergencyRegResult;
+ private @Nullable EmergencyRegistrationResult mEmergencyRegistrationResult;
/**
* Default constructor for Builder.
*/
- public Builder(int slotId, int subId, @SelectorType int selectorType) {
- mSlotId = slotId;
- mSubId = subId;
+ public Builder(int slotIndex, int subscriptionId, @SelectorType int selectorType) {
+ mSlotIndex = slotIndex;
+ mSubId = subscriptionId;
mSelectorType = selectorType;
}
@@ -399,41 +432,52 @@
* @param callId The call identifier.
* @return The same instance of the builder.
*/
- public @NonNull Builder setCallId(@NonNull String callId) {
+ public @NonNull Builder setCallId(@Nullable String callId) {
mCallId = callId;
return this;
}
/**
- * Sets the dialed number.
+ * Sets the dialed address.
*
- * @param number The dialed number.
+ * @param address The dialed address.
* @return The same instance of the builder.
*/
- public @NonNull Builder setNumber(@NonNull String number) {
- mNumber = number;
+ public @NonNull Builder setAddress(@Nullable Uri address) {
+ mAddress = address;
return this;
}
/**
* Sets whether it's a video call or not.
*
- * @param video Indicates it's a video call.
+ * @param isVideo Indicates it's a video call.
* @return The same instance of the builder.
*/
- public @NonNull Builder setVideoCall(boolean video) {
- mIsVideoCall = video;
+ public @NonNull Builder setVideoCall(boolean isVideo) {
+ mIsVideoCall = isVideo;
return this;
}
/**
* Sets whether it's an emergency service or not.
*
- * @param emergency Indicates it's emergency service.
+ * @param isEmergency Indicates it's emergency service.
* @return The same instance of the builder.
*/
- public @NonNull Builder setEmergency(boolean emergency) {
- mIsEmergency = emergency;
+ public @NonNull Builder setEmergency(boolean isEmergency) {
+ mIsEmergency = isEmergency;
+ return this;
+ }
+
+ /**
+ * Sets whether it's a test emergency number or not.
+ *
+ * @param isTest Indicates it's a test emergency number.
+ * @return The same instance of the builder.
+ */
+ public @NonNull Builder setTestEmergencyNumber(boolean isTest) {
+ mIsTestEmergencyNumber = isTest;
return this;
}
@@ -450,26 +494,12 @@
}
/**
- * Sets the Ut service attributes.
- * Only applicable for SELECTOR_TYPE_UT
- *
- * @param attr Ut services attributes.
- * @return The same instance of the builder.
- */
- /*
- public @NonNull Builder setUtAttributes(@NonNull UtAttributes attr);
- mUtAttributes = attr;
- return this;
- }
- */
-
- /**
* Sets an optional reason why the last PS attempt failed.
*
* @param info The reason why the last PS attempt failed.
* @return The same instance of the builder.
*/
- public @NonNull Builder setPsDisconnectCause(@NonNull ImsReasonInfo info) {
+ public @NonNull Builder setPsDisconnectCause(@Nullable ImsReasonInfo info) {
mImsReasonInfo = info;
return this;
}
@@ -491,8 +521,9 @@
* @param regResult The current registration result for emergency services.
* @return The same instance of the builder.
*/
- public @NonNull Builder setEmergencyRegResult(@NonNull EmergencyRegResult regResult) {
- mEmergencyRegResult = regResult;
+ public @NonNull Builder setEmergencyRegistrationResult(
+ @Nullable EmergencyRegistrationResult regResult) {
+ mEmergencyRegistrationResult = regResult;
return this;
}
@@ -501,9 +532,10 @@
* @return The SelectionAttributes object.
*/
public @NonNull SelectionAttributes build() {
- return new SelectionAttributes(mSlotId, mSubId, mCallId, mNumber, mSelectorType,
- mIsVideoCall, mIsEmergency, mIsExitedFromAirplaneMode, /*mUtAttributes,*/
- mImsReasonInfo, mCause, mEmergencyRegResult);
+ return new SelectionAttributes(mSlotIndex, mSubId, mCallId, mAddress,
+ mSelectorType, mIsVideoCall, mIsEmergency, mIsTestEmergencyNumber,
+ mIsExitedFromAirplaneMode, mImsReasonInfo,
+ mCause, mEmergencyRegistrationResult);
}
}
}
@@ -546,19 +578,6 @@
}
@Override
- public @NonNull WwanSelectorCallback onWwanSelected() {
- WwanSelectorCallback callback = null;
- try {
- IWwanSelectorCallback cb = mCallback.onWwanSelected();
- callback = new WwanSelectorCallbackWrapper(cb, mExecutor);
- } catch (Exception e) {
- Rlog.e(TAG, "onWwanSelected e=" + e);
- }
-
- return callback;
- }
-
- @Override
public void onWwanSelected(Consumer<WwanSelectorCallback> consumer) {
try {
mResultCallback = new ITransportSelectorResultCallbackAdapter(consumer, mExecutor);
@@ -627,15 +646,6 @@
}
@Override
- public void cancelSelection() {
- final DomainSelector domainSelector = mDomainSelectorWeakRef.get();
- if (domainSelector == null) return;
-
- executeMethodAsyncNoException(mExecutor,
- () -> domainSelector.cancelSelection(), TAG, "cancelSelection");
- }
-
- @Override
public void reselectDomain(@NonNull SelectionAttributes attr) {
final DomainSelector domainSelector = mDomainSelectorWeakRef.get();
if (domainSelector == null) return;
@@ -688,14 +698,15 @@
@Override
public void onRequestEmergencyNetworkScan(@NonNull List<Integer> preferredNetworks,
- @EmergencyScanType int scanType, @NonNull CancellationSignal signal,
- @NonNull Consumer<EmergencyRegResult> consumer) {
+ @EmergencyScanType int scanType, boolean resetScan,
+ @NonNull CancellationSignal signal,
+ @NonNull Consumer<EmergencyRegistrationResult> consumer) {
try {
if (signal != null) signal.setOnCancelListener(this);
mResultCallback = new IWwanSelectorResultCallbackAdapter(consumer, mExecutor);
mCallback.onRequestEmergencyNetworkScan(
preferredNetworks.stream().mapToInt(Integer::intValue).toArray(),
- scanType, mResultCallback);
+ scanType, resetScan, mResultCallback);
} catch (Exception e) {
Rlog.e(TAG, "onRequestEmergencyNetworkScan e=" + e);
}
@@ -713,17 +724,18 @@
private class IWwanSelectorResultCallbackAdapter
extends IWwanSelectorResultCallback.Stub {
- private final @NonNull Consumer<EmergencyRegResult> mConsumer;
+ private final @NonNull Consumer<EmergencyRegistrationResult> mConsumer;
private final @NonNull Executor mExecutor;
- IWwanSelectorResultCallbackAdapter(@NonNull Consumer<EmergencyRegResult> consumer,
+ IWwanSelectorResultCallbackAdapter(
+ @NonNull Consumer<EmergencyRegistrationResult> consumer,
@NonNull Executor executor) {
mConsumer = consumer;
mExecutor = executor;
}
@Override
- public void onComplete(@NonNull EmergencyRegResult result) {
+ public void onComplete(@NonNull EmergencyRegistrationResult result) {
if (mConsumer == null) return;
executeMethodAsyncNoException(mExecutor,
@@ -738,33 +750,41 @@
private @NonNull Executor mExecutor;
/**
- * Selects a domain for the given operation.
+ * Selects a calling domain given the SelectionAttributes of the call request.
+ * <p>
+ * When the framework generates a request to place a call, {@link #onDomainSelection}
+ * will be called in order to determine the domain (CS or PS). For PS calls, the transport
+ * (WWAN or WLAN) will also need to be determined.
+ * <p>
+ * Once the domain/transport has been selected or an error has occurred,
+ * {@link TransportSelectorCallback} must be used to communicate the result back
+ * to the framework.
*
* @param attr Required to determine the domain.
* @param callback The callback instance being registered.
*/
- public void onDomainSelection(@NonNull SelectionAttributes attr,
- @NonNull TransportSelectorCallback callback) {
- }
+ public abstract void onDomainSelection(@NonNull SelectionAttributes attr,
+ @NonNull TransportSelectorCallback callback);
/**
- * Notifies the change in {@link ServiceState} for a specific slot.
+ * Notifies the change in {@link ServiceState} for a specific logical slot index.
*
- * @param slotId For which the state changed.
- * @param subId For which the state changed.
+ * @param slotIndex For which the state changed.
+ * @param subscriptionId For which the state changed.
* @param serviceState Updated {@link ServiceState}.
*/
- public void onServiceStateUpdated(int slotId, int subId, @NonNull ServiceState serviceState) {
+ public void onServiceStateUpdated(int slotIndex, int subscriptionId,
+ @NonNull ServiceState serviceState) {
}
/**
- * Notifies the change in {@link BarringInfo} for a specific slot.
+ * Notifies the change in {@link BarringInfo} for a specific logical slot index.
*
- * @param slotId For which the state changed.
- * @param subId For which the state changed.
+ * @param slotIndex For which the state changed.
+ * @param subscriptionId For which the state changed.
* @param info Updated {@link BarringInfo}.
*/
- public void onBarringInfoUpdated(int slotId, int subId, @NonNull BarringInfo info) {
+ public void onBarringInfoUpdated(int slotIndex, int subscriptionId, @NonNull BarringInfo info) {
}
private final IBinder mDomainSelectionServiceController =
@@ -779,16 +799,19 @@
}
@Override
- public void updateServiceState(int slotId, int subId, @NonNull ServiceState serviceState) {
+ public void updateServiceState(int slotIndex, int subscriptionId,
+ @NonNull ServiceState serviceState) {
executeMethodAsyncNoException(getCachedExecutor(),
- () -> DomainSelectionService.this.onServiceStateUpdated(slotId,
- subId, serviceState), LOG_TAG, "onServiceStateUpdated");
+ () -> DomainSelectionService.this.onServiceStateUpdated(slotIndex,
+ subscriptionId, serviceState), LOG_TAG, "onServiceStateUpdated");
}
@Override
- public void updateBarringInfo(int slotId, int subId, @NonNull BarringInfo info) {
+ public void updateBarringInfo(int slotIndex, int subscriptionId,
+ @NonNull BarringInfo info) {
executeMethodAsyncNoException(getCachedExecutor(),
- () -> DomainSelectionService.this.onBarringInfoUpdated(slotId, subId, info),
+ () -> DomainSelectionService.this.onBarringInfoUpdated(slotIndex,
+ subscriptionId, info),
LOG_TAG, "onBarringInfoUpdated");
}
};
@@ -816,7 +839,8 @@
/** @hide */
@Override
- public IBinder onBind(Intent intent) {
+ public final @Nullable IBinder onBind(@Nullable Intent intent) {
+ if (intent == null) return null;
if (SERVICE_INTERFACE.equals(intent.getAction())) {
Log.i(LOG_TAG, "DomainSelectionService Bound.");
return mDomainSelectionServiceController;
@@ -825,13 +849,13 @@
}
/**
- * The DomainSelectionService will be able to define an {@link Executor} that the service
- * can use to execute the methods. It has set the default executor as Runnable::run,
+ * The Executor to use when calling callback methods from the framework.
+ * <p>
+ * By default, calls from the framework will use Binder threads to call these methods.
*
- * @return An {@link Executor} to be used.
+ * @return an {@link Executor} used to execute methods called remotely by the framework.
*/
- @SuppressLint("OnNameExpected")
- public @NonNull Executor getExecutor() {
+ public @NonNull Executor onCreateExecutor() {
return Runnable::run;
}
@@ -842,10 +866,10 @@
* @return {@link Executor} instance.
* @hide
*/
- public @NonNull Executor getCachedExecutor() {
+ public final @NonNull Executor getCachedExecutor() {
synchronized (mExecutorLock) {
if (mExecutor == null) {
- Executor e = getExecutor();
+ Executor e = onCreateExecutor();
mExecutor = (e != null) ? e : Runnable::run;
}
return mExecutor;
diff --git a/telephony/java/android/telephony/DomainSelector.java b/telephony/java/android/telephony/DomainSelector.java
index 0871831..5d25d3a 100644
--- a/telephony/java/android/telephony/DomainSelector.java
+++ b/telephony/java/android/telephony/DomainSelector.java
@@ -16,22 +16,23 @@
package android.telephony;
+import android.annotation.FlaggedApi;
import android.annotation.NonNull;
+import android.annotation.SystemApi;
import android.telephony.DomainSelectionService.SelectionAttributes;
+import com.android.internal.telephony.flags.Flags;
+
/**
* Implemented as part of the {@link DomainSelectionService} to implement domain selection
- * for a specific use case.
+ * for a specific use case and receive signals from the framework to reselect a new domain
+ * when a previous domain selection fails or finish a selection when the call connects successfully.
* @hide
*/
+@SystemApi
+@FlaggedApi(Flags.FLAG_USE_OEM_DOMAIN_SELECTION_SERVICE)
public interface DomainSelector {
/**
- * Cancel an ongoing selection operation. It is up to the DomainSelectionService
- * to clean up all ongoing operations with the framework.
- */
- void cancelSelection();
-
- /**
* Reselect a domain due to the call not setting up properly.
*
* @param attr attributes required to select the domain.
diff --git a/telephony/java/android/telephony/EmergencyRegResult.aidl b/telephony/java/android/telephony/EmergencyRegistrationResult.aidl
similarity index 93%
rename from telephony/java/android/telephony/EmergencyRegResult.aidl
rename to telephony/java/android/telephony/EmergencyRegistrationResult.aidl
index f722962..3056031 100644
--- a/telephony/java/android/telephony/EmergencyRegResult.aidl
+++ b/telephony/java/android/telephony/EmergencyRegistrationResult.aidl
@@ -16,4 +16,4 @@
package android.telephony;
-parcelable EmergencyRegResult;
+parcelable EmergencyRegistrationResult;
diff --git a/telephony/java/android/telephony/EmergencyRegResult.java b/telephony/java/android/telephony/EmergencyRegistrationResult.java
similarity index 84%
rename from telephony/java/android/telephony/EmergencyRegResult.java
rename to telephony/java/android/telephony/EmergencyRegistrationResult.java
index 5aed412..7041f5b 100644
--- a/telephony/java/android/telephony/EmergencyRegResult.java
+++ b/telephony/java/android/telephony/EmergencyRegistrationResult.java
@@ -16,18 +16,26 @@
package android.telephony;
+import android.annotation.FlaggedApi;
import android.annotation.NonNull;
+import android.annotation.SystemApi;
import android.os.Parcel;
import android.os.Parcelable;
import android.text.TextUtils;
+import com.android.internal.telephony.flags.Flags;
+
import java.util.Objects;
/**
- * Contains attributes required to determine the domain for a telephony service
+ * Contains attributes required to determine the domain for a telephony service, including
+ * the network registration state.
+ *
* @hide
*/
-public final class EmergencyRegResult implements Parcelable {
+@SystemApi
+@FlaggedApi(Flags.FLAG_USE_OEM_DOMAIN_SELECTION_SERVICE)
+public final class EmergencyRegistrationResult implements Parcelable {
/**
* Indicates the cellular network type of the acquired system.
@@ -77,7 +85,7 @@
* The ISO-3166-1 alpha-2 country code equivalent for the network's country code,
* empty string if unknown.
*/
- private @NonNull String mIso;
+ private @NonNull String mCountryIso;
/**
* Constructor
@@ -93,7 +101,7 @@
* @param iso The ISO-3166-1 alpha-2 country code equivalent, empty string if unknown.
* @hide
*/
- public EmergencyRegResult(
+ public EmergencyRegistrationResult(
@AccessNetworkConstants.RadioAccessNetworkType int accessNetwork,
@NetworkRegistrationInfo.RegistrationState int regState,
@NetworkRegistrationInfo.Domain int domain,
@@ -108,7 +116,7 @@
mNwProvidedEmf = emf;
mMcc = mcc;
mMnc = mnc;
- mIso = iso;
+ mCountryIso = iso;
}
/**
@@ -117,7 +125,7 @@
* @param s Source emergency scan result
* @hide
*/
- public EmergencyRegResult(@NonNull EmergencyRegResult s) {
+ public EmergencyRegistrationResult(@NonNull EmergencyRegistrationResult s) {
mAccessNetworkType = s.mAccessNetworkType;
mRegState = s.mRegState;
mDomain = s.mDomain;
@@ -127,13 +135,13 @@
mNwProvidedEmf = s.mNwProvidedEmf;
mMcc = s.mMcc;
mMnc = s.mMnc;
- mIso = s.mIso;
+ mCountryIso = s.mCountryIso;
}
/**
- * Construct a EmergencyRegResult object from the given parcel.
+ * Construct a EmergencyRegistrationResult object from the given parcel.
*/
- private EmergencyRegResult(@NonNull Parcel in) {
+ private EmergencyRegistrationResult(@NonNull Parcel in) {
readFromParcel(in);
}
@@ -226,8 +234,8 @@
*
* @return Country code.
*/
- public @NonNull String getIso() {
- return mIso;
+ public @NonNull String getCountryIso() {
+ return mCountryIso;
}
@Override
@@ -242,7 +250,7 @@
+ ", emf=" + mNwProvidedEmf
+ ", mcc=" + mMcc
+ ", mnc=" + mMnc
- + ", iso=" + mIso
+ + ", iso=" + mCountryIso
+ " }";
}
@@ -250,7 +258,7 @@
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
- EmergencyRegResult that = (EmergencyRegResult) o;
+ EmergencyRegistrationResult that = (EmergencyRegistrationResult) o;
return mAccessNetworkType == that.mAccessNetworkType
&& mRegState == that.mRegState
&& mDomain == that.mDomain
@@ -260,7 +268,7 @@
&& mNwProvidedEmf == that.mNwProvidedEmf
&& TextUtils.equals(mMcc, that.mMcc)
&& TextUtils.equals(mMnc, that.mMnc)
- && TextUtils.equals(mIso, that.mIso);
+ && TextUtils.equals(mCountryIso, that.mCountryIso);
}
@Override
@@ -268,7 +276,7 @@
return Objects.hash(mAccessNetworkType, mRegState, mDomain,
mIsVopsSupported, mIsEmcBearerSupported,
mNwProvidedEmc, mNwProvidedEmf,
- mMcc, mMnc, mIso);
+ mMcc, mMnc, mCountryIso);
}
@Override
@@ -287,7 +295,7 @@
out.writeInt(mNwProvidedEmf);
out.writeString8(mMcc);
out.writeString8(mMnc);
- out.writeString8(mIso);
+ out.writeString8(mCountryIso);
}
private void readFromParcel(@NonNull Parcel in) {
@@ -300,19 +308,19 @@
mNwProvidedEmf = in.readInt();
mMcc = in.readString8();
mMnc = in.readString8();
- mIso = in.readString8();
+ mCountryIso = in.readString8();
}
- public static final @NonNull Creator<EmergencyRegResult> CREATOR =
- new Creator<EmergencyRegResult>() {
- @Override
- public EmergencyRegResult createFromParcel(@NonNull Parcel in) {
- return new EmergencyRegResult(in);
- }
+ public static final @NonNull Creator<EmergencyRegistrationResult> CREATOR =
+ new Creator<EmergencyRegistrationResult>() {
+ @Override
+ public EmergencyRegistrationResult createFromParcel(@NonNull Parcel in) {
+ return new EmergencyRegistrationResult(in);
+ }
- @Override
- public EmergencyRegResult[] newArray(int size) {
- return new EmergencyRegResult[size];
- }
- };
+ @Override
+ public EmergencyRegistrationResult[] newArray(int size) {
+ return new EmergencyRegistrationResult[size];
+ }
+ };
}
diff --git a/telephony/java/android/telephony/PreciseDisconnectCause.java b/telephony/java/android/telephony/PreciseDisconnectCause.java
index 1cfd22c..d9437ab 100644
--- a/telephony/java/android/telephony/PreciseDisconnectCause.java
+++ b/telephony/java/android/telephony/PreciseDisconnectCause.java
@@ -16,8 +16,11 @@
package android.telephony;
+import android.annotation.FlaggedApi;
import android.annotation.SystemApi;
+import com.android.internal.telephony.flags.Flags;
+
/**
* Contains precise disconnect call causes generated by the framework and the RIL.
* @hide
@@ -238,18 +241,18 @@
/**
* Dialing emergency calls is currently unavailable.
* The call should be redialed on the other subscription silently.
- * If there is no other subscription available, the call may be redialed
+ * If there are no other subscriptions available then the call may be redialed
* on this subscription again.
- * @hide
*/
+ @FlaggedApi(Flags.FLAG_USE_OEM_DOMAIN_SELECTION_SERVICE)
public static final int EMERGENCY_TEMP_FAILURE = 325;
/**
* Dialing emergency calls is currently unavailable.
* The call should be redialed on the other subscription silently.
- * Even if there is no other subscription available, the call should not
+ * If there are no other subscriptions available then the call should not
* be redialed on this subscription again.
- * @hide
*/
+ @FlaggedApi(Flags.FLAG_USE_OEM_DOMAIN_SELECTION_SERVICE)
public static final int EMERGENCY_PERM_FAILURE = 326;
/** Mobile station (MS) is locked until next power cycle. */
diff --git a/telephony/java/android/telephony/SubscriptionInfo.java b/telephony/java/android/telephony/SubscriptionInfo.java
index a188581..82ed340 100644
--- a/telephony/java/android/telephony/SubscriptionInfo.java
+++ b/telephony/java/android/telephony/SubscriptionInfo.java
@@ -40,6 +40,7 @@
import android.telephony.SubscriptionManager.ProfileClass;
import android.telephony.SubscriptionManager.SimDisplayNameSource;
import android.telephony.SubscriptionManager.SubscriptionType;
+import android.telephony.SubscriptionManager.TransferStatus;
import android.telephony.SubscriptionManager.UsageSetting;
import android.text.TextUtils;
import android.util.DisplayMetrics;
@@ -236,6 +237,11 @@
@UsageSetting
private final int mUsageSetting;
+ /**
+ * Subscription's transfer status
+ */
+ private final int mTransferStatus;
+
// Below are the fields that do not exist in the database.
/**
@@ -393,6 +399,7 @@
this.mUsageSetting = usageSetting;
this.mIsOnlyNonTerrestrialNetwork = false;
this.mServiceCapabilities = 0;
+ this.mTransferStatus = 0;
}
/**
@@ -433,6 +440,7 @@
this.mUsageSetting = builder.mUsageSetting;
this.mIsOnlyNonTerrestrialNetwork = builder.mIsOnlyNonTerrestrialNetwork;
this.mServiceCapabilities = builder.mServiceCapabilities;
+ this.mTransferStatus = builder.mTransferStatus;
}
/**
@@ -928,6 +936,19 @@
return SubscriptionManager.getServiceCapabilitiesSet(mServiceCapabilities);
}
+ /**
+ * Get the transfer status for this subscription.
+ *
+ * @return The transfer status for this subscription.
+ *
+ * @hide
+ */
+ @FlaggedApi(Flags.FLAG_SUPPORT_PSIM_TO_ESIM_CONVERSION)
+ @SystemApi
+ public @TransferStatus int getTransferStatus() {
+ return mTransferStatus;
+ }
+
@NonNull
public static final Parcelable.Creator<SubscriptionInfo> CREATOR =
new Parcelable.Creator<SubscriptionInfo>() {
@@ -967,6 +988,7 @@
.setOnlyNonTerrestrialNetwork(source.readBoolean())
.setServiceCapabilities(
SubscriptionManager.getServiceCapabilitiesSet(source.readInt()))
+ .setTransferStatus(source.readInt())
.build();
}
@@ -1010,6 +1032,7 @@
dest.writeInt(mUsageSetting);
dest.writeBoolean(mIsOnlyNonTerrestrialNetwork);
dest.writeInt(mServiceCapabilities);
+ dest.writeInt(mTransferStatus);
}
@Override
@@ -1075,6 +1098,7 @@
+ " isOnlyNonTerrestrialNetwork=" + mIsOnlyNonTerrestrialNetwork
+ " serviceCapabilities=" + SubscriptionManager.getServiceCapabilitiesSet(
mServiceCapabilities).toString()
+ + " transferStatus=" + mTransferStatus
+ "]";
}
@@ -1101,7 +1125,8 @@
that.mCarrierConfigAccessRules) && Objects.equals(mGroupUuid, that.mGroupUuid)
&& mCountryIso.equals(that.mCountryIso) && mGroupOwner.equals(that.mGroupOwner)
&& mIsOnlyNonTerrestrialNetwork == that.mIsOnlyNonTerrestrialNetwork
- && mServiceCapabilities == that.mServiceCapabilities;
+ && mServiceCapabilities == that.mServiceCapabilities
+ && mTransferStatus == that.mTransferStatus;
}
@Override
@@ -1110,7 +1135,8 @@
mDisplayNameSource, mIconTint, mNumber, mDataRoaming, mMcc, mMnc, mIsEmbedded,
mCardString, mIsOpportunistic, mGroupUuid, mCountryIso, mCarrierId, mProfileClass,
mType, mGroupOwner, mAreUiccApplicationsEnabled, mPortIndex, mUsageSetting, mCardId,
- mIsGroupDisabled, mIsOnlyNonTerrestrialNetwork, mServiceCapabilities);
+ mIsGroupDisabled, mIsOnlyNonTerrestrialNetwork, mServiceCapabilities,
+ mTransferStatus);
result = 31 * result + Arrays.hashCode(mEhplmns);
result = 31 * result + Arrays.hashCode(mHplmns);
result = 31 * result + Arrays.hashCode(mNativeAccessRules);
@@ -1314,6 +1340,8 @@
*/
private boolean mIsOnlyNonTerrestrialNetwork = false;
+ private int mTransferStatus = 0;
+
/**
* Service capabilities bitmasks the subscription supports.
*/
@@ -1363,6 +1391,7 @@
mUsageSetting = info.mUsageSetting;
mIsOnlyNonTerrestrialNetwork = info.mIsOnlyNonTerrestrialNetwork;
mServiceCapabilities = info.mServiceCapabilities;
+ mTransferStatus = info.mTransferStatus;
}
/**
@@ -1785,6 +1814,18 @@
mServiceCapabilities = combinedCapabilities;
return this;
}
+ /**
+ * Set subscription's transfer status
+ *
+ * @param status Subscription's transfer status
+ * @return The builder.
+ */
+ @FlaggedApi(Flags.FLAG_SUPPORT_PSIM_TO_ESIM_CONVERSION)
+ @NonNull
+ public Builder setTransferStatus(@TransferStatus int status) {
+ mTransferStatus = status;
+ return this;
+ }
/**
* Build the {@link SubscriptionInfo}.
diff --git a/telephony/java/android/telephony/SubscriptionManager.java b/telephony/java/android/telephony/SubscriptionManager.java
index eb7e67d..3fde3b6 100644
--- a/telephony/java/android/telephony/SubscriptionManager.java
+++ b/telephony/java/android/telephony/SubscriptionManager.java
@@ -1148,6 +1148,14 @@
*/
public static final String SERVICE_CAPABILITIES = SimInfo.COLUMN_SERVICE_CAPABILITIES;
+ /**
+ * TelephonyProvider column name to identify eSIM's transfer status.
+ * By default, it's disabled.
+ * <P>Type: INTEGER (int)</P>
+ * @hide
+ */
+ public static final String TRANSFER_STATUS = SimInfo.COLUMN_TRANSFER_STATUS;
+
/** @hide */
@Retention(RetentionPolicy.SOURCE)
@IntDef(prefix = {"USAGE_SETTING_"},
@@ -1453,6 +1461,41 @@
private static final LruCache<Pair<String, Configuration>, Resources> sResourcesCache =
new LruCache<>(MAX_RESOURCE_CACHE_ENTRY_COUNT);
+
+ /**
+ * The profile has not been transferred or converted to an eSIM.
+ * @hide
+ */
+ @FlaggedApi(Flags.FLAG_SUPPORT_PSIM_TO_ESIM_CONVERSION)
+ @SystemApi
+ public static final int TRANSFER_STATUS_NONE = 0;
+
+ /**
+ * The existing profile of the old device has been transferred to an eSIM of the new device.
+ * @hide
+ */
+ @FlaggedApi(Flags.FLAG_SUPPORT_PSIM_TO_ESIM_CONVERSION)
+ @SystemApi
+ public static final int TRANSFER_STATUS_TRANSFERRED_OUT = 1;
+
+ /**
+ * The existing profile of the same device has been converted to an eSIM of the same device
+ * @hide
+ */
+ @FlaggedApi(Flags.FLAG_SUPPORT_PSIM_TO_ESIM_CONVERSION)
+ @SystemApi
+ public static final int TRANSFER_STATUS_CONVERTED = 2;
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(prefix = {"TRANSFER_STATUS"},
+ value = {
+ TRANSFER_STATUS_NONE,
+ TRANSFER_STATUS_TRANSFERRED_OUT,
+ TRANSFER_STATUS_CONVERTED,
+ })
+ public @interface TransferStatus {}
+
+
/**
* A listener class for monitoring changes to {@link SubscriptionInfo} records.
* <p>
@@ -1927,34 +1970,25 @@
* Then for SDK 35+, if the caller identity is personal profile, then this will return
* subscription 1 only and vice versa.
*
- * <p> The records will be sorted by {@link SubscriptionInfo#getSimSlotIndex} then by
- * {@link SubscriptionInfo#getSubscriptionId}.
+ * <p> Returned records will be sorted by {@link SubscriptionInfo#getSimSlotIndex} then by
+ * {@link SubscriptionInfo#getSubscriptionId}. Beginning with Android SDK 35, this method will
+ * never return null.
*
* <p>Requires Permission: {@link android.Manifest.permission#READ_PHONE_STATE READ_PHONE_STATE}
* or that the calling app has carrier privileges (see
* {@link TelephonyManager#hasCarrierPrivileges}).
*
- * @return Sorted list of the currently {@link SubscriptionInfo} records available on the device.
- * <ul>
- * <li>
- * If null is returned the current state is unknown but if a {@link OnSubscriptionsChangedListener}
- * has been registered {@link OnSubscriptionsChangedListener#onSubscriptionsChanged} will be
- * invoked in the future.
- * </li>
- * <li>
- * If the list is empty then there are no {@link SubscriptionInfo} records currently available.
- * </li>
- * <li>
- * if the list is non-empty the list is sorted by {@link SubscriptionInfo#getSimSlotIndex}
- * then by {@link SubscriptionInfo#getSubscriptionId}.
- * </li>
- * </ul>
+ * @return a list of the active {@link SubscriptionInfo} that is visible to the caller. If
+ * an empty list or null is returned, then there are no active subscriptions that
+ * are visible to the caller. If the number of active subscriptions available to
+ * any caller changes, then this change will be indicated by
+ * {@link OnSubscriptionsChangedListener#onSubscriptionsChanged}.
*
* @throws UnsupportedOperationException If the device does not have
- * {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}.
+ * {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}.
*/
@RequiresPermission(android.Manifest.permission.READ_PHONE_STATE)
- public List<SubscriptionInfo> getActiveSubscriptionInfoList() {
+ public @Nullable List<SubscriptionInfo> getActiveSubscriptionInfoList() {
List<SubscriptionInfo> activeList = null;
try {
@@ -1970,6 +2004,8 @@
if (activeList != null) {
activeList = activeList.stream().filter(subInfo -> isSubscriptionVisible(subInfo))
.collect(Collectors.toList());
+ } else {
+ activeList = Collections.emptyList();
}
return activeList;
}
@@ -1998,12 +2034,7 @@
* {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}.
*/
public @NonNull List<SubscriptionInfo> getCompleteActiveSubscriptionInfoList() {
- List<SubscriptionInfo> completeList = getActiveSubscriptionInfoList(
- /* userVisibleonly */false);
- if (completeList == null) {
- completeList = new ArrayList<>();
- }
- return completeList;
+ return getActiveSubscriptionInfoList(/* userVisibleonly */ false);
}
/**
@@ -2032,7 +2063,7 @@
*
* @hide
*/
- public @Nullable List<SubscriptionInfo> getActiveSubscriptionInfoList(boolean userVisibleOnly) {
+ public @NonNull List<SubscriptionInfo> getActiveSubscriptionInfoList(boolean userVisibleOnly) {
List<SubscriptionInfo> activeList = null;
try {
@@ -2045,11 +2076,13 @@
// ignore it
}
- if (!userVisibleOnly || activeList == null) {
- return activeList;
- } else {
+ if (activeList == null || activeList.isEmpty()) {
+ return Collections.emptyList();
+ } else if (userVisibleOnly) {
return activeList.stream().filter(subInfo -> isSubscriptionVisible(subInfo))
.collect(Collectors.toList());
+ } else {
+ return activeList;
}
}
@@ -2086,7 +2119,7 @@
* @hide
*/
@SystemApi
- public List<SubscriptionInfo> getAvailableSubscriptionInfoList() {
+ public @Nullable List<SubscriptionInfo> getAvailableSubscriptionInfoList() {
List<SubscriptionInfo> result = null;
try {
@@ -2098,7 +2131,7 @@
} catch (RemoteException ex) {
// ignore it
}
- return result;
+ return (result == null) ? Collections.emptyList() : result;
}
/**
@@ -2128,7 +2161,7 @@
* @throws UnsupportedOperationException If the device does not have
* {@link PackageManager#FEATURE_TELEPHONY_EUICC}.
*/
- public List<SubscriptionInfo> getAccessibleSubscriptionInfoList() {
+ public @Nullable List<SubscriptionInfo> getAccessibleSubscriptionInfoList() {
List<SubscriptionInfo> result = null;
try {
@@ -2139,7 +2172,7 @@
} catch (RemoteException ex) {
// ignore it
}
- return result;
+ return (result == null) ? Collections.emptyList() : result;
}
/**
@@ -4726,4 +4759,27 @@
public static int serviceCapabilityToBitmask(@ServiceCapability int capability) {
return 1 << (capability - 1);
}
+
+ /**
+ * Set the transfer status of the subscriptionInfo of the subId.
+ * @param subscriptionId The unique SubscriptionInfo key in database.
+ * @param status The transfer status to change.
+ *
+ *
+ * @hide
+ */
+ @FlaggedApi(Flags.FLAG_SUPPORT_PSIM_TO_ESIM_CONVERSION)
+ @SystemApi
+ @RequiresPermission(Manifest.permission.WRITE_EMBEDDED_SUBSCRIPTIONS)
+ public void setTransferStatus(int subscriptionId, @TransferStatus int status) {
+ try {
+ ISub iSub = TelephonyManager.getSubscriptionService();
+ if (iSub != null) {
+ iSub.setTransferStatus(subscriptionId, status);
+ }
+ } catch (RemoteException ex) {
+ logd("setTransferStatus for subId = " + subscriptionId + " failed.");
+ throw ex.rethrowFromSystemServer();
+ }
+ }
}
diff --git a/telephony/java/android/telephony/TelephonyManager.java b/telephony/java/android/telephony/TelephonyManager.java
index 89661a4..61c7a42 100644
--- a/telephony/java/android/telephony/TelephonyManager.java
+++ b/telephony/java/android/telephony/TelephonyManager.java
@@ -18709,9 +18709,9 @@
*/
@SystemApi
@FlaggedApi(com.android.server.telecom.flags.Flags.FLAG_TELECOM_RESOLVE_HIDDEN_DEPENDENCIES)
- public static final class EmergencyCallDiagnosticParams {
+ public static final class EmergencyCallDiagnosticData {
public static final class Builder {
- private boolean mCollectTelecomDumpSys;
+ private boolean mCollectTelecomDumpsys;
private boolean mCollectTelephonyDumpsys;
// If this is set to a value other than -1L, then the logcat collection is enabled.
@@ -18724,9 +18724,9 @@
* @param collectTelecomDumpsys Determines whether telecom dumpsys should be collected.
* @return Builder instance corresponding to the configured call diagnostic params.
*/
- public @NonNull Builder setTelecomDumpSysCollectionEnabled(
+ public @NonNull Builder setTelecomDumpsysCollectionEnabled(
boolean collectTelecomDumpsys) {
- mCollectTelecomDumpSys = collectTelecomDumpsys;
+ mCollectTelecomDumpsys = collectTelecomDumpsys;
return this;
}
@@ -18735,7 +18735,7 @@
* @param collectTelephonyDumpsys Determines if telephony dumpsys should be collected.
* @return Builder instance corresponding to the configured call diagnostic params.
*/
- public @NonNull Builder setTelephonyDumpSysCollectionEnabled(
+ public @NonNull Builder setTelephonyDumpsysCollectionEnabled(
boolean collectTelephonyDumpsys) {
mCollectTelephonyDumpsys = collectTelephonyDumpsys;
return this;
@@ -18753,35 +18753,35 @@
}
/**
- * Build the EmergencyCallDiagnosticParams from the provided Builder config.
- * @return {@link EmergencyCallDiagnosticParams} instance from provided builder.
+ * Build the EmergencyCallDiagnosticData from the provided Builder config.
+ * @return {@link EmergencyCallDiagnosticData} instance from provided builder.
*/
- public @NonNull EmergencyCallDiagnosticParams build() {
- return new EmergencyCallDiagnosticParams(mCollectTelecomDumpSys,
+ public @NonNull EmergencyCallDiagnosticData build() {
+ return new EmergencyCallDiagnosticData(mCollectTelecomDumpsys,
mCollectTelephonyDumpsys, mLogcatStartTimeMillis);
}
}
- private boolean mCollectTelecomDumpSys;
+ private boolean mCollectTelecomDumpsys;
private boolean mCollectTelephonyDumpsys;
private boolean mCollectLogcat;
private long mLogcatStartTimeMillis;
private static long sUnsetLogcatStartTime = -1L;
- private EmergencyCallDiagnosticParams(boolean collectTelecomDumpSys,
+ private EmergencyCallDiagnosticData(boolean collectTelecomDumpsys,
boolean collectTelephonyDumpsys, long logcatStartTimeMillis) {
- mCollectTelecomDumpSys = collectTelecomDumpSys;
+ mCollectTelecomDumpsys = collectTelecomDumpsys;
mCollectTelephonyDumpsys = collectTelephonyDumpsys;
mLogcatStartTimeMillis = logcatStartTimeMillis;
mCollectLogcat = logcatStartTimeMillis != sUnsetLogcatStartTime;
}
- public boolean isTelecomDumpSysCollectionEnabled() {
- return mCollectTelecomDumpSys;
+ public boolean isTelecomDumpsysCollectionEnabled() {
+ return mCollectTelecomDumpsys;
}
- public boolean isTelephonyDumpSysCollectionEnabled() {
+ public boolean isTelephonyDumpsysCollectionEnabled() {
return mCollectTelephonyDumpsys;
}
@@ -18796,12 +18796,12 @@
@Override
public String toString() {
- return "EmergencyCallDiagnosticParams{" +
- "mCollectTelecomDumpSys=" + mCollectTelecomDumpSys +
- ", mCollectTelephonyDumpsys=" + mCollectTelephonyDumpsys +
- ", mCollectLogcat=" + mCollectLogcat +
- ", mLogcatStartTimeMillis=" + mLogcatStartTimeMillis +
- '}';
+ return "EmergencyCallDiagnosticData{"
+ + "mCollectTelecomDumpsys=" + mCollectTelecomDumpsys
+ + ", mCollectTelephonyDumpsys=" + mCollectTelephonyDumpsys
+ + ", mCollectLogcat=" + mCollectLogcat
+ + ", mLogcatStartTimeMillis=" + mLogcatStartTimeMillis
+ + '}';
}
}
@@ -18809,7 +18809,7 @@
* Request telephony to persist state for debugging emergency call failures.
*
* @param dropboxTag Tag to use when persisting data to dropbox service.
- * @param params Parameters controlling what is collected.
+ * @param data Parameters controlling what is collected in the diagnostics.
*
* @hide
*/
@@ -18817,7 +18817,7 @@
@FlaggedApi(com.android.server.telecom.flags.Flags.FLAG_TELECOM_RESOLVE_HIDDEN_DEPENDENCIES)
@RequiresPermission(android.Manifest.permission.DUMP)
public void persistEmergencyCallDiagnosticData(@NonNull String dropboxTag,
- @NonNull EmergencyCallDiagnosticParams params) {
+ @NonNull EmergencyCallDiagnosticData data) {
try {
ITelephony telephony = ITelephony.Stub.asInterface(
TelephonyFrameworkInitializer
@@ -18826,10 +18826,10 @@
.get());
if (telephony != null) {
telephony.persistEmergencyCallDiagnosticData(dropboxTag,
- params.isLogcatCollectionEnabled(),
- params.getLogcatCollectionStartTimeMillis(),
- params.isTelecomDumpSysCollectionEnabled(),
- params.isTelephonyDumpSysCollectionEnabled());
+ data.isLogcatCollectionEnabled(),
+ data.getLogcatCollectionStartTimeMillis(),
+ data.isTelecomDumpsysCollectionEnabled(),
+ data.isTelephonyDumpsysCollectionEnabled());
}
} catch (RemoteException e) {
Log.e(TAG, "Error while persistEmergencyCallDiagnosticData: " + e);
@@ -19207,13 +19207,11 @@
/**
* Returns whether the domain selection service is supported.
*
- * <p>Requires Permission:
- * {@link android.Manifest.permission#READ_PRIVILEGED_PHONE_STATE READ_PRIVILEGED_PHONE_STATE}.
- *
* @return {@code true} if the domain selection service is supported.
* @hide
*/
- @TestApi
+ @SystemApi
+ @FlaggedApi(Flags.FLAG_USE_OEM_DOMAIN_SELECTION_SERVICE)
@RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE)
@RequiresFeature(PackageManager.FEATURE_TELEPHONY_CALLING)
public boolean isDomainSelectionSupported() {
diff --git a/telephony/java/android/telephony/TransportSelectorCallback.java b/telephony/java/android/telephony/TransportSelectorCallback.java
index 04752e4..0f5dd27 100644
--- a/telephony/java/android/telephony/TransportSelectorCallback.java
+++ b/telephony/java/android/telephony/TransportSelectorCallback.java
@@ -16,18 +16,28 @@
package android.telephony;
+import android.annotation.FlaggedApi;
import android.annotation.NonNull;
+import android.annotation.SystemApi;
import android.telephony.Annotation.DisconnectCauses;
+import com.android.internal.telephony.flags.Flags;
+
import java.util.function.Consumer;
/**
- * A callback class used to receive the transport selection result.
+ * A callback class used by the domain selection module to notify the framework of the result of
+ * selecting a domain for a call.
* @hide
*/
+@SystemApi
+@FlaggedApi(Flags.FLAG_USE_OEM_DOMAIN_SELECTION_SERVICE)
public interface TransportSelectorCallback {
/**
* Notify that {@link DomainSelector} instance has been created for the selection request.
+ * <p>
+ * DomainSelector callbacks run using the executor specified in
+ * {@link DomainSelectionService#onCreateExecutor}.
*
* @param selector the {@link DomainSelector} instance created.
*/
@@ -41,16 +51,13 @@
void onWlanSelected(boolean useEmergencyPdn);
/**
- * Notify that WWAN transport has been selected.
- */
- @NonNull WwanSelectorCallback onWwanSelected();
-
- /**
- * Notify that WWAN transport has been selected.
+ * Notify that WWAN transport has been selected and the next phase of selecting
+ * the PS or CS domain is starting.
*
- * @param consumer The callback to receive the result.
+ * @param consumer The callback used by the {@link DomainSelectionService} to optionally run
+ * emergency network scans and notify the framework of the WWAN transport result.
*/
- void onWwanSelected(Consumer<WwanSelectorCallback> consumer);
+ void onWwanSelected(@NonNull Consumer<WwanSelectorCallback> consumer);
/**
* Notify that selection has terminated because there is no decision that can be made
diff --git a/telephony/java/android/telephony/WwanSelectorCallback.java b/telephony/java/android/telephony/WwanSelectorCallback.java
index f9c2620..b900af3 100644
--- a/telephony/java/android/telephony/WwanSelectorCallback.java
+++ b/telephony/java/android/telephony/WwanSelectorCallback.java
@@ -16,30 +16,40 @@
package android.telephony;
+import android.annotation.FlaggedApi;
import android.annotation.NonNull;
+import android.annotation.SystemApi;
import android.os.CancellationSignal;
import android.telephony.DomainSelectionService.EmergencyScanType;
+import com.android.internal.telephony.flags.Flags;
+
import java.util.List;
import java.util.function.Consumer;
/**
- * A callback class used to receive the domain selection result.
+ * A callback class used to communicate with the framework to request network scans
+ * and notify the framework when a WWAN domain has been selected.
* @hide
*/
+@SystemApi
+@FlaggedApi(Flags.FLAG_USE_OEM_DOMAIN_SELECTION_SERVICE)
public interface WwanSelectorCallback {
/**
* Notify the framework that the {@link DomainSelectionService} has requested an emergency
* network scan as part of selection.
*
- * @param preferredNetworks the ordered list of preferred networks to scan.
- * @param scanType indicates the scan preference, such as full service or limited service.
- * @param signal notifies when the operation is canceled.
- * @param consumer the handler of the response.
+ * @param preferredNetworks The ordered list of preferred networks to scan.
+ * @param scanType Indicates the scan preference, such as full service or limited service.
+ * @param resetScan Indicates that the previous scan result shall be reset before scanning.
+ * @param signal Notifies when the operation is canceled.
+ * @param consumer The handler of the response, which will contain a one-shot result
+ * of the network scan.
*/
void onRequestEmergencyNetworkScan(@NonNull List<Integer> preferredNetworks,
- @EmergencyScanType int scanType,
- @NonNull CancellationSignal signal, @NonNull Consumer<EmergencyRegResult> consumer);
+ @EmergencyScanType int scanType, boolean resetScan,
+ @NonNull CancellationSignal signal,
+ @NonNull Consumer<EmergencyRegistrationResult> consumer);
/**
* Notifies the FW that the domain has been selected. After this method is called,
diff --git a/telephony/java/android/telephony/euicc/EuiccManager.java b/telephony/java/android/telephony/euicc/EuiccManager.java
index 9b8e62f..7935d24 100644
--- a/telephony/java/android/telephony/euicc/EuiccManager.java
+++ b/telephony/java/android/telephony/euicc/EuiccManager.java
@@ -16,12 +16,14 @@
package android.telephony.euicc;
import android.Manifest;
+import android.annotation.FlaggedApi;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.RequiresFeature;
import android.annotation.RequiresPermission;
import android.annotation.SdkConstant;
+import android.annotation.SuppressAutoDoc;
import android.annotation.SystemApi;
import android.app.Activity;
import android.app.PendingIntent;
@@ -50,6 +52,7 @@
import java.util.Collections;
import java.util.List;
import java.util.Locale;
+import java.util.Set;
import java.util.stream.Collectors;
/**
@@ -861,6 +864,10 @@
*/
public static final int ERROR_INVALID_PORT = 10017;
+ /** Temporary failure to retrieve available memory because eUICC is not ready. */
+ @FlaggedApi(Flags.FLAG_ESIM_AVAILABLE_MEMORY)
+ public static final long EUICC_MEMORY_FIELD_UNAVAILABLE = -1L;
+
/**
* Apps targeting on Android T and beyond will get exception whenever switchToSubscription
* without portIndex is called for disable subscription.
@@ -961,6 +968,35 @@
}
/**
+ * Returns the available memory in bytes of the eUICC.
+ *
+ * @return the available memory in bytes. May be {@link #EUICC_MEMORY_FIELD_UNAVAILABLE} if the
+ * eUICC is not ready. Check {@link #isEnabled} for more information.
+ * @throws UnsupportedOperationException If the device does not have
+ * {@link PackageManager#FEATURE_TELEPHONY_EUICC} or
+ * device doesn't support querying this information from the eUICC.
+ */
+ @SuppressAutoDoc // Blocked by b/72967236 - no support for carrier privileges
+ @FlaggedApi(Flags.FLAG_ESIM_AVAILABLE_MEMORY)
+ @RequiresPermission(
+ anyOf = {
+ Manifest.permission.READ_PHONE_STATE,
+ Manifest.permission.READ_PRIVILEGED_PHONE_STATE,
+ "carrier privileges"
+ })
+ public long getAvailableMemoryInBytes() {
+ if (!isEnabled()) {
+ return EUICC_MEMORY_FIELD_UNAVAILABLE;
+ }
+ try {
+ return getIEuiccController()
+ .getAvailableMemoryInBytes(mCardId, mContext.getOpPackageName());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
* Returns the current status of eUICC OTA.
*
* <p>Requires the {@link android.Manifest.permission#WRITE_EMBEDDED_SUBSCRIPTIONS} permission.
@@ -1707,4 +1743,57 @@
throw e.rethrowFromSystemServer();
}
}
+
+ /**
+ * Sets the supported carrier ids for pSIM conversion.
+ *
+ * <p>Any existing pSIM conversion supported carrier list will be replaced
+ * by the {@code carrierIds} set here.
+ *
+ * @param carrierIds is a list of carrierIds that supports pSIM conversion
+ *
+ * @throws UnsupportedOperationException If the device does not have
+ * {@link PackageManager#FEATURE_TELEPHONY_EUICC}.
+ * @throws IllegalStateException if this method is called when {@link #isEnabled} is false.
+ * @hide
+ */
+ @FlaggedApi(Flags.FLAG_SUPPORT_PSIM_TO_ESIM_CONVERSION)
+ @SystemApi
+ @RequiresPermission(Manifest.permission.WRITE_EMBEDDED_SUBSCRIPTIONS)
+ public void setPsimConversionSupportedCarriers(@NonNull Set<Integer> carrierIds) {
+ if (!isEnabled()) {
+ throw new IllegalStateException("Euicc is not enabled");
+ }
+ try {
+ int[] arr = carrierIds.stream().mapToInt(Integer::intValue).toArray();
+ getIEuiccController().setPsimConversionSupportedCarriers(arr);
+ } catch (RemoteException e) {
+ throw e.rethrowAsRuntimeException();
+ }
+ }
+
+ /**
+ * Returns whether the given carrier id supports pSIM conversion or not.
+ *
+ * @param carrierId to check whether pSIM conversion is supported or not
+ * @return whether the given carrier id supports pSIM conversion or not,
+ * or false if {@link #isEnabled} is false
+ *
+ * @throws UnsupportedOperationException If the device does not have
+ * {@link PackageManager#FEATURE_TELEPHONY_EUICC}.
+ * @hide
+ */
+ @FlaggedApi(Flags.FLAG_SUPPORT_PSIM_TO_ESIM_CONVERSION)
+ @SystemApi
+ @RequiresPermission(Manifest.permission.WRITE_EMBEDDED_SUBSCRIPTIONS)
+ public boolean isPsimConversionSupported(int carrierId) {
+ if (!isEnabled()) {
+ return false;
+ }
+ try {
+ return getIEuiccController().isPsimConversionSupported(carrierId);
+ } catch (RemoteException e) {
+ throw e.rethrowAsRuntimeException();
+ }
+ }
}
diff --git a/telephony/java/android/telephony/satellite/EnableRequestAttributes.java b/telephony/java/android/telephony/satellite/EnableRequestAttributes.java
new file mode 100644
index 0000000..bc9d230
--- /dev/null
+++ b/telephony/java/android/telephony/satellite/EnableRequestAttributes.java
@@ -0,0 +1,145 @@
+/*
+ * Copyright (C) 2024 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.telephony.satellite;
+
+import android.annotation.FlaggedApi;
+import android.annotation.NonNull;
+import android.annotation.SystemApi;
+
+import com.android.internal.telephony.flags.Flags;
+
+import java.util.concurrent.Executor;
+import java.util.function.Consumer;
+
+/**
+ * EnableRequestAttributes is used to store the attributes of the request
+ * {@link SatelliteManager#requestEnabled(EnableRequestAttributes, Executor, Consumer)}
+ * @hide
+ */
+@SystemApi
+@FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
+public class EnableRequestAttributes {
+ /** {@code true} to enable satellite and {@code false} to disable satellite */
+ private boolean mIsEnabled;
+ /**
+ * {@code true} to enable demo mode and {@code false} to disable. When disabling satellite,
+ * {@code mIsDemoMode} is always considered as {@code false} by Telephony.
+ */
+ private boolean mIsDemoMode;
+ /**
+ * {@code true} means satellite is enabled for emergency mode, {@code false} otherwise. When
+ * disabling satellite, {@code isEmergencyMode} is always considered as {@code false} by
+ * Telephony.
+ */
+ private boolean mIsEmergencyMode;
+
+ /**
+ * Constructor from builder.
+ *
+ * @param builder Builder of {@link EnableRequestAttributes}.
+ */
+ private EnableRequestAttributes(@NonNull Builder builder) {
+ this.mIsEnabled = builder.mIsEnabled;
+ this.mIsDemoMode = builder.mIsDemoMode;
+ this.mIsEmergencyMode = builder.mIsEmergencyMode;
+ }
+
+ /**
+ * @return Whether satellite is to be enabled
+ */
+ @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
+ public boolean isEnabled() {
+ return mIsEnabled;
+ }
+
+ /**
+ * @return Whether demo mode is to be enabled
+ */
+ @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
+ public boolean isDemoMode() {
+ return mIsDemoMode;
+ }
+
+ /**
+ * @return Whether satellite is to be enabled for emergency mode
+ */
+ @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
+ public boolean isEmergencyMode() {
+ return mIsEmergencyMode;
+ }
+
+ /**
+ * The builder class of {@link EnableRequestAttributes}
+ */
+ @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
+ public static final class Builder {
+ private boolean mIsEnabled;
+ private boolean mIsDemoMode = false;
+ private boolean mIsEmergencyMode = false;
+
+ @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
+ public Builder(boolean isEnabled) {
+ mIsEnabled = isEnabled;
+ }
+
+ /**
+ * Set demo mode
+ *
+ * @param isDemoMode {@code true} to enable demo mode and {@code false} to disable. When
+ * disabling satellite, {@code isDemoMode} is always considered as
+ * {@code false} by Telephony.
+ * @return The builder object
+ */
+ @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
+ @NonNull
+ public Builder setDemoMode(boolean isDemoMode) {
+ if (mIsEnabled) {
+ mIsDemoMode = isDemoMode;
+ }
+ return this;
+ }
+
+ /**
+ * Set emergency mode
+ *
+ * @param isEmergencyMode {@code true} means satellite is enabled for emergency mode,
+ * {@code false} otherwise. When disabling satellite,
+ * {@code isEmergencyMode} is always considered as {@code false} by
+ * Telephony.
+ * @return The builder object
+ */
+ @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
+ @NonNull
+ public Builder setEmergencyMode(boolean isEmergencyMode) {
+ if (mIsEnabled) {
+ mIsEmergencyMode = isEmergencyMode;
+ }
+ return this;
+ }
+
+ /**
+ * Build the {@link EnableRequestAttributes}
+ *
+ * @return The {@link EnableRequestAttributes} instance.
+ */
+ @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
+ @NonNull
+ public EnableRequestAttributes build() {
+ return new EnableRequestAttributes(this);
+ }
+ }
+}
diff --git a/telephony/java/android/telephony/satellite/SatelliteManager.java b/telephony/java/android/telephony/satellite/SatelliteManager.java
index b97822a..4a61114 100644
--- a/telephony/java/android/telephony/satellite/SatelliteManager.java
+++ b/telephony/java/android/telephony/satellite/SatelliteManager.java
@@ -162,6 +162,13 @@
/**
* Bundle key to get the response from
+ * {@link #requestIsEmergencyModeEnabled(Executor, OutcomeReceiver)}.
+ * @hide
+ */
+ public static final String KEY_EMERGENCY_MODE_ENABLED = "emergency_mode_enabled";
+
+ /**
+ * Bundle key to get the response from
* {@link #requestIsSupported(Executor, OutcomeReceiver)}.
* @hide
*/
@@ -341,6 +348,13 @@
@FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
public static final int SATELLITE_RESULT_ILLEGAL_STATE = 23;
+ /**
+ * Telephony framework timeout to receive ACK or response from the satellite modem after
+ * sending a request to the modem.
+ */
+ @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
+ public static final int SATELLITE_RESULT_MODEM_TIMEOUT = 24;
+
/** @hide */
@IntDef(prefix = {"SATELLITE_RESULT_"}, value = {
SATELLITE_RESULT_SUCCESS,
@@ -366,7 +380,8 @@
SATELLITE_RESULT_NOT_SUPPORTED,
SATELLITE_RESULT_REQUEST_IN_PROGRESS,
SATELLITE_RESULT_MODEM_BUSY,
- SATELLITE_RESULT_ILLEGAL_STATE
+ SATELLITE_RESULT_ILLEGAL_STATE,
+ SATELLITE_RESULT_MODEM_TIMEOUT
})
@Retention(RetentionPolicy.SOURCE)
public @interface SatelliteResult {}
@@ -482,20 +497,18 @@
* aligned with the satellite, user can send a message and also receive a reply in demo mode.
* If enableSatellite is {@code false}, enableDemoMode has no impact on the behavior.
*
- * @param enableSatellite {@code true} to enable the satellite modem and
- * {@code false} to disable.
- * @param enableDemoMode {@code true} to enable demo mode and {@code false} to disable.
+ * @param attributes The attributes of the enable request.
* @param executor The executor on which the error code listener will be called.
* @param resultListener Listener for the {@link SatelliteResult} result of the operation.
*
* @throws SecurityException if the caller doesn't have required permission.
- * @throws IllegalStateException if the Telephony process is not currently available.
*/
@RequiresPermission(Manifest.permission.SATELLITE_COMMUNICATION)
@FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
- public void requestEnabled(boolean enableSatellite, boolean enableDemoMode,
+ public void requestEnabled(@NonNull EnableRequestAttributes attributes,
@NonNull @CallbackExecutor Executor executor,
@SatelliteResult @NonNull Consumer<Integer> resultListener) {
+ Objects.requireNonNull(attributes);
Objects.requireNonNull(executor);
Objects.requireNonNull(resultListener);
@@ -509,14 +522,17 @@
() -> resultListener.accept(result)));
}
};
- telephony.requestSatelliteEnabled(mSubId, enableSatellite, enableDemoMode,
- errorCallback);
+ telephony.requestSatelliteEnabled(mSubId, attributes.isEnabled(),
+ attributes.isDemoMode(), attributes.isEmergencyMode(), errorCallback);
} else {
- throw new IllegalStateException("telephony service is null.");
+ Rlog.e(TAG, "requestEnabled() invalid telephony");
+ executor.execute(() -> Binder.withCleanCallingIdentity(
+ () -> resultListener.accept(SATELLITE_RESULT_ILLEGAL_STATE)));
}
} catch (RemoteException ex) {
- Rlog.e(TAG, "requestSatelliteEnabled() RemoteException: ", ex);
- ex.rethrowAsRuntimeException();
+ Rlog.e(TAG, "requestEnabled() exception: ", ex);
+ executor.execute(() -> Binder.withCleanCallingIdentity(
+ () -> resultListener.accept(SATELLITE_RESULT_ILLEGAL_STATE)));
}
}
@@ -566,12 +582,14 @@
};
telephony.requestIsSatelliteEnabled(mSubId, receiver);
} else {
+ loge("requestIsEnabled() invalid telephony");
executor.execute(() -> Binder.withCleanCallingIdentity(() -> callback.onError(
new SatelliteException(SATELLITE_RESULT_ILLEGAL_STATE))));
}
} catch (RemoteException ex) {
- loge("requestIsSatelliteEnabled() RemoteException: " + ex);
- ex.rethrowAsRuntimeException();
+ loge("requestIsEnabled() RemoteException: " + ex);
+ executor.execute(() -> Binder.withCleanCallingIdentity(() -> callback.onError(
+ new SatelliteException(SATELLITE_RESULT_ILLEGAL_STATE))));
}
}
@@ -621,11 +639,68 @@
};
telephony.requestIsDemoModeEnabled(mSubId, receiver);
} else {
+ loge("requestIsDemoModeEnabled() invalid telephony");
executor.execute(() -> Binder.withCleanCallingIdentity(() -> callback.onError(
new SatelliteException(SATELLITE_RESULT_ILLEGAL_STATE))));
}
} catch (RemoteException ex) {
loge("requestIsDemoModeEnabled() RemoteException: " + ex);
+ executor.execute(() -> Binder.withCleanCallingIdentity(() -> callback.onError(
+ new SatelliteException(SATELLITE_RESULT_ILLEGAL_STATE))));
+ }
+ }
+
+ /**
+ * Request to get whether the satellite service is enabled for emergency mode.
+ *
+ * @param executor The executor on which the callback will be called.
+ * @param callback The callback object to which the result will be delivered.
+ * If the request is successful, {@link OutcomeReceiver#onResult(Object)}
+ * will return a {@code boolean} with value {@code true} if satellite is enabled
+ * for emergency mode and {@code false} otherwise.
+ * If the request is not successful, {@link OutcomeReceiver#onError(Throwable)}
+ * will return a {@link SatelliteException} with the {@link SatelliteResult}.
+ *
+ * @throws SecurityException if the caller doesn't have required permission.
+ */
+ @RequiresPermission(Manifest.permission.SATELLITE_COMMUNICATION)
+ @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
+ public void requestIsEmergencyModeEnabled(@NonNull @CallbackExecutor Executor executor,
+ @NonNull OutcomeReceiver<Boolean, SatelliteException> callback) {
+ Objects.requireNonNull(executor);
+ Objects.requireNonNull(callback);
+
+ try {
+ ITelephony telephony = getITelephony();
+ if (telephony != null) {
+ ResultReceiver receiver = new ResultReceiver(null) {
+ @Override
+ protected void onReceiveResult(int resultCode, Bundle resultData) {
+ if (resultCode == SATELLITE_RESULT_SUCCESS) {
+ if (resultData.containsKey(KEY_EMERGENCY_MODE_ENABLED)) {
+ boolean isEmergencyModeEnabled =
+ resultData.getBoolean(KEY_EMERGENCY_MODE_ENABLED);
+ executor.execute(() -> Binder.withCleanCallingIdentity(() ->
+ callback.onResult(isEmergencyModeEnabled)));
+ } else {
+ loge("KEY_EMERGENCY_MODE_ENABLED does not exist.");
+ executor.execute(() -> Binder.withCleanCallingIdentity(() ->
+ callback.onError(new SatelliteException(
+ SATELLITE_RESULT_REQUEST_FAILED))));
+ }
+ } else {
+ executor.execute(() -> Binder.withCleanCallingIdentity(() ->
+ callback.onError(new SatelliteException(resultCode))));
+ }
+ }
+ };
+ telephony.requestIsEmergencyModeEnabled(mSubId, receiver);
+ } else {
+ executor.execute(() -> Binder.withCleanCallingIdentity(() -> callback.onError(
+ new SatelliteException(SATELLITE_RESULT_ILLEGAL_STATE))));
+ }
+ } catch (RemoteException ex) {
+ loge("requestIsEmergencyModeEnabled() RemoteException: " + ex);
ex.rethrowAsRuntimeException();
}
}
@@ -678,12 +753,14 @@
};
telephony.requestIsSatelliteSupported(mSubId, receiver);
} else {
+ loge("requestIsSupported() invalid telephony");
executor.execute(() -> Binder.withCleanCallingIdentity(() -> callback.onError(
new SatelliteException(SATELLITE_RESULT_ILLEGAL_STATE))));
}
} catch (RemoteException ex) {
- loge("requestIsSatelliteSupported() RemoteException: " + ex);
- ex.rethrowAsRuntimeException();
+ loge("requestIsSupported() RemoteException: " + ex);
+ executor.execute(() -> Binder.withCleanCallingIdentity(() -> callback.onError(
+ new SatelliteException(SATELLITE_RESULT_ILLEGAL_STATE))));
}
}
@@ -733,12 +810,14 @@
};
telephony.requestSatelliteCapabilities(mSubId, receiver);
} else {
+ loge("requestCapabilities() invalid telephony");
executor.execute(() -> Binder.withCleanCallingIdentity(() -> callback.onError(
new SatelliteException(SATELLITE_RESULT_ILLEGAL_STATE))));
}
} catch (RemoteException ex) {
- loge("requestSatelliteCapabilities() RemoteException: " + ex);
- ex.rethrowAsRuntimeException();
+ loge("requestCapabilities() RemoteException: " + ex);
+ executor.execute(() -> Binder.withCleanCallingIdentity(() -> callback.onError(
+ new SatelliteException(SATELLITE_RESULT_ILLEGAL_STATE))));
}
}
@@ -1014,12 +1093,14 @@
telephony.startSatelliteTransmissionUpdates(mSubId, errorCallback,
internalCallback);
} else {
+ loge("startTransmissionUpdates() invalid telephony");
executor.execute(() -> Binder.withCleanCallingIdentity(
() -> resultListener.accept(SATELLITE_RESULT_ILLEGAL_STATE)));
}
} catch (RemoteException ex) {
- loge("startSatelliteTransmissionUpdates() RemoteException: " + ex);
- ex.rethrowAsRuntimeException();
+ loge("startTransmissionUpdates() RemoteException: " + ex);
+ executor.execute(() -> Binder.withCleanCallingIdentity(
+ () -> resultListener.accept(SATELLITE_RESULT_ILLEGAL_STATE)));
}
}
@@ -1069,12 +1150,14 @@
() -> resultListener.accept(SATELLITE_RESULT_INVALID_ARGUMENTS)));
}
} else {
+ loge("stopTransmissionUpdates() invalid telephony");
executor.execute(() -> Binder.withCleanCallingIdentity(
() -> resultListener.accept(SATELLITE_RESULT_ILLEGAL_STATE)));
}
} catch (RemoteException ex) {
- loge("stopSatelliteTransmissionUpdates() RemoteException: " + ex);
- ex.rethrowAsRuntimeException();
+ loge("stopTransmissionUpdates() RemoteException: " + ex);
+ executor.execute(() -> Binder.withCleanCallingIdentity(
+ () -> resultListener.accept(SATELLITE_RESULT_ILLEGAL_STATE)));
}
}
@@ -1092,7 +1175,6 @@
* @param resultListener Listener for the {@link SatelliteResult} result of the operation.
*
* @throws SecurityException if the caller doesn't have required permission.
- * @throws IllegalStateException if the Telephony process is not currently available.
*/
@RequiresPermission(Manifest.permission.SATELLITE_COMMUNICATION)
@FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
@@ -1119,12 +1201,14 @@
cancelRemote = telephony.provisionSatelliteService(mSubId, token, provisionData,
errorCallback);
} else {
+ loge("provisionService() invalid telephony");
executor.execute(() -> Binder.withCleanCallingIdentity(
() -> resultListener.accept(SATELLITE_RESULT_ILLEGAL_STATE)));
}
} catch (RemoteException ex) {
- loge("provisionSatelliteService() RemoteException=" + ex);
- ex.rethrowAsRuntimeException();
+ loge("provisionService() RemoteException=" + ex);
+ executor.execute(() -> Binder.withCleanCallingIdentity(
+ () -> resultListener.accept(SATELLITE_RESULT_ILLEGAL_STATE)));
}
if (cancellationSignal != null) {
cancellationSignal.setRemote(cancelRemote);
@@ -1168,12 +1252,14 @@
};
telephony.deprovisionSatelliteService(mSubId, token, errorCallback);
} else {
+ loge("deprovisionService() invalid telephony");
executor.execute(() -> Binder.withCleanCallingIdentity(
() -> resultListener.accept(SATELLITE_RESULT_ILLEGAL_STATE)));
}
} catch (RemoteException ex) {
- loge("deprovisionSatelliteService() RemoteException=" + ex);
- ex.rethrowAsRuntimeException();
+ loge("deprovisionService() RemoteException ex=" + ex);
+ executor.execute(() -> Binder.withCleanCallingIdentity(
+ () -> resultListener.accept(SATELLITE_RESULT_ILLEGAL_STATE)));
}
}
@@ -1215,7 +1301,7 @@
throw new IllegalStateException("telephony service is null.");
}
} catch (RemoteException ex) {
- loge("registerForSatelliteProvisionStateChanged() RemoteException: " + ex);
+ loge("registerForProvisionStateChanged() RemoteException: " + ex);
ex.rethrowAsRuntimeException();
}
return SATELLITE_RESULT_REQUEST_FAILED;
@@ -1302,12 +1388,14 @@
};
telephony.requestIsSatelliteProvisioned(mSubId, receiver);
} else {
+ loge("requestIsProvisioned() invalid telephony");
executor.execute(() -> Binder.withCleanCallingIdentity(() -> callback.onError(
new SatelliteException(SATELLITE_RESULT_ILLEGAL_STATE))));
}
} catch (RemoteException ex) {
- loge("requestIsSatelliteProvisioned() RemoteException: " + ex);
- ex.rethrowAsRuntimeException();
+ loge("requestIsProvisioned() RemoteException: " + ex);
+ executor.execute(() -> Binder.withCleanCallingIdentity(() -> callback.onError(
+ new SatelliteException(SATELLITE_RESULT_ILLEGAL_STATE))));
}
}
@@ -1347,7 +1435,7 @@
throw new IllegalStateException("telephony service is null.");
}
} catch (RemoteException ex) {
- loge("registerForSatelliteModemStateChanged() RemoteException:" + ex);
+ loge("registerForModemStateChanged() RemoteException:" + ex);
ex.rethrowAsRuntimeException();
}
return SATELLITE_RESULT_REQUEST_FAILED;
@@ -1516,12 +1604,14 @@
};
telephony.pollPendingDatagrams(mSubId, internalCallback);
} else {
+ loge("pollPendingDatagrams() invalid telephony");
executor.execute(() -> Binder.withCleanCallingIdentity(
() -> resultListener.accept(SATELLITE_RESULT_ILLEGAL_STATE)));
}
} catch (RemoteException ex) {
loge("pollPendingDatagrams() RemoteException:" + ex);
- ex.rethrowAsRuntimeException();
+ executor.execute(() -> Binder.withCleanCallingIdentity(
+ () -> resultListener.accept(SATELLITE_RESULT_ILLEGAL_STATE)));
}
}
@@ -1573,12 +1663,14 @@
telephony.sendDatagram(mSubId, datagramType, datagram,
needFullScreenPointingUI, internalCallback);
} else {
+ loge("sendDatagram() invalid telephony");
executor.execute(() -> Binder.withCleanCallingIdentity(
() -> resultListener.accept(SATELLITE_RESULT_ILLEGAL_STATE)));
}
} catch (RemoteException ex) {
loge("sendDatagram() RemoteException:" + ex);
- ex.rethrowAsRuntimeException();
+ executor.execute(() -> Binder.withCleanCallingIdentity(
+ () -> resultListener.accept(SATELLITE_RESULT_ILLEGAL_STATE)));
}
}
@@ -1628,16 +1720,16 @@
}
}
};
- telephony.requestIsCommunicationAllowedForCurrentLocation(mSubId,
- receiver);
+ telephony.requestIsCommunicationAllowedForCurrentLocation(mSubId, receiver);
} else {
+ loge("requestIsCommunicationAllowedForCurrentLocation() invalid telephony");
executor.execute(() -> Binder.withCleanCallingIdentity(() -> callback.onError(
new SatelliteException(SATELLITE_RESULT_ILLEGAL_STATE))));
}
} catch (RemoteException ex) {
- loge("requestIsCommunicationAllowedForCurrentLocation() RemoteException: "
- + ex);
- ex.rethrowAsRuntimeException();
+ loge("requestIsCommunicationAllowedForCurrentLocation() RemoteException: " + ex);
+ executor.execute(() -> Binder.withCleanCallingIdentity(() -> callback.onError(
+ new SatelliteException(SATELLITE_RESULT_ILLEGAL_STATE))));
}
}
@@ -1688,12 +1780,14 @@
};
telephony.requestTimeForNextSatelliteVisibility(mSubId, receiver);
} else {
+ loge("requestTimeForNextSatelliteVisibility() invalid telephony");
executor.execute(() -> Binder.withCleanCallingIdentity(() -> callback.onError(
new SatelliteException(SATELLITE_RESULT_ILLEGAL_STATE))));
}
} catch (RemoteException ex) {
loge("requestTimeForNextSatelliteVisibility() RemoteException: " + ex);
- ex.rethrowAsRuntimeException();
+ executor.execute(() -> Binder.withCleanCallingIdentity(() -> callback.onError(
+ new SatelliteException(SATELLITE_RESULT_ILLEGAL_STATE))));
}
}
@@ -1720,7 +1814,7 @@
throw new IllegalStateException("telephony service is null.");
}
} catch (RemoteException ex) {
- loge("informDeviceAlignedToSatellite() RemoteException:" + ex);
+ loge("setDeviceAlignedWithSatellite() RemoteException:" + ex);
ex.rethrowAsRuntimeException();
}
}
@@ -1830,12 +1924,14 @@
};
telephony.addAttachRestrictionForCarrier(subId, reason, errorCallback);
} else {
+ loge("addAttachRestrictionForCarrier() invalid telephony");
executor.execute(() -> Binder.withCleanCallingIdentity(
() -> resultListener.accept(SATELLITE_RESULT_ILLEGAL_STATE)));
}
} catch (RemoteException ex) {
loge("addAttachRestrictionForCarrier() RemoteException:" + ex);
- ex.rethrowAsRuntimeException();
+ executor.execute(() -> Binder.withCleanCallingIdentity(
+ () -> resultListener.accept(SATELLITE_RESULT_ILLEGAL_STATE)));
}
}
@@ -1873,12 +1969,14 @@
};
telephony.removeAttachRestrictionForCarrier(subId, reason, errorCallback);
} else {
+ loge("removeAttachRestrictionForCarrier() invalid telephony");
executor.execute(() -> Binder.withCleanCallingIdentity(
() -> resultListener.accept(SATELLITE_RESULT_ILLEGAL_STATE)));
}
} catch (RemoteException ex) {
loge("removeAttachRestrictionForCarrier() RemoteException:" + ex);
- ex.rethrowAsRuntimeException();
+ executor.execute(() -> Binder.withCleanCallingIdentity(
+ () -> resultListener.accept(SATELLITE_RESULT_ILLEGAL_STATE)));
}
}
@@ -1939,10 +2037,7 @@
* The {@link NtnSignalStrength#NTN_SIGNAL_STRENGTH_NONE} will be returned if there is no
* signal strength data available.
* If the request is not successful, {@link OutcomeReceiver#onError(Throwable)} will return a
- * {@link SatelliteException} with the {@link SatelliteResult}, or return a
- * {@link IllegalStateException} if the Telephony process is not currently available or
- * satellite is not supported, or return a {@link RuntimeException} when remote procedure call
- * has failed.
+ * {@link SatelliteException} with the {@link SatelliteResult}.
*
* @throws SecurityException if the caller doesn't have required permission.
*/
@@ -1980,12 +2075,14 @@
};
telephony.requestNtnSignalStrength(mSubId, receiver);
} else {
+ loge("requestNtnSignalStrength() invalid telephony");
executor.execute(() -> Binder.withCleanCallingIdentity(() -> callback.onError(
new SatelliteException(SATELLITE_RESULT_ILLEGAL_STATE))));
}
} catch (RemoteException ex) {
loge("requestNtnSignalStrength() RemoteException: " + ex);
- ex.rethrowAsRuntimeException();
+ executor.execute(() -> Binder.withCleanCallingIdentity(() -> callback.onError(
+ new SatelliteException(SATELLITE_RESULT_ILLEGAL_STATE))));
}
}
@@ -2187,14 +2284,11 @@
return new ArrayList<>();
}
- private static ITelephony getITelephony() {
+ @Nullable private static ITelephony getITelephony() {
ITelephony binder = ITelephony.Stub.asInterface(TelephonyFrameworkInitializer
.getTelephonyServiceManager()
.getTelephonyServiceRegisterer()
.get());
- if (binder == null) {
- throw new RuntimeException("Could not find Telephony Service.");
- }
return binder;
}
diff --git a/telephony/java/com/android/internal/telephony/IDomainSelector.aidl b/telephony/java/com/android/internal/telephony/IDomainSelector.aidl
index d94840b..0eeadee7 100644
--- a/telephony/java/com/android/internal/telephony/IDomainSelector.aidl
+++ b/telephony/java/com/android/internal/telephony/IDomainSelector.aidl
@@ -19,7 +19,6 @@
import android.telephony.DomainSelectionService.SelectionAttributes;
oneway interface IDomainSelector {
- void cancelSelection();
void reselectDomain(in SelectionAttributes attr);
void finishSelection();
}
diff --git a/telephony/java/com/android/internal/telephony/ISub.aidl b/telephony/java/com/android/internal/telephony/ISub.aidl
index 3f41d56..cc770aa 100644
--- a/telephony/java/com/android/internal/telephony/ISub.aidl
+++ b/telephony/java/com/android/internal/telephony/ISub.aidl
@@ -376,4 +376,12 @@
* @param data with the sim specific configs to be backed up.
*/
void restoreAllSimSpecificSettingsFromBackup(in byte[] data);
+
+ /**
+ * Set the transfer status of the subscriptionInfo that corresponds to subId.
+ * @param subId The unique SubscriptionInfo key in database.
+ * @param status The transfer status to change. This value must be one of the following.
+ */
+ @EnforcePermission("WRITE_EMBEDDED_SUBSCRIPTIONS")
+ void setTransferStatus(int subId, int status);
}
diff --git a/telephony/java/com/android/internal/telephony/ITelephony.aidl b/telephony/java/com/android/internal/telephony/ITelephony.aidl
index a1fc064..bd47b1f 100644
--- a/telephony/java/com/android/internal/telephony/ITelephony.aidl
+++ b/telephony/java/com/android/internal/telephony/ITelephony.aidl
@@ -2742,14 +2742,19 @@
* Request to enable or disable the satellite modem.
*
* @param subId The subId of the subscription to enable or disable the satellite modem for.
- * @param enable True to enable the satellite modem and false to disable.
- * @param isDemoModeEnabled True if demo mode is enabled and false otherwise.
+ * @param enableSatellite True to enable the satellite modem and false to disable.
+ * @param enableDemoMode True if demo mode is enabled and false otherwise. When
+ * disabling satellite, {@code enableDemoMode} is always considered as
+ * {@code false} by Telephony.
+ * @param isEmergency {@code true} means satellite is enabled for emergency mode, {@code false}
+ * otherwise. When disabling satellite, {@code isEmergency} is always
+ * considered as {@code false} by Telephony.
* @param callback The callback to get the result of the request.
*/
@JavaPassthrough(annotation="@android.annotation.RequiresPermission("
+ "android.Manifest.permission.SATELLITE_COMMUNICATION)")
- void requestSatelliteEnabled(int subId, boolean enable, boolean isDemoModeEnabled,
- in IIntegerConsumer callback);
+ void requestSatelliteEnabled(int subId, boolean enableSatellite, boolean enableDemoMode,
+ boolean isEmergency, in IIntegerConsumer callback);
/**
* Request to get whether the satellite modem is enabled.
@@ -2775,6 +2780,18 @@
void requestIsDemoModeEnabled(int subId, in ResultReceiver receiver);
/**
+ * Request to get whether the satellite service is enabled with emergency mode.
+ *
+ * @param subId The subId of the subscription to request whether the satellite demo mode is
+ * enabled for.
+ * @param receiver Result receiver to get the error code of the request and whether the
+ * satellite is enabled with emergency mode.
+ */
+ @JavaPassthrough(annotation="@android.annotation.RequiresPermission("
+ + "android.Manifest.permission.SATELLITE_COMMUNICATION)")
+ void requestIsEmergencyModeEnabled(int subId, in ResultReceiver receiver);
+
+ /**
* Request to get whether the satellite service is supported on the device.
*
* @param subId The subId of the subscription to check whether satellite is supported for.
@@ -3203,6 +3220,18 @@
boolean setShouldSendDatagramToModemInDemoMode(boolean shouldSendToModemInDemoMode);
/**
+ * @return {@code true} if the DomainSelectionService is set,
+ * {@code false} otherwise.
+ */
+ boolean setDomainSelectionServiceOverride(in ComponentName componentName);
+
+ /**
+ * @return {@code true} if the DomainSelectionService override is cleared,
+ * {@code false} otherwise.
+ */
+ boolean clearDomainSelectionServiceOverride();
+
+ /**
* Enable or disable notifications sent for cellular identifier disclosure events.
*
* Disclosure events are defined as instances where a device has sent a cellular identifier
diff --git a/telephony/java/com/android/internal/telephony/ITransportSelectorCallback.aidl b/telephony/java/com/android/internal/telephony/ITransportSelectorCallback.aidl
index 6777256d..26daacd 100644
--- a/telephony/java/com/android/internal/telephony/ITransportSelectorCallback.aidl
+++ b/telephony/java/com/android/internal/telephony/ITransportSelectorCallback.aidl
@@ -20,10 +20,9 @@
import com.android.internal.telephony.ITransportSelectorResultCallback;
import com.android.internal.telephony.IWwanSelectorCallback;
-interface ITransportSelectorCallback {
- oneway void onCreated(in IDomainSelector selector);
- oneway void onWlanSelected(boolean useEmergencyPdn);
- IWwanSelectorCallback onWwanSelected();
- oneway void onWwanSelectedAsync(in ITransportSelectorResultCallback cb);
- oneway void onSelectionTerminated(int cause);
+oneway interface ITransportSelectorCallback {
+ void onCreated(in IDomainSelector selector);
+ void onWlanSelected(boolean useEmergencyPdn);
+ void onWwanSelectedAsync(in ITransportSelectorResultCallback cb);
+ void onSelectionTerminated(int cause);
}
diff --git a/telephony/java/com/android/internal/telephony/IWwanSelectorCallback.aidl b/telephony/java/com/android/internal/telephony/IWwanSelectorCallback.aidl
index 94e7c87..87955ac 100644
--- a/telephony/java/com/android/internal/telephony/IWwanSelectorCallback.aidl
+++ b/telephony/java/com/android/internal/telephony/IWwanSelectorCallback.aidl
@@ -20,7 +20,7 @@
oneway interface IWwanSelectorCallback {
void onRequestEmergencyNetworkScan(in int[] preferredNetworks,
- int scanType, in IWwanSelectorResultCallback cb);
+ int scanType, boolean resetScan, in IWwanSelectorResultCallback cb);
void onDomainSelected(int domain, boolean useEmergencyPdn);
void onCancel();
}
diff --git a/telephony/java/com/android/internal/telephony/IWwanSelectorResultCallback.aidl b/telephony/java/com/android/internal/telephony/IWwanSelectorResultCallback.aidl
index 0d61fcb..091974a 100644
--- a/telephony/java/com/android/internal/telephony/IWwanSelectorResultCallback.aidl
+++ b/telephony/java/com/android/internal/telephony/IWwanSelectorResultCallback.aidl
@@ -16,8 +16,8 @@
package com.android.internal.telephony;
-import android.telephony.EmergencyRegResult;
+import android.telephony.EmergencyRegistrationResult;
oneway interface IWwanSelectorResultCallback {
- void onComplete(in EmergencyRegResult result);
+ void onComplete(in EmergencyRegistrationResult result);
}
diff --git a/telephony/java/com/android/internal/telephony/PhoneConstants.java b/telephony/java/com/android/internal/telephony/PhoneConstants.java
index 07f2916..a9ebd5c 100644
--- a/telephony/java/com/android/internal/telephony/PhoneConstants.java
+++ b/telephony/java/com/android/internal/telephony/PhoneConstants.java
@@ -250,9 +250,6 @@
*/
public static final int DOMAIN_NON_3GPP_PS = 4;
- /** Key to enable comparison of domain selection results from legacy and new code. */
- public static final String EXTRA_COMPARE_DOMAIN = "compare_domain";
-
/** The key to specify the emergency service category */
public static final String EXTRA_EMERGENCY_SERVICE_CATEGORY = "emergency_service_category";
}
diff --git a/telephony/java/com/android/internal/telephony/euicc/IEuiccController.aidl b/telephony/java/com/android/internal/telephony/euicc/IEuiccController.aidl
index 19f1a5b..053bc7d 100644
--- a/telephony/java/com/android/internal/telephony/euicc/IEuiccController.aidl
+++ b/telephony/java/com/android/internal/telephony/euicc/IEuiccController.aidl
@@ -57,4 +57,7 @@
boolean isSimPortAvailable(int cardId, int portIndex, String callingPackage);
boolean hasCarrierPrivilegesForPackageOnAnyPhone(String callingPackage);
boolean isCompatChangeEnabled(String callingPackage, long changeId);
+ void setPsimConversionSupportedCarriers(in int[] carrierIds);
+ boolean isPsimConversionSupported(in int carrierId);
+ long getAvailableMemoryInBytes(int cardId, String callingPackage);
}
diff --git a/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/PipAppHelper.kt b/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/PipAppHelper.kt
index 73cc2f2..f628af1 100644
--- a/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/PipAppHelper.kt
+++ b/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/PipAppHelper.kt
@@ -340,6 +340,14 @@
wmHelper.StateSyncBuilder().withPipGone().withHomeActivityVisible().waitForAndVerify()
}
+ open fun tapPipToShowMenu(wmHelper: WindowManagerStateHelper) {
+ val windowRect = getWindowRect(wmHelper)
+ uiDevice.click(windowRect.centerX(), windowRect.centerY())
+ // search and interact with the dismiss button
+ val dismissSelector = By.res(SYSTEMUI_PACKAGE, "dismiss")
+ uiDevice.wait(Until.hasObject(dismissSelector), FIND_TIMEOUT)
+ }
+
/** Close the pip window by pressing the expand button */
fun expandPipWindowToApp(wmHelper: WindowManagerStateHelper) {
val windowRect = getWindowRect(wmHelper)
diff --git a/tests/Input/Android.bp b/tests/Input/Android.bp
index cf2d5d6..d17cd1f 100644
--- a/tests/Input/Android.bp
+++ b/tests/Input/Android.bp
@@ -1,4 +1,5 @@
package {
+ default_team: "trendy_team_input_framework",
// See: http://go/android-license-faq
// A large-scale-change added 'default_applicable_licenses' to import
// all of the 'license_kinds' from "frameworks_base_license"
diff --git a/tests/Input/src/com/android/test/input/InputDeviceTest.java b/tests/Input/src/com/android/test/input/InputDeviceTest.java
index 5434c82..5f1bc87 100644
--- a/tests/Input/src/com/android/test/input/InputDeviceTest.java
+++ b/tests/Input/src/com/android/test/input/InputDeviceTest.java
@@ -67,8 +67,14 @@
assertEquals("keyCharacterMap not equal", keyCharacterMap, outKeyCharacterMap);
for (int j = 0; j < device.getMotionRanges().size(); j++) {
- assertMotionRangeEquals(device.getMotionRanges().get(j),
- outDevice.getMotionRanges().get(j));
+ InputDevice.MotionRange motionRange = device.getMotionRanges().get(j);
+ assertMotionRangeEquals(motionRange, outDevice.getMotionRanges().get(j));
+
+ int axis = motionRange.getAxis();
+ int source = motionRange.getSource();
+ assertEquals(
+ device.getViewBehavior().shouldSmoothScroll(axis, source),
+ outDevice.getViewBehavior().shouldSmoothScroll(axis, source));
}
}
@@ -93,7 +99,8 @@
.setHasBattery(true)
.setKeyboardLanguageTag("en-US")
.setKeyboardLayoutType("qwerty")
- .setUsiVersion(new HostUsiVersion(2, 0));
+ .setUsiVersion(new HostUsiVersion(2, 0))
+ .setShouldSmoothScroll(true);
for (int i = 0; i < 30; i++) {
deviceBuilder.addMotionRange(
diff --git a/tests/InputMethodStressTest/Android.bp b/tests/InputMethodStressTest/Android.bp
index 58ceb3f..5ed8d8d 100644
--- a/tests/InputMethodStressTest/Android.bp
+++ b/tests/InputMethodStressTest/Android.bp
@@ -13,6 +13,7 @@
// limitations under the License.
package {
+ default_team: "trendy_team_input_method_framework",
default_applicable_licenses: ["frameworks_base_license"],
}
diff --git a/tests/StagedInstallTest/Android.bp b/tests/StagedInstallTest/Android.bp
index 23efe54..2751141 100644
--- a/tests/StagedInstallTest/Android.bp
+++ b/tests/StagedInstallTest/Android.bp
@@ -13,6 +13,7 @@
// limitations under the License.
package {
+ default_team: "trendy_team_framework_android_packages",
// See: http://go/android-license-faq
// A large-scale-change added 'default_applicable_licenses' to import
// all of the 'license_kinds' from "frameworks_base_license"
diff --git a/tests/UpdatableSystemFontTest/Android.bp b/tests/UpdatableSystemFontTest/Android.bp
index ddb3649..12d4338 100644
--- a/tests/UpdatableSystemFontTest/Android.bp
+++ b/tests/UpdatableSystemFontTest/Android.bp
@@ -13,6 +13,7 @@
// limitations under the License.
package {
+ default_team: "trendy_team_android_gpu",
// See: http://go/android-license-faq
// A large-scale-change added 'default_applicable_licenses' to import
// all of the 'license_kinds' from "frameworks_base_license"
diff --git a/tests/UpdatableSystemFontTest/EmojiRenderingTestApp/Android.bp b/tests/UpdatableSystemFontTest/EmojiRenderingTestApp/Android.bp
index ed34fa9..0f21035 100644
--- a/tests/UpdatableSystemFontTest/EmojiRenderingTestApp/Android.bp
+++ b/tests/UpdatableSystemFontTest/EmojiRenderingTestApp/Android.bp
@@ -13,6 +13,7 @@
// limitations under the License.
package {
+ default_team: "trendy_team_android_gpu",
// See: http://go/android-license-faq
// A large-scale-change added 'default_applicable_licenses' to import
// all of the 'license_kinds' from "frameworks_base_license"
diff --git a/tests/UpdatableSystemFontTest/testdata/Android.bp b/tests/UpdatableSystemFontTest/testdata/Android.bp
index 0bdb3a8..38355530 100644
--- a/tests/UpdatableSystemFontTest/testdata/Android.bp
+++ b/tests/UpdatableSystemFontTest/testdata/Android.bp
@@ -13,6 +13,7 @@
// limitations under the License.
package {
+ default_team: "trendy_team_android_gpu",
// See: http://go/android-license-faq
// A large-scale-change added 'default_applicable_licenses' to import
// all of the 'license_kinds' from "frameworks_base_license"
diff --git a/tools/aapt/ApkBuilder.h b/tools/aapt/ApkBuilder.h
index 5d3abc6..9276402 100644
--- a/tools/aapt/ApkBuilder.h
+++ b/tools/aapt/ApkBuilder.h
@@ -44,7 +44,7 @@
android::status_t createSplitForConfigs(const std::set<ConfigDescription>& configs);
/**
- * Adds a file to be written to the final APK. It's name must not collide
+ * Adds a file to be written to the final APK. Its name must not collide
* with that of any files previously added. When a Split APK is being
* generated, duplicates can exist as long as they are in different splits
* (resources.arsc, AndroidManifest.xml).
diff --git a/tools/aapt/Main.cpp b/tools/aapt/Main.cpp
index 2f2ef92..66a0510 100644
--- a/tools/aapt/Main.cpp
+++ b/tools/aapt/Main.cpp
@@ -187,7 +187,7 @@
" be loaded alongside the base APK at runtime.\n"
" --feature-of\n"
" Builds a split APK that is a feature of the apk specified here. Resources\n"
- " in the base APK can be referenced from the the feature APK.\n"
+ " in the base APK can be referenced from the feature APK.\n"
" --feature-after\n"
" An app can have multiple Feature Split APKs which must be totally ordered.\n"
" If --feature-of is specified, this flag specifies which Feature Split APK\n"
diff --git a/tools/aapt2/Android.bp b/tools/aapt2/Android.bp
index 938a5ed..b054a57 100644
--- a/tools/aapt2/Android.bp
+++ b/tools/aapt2/Android.bp
@@ -262,6 +262,23 @@
"$(genDir)/aapt2_tests " +
"--gtest_output=xml:$(out) " +
">/dev/null 2>&1 ; true",
+ dist: {
+ targets: ["aapt2_run_host_unit_tests"],
+ dir: "gtest",
+ dest: "aapt2_host_unit_tests_result.xml",
+ },
+ arch: {
+ x86: {
+ dist: {
+ suffix: "_x86",
+ },
+ },
+ x86_64: {
+ dist: {
+ suffix: "_x86_64",
+ },
+ },
+ },
}
phony_rule {
diff --git a/tools/codegen/src/com/android/codegen/FileInfo.kt b/tools/codegen/src/com/android/codegen/FileInfo.kt
index a1d0389..cc3a156 100644
--- a/tools/codegen/src/com/android/codegen/FileInfo.kt
+++ b/tools/codegen/src/com/android/codegen/FileInfo.kt
@@ -238,7 +238,7 @@
} else if (classBounds.isDataclass) {
// Insert placeholder for generated code to be inserted for the 1st time
- chunks.last = (chunks.last as Code)
+ chunks[chunks.lastIndex] = (chunks.last() as Code)
.lines
.dropLastWhile { it.isBlank() }
.run {
@@ -286,4 +286,4 @@
.let { addAll(it) }
}
}
-}
\ No newline at end of file
+}
diff --git a/tools/codegen/src/com/android/codegen/Utils.kt b/tools/codegen/src/com/android/codegen/Utils.kt
index 9ceb204..a40bdd7 100644
--- a/tools/codegen/src/com/android/codegen/Utils.kt
+++ b/tools/codegen/src/com/android/codegen/Utils.kt
@@ -137,14 +137,4 @@
cause)
}
-var <T> MutableList<T>.last
- get() = last()
- set(value) {
- if (isEmpty()) {
- add(value)
- } else {
- this[size - 1] = value
- }
- }
-
-inline fun <T> buildList(init: MutableList<T>.() -> Unit) = mutableListOf<T>().apply(init)
\ No newline at end of file
+inline fun <T> buildList(init: MutableList<T>.() -> Unit) = mutableListOf<T>().apply(init)