diff options
916 files changed, 24970 insertions, 5655 deletions
diff --git a/AconfigFlags.bp b/AconfigFlags.bp index 0ee7ace5f661..cd991c70d719 100644 --- a/AconfigFlags.bp +++ b/AconfigFlags.bp @@ -19,10 +19,12 @@ aconfig_srcjars = [ ":android.app.flags-aconfig-java{.generated_srcjars}", ":android.app.smartspace.flags-aconfig-java{.generated_srcjars}", ":android.app.usage.flags-aconfig-java{.generated_srcjars}", + ":android.app.wearable.flags-aconfig-java{.generated_srcjars}", ":android.appwidget.flags-aconfig-java{.generated_srcjars}", ":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}", @@ -67,6 +69,7 @@ aconfig_srcjars = [ ":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}", @@ -1123,3 +1126,35 @@ java_aconfig_library { ], defaults: ["framework-minus-apex-aconfig-java-defaults"], } + +// Wearable Sensing +aconfig_declarations { + name: "android.app.wearable.flags-aconfig", + package: "android.app.wearable", + srcs: ["core/java/android/app/wearable/*.aconfig"], +} + +java_aconfig_library { + name: "android.app.wearable.flags-aconfig-java", + 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"], +} diff --git a/MEMORY_OWNERS b/MEMORY_OWNERS new file mode 100644 index 000000000000..89ce5140d8ea --- /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/service/aconfig/job.aconfig b/apex/jobscheduler/service/aconfig/job.aconfig index 5d65d9d0629f..8a5206fa3f09 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 @@ flag { 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 6550f26436d4..012ede274bc1 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 @@ class JobConcurrencyManager { 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 57467e3cc83d..cea16d6213af 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.ParceledListSlice; 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.text.format.DateUtils; 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; @@ -538,7 +540,9 @@ public class JobSchedulerService extends com.android.server.SystemService 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 +558,8 @@ public class JobSchedulerService extends com.android.server.SystemService 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 +608,8 @@ public class JobSchedulerService extends com.android.server.SystemService sc.onConstantsUpdatedLocked(); } } + + mHandler.sendEmptyMessage(MSG_CHECK_JOB); } @Override @@ -646,8 +654,12 @@ public class JobSchedulerService extends com.android.server.SystemService */ 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 +677,10 @@ public class JobSchedulerService extends com.android.server.SystemService "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 +729,11 @@ public class JobSchedulerService extends com.android.server.SystemService 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 +745,15 @@ public class JobSchedulerService extends com.android.server.SystemService 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 +791,23 @@ public class JobSchedulerService extends com.android.server.SystemService 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 +863,17 @@ public class JobSchedulerService extends com.android.server.SystemService */ 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 +1024,31 @@ public class JobSchedulerService extends com.android.server.SystemService 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 +1096,46 @@ public class JobSchedulerService extends com.android.server.SystemService 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 +1280,11 @@ public class JobSchedulerService extends com.android.server.SystemService 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 +1300,10 @@ public class JobSchedulerService extends com.android.server.SystemService .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 +2954,9 @@ public class JobSchedulerService extends com.android.server.SystemService 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 +3582,7 @@ public class JobSchedulerService extends com.android.server.SystemService } 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 +3610,27 @@ public class JobSchedulerService extends com.android.server.SystemService * 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 +3677,77 @@ public class JobSchedulerService extends com.android.server.SystemService 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 +3787,131 @@ public class JobSchedulerService extends com.android.server.SystemService @GuardedBy("mLock") @VisibleForTesting void postProcessLocked() { - if (unbatchedCount > 0 - || forceBatchedCount >= mConstants.MIN_READY_NON_ACTIVE_JOBS_COUNT) { + 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; + } + + // 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 jobs."); + Slog.d(TAG, "maybeQueueReadyJobsForExecutionLocked: Running " + + jobsToRun + " jobs."); } - noteJobsPending(runnableJobs); - mPendingJobQueue.addAll(runnableJobs); + 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 +3922,10 @@ public class JobSchedulerService extends com.android.server.SystemService @VisibleForTesting void reset() { - forceBatchedCount = 0; - unbatchedCount = 0; runnableJobs.clear(); + mBatches.clear(); + mUnbatchedJobs.clear(); + mUnbatchedJobCount.clear(); } } @@ -5468,8 +5753,14 @@ public class JobSchedulerService extends com.android.server.SystemService 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(); 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 0cf6a7a8a4f6..90b4630e9cff 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 @@ public final class JobSchedulerShellCommand extends BasicShellCommandHandler { 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 4f4096f69ad5..813cf8710ab1 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 @@ package com.android.server.job; import android.annotation.NonNull; import android.annotation.Nullable; +import android.util.ArraySet; import android.util.Pools; import android.util.SparseArray; @@ -96,10 +97,10 @@ class PendingJobQueue { } } - 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/ConnectivityController.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/ConnectivityController.java index f40508302ee3..6ed42ec990ea 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.NET_CAPABILITY_TEMPORARILY_NOT_MET 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.GuardedBy; 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 @@ public final class ConnectivityController extends RestrictingController implemen @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 @@ public final class ConnectivityController extends RestrictingController implemen 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 @@ public final class ConnectivityController extends RestrictingController implemen } } + @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 @@ public final class ConnectivityController extends RestrictingController implemen 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 @@ public final class ConnectivityController extends RestrictingController implemen // 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 @@ public final class ConnectivityController extends RestrictingController implemen } @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 @@ public final class ConnectivityController extends RestrictingController implemen } /** + * 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 @@ public final class ConnectivityController extends RestrictingController implemen 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 @@ public final class ConnectivityController extends RestrictingController implemen } }; + /** 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 @@ public final class ConnectivityController extends RestrictingController implemen } } 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 @@ public final class ConnectivityController extends RestrictingController implemen @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 final class ConnectivityController extends RestrictingController implemen 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 @@ public final class ConnectivityController extends RestrictingController implemen 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 @@ public final class ConnectivityController extends RestrictingController implemen 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 @@ public final class ConnectivityController extends RestrictingController implemen 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 @@ public final class ConnectivityController extends RestrictingController implemen 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/api/Android.bp b/api/Android.bp index b3b18b66e097..27c372a4d4d1 100644 --- a/api/Android.bp +++ b/api/Android.bp @@ -115,6 +115,7 @@ combined_apis { "framework-pdf", "framework-permission", "framework-permission-s", + "framework-profiling", "framework-scheduling", "framework-sdkextensions", "framework-statsd", @@ -222,7 +223,7 @@ genrule { 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 @@ genrule { 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 @@ genrule { 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 @@ genrule { 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", ], } diff --git a/api/ApiDocs.bp b/api/ApiDocs.bp index 7ae3224e7500..e8fcf4b2b32d 100644 --- a/api/ApiDocs.bp +++ b/api/ApiDocs.bp @@ -67,6 +67,7 @@ stubs_defaults { ":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 @@ droiddoc { name: "offline-sdk-docs", defaults: ["framework-docs-default"], srcs: [ - ":framework-doc-stubs", + ":framework-doc-stubs{.exportable}", ], hdf: [ "android.whichdoc offline", @@ -241,7 +242,7 @@ droiddoc { 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 @@ droiddoc { 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 { droiddoc { name: "ds-docs-kt", srcs: [ - ":framework-doc-stubs", + ":framework-doc-stubs{.exportable}", ], flags: [ "-noJdkLink", @@ -373,7 +374,7 @@ droiddoc { name: "ds-static-docs", defaults: ["framework-docs-default"], srcs: [ - ":framework-doc-stubs", + ":framework-doc-stubs{.exportable}", ], hdf: [ "android.whichdoc online", @@ -390,7 +391,7 @@ droiddoc { 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 59c01284ab27..852abdfdf602 100644 --- a/api/StubLibraries.bp +++ b/api/StubLibraries.bp @@ -961,7 +961,7 @@ java_api_library { 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 { 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 { 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 { 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/boot/Android.bp b/boot/Android.bp index 228d060bf9cf..cdfa7c80bc93 100644 --- a/boot/Android.bp +++ b/boot/Android.bp @@ -122,6 +122,10 @@ custom_platform_bootclasspath { 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 787055c8cd89..25d3a341bf6e 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 @@ public class Device { 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/current.txt b/core/api/current.txt index 0a628775fd1e..d410686c9ea1 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -13,6 +13,7 @@ package android { field public static final String ACCESS_CHECKIN_PROPERTIES = "android.permission.ACCESS_CHECKIN_PROPERTIES"; field public static final String ACCESS_COARSE_LOCATION = "android.permission.ACCESS_COARSE_LOCATION"; field public static final String ACCESS_FINE_LOCATION = "android.permission.ACCESS_FINE_LOCATION"; + field @FlaggedApi("android.multiuser.enable_permission_to_access_hidden_profiles") public static final String ACCESS_HIDDEN_PROFILES = "android.permission.ACCESS_HIDDEN_PROFILES"; field public static final String ACCESS_LOCATION_EXTRA_COMMANDS = "android.permission.ACCESS_LOCATION_EXTRA_COMMANDS"; field public static final String ACCESS_MEDIA_LOCATION = "android.permission.ACCESS_MEDIA_LOCATION"; field public static final String ACCESS_NETWORK_STATE = "android.permission.ACCESS_NETWORK_STATE"; @@ -89,6 +90,7 @@ package android { 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"; @@ -504,6 +506,7 @@ package android { 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 @@ -1794,6 +1797,7 @@ package android { 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 @@ -6355,6 +6359,7 @@ package android.app { field public static final String CATEGORY_STOPWATCH = "stopwatch"; field public static final String CATEGORY_SYSTEM = "sys"; field public static final String CATEGORY_TRANSPORT = "transport"; + field @FlaggedApi("android.app.category_voicemail") public static final String CATEGORY_VOICEMAIL = "voicemail"; field public static final String CATEGORY_WORKOUT = "workout"; field @ColorInt public static final int COLOR_DEFAULT = 0; // 0x0 field @NonNull public static final android.os.Parcelable.Creator<android.app.Notification> CREATOR; @@ -6877,6 +6882,7 @@ package android.app { 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(); @@ -6896,6 +6902,7 @@ package android.app { 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(); @@ -9509,8 +9516,8 @@ package android.appwidget { 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); @@ -13836,6 +13843,7 @@ package android.content.res { 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 @@ -15573,6 +15581,7 @@ package android.graphics { method public boolean clipRect(float, float, float, float); method public boolean clipRect(int, int, int, int); method public void concat(@Nullable android.graphics.Matrix); + method @FlaggedApi("com.android.graphics.hwui.flags.matrix_44") public void concat44(@Nullable android.graphics.Matrix44); method public void disableZ(); method public void drawARGB(int, int, int, int); method public void drawArc(@NonNull android.graphics.RectF, float, float, boolean, @NonNull android.graphics.Paint); @@ -16221,6 +16230,24 @@ package android.graphics { enum_constant public static final android.graphics.Matrix.ScaleToFit START; } + @FlaggedApi("com.android.graphics.hwui.flags.matrix_44") public class Matrix44 { + ctor @FlaggedApi("com.android.graphics.hwui.flags.matrix_44") public Matrix44(); + ctor @FlaggedApi("com.android.graphics.hwui.flags.matrix_44") public Matrix44(@NonNull android.graphics.Matrix); + method @FlaggedApi("com.android.graphics.hwui.flags.matrix_44") @NonNull public android.graphics.Matrix44 concat(@NonNull android.graphics.Matrix44); + method @FlaggedApi("com.android.graphics.hwui.flags.matrix_44") public float get(int, int); + method @FlaggedApi("com.android.graphics.hwui.flags.matrix_44") public void getValues(@NonNull float[]); + method @FlaggedApi("com.android.graphics.hwui.flags.matrix_44") public boolean invert(); + method @FlaggedApi("com.android.graphics.hwui.flags.matrix_44") public boolean isIdentity(); + method @FlaggedApi("com.android.graphics.hwui.flags.matrix_44") @NonNull public float[] map(float, float, float, float); + method @FlaggedApi("com.android.graphics.hwui.flags.matrix_44") public void map(float, float, float, float, @NonNull float[]); + method @FlaggedApi("com.android.graphics.hwui.flags.matrix_44") public void reset(); + method @FlaggedApi("com.android.graphics.hwui.flags.matrix_44") @NonNull public android.graphics.Matrix44 rotate(float, float, float, float); + method @FlaggedApi("com.android.graphics.hwui.flags.matrix_44") @NonNull public android.graphics.Matrix44 scale(float, float, float); + method @FlaggedApi("com.android.graphics.hwui.flags.matrix_44") public void set(int, int, float); + method @FlaggedApi("com.android.graphics.hwui.flags.matrix_44") public void setValues(@NonNull float[]); + method @FlaggedApi("com.android.graphics.hwui.flags.matrix_44") @NonNull public android.graphics.Matrix44 translate(float, float, float); + } + public class Mesh { ctor public Mesh(@NonNull android.graphics.MeshSpecification, int, @NonNull java.nio.Buffer, int, @NonNull android.graphics.RectF); ctor public Mesh(@NonNull android.graphics.MeshSpecification, int, @NonNull java.nio.Buffer, int, @NonNull java.nio.ShortBuffer, @NonNull android.graphics.RectF); @@ -19129,11 +19156,13 @@ package android.hardware.camera2 { } 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); @@ -20244,6 +20273,7 @@ package android.hardware.input { 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); @@ -21174,6 +21204,7 @@ package android.media { method public int getStreamMinVolume(int); method public int getStreamVolume(int); method public float getStreamVolumeDb(int, int, int); + method @FlaggedApi("android.media.audio.supported_device_types_api") @NonNull public java.util.Set<java.lang.Integer> getSupportedDeviceTypes(int); method @NonNull public java.util.List<android.media.AudioMixerAttributes> getSupportedMixerAttributes(@NonNull android.media.AudioDeviceInfo); method @Deprecated public int getVibrateSetting(int); method public int getVolumeGroupIdForAttributes(@NonNull android.media.AudioAttributes); @@ -27295,6 +27326,7 @@ package android.media.tv { 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 @@ -27386,6 +27418,7 @@ package android.media.tv { 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); @@ -27393,6 +27426,7 @@ package android.media.tv { 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(); @@ -27527,6 +27561,7 @@ package android.media.tv { 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); @@ -27539,6 +27574,7 @@ package android.media.tv { method public void setTvMessageEnabled(int, 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(); @@ -27588,6 +27624,59 @@ package android.media.tv { 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(); + } + + 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 @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 @CallSuper public void layoutSurface(int, int, int, int); + 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 abstract void onRelease(); + method public abstract boolean onSetSurface(@Nullable android.view.Surface); + method public void onSurfaceChanged(int, int, int); + method public boolean onTouchEvent(@NonNull android.view.MotionEvent); + method public boolean onTrackballEvent(@NonNull android.view.MotionEvent); + } + + @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 onAttachedToWindow(); + method public void onDetachedFromWindow(); + method public void onLayout(boolean, int, int, int, int); + method public void onMeasure(int, int); + method public void onVisibilityChanged(@NonNull android.view.View, int); + method public void prepareAdService(@NonNull String, @NonNull String); } } @@ -27725,6 +27814,7 @@ package android.media.tv.interactive { 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); @@ -27759,6 +27849,7 @@ package android.media.tv.interactive { 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 @CallSuper public void requestStartRecording(@NonNull String, @Nullable android.net.Uri); method @CallSuper public void requestStopRecording(@NonNull String); @@ -27823,6 +27914,7 @@ package android.media.tv.interactive { 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); @@ -27858,6 +27950,7 @@ package android.media.tv.interactive { 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 public void onRequestStartRecording(@NonNull String, @NonNull String, @Nullable android.net.Uri); method public void onRequestStopRecording(@NonNull String, @NonNull String); @@ -43445,13 +43538,44 @@ package android.telephony { } 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 { @@ -44523,6 +44647,7 @@ package android.telephony { method @Nullable public android.telephony.CellIdentity getCellIdentity(); method public int getDomain(); method @Nullable public String getRegisteredPlmn(); + method @FlaggedApi("com.android.internal.telephony.flags.network_registration_info_reject_cause") public int getRejectCause(); method public int getTransportType(); method public boolean isNetworkRegistered(); method public boolean isNetworkRoaming(); @@ -45133,13 +45258,13 @@ package android.telephony { 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(); @@ -50413,6 +50538,10 @@ package android.view { 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(); @@ -53761,6 +53890,7 @@ package android.view { 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(); @@ -53770,11 +53900,13 @@ package android.view { 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"; @@ -53789,6 +53921,8 @@ package android.view { 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 { @@ -59502,7 +59636,7 @@ package android.widget { 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/module-lib-current.txt b/core/api/module-lib-current.txt index 55ed1f559f51..783bebd23df3 100644 --- a/core/api/module-lib-current.txt +++ b/core/api/module-lib-current.txt @@ -132,6 +132,11 @@ package android.content.pm { 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 @@ package android.media { 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(); @@ -378,6 +384,10 @@ package android.os { 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 +437,8 @@ package android.os { 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/system-current.txt b/core/api/system-current.txt index 3b18dac88c5f..66e03db5923b 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -287,7 +287,7 @@ package android { 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"; @@ -414,6 +414,7 @@ package android { field public static final String WRITE_OBB = "android.permission.WRITE_OBB"; field public static final String WRITE_SECURITY_LOG = "android.permission.WRITE_SECURITY_LOG"; field public static final String WRITE_SMS = "android.permission.WRITE_SMS"; + field @FlaggedApi("android.provider.user_keys") public static final String WRITE_VERIFICATION_STATE_E2EE_CONTACT_KEYS = "android.permission.WRITE_VERIFICATION_STATE_E2EE_CONTACT_KEYS"; } public static final class Manifest.permission_group { @@ -3157,6 +3158,7 @@ package android.app.wearable { 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_unsupported_operation_status_code") public static final int STATUS_UNSUPPORTED_OPERATION = 6; // 0x6 field public static final int STATUS_WEARABLE_UNAVAILABLE = 4; // 0x4 } @@ -3209,6 +3211,11 @@ package android.companion { package android.companion.virtual { + public final class VirtualDevice implements android.os.Parcelable { + method @FlaggedApi("android.companion.virtual.flags.vdm_public_apis") public boolean hasCustomAudioInputSupport(); + method @FlaggedApi("android.companion.virtual.flags.vdm_public_apis") public boolean hasCustomCameraSupport(); + } + public final class VirtualDeviceManager { method @NonNull @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public android.companion.virtual.VirtualDeviceManager.VirtualDevice createVirtualDevice(int, @NonNull android.companion.virtual.VirtualDeviceParams); method @FlaggedApi("android.companion.virtual.flags.persistent_device_id_api") @NonNull public java.util.Set<java.lang.String> getAllPersistentDeviceIds(); @@ -3984,6 +3991,7 @@ package android.content.pm { 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(); @@ -4036,6 +4044,10 @@ package android.content.pm { 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 @@ -4385,6 +4397,59 @@ package android.content.rollback { 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 { + method public int describeContents(); + method @NonNull public String getAppPackageName(); + 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(); @@ -4394,6 +4459,32 @@ package android.credentials.selection { 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(); @@ -4403,6 +4494,26 @@ package android.credentials.selection { 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 @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.credentials.selection.RequestToken getRequestToken(); + method @NonNull public String getType(); + method public boolean hasPermissionToOverrideDefault(); + 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); @@ -4574,6 +4685,7 @@ package android.hardware.camera2.extension { 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); @@ -10056,6 +10168,7 @@ package android.nfc.cardemulation { @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); @@ -10069,6 +10182,7 @@ package android.nfc.cardemulation { 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); @@ -11298,6 +11412,12 @@ package android.provider { field public static final int ERROR_UNKNOWN = 0; // 0x0 } + @FlaggedApi("android.provider.user_keys") public class ContactKeysManager { + method @RequiresPermission(allOf={android.Manifest.permission.WRITE_VERIFICATION_STATE_E2EE_CONTACT_KEYS, android.Manifest.permission.WRITE_CONTACTS}) public boolean updateContactKeyLocalVerificationState(@NonNull String, @NonNull String, @NonNull String, @NonNull String, int); + method @RequiresPermission(allOf={android.Manifest.permission.WRITE_VERIFICATION_STATE_E2EE_CONTACT_KEYS, android.Manifest.permission.WRITE_CONTACTS}) public boolean updateContactKeyRemoteVerificationState(@NonNull String, @NonNull String, @NonNull String, @NonNull String, int); + method @RequiresPermission(allOf={android.Manifest.permission.WRITE_VERIFICATION_STATE_E2EE_CONTACT_KEYS, android.Manifest.permission.WRITE_CONTACTS}) public boolean updateSelfKeyRemoteVerificationState(@NonNull String, @NonNull String, @NonNull String, int); + } + @Deprecated public static final class ContactsContract.MetadataSync implements android.provider.BaseColumns android.provider.ContactsContract.MetadataSyncColumns { field @Deprecated public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/contact_metadata"; field @Deprecated public static final String CONTENT_TYPE = "vnd.android.cursor.dir/contact_metadata"; @@ -13436,6 +13556,7 @@ package android.telecom { 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(); @@ -13450,7 +13571,9 @@ package android.telecom { 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"; @@ -13718,6 +13841,7 @@ package android.telecom { 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"; @@ -14050,6 +14174,73 @@ package android.telephony { method @NonNull public android.telephony.DataThrottlingRequest.Builder setDataThrottlingAction(int); } + @FlaggedApi("com.android.internal.telephony.flags.use_oem_domain_selection_service") public class DomainSelectionService extends android.app.Service { + ctor public DomainSelectionService(); + method public void onBarringInfoUpdated(int, int, @NonNull android.telephony.BarringInfo); + method @Nullable public android.os.IBinder onBind(@Nullable android.content.Intent); + method @NonNull public java.util.concurrent.Executor onCreateExecutor(); + method public 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.EmergencyRegResult getEmergencyRegResult(); + 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(@NonNull android.net.Uri); + method @NonNull public android.telephony.DomainSelectionService.SelectionAttributes.Builder setCallId(@NonNull 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 setEmergencyRegResult(@NonNull android.telephony.EmergencyRegResult); + method @NonNull public android.telephony.DomainSelectionService.SelectionAttributes.Builder setExitedFromAirplaneMode(boolean); + method @NonNull public android.telephony.DomainSelectionService.SelectionAttributes.Builder setPsDisconnectCause(@NonNull 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 EmergencyRegResult 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.EmergencyRegResult> CREATOR; + } + public final class ImsiEncryptionInfo implements android.os.Parcelable { method public int describeContents(); method @Nullable public String getKeyIdentifier(); @@ -14123,7 +14314,6 @@ package android.telephony { method @Nullable public android.telephony.DataSpecificRegistrationInfo getDataSpecificInfo(); method public int getNetworkRegistrationState(); method @Deprecated public int getRegistrationState(); - method public int getRejectCause(); method public int getRoamingType(); method public boolean isEmergencyEnabled(); method public void writeToParcel(android.os.Parcel, int); @@ -14321,6 +14511,8 @@ package android.telephony { 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 @@ -14568,7 +14760,7 @@ package android.telephony { 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); @@ -14803,6 +14995,7 @@ package android.telephony { 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(); @@ -14822,7 +15015,7 @@ package android.telephony { 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); @@ -15001,19 +15194,19 @@ package android.telephony { 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 { @@ -15027,7 +15220,7 @@ package android.telephony { @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 { @@ -15048,6 +15241,13 @@ package android.telephony { 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(); @@ -15103,6 +15303,11 @@ package android.telephony { 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.EmergencyRegResult>); + } + } package android.telephony.cdma { diff --git a/core/api/test-current.txt b/core/api/test-current.txt index 77add41f6805..a21d7c464bf6 100644 --- a/core/api/test-current.txt +++ b/core/api/test-current.txt @@ -1267,22 +1267,6 @@ package android.credentials { 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); @@ -1298,6 +1282,10 @@ package android.credentials.selection { field public static final int RESULT_CODE_DIALOG_USER_CANCELED = 0; // 0x0 } + @FlaggedApi("android.credentials.flags.configurable_selector_ui_enabled") public final class CancelSelectionRequest implements android.os.Parcelable { + ctor @FlaggedApi("android.credentials.flags.configurable_selector_ui_enabled") public CancelSelectionRequest(@NonNull android.os.IBinder, boolean, @NonNull String); + } + @FlaggedApi("android.credentials.flags.configurable_selector_ui_enabled") public final class CreateCredentialProviderData extends android.credentials.selection.ProviderData implements android.os.Parcelable { ctor public CreateCredentialProviderData(@NonNull String, @NonNull java.util.List<android.credentials.selection.Entry>, @Nullable android.credentials.selection.Entry); method @Nullable public android.credentials.selection.Entry getRemoteEntry(); @@ -1317,19 +1305,6 @@ package android.credentials.selection { 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 +1332,8 @@ package android.credentials.selection { } @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 +1347,12 @@ package android.credentials.selection { } @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>); + 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); + } + + @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 +1708,7 @@ package android.hardware.input { 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 +1718,7 @@ package android.hardware.input { 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 } } @@ -2882,6 +2847,10 @@ package android.provider { 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"; } @@ -3309,7 +3278,6 @@ package android.telephony { 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/java/android/app/ActivityManager.java b/core/java/android/app/ActivityManager.java index 6285eb3b2096..084c71f47603 100644 --- a/core/java/android/app/ActivityManager.java +++ b/core/java/android/app/ActivityManager.java @@ -147,6 +147,7 @@ import java.util.function.Consumer; * </p> */ @SystemService(Context.ACTIVITY_SERVICE) +@android.ravenwood.annotation.RavenwoodKeepPartialClass public class ActivityManager { private static String TAG = "ActivityManager"; @@ -966,6 +967,7 @@ public class ActivityManager { * 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 @@ public class ActivityManager { } /** @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 @@ public class ActivityManager { * 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 @@ public class ActivityManager { } /** @hide */ + @android.ravenwood.annotation.RavenwoodKeep public static String getCapabilitiesSummary(@ProcessCapability int caps) { final StringBuilder sb = new StringBuilder(); printCapabilitiesSummary(sb, caps); @@ -1018,6 +1023,7 @@ public class ActivityManager { * @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 class ActivityManager { 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 @@ public class ActivityManager { 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 @@ public class ActivityManager { * 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 class ActivityManager { 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 @@ public class ActivityManager { * 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 @@ public class ActivityManager { } } + /** @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 @@ public class ActivityManager { "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 @@ public class ActivityManager { } } + /** @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 @@ public class ActivityManager { /** * @hide */ + @android.ravenwood.annotation.RavenwoodReplace public static boolean isSystemReady() { if (!sSystemReady) { if (ActivityThread.isSystem()) { @@ -5334,6 +5374,12 @@ public class ActivityManager { return sSystemReady; } + /** @hide */ + public static boolean isSystemReady$ravenwood() { + // Ravenwood environment is always considered as booted and ready + return true; + } + /** * @hide */ @@ -5661,11 +5707,13 @@ public class ActivityManager { } /** @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) { diff --git a/core/java/android/app/ActivityOptions.java b/core/java/android/app/ActivityOptions.java index 111895e3053b..1edf4bdb0d7e 100644 --- a/core/java/android/app/ActivityOptions.java +++ b/core/java/android/app/ActivityOptions.java @@ -1974,8 +1974,8 @@ public class ActivityOptions extends ComponentOptions { binder = new Binder(descriptor); } - private LaunchCookie(Parcel in) { - this.binder = in.readStrongBinder(); + private LaunchCookie(IBinder binder) { + this.binder = binder; } /** @hide */ @@ -1996,7 +1996,11 @@ public class ActivityOptions extends ComponentOptions { /** @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 @@ public class ActivityOptions extends ComponentOptions { @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 5d2a26e13ee7..4c54b03bd6d6 100644 --- a/core/java/android/app/ActivityThread.java +++ b/core/java/android/app/ActivityThread.java @@ -303,7 +303,7 @@ public final class ActivityThread extends ClientTransactionHandler public static final boolean DEBUG_MEMORY_TRIM = false; private static final boolean DEBUG_PROVIDER = false; public static final boolean DEBUG_ORDER = false; - private static final boolean DEBUG_APP_INFO = true; + private static final boolean DEBUG_APP_INFO = false; private static final long MIN_TIME_BETWEEN_GCS = 5*1000; /** * The delay to release the provider when it has no more references. It reduces the number of diff --git a/core/java/android/app/AppOpsManager.java b/core/java/android/app/AppOpsManager.java index 79070595e831..0dbce97c978b 100644 --- a/core/java/android/app/AppOpsManager.java +++ b/core/java/android/app/AppOpsManager.java @@ -1550,7 +1550,7 @@ public class AppOpsManager { AppProtoEnums.APP_OP_READ_SYSTEM_GRAMMATICAL_GENDER; /** - * Allows an app whose primary use case is to backup or sync content to run longer jobs. + * Allows an app with a major use case of backing-up or syncing content to run longer jobs. * * @hide */ diff --git a/core/java/android/app/ApplicationPackageManager.java b/core/java/android/app/ApplicationPackageManager.java index 4f1db7d3784a..d8aded40df7b 100644 --- a/core/java/android/app/ApplicationPackageManager.java +++ b/core/java/android/app/ApplicationPackageManager.java @@ -1269,6 +1269,22 @@ public class ApplicationPackageManager extends PackageManager { 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/IActivityTaskManager.aidl b/core/java/android/app/IActivityTaskManager.aidl index d54074818b41..e2e2f1d3401e 100644 --- a/core/java/android/app/IActivityTaskManager.aidl +++ b/core/java/android/app/IActivityTaskManager.aidl @@ -225,7 +225,7 @@ interface IActivityTaskManager { 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/KeyguardManager.java b/core/java/android/app/KeyguardManager.java index 8b8576a0b25e..b5e507458a30 100644 --- a/core/java/android/app/KeyguardManager.java +++ b/core/java/android/app/KeyguardManager.java @@ -721,28 +721,28 @@ public class KeyguardManager { /** * 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 a81ad3c429ea..0a34d36f204e 100644 --- a/core/java/android/app/Notification.java +++ b/core/java/android/app/Notification.java @@ -1061,6 +1061,12 @@ public class Notification implements Parcelable public static final String CATEGORY_MISSED_CALL = "missed_call"; /** + * Notification category: voicemail. + */ + @FlaggedApi(Flags.FLAG_CATEGORY_VOICEMAIL) + public static final String CATEGORY_VOICEMAIL = "voicemail"; + + /** * One of the predefined notification categories (see the <code>CATEGORY_*</code> constants) * that best describes this Notification. May be used by the system for ranking and filtering. */ @@ -4643,13 +4649,24 @@ public class Notification implements Parcelable * 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. @@ -5940,6 +5957,12 @@ public class Notification implements Parcelable // 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 (CallStyle.USE_NEW_ACTION_LAYOUT) { + 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) { @@ -6415,7 +6438,15 @@ public class Notification implements Parcelable // 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 && CallStyle.USE_NEW_ACTION_LAYOUT) { + 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) { @@ -6432,7 +6463,14 @@ public class Notification implements Parcelable button.setColorStateList(R.id.action0, "setButtonBackground", ColorStateList.valueOf(buttonFillColor)); if (p.mCallStyleActions) { - button.setImageViewIcon(R.id.action0, action.getIcon()); + if (CallStyle.USE_NEW_ACTION_LAYOUT) { + 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 = @@ -9559,6 +9597,15 @@ public class Notification implements Parcelable * </pre> */ public static class CallStyle extends Style { + /** + * @hide + */ + public static final boolean USE_NEW_ACTION_LAYOUT = false; + + /** + * @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 15d692ab1673..ab5395e81aa3 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.media.RingtoneManager; 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 org.xmlpull.v1.XmlSerializer; 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 @@ public final class NotificationChannel implements Parcelable { 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 @@ public final class NotificationChannel implements Parcelable { 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 @@ public final class NotificationChannel implements Parcelable { 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 @@ public final class NotificationChannel implements Parcelable { 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 @@ public final class NotificationChannel implements Parcelable { /** * 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 final class NotificationChannel implements Parcelable { */ 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 @@ public final class NotificationChannel implements Parcelable { * 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 @@ public final class NotificationChannel implements Parcelable { 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 @@ public final class NotificationChannel implements Parcelable { 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 @@ public final class NotificationChannel implements Parcelable { 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 @@ public final class NotificationChannel implements Parcelable { 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 @@ public final class NotificationChannel implements Parcelable { && 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 @@ public final class NotificationChannel implements Parcelable { 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 @@ public final class NotificationChannel implements Parcelable { + ", 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 @@ public final class NotificationChannel implements Parcelable { } 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 edf0a46b1433..6a114f908a22 100644 --- a/core/java/android/app/QueuedWork.java +++ b/core/java/android/app/QueuedWork.java @@ -114,6 +114,22 @@ public class QueuedWork { } /** + * 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 +172,13 @@ public class QueuedWork { 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 +269,7 @@ public class QueuedWork { 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/SystemServiceRegistry.java b/core/java/android/app/SystemServiceRegistry.java index b21b0f3bcca8..ba9c8952892b 100644 --- a/core/java/android/app/SystemServiceRegistry.java +++ b/core/java/android/app/SystemServiceRegistry.java @@ -1618,6 +1618,19 @@ public final class SystemServiceRegistry { return new ContactKeysManager(ctx); }}); + // DO NOT do a flag check like this unless the flag is read-only. + // (because this code is executed during preload in zygote.) + // If the flag is mutable, the check should be inside CachedServiceFetcher. + if (Flags.bicClient()) { + registerService(Context.BACKGROUND_INSTALL_CONTROL_SERVICE, + BackgroundInstallControlManager.class, + new CachedServiceFetcher<BackgroundInstallControlManager>() { + @Override + public BackgroundInstallControlManager createService(ContextImpl ctx) { + return new BackgroundInstallControlManager(ctx); + } + }); + } sInitializing = true; try { // Note: the following functions need to be @SystemApis, once they become mainline diff --git a/core/java/android/app/UiModeManager.java b/core/java/android/app/UiModeManager.java index a27132872521..a045eae9e108 100644 --- a/core/java/android/app/UiModeManager.java +++ b/core/java/android/app/UiModeManager.java @@ -16,6 +16,8 @@ package android.app; +import static android.app.Flags.enableNightModeCache; + import android.annotation.CallbackExecutor; import android.annotation.FlaggedApi; import android.annotation.FloatRange; @@ -31,6 +33,7 @@ import android.compat.annotation.UnsupportedAppUsage; import android.content.Context; import android.content.res.Configuration; import android.os.Binder; +import android.os.IpcDataCache; import android.os.RemoteException; import android.os.ServiceManager; import android.os.ServiceManager.ServiceNotFoundException; @@ -874,6 +877,51 @@ public class UiModeManager { } } + private Integer getNightModeFromServer() { + try { + if (sGlobals != null) { + return sGlobals.mService.getNightMode(); + } + return -1; + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + + /** + * Retrieve the night mode for the user. + */ + private final IpcDataCache.QueryHandler<Void, Integer> mNightModeQuery = + new IpcDataCache.QueryHandler<>() { + + @Override + @NonNull + public Integer apply(Void query) { + return getNightModeFromServer(); + } + }; + + private static final String NIGHT_MODE_API = "getNightMode"; + + /** + * Cache the night mode for a user. + */ + private final IpcDataCache<Void, Integer> mNightModeCache = + new IpcDataCache<>(1, IpcDataCache.MODULE_SYSTEM, + NIGHT_MODE_API, /* cacheName= */ "NightModeCache", mNightModeQuery); + + /** + * Invalidate the night mode cache. + * + * @hide + */ + @FlaggedApi(Flags.FLAG_ENABLE_NIGHT_MODE_CACHE) + public static void invalidateNightModeCache() { + IpcDataCache.invalidateCache(IpcDataCache.MODULE_SYSTEM, + NIGHT_MODE_API); + } + /** * Returns the currently configured night mode. * <p> @@ -890,14 +938,11 @@ public class UiModeManager { * @see #setNightMode(int) */ public @NightMode int getNightMode() { - if (sGlobals != null) { - try { - return sGlobals.mService.getNightMode(); - } catch (RemoteException e) { - throw e.rethrowFromSystemServer(); - } + if (enableNightModeCache()) { + return mNightModeCache.query(null); + } else { + return getNightModeFromServer(); } - return -1; } /** diff --git a/core/java/android/app/contextualsearch/OWNERS b/core/java/android/app/contextualsearch/OWNERS new file mode 100644 index 000000000000..0c2612c9957b --- /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 d11c6c50da3b..c40b23ed3cd0 100644 --- a/core/java/android/app/notification.aconfig +++ b/core/java/android/app/notification.aconfig @@ -43,3 +43,17 @@ flag { description: "Fixes the behavior of KeyguardManager#setPrivateNotificationsAllowed()" bug: "309920145" } + +flag { + name: "category_voicemail" + namespace: "wear_sysui" + 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" +}
\ 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 5e5526802fe9..6357a20f592b 100644 --- a/core/java/android/app/servertransaction/ClientTransaction.java +++ b/core/java/android/app/servertransaction/ClientTransaction.java @@ -298,6 +298,33 @@ public class ClientTransaction implements Parcelable, ObjectPoolItem { 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/TransactionExecutor.java b/core/java/android/app/servertransaction/TransactionExecutor.java index ba940770898a..406e00a84710 100644 --- a/core/java/android/app/servertransaction/TransactionExecutor.java +++ b/core/java/android/app/servertransaction/TransactionExecutor.java @@ -97,6 +97,10 @@ public class TransactionExecutor { 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/ui_mode_manager.aconfig b/core/java/android/app/ui_mode_manager.aconfig new file mode 100644 index 000000000000..1ae5264a7e8e --- /dev/null +++ b/core/java/android/app/ui_mode_manager.aconfig @@ -0,0 +1,8 @@ +package: "android.app" + +flag { + namespace: "system_performance" + name: "enable_night_mode_cache" + description: "Enables the use of binder caching for system night mode." + bug: "255999432" +}
\ No newline at end of file diff --git a/core/java/android/app/wearable/WearableSensingManager.java b/core/java/android/app/wearable/WearableSensingManager.java index f1ca0864a6dd..eca0039c20f4 100644 --- a/core/java/android/app/wearable/WearableSensingManager.java +++ b/core/java/android/app/wearable/WearableSensingManager.java @@ -18,6 +18,7 @@ package android.app.wearable; import android.Manifest; import android.annotation.CallbackExecutor; +import android.annotation.FlaggedApi; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; @@ -99,6 +100,13 @@ public class WearableSensingManager { */ public static final int STATUS_ACCESS_DENIED = 5; + /** + * 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; + /** @hide */ @IntDef(prefix = { "STATUS_" }, value = { STATUS_UNKNOWN, @@ -106,7 +114,8 @@ public class WearableSensingManager { STATUS_UNSUPPORTED, STATUS_SERVICE_UNAVAILABLE, STATUS_WEARABLE_UNAVAILABLE, - STATUS_ACCESS_DENIED + STATUS_ACCESS_DENIED, + STATUS_UNSUPPORTED_OPERATION }) @Retention(RetentionPolicy.SOURCE) public @interface StatusCode {} diff --git a/core/java/android/app/wearable/flags.aconfig b/core/java/android/app/wearable/flags.aconfig new file mode 100644 index 000000000000..5e8bdb5a2997 --- /dev/null +++ b/core/java/android/app/wearable/flags.aconfig @@ -0,0 +1,29 @@ +package: "android.app.wearable" + +flag { + name: "enable_unsupported_operation_status_code" + 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 6204edc5b2e8..eb82e1fbb16c 100644 --- a/core/java/android/appwidget/AppWidgetManager.java +++ b/core/java/android/appwidget/AppWidgetManager.java @@ -822,18 +822,7 @@ public class AppWidgetManager { * * @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 @@ public class AppWidgetManager { * * @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/companion/virtual/VirtualDevice.java b/core/java/android/companion/virtual/VirtualDevice.java index d0c8be6ca896..97fa2ba2638d 100644 --- a/core/java/android/companion/virtual/VirtualDevice.java +++ b/core/java/android/companion/virtual/VirtualDevice.java @@ -17,11 +17,14 @@ package android.companion.virtual; import static android.companion.virtual.VirtualDeviceParams.DEVICE_POLICY_CUSTOM; +import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_AUDIO; +import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_CAMERA; import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_SENSORS; import android.annotation.FlaggedApi; import android.annotation.NonNull; import android.annotation.Nullable; +import android.annotation.SystemApi; import android.companion.virtual.flags.Flags; import android.content.Context; import android.os.Parcel; @@ -164,6 +167,44 @@ public final class VirtualDevice implements Parcelable { } } + /** + * Returns whether this device may have custom audio input device. + * + * @hide + */ + @SystemApi + @FlaggedApi(Flags.FLAG_VDM_PUBLIC_APIS) + public boolean hasCustomAudioInputSupport() { + try { + return mVirtualDevice.getDevicePolicy(POLICY_TYPE_AUDIO) == DEVICE_POLICY_CUSTOM; + // TODO(b/291735254): also check for a custom audio injection mix for this device id. + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Returns whether this device may have custom cameras. + * + * <p>Returning {@code true} does not necessarily mean that this device has cameras, it only + * means that a {@link android.hardware.camera2.CameraManager} instance created from a + * {@link Context} associated with this device will return this device's cameras, if any.</p> + * + * @see Context#getDeviceId() + * @see Context#createDeviceContext(int) + * + * @hide + */ + @SystemApi + @FlaggedApi(Flags.FLAG_VDM_PUBLIC_APIS) + public boolean hasCustomCameraSupport() { + try { + return mVirtualDevice.getDevicePolicy(POLICY_TYPE_CAMERA) == DEVICE_POLICY_CUSTOM; + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + @Override public int describeContents() { return 0; diff --git a/core/java/android/companion/virtual/VirtualDeviceInternal.java b/core/java/android/companion/virtual/VirtualDeviceInternal.java index c1e443d1729d..39f6de75928a 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.audio.VirtualAudioDevice; 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 @@ public class VirtualDeviceInternal { @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 d26890faee79..ab8db6e59ddb 100644 --- a/core/java/android/companion/virtual/flags/flags.aconfig +++ b/core/java/android/companion/virtual/flags/flags.aconfig @@ -21,3 +21,11 @@ flag { 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/AttributionSource.java b/core/java/android/content/AttributionSource.java index bde562dbf95b..af1301140358 100644 --- a/core/java/android/content/AttributionSource.java +++ b/core/java/android/content/AttributionSource.java @@ -409,9 +409,10 @@ public final class AttributionSource implements Parcelable { "packageName = " + mAttributionSourceState.packageName + ", " + "attributionTag = " + mAttributionSourceState.attributionTag + ", " + "token = " + mAttributionSourceState.token + ", " + + "deviceId = " + mAttributionSourceState.deviceId + ", " + "next = " + (mAttributionSourceState.next != null - && mAttributionSourceState.next.length > 0 - ? mAttributionSourceState.next[0] : null) + + && mAttributionSourceState.next.length > 0 + ? new AttributionSource(mAttributionSourceState.next[0]).toString() : null) + " }"; } return super.toString(); diff --git a/core/java/android/content/ContentProvider.java b/core/java/android/content/ContentProvider.java index e9b94c9f5791..87fb84331af3 100644 --- a/core/java/android/content/ContentProvider.java +++ b/core/java/android/content/ContentProvider.java @@ -185,7 +185,7 @@ public abstract class ContentProvider implements ContentInterface, ComponentCall * * @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 9253998b325a..a126363237b8 100644 --- a/core/java/android/content/ContentResolver.java +++ b/core/java/android/content/ContentResolver.java @@ -387,7 +387,7 @@ public abstract class ContentResolver implements ContentInterface { * {@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/IPackageInstaller.aidl b/core/java/android/content/pm/IPackageInstaller.aidl index 1f25fd039dd8..451c0e5e079a 100644 --- a/core/java/android/content/pm/IPackageInstaller.aidl +++ b/core/java/android/content/pm/IPackageInstaller.aidl @@ -80,7 +80,7 @@ interface IPackageInstaller { 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/IPackageManager.aidl b/core/java/android/content/pm/IPackageManager.aidl index 380de965b143..08f185366c49 100644 --- a/core/java/android/content/pm/IPackageManager.aidl +++ b/core/java/android/content/pm/IPackageManager.aidl @@ -843,4 +843,7 @@ interface IPackageManager { 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); } diff --git a/core/java/android/content/pm/OWNERS b/core/java/android/content/pm/OWNERS index 53dd3bffe06c..fb9560889507 100644 --- a/core/java/android/content/pm/OWNERS +++ b/core/java/android/content/pm/OWNERS @@ -10,3 +10,4 @@ per-file *Launcher* = file:/core/java/android/content/pm/LAUNCHER_OWNERS 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 c4bf18d70242..5df23c0ff44a 100644 --- a/core/java/android/content/pm/PackageInstaller.java +++ b/core/java/android/content/pm/PackageInstaller.java @@ -2362,8 +2362,8 @@ public class PackageInstaller { 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 8744eaee4341..407ffbb7288f 100644 --- a/core/java/android/content/pm/PackageManager.java +++ b/core/java/android/content/pm/PackageManager.java @@ -2992,6 +2992,46 @@ public abstract class PackageManager { @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 @@ public abstract class PackageManager { * 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 @@ public abstract class PackageManager { * 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 @@ public abstract class PackageManager { 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/ProcessInfo.java b/core/java/android/content/pm/ProcessInfo.java index 632c0f54375c..f84b46db144b 100644 --- a/core/java/android/content/pm/ProcessInfo.java +++ b/core/java/android/content/pm/ProcessInfo.java @@ -18,10 +18,8 @@ package android.content.pm; 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 class ProcessInfo implements Parcelable { */ 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 @@ public class ProcessInfo implements Parcelable { 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 @@ public class ProcessInfo implements Parcelable { * 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 @@ public class ProcessInfo implements Parcelable { @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 @@ public class ProcessInfo implements Parcelable { 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 @@ public class ProcessInfo implements Parcelable { // 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 @@ public class ProcessInfo implements Parcelable { // 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 @@ public class ProcessInfo implements Parcelable { 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 @@ public class ProcessInfo implements Parcelable { }; @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 000000000000..4d1b136915f2 --- /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/core/java/android/content/pm/SignedPackageParcel.aidl b/core/java/android/content/pm/SignedPackageParcel.aidl new file mode 100644 index 000000000000..7957f7f99d4a --- /dev/null +++ b/core/java/android/content/pm/SignedPackageParcel.aidl @@ -0,0 +1,25 @@ +/* + * 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.content.ComponentName; + +/** @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 1d0e2db78bcb..f54b2ac06929 100644 --- a/core/java/android/content/pm/UserProperties.java +++ b/core/java/android/content/pm/UserProperties.java @@ -77,6 +77,8 @@ public final class UserProperties implements Parcelable { 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 +98,8 @@ public final class UserProperties implements Parcelable { 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 +122,7 @@ public final class UserProperties implements Parcelable { 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; @@ -532,6 +536,7 @@ public final class UserProperties implements Parcelable { setDeleteAppWithParent(orig.getDeleteAppWithParent()); setAlwaysVisible(orig.getAlwaysVisible()); setAllowStoppingUserWithDelayedLocking(orig.getAllowStoppingUserWithDelayedLocking()); + setItemsRestrictedOnHomeScreen(orig.areItemsRestrictedOnHomeScreen()); } if (hasManagePermission) { // Add items that require MANAGE_USERS or stronger. @@ -1014,6 +1019,38 @@ public final class UserProperties implements Parcelable { } 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 = @@ -1042,7 +1079,8 @@ public final class UserProperties implements Parcelable { + ", mDeleteAppWithParent=" + getDeleteAppWithParent() + ", mAlwaysVisible=" + getAlwaysVisible() + ", mCrossProfileContentSharingStrategy=" + getCrossProfileContentSharingStrategy() - + profileApiVisibility + + ", mProfileApiVisibility=" + profileApiVisibility + + ", mItemsRestrictedOnHomeScreen=" + areItemsRestrictedOnHomeScreen() + "}"; } @@ -1079,6 +1117,7 @@ public final class UserProperties implements Parcelable { if (android.multiuser.Flags.supportHidingProfiles()) { pw.println(prefix + " mProfileApiVisibility=" + getProfileApiVisibility()); } + pw.println(prefix + " mItemsRestrictedOnHomeScreen=" + areItemsRestrictedOnHomeScreen()); } /** @@ -1168,6 +1207,9 @@ public final class UserProperties implements Parcelable { 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); } @@ -1256,6 +1298,10 @@ public final class UserProperties implements Parcelable { mProfileApiVisibility); } } + if (isPresent(INDEX_ITEMS_RESTRICTED_ON_HOME_SCREEN)) { + serializer.attributeBoolean(null, ITEMS_RESTRICTED_ON_HOME_SCREEN, + mItemsRestrictedOnHomeScreen); + } } // For use only with an object that has already had any permission-lacking fields stripped out. @@ -1280,6 +1326,7 @@ public final class UserProperties implements Parcelable { dest.writeBoolean(mAlwaysVisible); dest.writeInt(mCrossProfileContentSharingStrategy); dest.writeInt(mProfileApiVisibility); + dest.writeBoolean(mItemsRestrictedOnHomeScreen); } /** @@ -1308,6 +1355,7 @@ public final class UserProperties implements Parcelable { mAlwaysVisible = source.readBoolean(); mCrossProfileContentSharingStrategy = source.readInt(); mProfileApiVisibility = source.readInt(); + mItemsRestrictedOnHomeScreen = source.readBoolean(); } @Override @@ -1358,6 +1406,7 @@ public final class UserProperties implements Parcelable { private @CrossProfileContentSharingStrategy int mCrossProfileContentSharingStrategy = CROSS_PROFILE_CONTENT_SHARING_NO_DELEGATION; private @ProfileApiVisibility int mProfileApiVisibility = 0; + private boolean mItemsRestrictedOnHomeScreen = false; /** * @hide @@ -1523,6 +1572,15 @@ public final class UserProperties implements Parcelable { 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 +1606,8 @@ public final class UserProperties implements Parcelable { mDeleteAppWithParent, mAlwaysVisible, mCrossProfileContentSharingStrategy, - mProfileApiVisibility); + mProfileApiVisibility, + mItemsRestrictedOnHomeScreen); } } // end Builder @@ -1570,7 +1629,8 @@ public final class UserProperties implements Parcelable { boolean deleteAppWithParent, boolean alwaysVisible, @CrossProfileContentSharingStrategy int crossProfileContentSharingStrategy, - @ProfileApiVisibility int profileApiVisibility) { + @ProfileApiVisibility int profileApiVisibility, + boolean itemsRestrictedOnHomeScreen) { mDefaultProperties = null; setShowInLauncher(showInLauncher); setStartWithParent(startWithParent); @@ -1593,5 +1653,6 @@ public final class UserProperties implements Parcelable { if (android.multiuser.Flags.supportHidingProfiles()) { setProfileApiVisibility(profileApiVisibility); } + setItemsRestrictedOnHomeScreen(itemsRestrictedOnHomeScreen); } } diff --git a/core/java/android/content/pm/flags.aconfig b/core/java/android/content/pm/flags.aconfig index caff4576c9c3..f31521da354f 100644 --- a/core/java/android/content/pm/flags.aconfig +++ b/core/java/android/content/pm/flags.aconfig @@ -183,3 +183,26 @@ flag { description: "Feature flag to enable permission EMERGENCY_INSTALL_PACKAGES" bug: "321080601" } + +flag { + name: "asl_in_apk_app_metadata_source" + namespace: "package_manager_service" + description: "Feature flag to allow to know if the Android Safety Label (ASL) of an app is provided by the app's APK itself, or provided by an installer." + bug: "287487923" + is_fixed_read_only: true +} + +flag { + name: "force_multi_arch_native_libs_match" + namespace: "package_manager_service" + description: "Feature flag to force an multiArch app's native libraries to match with the natively supported ABIs of the device" + 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" +} diff --git a/core/java/android/content/pm/multiuser.aconfig b/core/java/android/content/pm/multiuser.aconfig index efb8607f75f7..4b27953dedb5 100644 --- a/core/java/android/content/pm/multiuser.aconfig +++ b/core/java/android/content/pm/multiuser.aconfig @@ -108,3 +108,24 @@ flag { bug: "316362775" is_fixed_read_only: true } + +flag { + name: "enable_permission_to_access_hidden_profiles" + namespace: "profile_experiences" + description: "Add permission to access API hidden users data via system APIs" + bug: "321988638" +} + +flag { + name: "handle_interleaved_settings_for_private_space" + namespace: "profile_experiences" + 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/overlay/OverlayPaths.java b/core/java/android/content/pm/overlay/OverlayPaths.java index bd74b0b9293c..a4db733af013 100644 --- a/core/java/android/content/pm/overlay/OverlayPaths.java +++ b/core/java/android/content/pm/overlay/OverlayPaths.java @@ -49,13 +49,6 @@ public class OverlayPaths { public static class Builder { final OverlayPaths mPaths = new OverlayPaths(); - public Builder() {} - - public Builder(@NonNull OverlayPaths base) { - mPaths.mResourceDirs.addAll(base.getResourceDirs()); - mPaths.mOverlayPaths.addAll(base.getOverlayPaths()); - } - /** * Adds a non-APK path to the contents of {@link OverlayPaths#getOverlayPaths()}. */ diff --git a/core/java/android/content/pm/parsing/ApkLiteParseUtils.java b/core/java/android/content/pm/parsing/ApkLiteParseUtils.java index 462667926053..69f9a7db6a85 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.AttributeSet; 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 @@ public class ApkLiteParseUtils { 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. @@ -518,6 +521,28 @@ public class ApkLiteParseUtils { 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())) { diff --git a/core/java/android/content/res/Resources.java b/core/java/android/content/res/Resources.java index 1b37092f77b6..36719805b9c3 100644 --- a/core/java/android/content/res/Resources.java +++ b/core/java/android/content/res/Resources.java @@ -29,6 +29,7 @@ import android.annotation.ColorRes; 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.compat.annotation.UnsupportedAppUsage; 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 @@ public class Resources { } } } + + /** + * 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/ResourcesImpl.java b/core/java/android/content/res/ResourcesImpl.java index c7790bd96c62..5e442b819774 100644 --- a/core/java/android/content/res/ResourcesImpl.java +++ b/core/java/android/content/res/ResourcesImpl.java @@ -273,14 +273,27 @@ public class ResourcesImpl { throw new NotFoundException("String resource name " + name); } + private static boolean isIntLike(@NonNull String s) { + if (s.isEmpty() || s.length() > 10) return false; + for (int i = 0, size = s.length(); i < size; i++) { + final char c = s.charAt(i); + if (c < '0' || c > '9') { + return false; + } + } + return true; + } + int getIdentifier(String name, String defType, String defPackage) { if (name == null) { throw new NullPointerException("name is null"); } - try { - return Integer.parseInt(name); - } catch (Exception e) { - // Ignore + if (isIntLike(name)) { + try { + return Integer.parseInt(name); + } catch (Exception e) { + // Ignore + } } return mAssets.getResourceIdentifier(name, defType, defPackage); } diff --git a/core/java/android/content/res/flags.aconfig b/core/java/android/content/res/flags.aconfig index db81e847d6bd..f660770d2fc8 100644 --- a/core/java/android/content/res/flags.aconfig +++ b/core/java/android/content/res/flags.aconfig @@ -40,3 +40,12 @@ flag { 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/flags.aconfig b/core/java/android/credentials/flags.aconfig index 90cd4718fcee..1ca11e698d5d 100644 --- a/core/java/android/credentials/flags.aconfig +++ b/core/java/android/credentials/flags.aconfig @@ -47,4 +47,11 @@ flag { name: "configurable_selector_ui_enabled" description: "Enables OEM configurable Credential Selector UI" bug: "319448437" -}
\ No newline at end of file +} + +flag { + namespace: "credential_manager" + name: "credman_biometric_api_enabled" + description: "Enables Credential Manager to work with the Biometric Authenticate API" + bug: "323211850" +} diff --git a/core/java/android/credentials/selection/AuthenticationEntry.java b/core/java/android/credentials/selection/AuthenticationEntry.java index 54589e1aed8b..dd6ca9e8ec51 100644 --- a/core/java/android/credentials/selection/AuthenticationEntry.java +++ b/core/java/android/credentials/selection/AuthenticationEntry.java @@ -23,9 +23,13 @@ import android.annotation.IntDef; 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 com.android.internal.util.AnnotationValidations; 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 final class AuthenticationEntry implements Parcelable { 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 @@ public final class AuthenticationEntry implements Parcelable { } /** - * 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 @@ public final class AuthenticationEntry implements Parcelable { 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 @@ public final class AuthenticationEntry implements Parcelable { } /** - * 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 000000000000..2662d761c1c0 --- /dev/null +++ b/core/java/android/credentials/selection/CancelSelectionRequest.java @@ -0,0 +1,147 @@ +/* + * 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.annotation.TestApi; +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 mAppPackageName; + + /** + * 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 getAppPackageName() { + return mAppPackageName; + } + + /** + * 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}. + * + * @hide + */ + @TestApi + @FlaggedApi(FLAG_CONFIGURABLE_SELECTOR_UI_ENABLED) + public CancelSelectionRequest(@NonNull IBinder token, boolean shouldShowCancellationExplanation, + @NonNull String appPackageName) { + mToken = token; + mShouldShowCancellationExplanation = shouldShowCancellationExplanation; + mAppPackageName = appPackageName; + } + + private CancelSelectionRequest(@NonNull Parcel in) { + mToken = in.readStrongBinder(); + AnnotationValidations.validate(NonNull.class, null, mToken); + mShouldShowCancellationExplanation = 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(mShouldShowCancellationExplanation); + dest.writeString8(mAppPackageName); + } + + @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 fca0e2ad184e..000000000000 --- 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/CreateCredentialProviderData.java b/core/java/android/credentials/selection/CreateCredentialProviderData.java index fc80ea8cec8a..ba9b00a13a82 100644 --- a/core/java/android/credentials/selection/CreateCredentialProviderData.java +++ b/core/java/android/credentials/selection/CreateCredentialProviderData.java @@ -118,9 +118,12 @@ public final class CreateCredentialProviderData extends ProviderData implements @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 78b9fd445f49..6d02f0d036bb 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 @@ public final class CreateCredentialProviderInfo { * 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 @@ public final class CreateCredentialProviderInfo { * * @hide */ + @SystemApi + @FlaggedApi(FLAG_CONFIGURABLE_SELECTOR_UI_ENABLED) public static final class Builder { @NonNull private String mProviderName; @@ -85,7 +107,12 @@ public final class CreateCredentialProviderInfo { @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 @@ public final class CreateCredentialProviderInfo { 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 b6f6ad4d60d7..4238612bedba 100644 --- a/core/java/android/credentials/selection/DisabledProviderData.java +++ b/core/java/android/credentials/selection/DisabledProviderData.java @@ -63,14 +63,14 @@ public final class DisabledProviderData extends ProviderData implements Parcelab } 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 7d7dbc2dd689..7764d2e57995 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 @@ public final class DisabledProviderInfo { * * @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 bcf4ee3fb819..a131f3f5ecd5 100644 --- a/core/java/android/credentials/selection/Entry.java +++ b/core/java/android/credentials/selection/Entry.java @@ -22,7 +22,7 @@ import android.annotation.FlaggedApi; 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 @@ import com.android.internal.util.AnnotationValidations; * * @hide */ -@TestApi +@SystemApi @FlaggedApi(FLAG_CONFIGURABLE_SELECTOR_UI_ENABLED) public final class Entry implements Parcelable { @NonNull @@ -67,30 +67,34 @@ public final class Entry implements Parcelable { } /** - * 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 @@ public final class Entry implements Parcelable { } /** - * 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 db0fb84fa76c..1e2f27cddaec 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 @@ public final class GetCredentialProviderInfo { /** * 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 @@ public final class GetCredentialProviderInfo { * * @hide */ + @SystemApi + @FlaggedApi(FLAG_CONFIGURABLE_SELECTOR_UI_ENABLED) public static final class Builder { @NonNull private String mProviderName; @@ -123,6 +150,7 @@ public final class GetCredentialProviderInfo { /** * 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 @@ public final class GetCredentialProviderInfo { 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 c3a09ae61754..1837976f5d1f 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 @@ import java.util.ArrayList; 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 @@ public class IntentFactory { @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 @@ public class IntentFactory { .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 @@ public class IntentFactory { } /** - * 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,8 @@ public class IntentFactory { 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(requestToken, shouldShowCancellationUi, appPackageName)); return intent; } @@ -132,5 +230,8 @@ public class IntentFactory { 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 6bcd05afc00f..2c3a320b6750 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 @@ import java.util.List; * * @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 @@ public final class IntentHelper { /** * 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 7d6ea7ee6b8c..2fd322adb79c 100644 --- a/core/java/android/credentials/selection/RequestInfo.java +++ b/core/java/android/credentials/selection/RequestInfo.java @@ -22,6 +22,7 @@ import android.annotation.FlaggedApi; 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 @@ import java.util.List; * * @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 @@ public final class RequestInfo implements Parcelable { /** @hide */ @Retention(RetentionPolicy.SOURCE) - @StringDef(value = {TYPE_GET, TYPE_CREATE}) + @StringDef(value = {TYPE_GET, TYPE_CREATE, TYPE_UNDEFINED}) public @interface RequestType { } @@ -107,18 +110,13 @@ public final class RequestInfo implements Parcelable { 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<>()); - } - - /** 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, @@ -129,7 +127,13 @@ public final class RequestInfo implements Parcelable { hasPermissionToOverrideDefault, defaultProviderIds); } - /** 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, @@ -140,24 +144,17 @@ public final class RequestInfo implements Parcelable { /*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<>()); - } - /** Returns whether the calling package has the permission. */ public boolean hasPermissionToOverrideDefault() { 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; @@ -185,6 +182,12 @@ public final class RequestInfo implements Parcelable { 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. diff --git a/core/java/android/credentials/selection/RequestToken.java b/core/java/android/credentials/selection/RequestToken.java new file mode 100644 index 000000000000..27b83f873732 --- /dev/null +++ b/core/java/android/credentials/selection/RequestToken.java @@ -0,0 +1,65 @@ +/* + * 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. + * + * @hide + */ +@SystemApi +@FlaggedApi(FLAG_CONFIGURABLE_SELECTOR_UI_ENABLED) +public final class RequestToken { + + @NonNull + private final IBinder 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 cb1d3f5252b2..3b7ade25ecb2 100644 --- a/core/java/android/database/Cursor.java +++ b/core/java/android/database/Cursor.java @@ -511,7 +511,7 @@ public interface Cursor extends Closeable { 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/ddm/DdmHandleViewDebug.java b/core/java/android/ddm/DdmHandleViewDebug.java index 0f66fcbdbec9..5cbf24f3ba54 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.ByteArrayOutputStream; 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 @@ public class DdmHandleViewDebug extends DdmHandle { * * 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 @@ public class DdmHandleViewDebug extends DdmHandle { 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 d51e62e709c2..1488cff3a35e 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 @@ oneway interface AuthenticationStateListener { * @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 665d8d280bc6..451d6fba0105 100644 --- a/core/java/android/hardware/camera2/CameraCharacteristics.java +++ b/core/java/android/hardware/camera2/CameraCharacteristics.java @@ -5313,7 +5313,7 @@ public final class CameraCharacteristics extends CameraMetadata<CameraCharacteri * </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 1867a17d8756..7abe821b7013 100644 --- a/core/java/android/hardware/camera2/CameraExtensionCharacteristics.java +++ b/core/java/android/hardware/camera2/CameraExtensionCharacteristics.java @@ -624,6 +624,120 @@ public final class CameraExtensionCharacteristics { } /** + * 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 fb2df546f545..665357726ebf 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.CaptureResult; 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 @@ public abstract class AdvancedExtender { 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 @@ public abstract class AdvancedExtender { } } + 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 class AdvancedExtender { 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 @@ public abstract class AdvancedExtender { @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 @@ public abstract class AdvancedExtender { // 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 1426d7bceb7f..fa0d14a3f05a 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.FlaggedApi; 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 android.util.Log; 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 @@ import com.android.internal.camera.flags.Flags; @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 @@ public abstract class CameraExtensionService extends Service { 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 @@ public abstract class CameraExtensionService extends Service { @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 101442f28c29..3071f0d8d53c 100644 --- a/core/java/android/hardware/camera2/extension/IAdvancedExtenderImpl.aidl +++ b/core/java/android/hardware/camera2/extension/IAdvancedExtenderImpl.aidl @@ -38,4 +38,5 @@ interface IAdvancedExtenderImpl 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 6ed0c1404212..9c5136bcf903 100644 --- a/core/java/android/hardware/camera2/extension/SessionProcessor.java +++ b/core/java/android/hardware/camera2/extension/SessionProcessor.java @@ -76,10 +76,15 @@ import java.util.concurrent.Executor; @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 @@ public abstract class SessionProcessor { @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/display/DisplayManagerInternal.java b/core/java/android/hardware/display/DisplayManagerInternal.java index 64a62a9299b8..f18a0b758bc5 100644 --- a/core/java/android/hardware/display/DisplayManagerInternal.java +++ b/core/java/android/hardware/display/DisplayManagerInternal.java @@ -202,8 +202,11 @@ public abstract class DisplayManagerInternal { /** * Called by the window manager to perform traversals while holding a * surface flinger transaction. + * @param t The default transaction. + * @param displayTransactions The transactions mapped by display id. */ - public abstract void performTraversal(Transaction t); + public abstract void performTraversal(Transaction t, + SparseArray<SurfaceControl.Transaction> displayTransactions); /** * Tells the display manager about properties of the display that depend on the windows on it. diff --git a/core/java/android/hardware/input/IInputManager.aidl b/core/java/android/hardware/input/IInputManager.aidl index 7bea9aeeb86b..1f5495999416 100644 --- a/core/java/android/hardware/input/IInputManager.aidl +++ b/core/java/android/hardware/input/IInputManager.aidl @@ -67,6 +67,9 @@ interface IInputManager { 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 4ebbde732747..744dfae97108 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.Nullable; 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 @@ public final class InputManager { } /** + * 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 @@ public final class InputManager { } /** + * 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 89fa5fb47a07..54e34ecf7006 100644 --- a/core/java/android/hardware/input/InputSettings.java +++ b/core/java/android/hardware/input/InputSettings.java @@ -54,8 +54,8 @@ public class InputSettings { /** * Pointer Speed: The default pointer speed (0). - * @hide */ + @SuppressLint("UnflaggedApi") // TestApi without associated feature. public static final int DEFAULT_POINTER_SPEED = 0; /** diff --git a/core/java/android/hardware/input/PhysicalKeyLayout.java b/core/java/android/hardware/input/PhysicalKeyLayout.java index 241c452a75eb..3454c399375a 100644 --- a/core/java/android/hardware/input/PhysicalKeyLayout.java +++ b/core/java/android/hardware/input/PhysicalKeyLayout.java @@ -23,8 +23,6 @@ import android.util.SparseIntArray; 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 @@ final class PhysicalKeyLayout { } 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)); } } @@ -400,11 +398,11 @@ final class PhysicalKeyLayout { private final String mAltGrText; 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); } public String getBaseText() { diff --git a/core/java/android/hardware/radio/OWNERS b/core/java/android/hardware/radio/OWNERS index 302fdd73ccf3..51a85e48832e 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/os/Environment.java b/core/java/android/os/Environment.java index 536ef31f334a..a459aaa42930 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 @@ public class Environment { * 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 b7649ba9700b..650aeadde40f 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 @@ interface ISystemConfig { * @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 c0d1fb9c6a88..800ba6d3a542 100644 --- a/core/java/android/os/IUserManager.aidl +++ b/core/java/android/os/IUserManager.aidl @@ -150,4 +150,5 @@ interface IUserManager { 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/SystemConfigManager.java b/core/java/android/os/SystemConfigManager.java index 21ffbf18dbc3..13bc3981a64f 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.Collections; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.stream.Collectors; /** @@ -175,4 +179,69 @@ public class SystemConfigManager { 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 d6df8d940904..0da19df0b0af 100644 --- a/core/java/android/os/UserManager.java +++ b/core/java/android/os/UserManager.java @@ -3438,6 +3438,7 @@ public class UserManager { } /** + * @see #isVisibleBackgroundUsersSupported() * @hide */ public static boolean isVisibleBackgroundUsersEnabled() { @@ -3447,14 +3448,21 @@ public class UserManager { } /** - * 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 @@ public class UserManager { } /** - * 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 @@ public class UserManager { } /** + * @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 5078dc351f6f..46705a31f395 100644 --- a/core/java/android/os/VibrationAttributes.java +++ b/core/java/android/os/VibrationAttributes.java @@ -29,6 +29,7 @@ import java.util.Objects; /** * 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 @@ public final class VibrationAttributes implements Parcelable { * 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/ZygoteProcess.java b/core/java/android/os/ZygoteProcess.java index f3496e7f2592..b1ef05a6f00c 100644 --- a/core/java/android/os/ZygoteProcess.java +++ b/core/java/android/os/ZygoteProcess.java @@ -71,7 +71,7 @@ import java.util.UUID; */ 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/provider/ContactKeysManager.java b/core/java/android/provider/ContactKeysManager.java index ecde69917820..bef645698b4a 100644 --- a/core/java/android/provider/ContactKeysManager.java +++ b/core/java/android/provider/ContactKeysManager.java @@ -21,6 +21,7 @@ import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.RequiresPermission; +import android.annotation.SystemApi; import android.content.ContentProviderClient; import android.content.ContentResolver; import android.content.Context; @@ -247,6 +248,44 @@ public class ContactKeysManager { } /** + * Updates a contact key entry's local verification state that belongs to the app identified + * by ownerPackageName. + * + * @param lookupKey the value that references the contact + * @param deviceId an app-specified identifier for the device + * @param accountId an app-specified identifier for the account + * @param ownerPackageName the package name of the app that owns the key + * @param localVerificationState the new local verification state + * + * @return true if the entry was updated, false otherwise. + * + * @hide + */ + @SystemApi + @RequiresPermission(allOf = { + android.Manifest.permission.WRITE_VERIFICATION_STATE_E2EE_CONTACT_KEYS, + android.Manifest.permission.WRITE_CONTACTS}) + public boolean updateContactKeyLocalVerificationState(@NonNull String lookupKey, + @NonNull String deviceId, + @NonNull String accountId, + @NonNull String ownerPackageName, + @VerificationState int localVerificationState) { + validateVerificationState(localVerificationState); + + final Bundle extras = new Bundle(); + extras.putString(ContactKeys.LOOKUP_KEY, Objects.requireNonNull(lookupKey)); + extras.putString(ContactKeys.DEVICE_ID, Objects.requireNonNull(deviceId)); + extras.putString(ContactKeys.ACCOUNT_ID, Objects.requireNonNull(accountId)); + extras.putString(ContactKeys.OWNER_PACKAGE_NAME, Objects.requireNonNull(ownerPackageName)); + extras.putInt(ContactKeys.LOCAL_VERIFICATION_STATE, localVerificationState); + + final Bundle response = nullSafeCall(mContentResolver, + ContactKeys.UPDATE_CONTACT_KEY_LOCAL_VERIFICATION_STATE_METHOD, extras); + + return response != null && response.getBoolean(ContactKeys.KEY_UPDATED_ROWS); + } + + /** * Updates a contact key entry's remote verification state that belongs to the caller app. * * @param lookupKey the value that references the contact @@ -275,6 +314,45 @@ public class ContactKeysManager { return response != null && response.getBoolean(ContactKeys.KEY_UPDATED_ROWS); } + /** + * Updates a contact key entry's remote verification state that belongs to the app identified + * by ownerPackageName. + * + * @param lookupKey the value that references the contact + * @param deviceId an app-specified identifier for the device + * @param accountId an app-specified identifier for the account + * @param ownerPackageName the package name of the app that owns the key + * @param remoteVerificationState the new remote verification state + * + * @return true if the entry was updated, false otherwise. + * + * @hide + */ + @SystemApi + @RequiresPermission(allOf = { + android.Manifest.permission.WRITE_VERIFICATION_STATE_E2EE_CONTACT_KEYS, + android.Manifest.permission.WRITE_CONTACTS}) + public boolean updateContactKeyRemoteVerificationState(@NonNull String lookupKey, + @NonNull String deviceId, + @NonNull String accountId, + @NonNull String ownerPackageName, + @VerificationState int remoteVerificationState) { + validateVerificationState(remoteVerificationState); + + final Bundle extras = new Bundle(); + extras.putString(ContactKeys.LOOKUP_KEY, Objects.requireNonNull(lookupKey)); + extras.putString(ContactKeys.DEVICE_ID, Objects.requireNonNull(deviceId)); + extras.putString(ContactKeys.ACCOUNT_ID, Objects.requireNonNull(accountId)); + extras.putString(ContactKeys.OWNER_PACKAGE_NAME, Objects.requireNonNull(ownerPackageName)); + extras.putInt(ContactKeys.REMOTE_VERIFICATION_STATE, remoteVerificationState); + + final Bundle response = nullSafeCall(mContentResolver, + ContactKeys.UPDATE_CONTACT_KEY_REMOTE_VERIFICATION_STATE_METHOD, extras); + + return response != null && response.getBoolean(ContactKeys.KEY_UPDATED_ROWS); + } + + private static void validateVerificationState(int verificationState) { if (verificationState != UNVERIFIED && verificationState != VERIFICATION_FAILED @@ -297,12 +375,12 @@ public class ContactKeysManager { public boolean removeContactKey(@NonNull String lookupKey, @NonNull String deviceId, @NonNull String accountId) { - Bundle extras = new Bundle(); + final Bundle extras = new Bundle(); extras.putString(ContactKeys.LOOKUP_KEY, Objects.requireNonNull(lookupKey)); extras.putString(ContactKeys.DEVICE_ID, Objects.requireNonNull(deviceId)); extras.putString(ContactKeys.ACCOUNT_ID, Objects.requireNonNull(accountId)); - Bundle response = nullSafeCall(mContentResolver, + final Bundle response = nullSafeCall(mContentResolver, ContactKeys.REMOVE_CONTACT_KEY_METHOD, extras); return response != null && response.getBoolean(ContactKeys.KEY_UPDATED_ROWS); @@ -369,6 +447,41 @@ public class ContactKeysManager { } /** + * Updates a self key entry's remote verification state that belongs to the app identified + * by ownerPackageName. + * + * @param deviceId an app-specified identifier for the device + * @param accountId an app-specified identifier for the account + * @param ownerPackageName the package name of the app that owns the key + * @param remoteVerificationState the new remote verification state + * + * @return true if the entry was updated, false otherwise. + * + * @hide + */ + @SystemApi + @RequiresPermission(allOf = { + android.Manifest.permission.WRITE_VERIFICATION_STATE_E2EE_CONTACT_KEYS, + android.Manifest.permission.WRITE_CONTACTS}) + public boolean updateSelfKeyRemoteVerificationState(@NonNull String deviceId, + @NonNull String accountId, + @NonNull String ownerPackageName, + @VerificationState int remoteVerificationState) { + validateVerificationState(remoteVerificationState); + + Bundle extras = new Bundle(); + extras.putString(ContactKeys.DEVICE_ID, Objects.requireNonNull(deviceId)); + extras.putString(ContactKeys.ACCOUNT_ID, Objects.requireNonNull(accountId)); + extras.putString(ContactKeys.OWNER_PACKAGE_NAME, Objects.requireNonNull(ownerPackageName)); + extras.putInt(ContactKeys.REMOTE_VERIFICATION_STATE, remoteVerificationState); + + Bundle response = nullSafeCall(mContentResolver, + ContactKeys.UPDATE_SELF_KEY_REMOTE_VERIFICATION_STATE_METHOD, extras); + + return response != null && response.getBoolean(ContactKeys.KEY_UPDATED_ROWS); + } + + /** * Maximum size of a contact key. */ public static int getMaxKeySizeBytes() { diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index 524b7336718f..76fda0636282 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -6015,8 +6015,10 @@ public final class Settings { * +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"; /** diff --git a/core/java/android/service/chooser/flags.aconfig b/core/java/android/service/chooser/flags.aconfig index 597838305a91..8aeaacf6a412 100644 --- a/core/java/android/service/chooser/flags.aconfig +++ b/core/java/android/service/chooser/flags.aconfig @@ -7,3 +7,9 @@ flag { 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 000000000000..463adf48dd3e --- /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/telephony/TelephonyRegistryManager.java b/core/java/android/telephony/TelephonyRegistryManager.java index 0a813a3edb1f..d39c4ce4ce24 100644 --- a/core/java/android/telephony/TelephonyRegistryManager.java +++ b/core/java/android/telephony/TelephonyRegistryManager.java @@ -19,12 +19,15 @@ import android.annotation.CallbackExecutor; 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 @@ import java.util.stream.Collectors; * 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 @@ public class TelephonyRegistryManager { } /** - * 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 @@ public class TelephonyRegistryManager { } /** - * 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/tracing/perfetto/DataSourceInstance.java b/core/java/android/tracing/perfetto/DataSourceInstance.java index 49945013ae87..3710b4df33e8 100644 --- a/core/java/android/tracing/perfetto/DataSourceInstance.java +++ b/core/java/android/tracing/perfetto/DataSourceInstance.java @@ -69,4 +69,8 @@ public abstract class DataSourceInstance implements AutoCloseable { 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 baece75cbf09..82559dab0e5b 100644 --- a/core/java/android/tracing/transition/TransitionDataSource.java +++ b/core/java/android/tracing/transition/TransitionDataSource.java @@ -24,8 +24,8 @@ import android.tracing.perfetto.StartCallbackArguments; import android.tracing.perfetto.StopCallbackArguments; import android.util.proto.ProtoInputStream; -import java.util.HashMap; import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; /** * @hide @@ -38,6 +38,9 @@ public class TransitionDataSource private final Runnable mOnFlushStaticCallback; private final Runnable mOnStopStaticCallback; + private final ConcurrentHashMap<Integer, ConcurrentHashMap<String, Integer>> mHandlerMappings = + new ConcurrentHashMap<>(); + public TransitionDataSource(Runnable onStart, Runnable onFlush, Runnable onStop) { super(DATA_SOURCE_NAME); this.mOnStartStaticCallback = onStart; @@ -47,11 +50,16 @@ public class TransitionDataSource @Override protected TlsState createTlsState(CreateTlsStateArgs<DataSourceInstance> args) { - return new TlsState(); + return new TlsState(args.getDataSourceInstanceLocked().getInstanceIndex()); } public class TlsState { - public final Map<String, Integer> handlerMapping = new HashMap<>(); + public final Map<String, Integer> handlerMapping; + + public TlsState(int instanceIndex) { + handlerMapping = mHandlerMappings + .computeIfAbsent(instanceIndex, index -> new ConcurrentHashMap<>()); + } } @Override @@ -70,6 +78,7 @@ public class TransitionDataSource @Override protected void onStop(StopCallbackArguments args) { mOnStopStaticCallback.run(); + mHandlerMappings.remove(instanceIndex); } }; } diff --git a/core/java/android/view/DisplayCutout.java b/core/java/android/view/DisplayCutout.java index f819c9b2f5dc..db665a92ec5c 100644 --- a/core/java/android/view/DisplayCutout.java +++ b/core/java/android/view/DisplayCutout.java @@ -27,9 +27,7 @@ import static android.view.DisplayCutoutProto.INSETS; 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 @@ public final class DisplayCutout { // 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 @@ public final class DisplayCutout { 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); + 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)); + } + 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)); } - override[rotation] = side; } - return override; + return rawOverrides; } /** diff --git a/core/java/android/view/FocusFinder.java b/core/java/android/view/FocusFinder.java index 064bc6947fc4..35b137a322e3 100644 --- a/core/java/android/view/FocusFinder.java +++ b/core/java/android/view/FocusFinder.java @@ -314,13 +314,20 @@ public class FocusFinder { if (count < 2) { return null; } + View next = null; + final boolean[] looped = new boolean[1]; switch (direction) { case View.FOCUS_FORWARD: - return getNextFocusable(focused, focusables, count); + next = getNextFocusable(focused, focusables, count, looped); + break; case View.FOCUS_BACKWARD: - return getPreviousFocusable(focused, focusables, count); + next = getPreviousFocusable(focused, focusables, count, looped); + break; } - return focusables.get(count - 1); + if (root != null && root.mAttachInfo != null && root == root.getRootView()) { + root.mAttachInfo.mNextFocusLooped = looped[0]; + } + return next != null ? next : focusables.get(count - 1); } private void setFocusBottomRight(ViewGroup root, Rect focusedRect) { @@ -375,7 +382,8 @@ public class FocusFinder { return closest; } - private static View getNextFocusable(View focused, ArrayList<View> focusables, int count) { + private static View getNextFocusable(View focused, ArrayList<View> focusables, int count, + boolean[] outLooped) { if (count < 2) { return null; } @@ -385,10 +393,12 @@ public class FocusFinder { return focusables.get(position + 1); } } + outLooped[0] = true; return focusables.get(0); } - private static View getPreviousFocusable(View focused, ArrayList<View> focusables, int count) { + private static View getPreviousFocusable(View focused, ArrayList<View> focusables, int count, + boolean[] outLooped) { if (count < 2) { return null; } @@ -398,6 +408,7 @@ public class FocusFinder { return focusables.get(position - 1); } } + outLooped[0] = true; return focusables.get(count - 1); } diff --git a/core/java/android/view/IWindowManager.aidl b/core/java/android/view/IWindowManager.aidl index 7903050e41d4..99863d013970 100644 --- a/core/java/android/view/IWindowManager.aidl +++ b/core/java/android/view/IWindowManager.aidl @@ -1085,7 +1085,9 @@ interface IWindowManager 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 f2c3abc8edb4..891e2a2d4b20 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.BatteryState; 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 @@ public final class InputDevice implements Parcelable { 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 @@ public final class InputDevice implements Parcelable { addMotionRange(in.readInt(), in.readInt(), in.readFloat(), in.readFloat(), in.readFloat(), in.readFloat(), in.readFloat()); } + + mViewBehavior.mShouldSmoothScroll = in.readBoolean(); } /** @@ -571,6 +579,7 @@ public final class InputDevice implements Parcelable { 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 @@ public final class InputDevice implements Parcelable { 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 @@ public final class InputDevice implements Parcelable { range.getResolution()); } + device.setShouldSmoothScroll(mShouldSmoothScroll); + return device; } } @@ -1123,6 +1144,22 @@ public final class InputDevice implements Parcelable { 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 @@ public final class InputDevice implements Parcelable { 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 @@ public final class InputDevice implements Parcelable { } } + /** + * 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 @@ public final class InputDevice implements Parcelable { 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 59ec60545d6d..9db1060abad4 100644 --- a/core/java/android/view/InputWindowHandle.java +++ b/core/java/android/view/InputWindowHandle.java @@ -162,6 +162,12 @@ public final class InputWindowHandle { 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 @@ public final class InputWindowHandle { focusTransferTarget = other.focusTransferTarget; contentSize = new Size(other.contentSize.getWidth(), other.contentSize.getHeight()); alpha = other.alpha; + canOccludePresentation = other.canOccludePresentation; } @Override @@ -219,6 +226,7 @@ public final class InputWindowHandle { .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 000000000000..ee55737cc27e --- /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 3ed03859ffa6..3c0ac06ae13f 100644 --- a/core/java/android/view/SurfaceControl.java +++ b/core/java/android/view/SurfaceControl.java @@ -168,6 +168,8 @@ public final class SurfaceControl implements Parcelable { 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 final class SurfaceControl implements Parcelable { 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 @@ public final class SurfaceControl implements Parcelable { } /** + * 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 163dfa21c3ef..021bbf7b9c9f 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 @@ public class TextureView extends View { 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 @@ public class TextureView extends View { 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 1b22fda9c31a..2366ff77692b 100644 --- a/core/java/android/view/View.java +++ b/core/java/android/view/View.java @@ -5537,10 +5537,20 @@ public class View implements Drawable.Callback, KeyEvent.Callback, */ 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) @@ -20253,7 +20263,10 @@ public class View implements Drawable.Callback, KeyEvent.Callback, } // 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 +20371,10 @@ public class View implements Drawable.Callback, KeyEvent.Callback, 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); } } @@ -31336,6 +31352,13 @@ public class View implements Drawable.Callback, KeyEvent.Callback, final ArrayList<View> mTempArrayList = new ArrayList<View>(24); /** + * Indicates if the next focus will be looped back to the first focusable view of the entire + * hierarchy when finding in the direction of {@link #FOCUS_FORWARD} or to the last + * focusable view when finding in the direction of {@link #FOCUS_BACKWARD}. + */ + boolean mNextFocusLooped = false; + + /** * The id of the window for accessibility purposes. */ int mAccessibilityWindowId = AccessibilityWindowInfo.UNDEFINED_WINDOW_ID; @@ -33124,11 +33147,20 @@ public class View implements Drawable.Callback, KeyEvent.Callback, } 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() { @@ -33137,22 +33169,22 @@ public class View implements Drawable.Callback, KeyEvent.Callback, 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); } @@ -33231,4 +33263,27 @@ public class View implements Drawable.Callback, KeyEvent.Callback, } 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 25e0eca12bf3..4f1fb40ab214 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.Debug; 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.RetentionPolicy; 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.Executor; 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 @@ import java.util.stream.Stream; * 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 @@ public class ViewDebug { 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 @@ public class ViewDebug { 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 @@ public class ViewDebug { 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>(); - - 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); - } + 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]; - latch.countDown(); - } - }); + deserializeMethodParameters(args, argTypes, params); + } + Method method; try { - latch.await(); - } catch (InterruptedException e) { - throw new RuntimeException(e); + 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()); } - if (exception.get() != null) { - throw new RuntimeException(exception.get()); + try { + // 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); } - - return result.get(); } /** @@ -1961,4 +2011,175 @@ public class ViewDebug { */ 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 c18aeee52460..653435403b74 100644 --- a/core/java/android/view/ViewRootImpl.java +++ b/core/java/android/view/ViewRootImpl.java @@ -26,6 +26,7 @@ import static android.view.Display.INVALID_DISPLAY; 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 @@ public final class ViewRootImpl implements ViewParent, // 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 @@ public final class ViewRootImpl implements ViewParent, 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 @@ public final class ViewRootImpl implements ViewParent, setPreferredFrameRate(mPreferredFrameRate); setPreferredFrameRateCategory(mPreferredFrameRateCategory); mPreferredFrameRateCategory = FRAME_RATE_CATEGORY_NO_PREFERENCE; - mPreferredFrameRate = 0; } private void createSyncIfNeeded() { @@ -6127,6 +6139,7 @@ public final class ViewRootImpl implements ViewParent, 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 @@ public final class ViewRootImpl implements ViewParent, * 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 @@ public final class ViewRootImpl implements ViewParent, } updatePointerIcon(mPointerIconEvent); break; + case MSG_FRAME_RATE_SETTING: + mPreferredFrameRate = 0; + setPreferredFrameRate(mPreferredFrameRate); + break; } } } @@ -7283,8 +7301,18 @@ public final class ViewRootImpl implements ViewParent, if (direction != 0) { View focused = mView.findFocus(); if (focused != null) { + mAttachInfo.mNextFocusLooped = false; View v = focused.focusSearch(direction); if (v != null && v != focused) { + if (mAttachInfo.mNextFocusLooped) { + // The next focus is looped. Let's try to move the focus to the adjacent + // window. Note: we still need to move the focus in this window + // regardless of what moveFocusToAdjacentWindow returns, so the focus + // can be looped back from the focus in the adjacent window to next + // focus of this window. + moveFocusToAdjacentWindow(direction); + } + // do the math the get the interesting rect // of previous focused into the coord system of // newly focused view @@ -7465,7 +7493,7 @@ public final class ViewRootImpl implements ViewParent, // 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); } /** @@ -7473,7 +7501,7 @@ public final class ViewRootImpl implements ViewParent, * 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, @@ -12217,17 +12245,32 @@ public final class ViewRootImpl implements ViewParent, 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) { @@ -12246,12 +12289,19 @@ public final class ViewRootImpl implements ViewParent, 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); } } @@ -12268,14 +12318,14 @@ public final class ViewRootImpl implements ViewParent, 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(); @@ -12319,6 +12369,9 @@ public final class ViewRootImpl implements ViewParent, } 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/WindowManager.java b/core/java/android/view/WindowManager.java index c78826116426..ac2a66e79d1d 100644 --- a/core/java/android/view/WindowManager.java +++ b/core/java/android/view/WindowManager.java @@ -128,8 +128,10 @@ import android.window.TrustedPresentationThresholds; 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 @@ public interface WindowManager extends ViewManager { "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 @@ public interface WindowManager extends ViewManager { 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/WindowManagerImpl.java b/core/java/android/view/WindowManagerImpl.java index 5072ad755cba..eaf45c488634 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.FIRST_SUB_WINDOW; 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 final class WindowManagerImpl implements WindowManager { 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 ef1bf5a5c548..0deaca1898c6 100644 --- a/core/java/android/view/accessibility/AccessibilityManager.java +++ b/core/java/android/view/accessibility/AccessibilityManager.java @@ -67,7 +67,6 @@ import android.view.View; 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 final class AccessibilityManager { 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 final class AccessibilityManager { 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 @@ public final class AccessibilityManager { @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(); @@ -2424,7 +2464,7 @@ public final class AccessibilityManager { } } try { - service.attachAccessibilityOverlayToDisplay_enforcePermission( + service.attachAccessibilityOverlayToDisplay( displayId, surfaceControl); } catch (RemoteException re) { throw re.rethrowFromSystemServer(); diff --git a/core/java/android/view/accessibility/IAccessibilityManager.aidl b/core/java/android/view/accessibility/IAccessibilityManager.aidl index 1c5d29e0ff1e..eca15869f581 100644 --- a/core/java/android/view/accessibility/IAccessibilityManager.aidl +++ b/core/java/android/view/accessibility/IAccessibilityManager.aidl @@ -139,5 +139,5 @@ interface IAccessibilityManager { WindowTransformationSpec getWindowTransformationSpec(int windowId); @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.INTERNAL_SYSTEM_WINDOW)") - void attachAccessibilityOverlayToDisplay_enforcePermission(int displayId, in SurfaceControl surfaceControl); + void attachAccessibilityOverlayToDisplay(int displayId, in SurfaceControl surfaceControl); } diff --git a/core/java/android/view/flags/refresh_rate_flags.aconfig b/core/java/android/view/flags/refresh_rate_flags.aconfig index 0aa516e08697..9d613bcae29a 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 @@ flag { 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/webkit/URLUtil.java b/core/java/android/webkit/URLUtil.java index c6271d27cb37..2f765ae6de38 100644 --- a/core/java/android/webkit/URLUtil.java +++ b/core/java/android/webkit/URLUtil.java @@ -16,7 +16,6 @@ package android.webkit; -import android.annotation.FlaggedApi; import android.annotation.NonNull; import android.annotation.Nullable; import android.compat.Compatibility; @@ -47,8 +46,7 @@ public final class URLUtil { */ @ChangeId @EnabledSince(targetSdkVersion = Build.VERSION_CODES.VANILLA_ICE_CREAM) - @FlaggedApi(android.os.Flags.FLAG_ANDROID_OS_BUILD_VANILLA_ICE_CREAM) - public static final long PARSE_CONTENT_DISPOSITION_USING_RFC_6266 = 319400769L; + static final long PARSE_CONTENT_DISPOSITION_USING_RFC_6266 = 319400769L; private static final String LOGTAG = "webkit"; private static final boolean TRACE = false; diff --git a/core/java/android/widget/AbsListView.java b/core/java/android/widget/AbsListView.java index 1fdd1a5a5a5f..f54ef3868f53 100644 --- a/core/java/android/widget/AbsListView.java +++ b/core/java/android/widget/AbsListView.java @@ -425,12 +425,12 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te 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 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te 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 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te } /** - * 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 76e97ad7a0f6..3b7e1e91f839 100644 --- a/core/java/android/widget/AbsSpinner.java +++ b/core/java/android/widget/AbsSpinner.java @@ -170,7 +170,7 @@ public abstract class AbsSpinner extends AdapterView<SpinnerAdapter> { * @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 e20357fa2dd9..1dc90edbd1e5 100644 --- a/core/java/android/widget/OWNERS +++ b/core/java/android/widget/OWNERS @@ -15,3 +15,5 @@ per-file SpellChecker.java = file:../view/inputmethod/OWNERS 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/RemoteViews.java b/core/java/android/widget/RemoteViews.java index 2433bd865e73..738bb1f0006a 100644 --- a/core/java/android/widget/RemoteViews.java +++ b/core/java/android/widget/RemoteViews.java @@ -5044,10 +5044,7 @@ public class RemoteViews implements Parcelable, Filter { * @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)); diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java index 90d51402f5b1..9a4106d930b4 100644 --- a/core/java/android/widget/TextView.java +++ b/core/java/android/widget/TextView.java @@ -1259,6 +1259,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener int lastBaselineToBottomHeight = -1; float lineHeight = -1f; int lineHeightUnit = -1; + boolean hasUseBoundForWidthValue = false; readTextAppearance(context, a, attributes, true /* styleArray */); @@ -1613,6 +1614,9 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener 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 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener 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/widget/flags/notification_widget_flags.aconfig b/core/java/android/widget/flags/notification_widget_flags.aconfig new file mode 100644 index 000000000000..9f0b7c3dfb02 --- /dev/null +++ b/core/java/android/widget/flags/notification_widget_flags.aconfig @@ -0,0 +1,8 @@ +package: "android.widget.flags" + +flag { + name: "notif_linearlayout_optimized" + namespace: "systemui" + description: "Enables notification specific LinearLayout optimization" + bug: "316110233" +}
\ No newline at end of file diff --git a/core/java/android/window/SnapshotDrawerUtils.java b/core/java/android/window/SnapshotDrawerUtils.java index cc875ad7ad1b..c76c7a482865 100644 --- a/core/java/android/window/SnapshotDrawerUtils.java +++ b/core/java/android/window/SnapshotDrawerUtils.java @@ -21,6 +21,7 @@ import static android.graphics.Color.alpha; 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 @@ public class SnapshotDrawerUtils { | 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 e32c8e58bb21..739cf0eeca0c 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.IBinder; 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 @@ public final class WindowMetricsController { 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 65075aea7b27..baefe7be1f54 100644 --- a/core/java/android/window/WindowOnBackInvokedDispatcher.java +++ b/core/java/android/window/WindowOnBackInvokedDispatcher.java @@ -226,6 +226,9 @@ public class WindowOnBackInvokedDispatcher implements OnBackInvokedDispatcher { 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(); } 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 1de77f6d29e7..82067defd336 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 @@ flag { 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 f234637a7d82..14fb17c09031 100644 --- a/core/java/android/window/flags/windowing_frontend.aconfig +++ b/core/java/android/window/flags/windowing_frontend.aconfig @@ -84,6 +84,14 @@ flag { } 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 4a6e8d761061..bc63881163f0 100644 --- a/core/java/android/window/flags/windowing_sdk.aconfig +++ b/core/java/android/window/flags/windowing_sdk.aconfig @@ -16,7 +16,7 @@ flag { 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 @@ flag { 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 @@ flag { 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 { 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 b4395a77c24b..de0f070b01a3 100644 --- a/core/java/com/android/internal/accessibility/AccessibilityShortcutController.java +++ b/core/java/com/android/internal/accessibility/AccessibilityShortcutController.java @@ -18,6 +18,7 @@ package com.android.internal.accessibility; 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 @@ public class AccessibilityShortcutController { } 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 @@ public class AccessibilityShortcutController { 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 353e1826a27a..7ec8838699b3 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 @@ public final class ShortcutConstants { * 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 @@ public final class ShortcutConstants { * 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 33048dca12b0..063154d9a6d6 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 @@ import com.android.internal.accessibility.common.ShortcutConstants.ShortcutMenuM */ 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 @@ class AccessibilityActivityTarget extends AccessibilityTarget { 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 e084ebdaf3a8..7eb09e59601b 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.GridView; 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 @@ public class AccessibilityButtonChooserActivity extends Activity { 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 7406da42507f..2b6913ca5e5a 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 @@ class AccessibilityServiceTarget extends AccessibilityTarget { 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 @@ class AccessibilityServiceTarget extends AccessibilityTarget { 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 8e2ec1bfd53d..2e80b7e19516 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.view.accessibility.Flags; 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 @@ import java.util.List; * 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 @@ public class AccessibilityShortcutChooserActivity extends Activity { 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 4ab1ee9d6b46..652cb5233461 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.content.Context; 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 @@ import com.android.internal.annotations.VisibleForTesting; 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 @@ public abstract class AccessibilityTarget implements TargetOperations, OnTargetS 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 @@ public abstract class AccessibilityTarget implements TargetOperations, OnTargetS 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 abstract class AccessibilityTarget implements TargetOperations, OnTargetS 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 @@ public abstract class AccessibilityTarget implements TargetOperations, OnTargetS 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 bd63e23a066f..51a5ddfa8dd6 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.text.BidiFormatter; 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 @@ public final class AccessibilityTargetHelper { * @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 @@ public final class AccessibilityTargetHelper { * @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 @@ public final class AccessibilityTargetHelper { } 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 @@ public final class AccessibilityTargetHelper { } 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 @@ public final class AccessibilityTargetHelper { 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 @@ public final class AccessibilityTargetHelper { } 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 @@ public final class AccessibilityTargetHelper { } 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 @@ public final class AccessibilityTargetHelper { 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 641a9f18e3d6..1bc8b84e6869 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.annotation.NonNull; 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 @@ import java.util.Set; 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 @@ public class InvisibleToggleAccessibilityServiceTarget extends AccessibilityServ 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 2204c0beb4fd..c22f17dfa967 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 @@ package com.android.internal.accessibility.dialog; 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 @@ import com.android.internal.accessibility.common.ShortcutConstants.Accessibility */ 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 a6ef73e90772..a4ffef6bfbc2 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.accessibilityservice.AccessibilityServiceInfo; 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 @@ class ToggleAccessibilityServiceTarget extends AccessibilityServiceTarget { 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 2a9c555efce8..11e668f51774 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.content.Context; 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 @@ import com.android.internal.accessibility.dialog.TargetAdapter.ViewHolder; */ 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 4926e7218530..04f5061fbd8e 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.accessibilityservice.AccessibilityServiceInfo; 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 @@ import com.android.internal.accessibility.common.ShortcutConstants.Accessibility */ 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 @@ class VolumeShortcutToggleAccessibilityServiceTarget extends AccessibilityServic @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 1e4bcf21c635..6b074a610818 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_BUTTON_MODE_GESTURE 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 static com.android.internal.util.FrameworkStatsLog.NON_A11Y_TOOL_SERVICE_ 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 @@ public final class AccessibilityStatsLogUtils { /** * 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 @@ public final class AccessibilityStatsLogUtils { /** * 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 @@ public final class AccessibilityStatsLogUtils { * @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 @@ public final class AccessibilityStatsLogUtils { } 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 @@ public final class AccessibilityStatsLogUtils { } 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 276c5c4ffdea..3fd303038c57 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.provider.Settings; 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 @@ public final class ShortcutUtils { * @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 @@ public final class ShortcutUtils { 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 @@ public final class ShortcutUtils { } /** + * 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 final class ShortcutUtils { 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/pkg/component/ParsedProcess.java b/core/java/com/android/internal/pm/pkg/component/ParsedProcess.java index e5247f99f398..852ed1c3e2ac 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 @@ public interface ParsedProcess { @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 212fb867e7df..ff9b11a3d06a 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 @@ public class ParsedProcessImpl implements ParsedProcess, Parcelable { @ApplicationInfo.NativeHeapZeroInitialized private int nativeHeapZeroInitialized = ApplicationInfo.ZEROINIT_DEFAULT; + private boolean useEmbeddedDex; + public ParsedProcessImpl() { } @@ -65,6 +67,7 @@ public class ParsedProcessImpl implements ParsedProcess, Parcelable { gwpAsanMode = other.getGwpAsanMode(); memtagMode = other.getMemtagMode(); nativeHeapZeroInitialized = other.getNativeHeapZeroInitialized(); + useEmbeddedDex = other.isUseEmbeddedDex(); } public void addStateFrom(@NonNull ParsedProcess other) { @@ -72,6 +75,7 @@ public class ParsedProcessImpl implements ParsedProcess, Parcelable { 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 @@ public class ParsedProcessImpl implements ParsedProcess, Parcelable { @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 @@ public class ParsedProcessImpl implements ParsedProcess, Parcelable { 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 @@ public class ParsedProcessImpl implements ParsedProcess, Parcelable { } @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 @@ public class ParsedProcessImpl implements ParsedProcess, Parcelable { } @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 @@ public class ParsedProcessImpl implements ParsedProcess, Parcelable { // 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 @@ public class ParsedProcessImpl implements ParsedProcess, Parcelable { // 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 @@ public class ParsedProcessImpl implements ParsedProcess, Parcelable { 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 @@ public class ParsedProcessImpl implements ParsedProcess, Parcelable { }; @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 3b2056e7892e..dd58815fdc5e 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.ArrayMap; 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 @@ public class ParsedProcessUtils { 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 000000000000..ea9abdbc4388 --- /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/widget/EmphasizedNotificationButton.java b/core/java/com/android/internal/widget/EmphasizedNotificationButton.java index ce6af49eef0b..5cda3f2b2bc0 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.Notification.CallStyle.DEBUG_NEW_ACTION_LAYOUT; +import static android.app.Notification.CallStyle.USE_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 @@ public class EmphasizedNotificationButton extends Button { 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 class EmphasizedNotificationButton extends Button { 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 @@ public class EmphasizedNotificationButton extends Button { 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 (!USE_NEW_ACTION_LAYOUT) { + 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 (!USE_NEW_ACTION_LAYOUT) { + 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 (!USE_NEW_ACTION_LAYOUT) { + 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 class EmphasizedNotificationButton extends Button { 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 627e8779f9d0..e59132780c56 100644 --- a/core/java/com/android/internal/widget/LockSettingsInternal.java +++ b/core/java/com/android/internal/widget/LockSettingsInternal.java @@ -171,11 +171,11 @@ public abstract class LockSettingsInternal { * 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 index 25e30034fe8f..869e676f4a42 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 @@ package com.android.internal.widget; * 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 @@ oneway interface ILockSettingsStateListener { * @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/MessagingGroup.java b/core/java/com/android/internal/widget/MessagingGroup.java index 30e4099f9a6f..f8f104943c61 100644 --- a/core/java/com/android/internal/widget/MessagingGroup.java +++ b/core/java/com/android/internal/widget/MessagingGroup.java @@ -55,7 +55,9 @@ import java.util.List; * 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/NotificationActionListLayout.java b/core/java/com/android/internal/widget/NotificationActionListLayout.java index a7a69c9e43fb..69d254499ef4 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.Notification.CallStyle.USE_NEW_ACTION_LAYOUT; + 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 @@ import java.util.Comparator; */ @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 @@ public class NotificationActionListLayout extends LinearLayout { } } + 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 @@ public class NotificationActionListLayout extends LinearLayout { 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 @@ public class NotificationActionListLayout extends LinearLayout { } /** + * 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 && !USE_NEW_ACTION_LAYOUT) { + 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 @@ public class NotificationActionListLayout extends LinearLayout { } } + 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 000000000000..b5e9b8f537e4 --- /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 7b154a54fc85..bddad94f8688 100644 --- a/core/java/com/android/internal/widget/RemeasuringLinearLayout.java +++ b/core/java/com/android/internal/widget/RemeasuringLinearLayout.java @@ -30,7 +30,7 @@ import java.util.ArrayList; * 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 @@ public class RemeasuringLinearLayout extends LinearLayout { 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 @@ public class RemeasuringLinearLayout extends LinearLayout { 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 000000000000..61724ec128c2 --- /dev/null +++ b/core/java/com/android/internal/widget/remotecompose/OWNERS @@ -0,0 +1,6 @@ +nicolasroard@google.com +hoford@google.com +jnichol@google.com +sihua@google.com +sunnygoyal@google.com +oscarad@google.com diff --git a/core/java/com/android/internal/widget/remotecompose/core/CompanionOperation.java b/core/java/com/android/internal/widget/remotecompose/core/CompanionOperation.java new file mode 100644 index 000000000000..ce8ca0d781e4 --- /dev/null +++ b/core/java/com/android/internal/widget/remotecompose/core/CompanionOperation.java @@ -0,0 +1,30 @@ +/* + * 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.List; + +/** + * Interface for the companion operations + */ +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 000000000000..0e4c7430afb5 --- /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 000000000000..7cb9a4272704 --- /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 000000000000..b8bb1f0f3519 --- /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 000000000000..6999cdeadfd7 --- /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 000000000000..2f3fe5739b58 --- /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/core/java/com/android/internal/widget/remotecompose/core/Platform.java b/core/java/com/android/internal/widget/remotecompose/core/Platform.java new file mode 100644 index 000000000000..abda0c0d9a0c --- /dev/null +++ b/core/java/com/android/internal/widget/remotecompose/core/Platform.java @@ -0,0 +1,24 @@ +/* + * 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; + +/** + * Services that are needed to be provided by the platform during encoding. + */ +public interface Platform { + byte[] imageToByteArray(Object image); +} + 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 000000000000..3ab6c4744fb9 --- /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/core/java/com/android/internal/widget/remotecompose/core/RemoteComposeOperation.java b/core/java/com/android/internal/widget/remotecompose/core/RemoteComposeOperation.java new file mode 100644 index 000000000000..c7ec33593286 --- /dev/null +++ b/core/java/com/android/internal/widget/remotecompose/core/RemoteComposeOperation.java @@ -0,0 +1,20 @@ +/* + * 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; + +public interface RemoteComposeOperation extends Operation { + +} 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 000000000000..17e8c839a080 --- /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 000000000000..1b7c6fd0f218 --- /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 000000000000..3e701c17291b --- /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 000000000000..4bfdc59aad3a --- /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 000000000000..a3cd1973f647 --- /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 000000000000..3fbdf94427d1 --- /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 000000000000..eca43c5e3281 --- /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 000000000000..ad4caea7aef8 --- /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 000000000000..64c7f3ef2d44 --- /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 000000000000..5b622ae96d0b --- /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 000000000000..cbe9c12e666c --- /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 000000000000..8051ef1ab37c --- /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 000000000000..bcda27a40f64 --- /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 000000000000..cc1f3ddcc093 --- /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 000000000000..3799cf6baac9 --- /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 000000000000..ce15855fecfc --- /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 000000000000..672dae32d8e0 --- /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 000000000000..a3bb73e22c59 --- /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/jni/android_hardware_input_InputWindowHandle.cpp b/core/jni/android_hardware_input_InputWindowHandle.cpp index ae23942f2500..bed776836043 100644 --- a/core/jni/android_hardware_input_InputWindowHandle.cpp +++ b/core/jni/android_hardware_input_InputWindowHandle.cpp @@ -75,6 +75,7 @@ static struct { jfieldID windowToken; jfieldID focusTransferTarget; jfieldID alpha; + jfieldID canOccludePresentation; } gInputWindowHandleClassInfo; static struct { @@ -327,6 +328,8 @@ jobject android_view_InputWindowHandle_fromWindowInfo(JNIEnv* env, gui::WindowIn javaObjectForIBinder(env, windowInfo.windowToken)); env->SetFloatField(inputWindowHandle, gInputWindowHandleClassInfo.alpha, windowInfo.alpha); + env->SetBooleanField(inputWindowHandle, gInputWindowHandleClassInfo.canOccludePresentation, + windowInfo.canOccludePresentation); return inputWindowHandle; } @@ -451,6 +454,9 @@ int register_android_view_InputWindowHandle(JNIEnv* env) { 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_media_AudioSystem.cpp b/core/jni/android_media_AudioSystem.cpp index 3413ededb889..969e47b6a803 100644 --- a/core/jni/android_media_AudioSystem.cpp +++ b/core/jni/android_media_AudioSystem.cpp @@ -25,6 +25,7 @@ #include <android_os_Parcel.h> #include <audiomanager/AudioManager.h> #include <jni.h> +#include <media/AidlConversion.h> #include <media/AudioContainers.h> #include <media/AudioPolicy.h> #include <media/AudioSystem.h> @@ -66,6 +67,11 @@ static struct { jmethodID toArray; } gArrayListMethods; +static jclass gIntArrayClass; +static struct { + jmethodID add; +} gIntArrayMethods; + static jclass gBooleanClass; static jmethodID gBooleanCstor; @@ -1607,6 +1613,48 @@ android_media_AudioSystem_listAudioPorts(JNIEnv *env, jobject clazz, return jStatus; } +// From AudioDeviceInfo +static const int GET_DEVICES_INPUTS = 0x0001; +static const int GET_DEVICES_OUTPUTS = 0x0002; + +static int android_media_AudioSystem_getSupportedDeviceTypes(JNIEnv *env, jobject clazz, + jint direction, jobject jDeviceTypes) { + if (jDeviceTypes == NULL) { + ALOGE("%s NULL Device Types IntArray", __func__); + return AUDIO_JAVA_BAD_VALUE; + } + if (!env->IsInstanceOf(jDeviceTypes, gIntArrayClass)) { + ALOGE("%s not an IntArray", __func__); + return AUDIO_JAVA_BAD_VALUE; + } + + // Convert AudioManager.GET_DEVICES_ flags to AUDIO_PORT_ROLE_ constants + audio_port_role_t role; + if (direction == GET_DEVICES_INPUTS) { + role = AUDIO_PORT_ROLE_SOURCE; + } else if (direction == GET_DEVICES_OUTPUTS) { + role = AUDIO_PORT_ROLE_SINK; + } else { + ALOGE("%s invalid direction : 0x%X", __func__, direction); + return AUDIO_JAVA_BAD_VALUE; + } + + std::vector<media::AudioPortFw> deviceList; + AudioSystem::listDeclaredDevicePorts(static_cast<media::AudioPortRole>(role), &deviceList); + + // Walk the device list + for (const auto &device : deviceList) { + ConversionResult<audio_port_v7> result = aidl2legacy_AudioPortFw_audio_port_v7(device); + + struct audio_port_v7 port = VALUE_OR_RETURN_STATUS(result); + assert(port.type == AUDIO_PORT_TYPE_DEVICE); + + env->CallVoidMethod(jDeviceTypes, gIntArrayMethods.add, port.ext.device.type); + } + + return AUDIO_JAVA_SUCCESS; +} + static int android_media_AudioSystem_createAudioPatch(JNIEnv *env, jobject clazz, jobjectArray jPatches, jobjectArray jSources, jobjectArray jSinks) @@ -3184,6 +3232,8 @@ static const JNINativeMethod gMethods[] = android_media_AudioSystem_setAudioFlingerBinder), MAKE_JNI_NATIVE_METHOD("listAudioPorts", "(Ljava/util/ArrayList;[I)I", android_media_AudioSystem_listAudioPorts), + MAKE_JNI_NATIVE_METHOD("getSupportedDeviceTypes", "(ILandroid/util/IntArray;)I", + android_media_AudioSystem_getSupportedDeviceTypes), MAKE_JNI_NATIVE_METHOD("createAudioPatch", "([Landroid/media/AudioPatch;[Landroid/media/" "AudioPortConfig;[Landroid/media/AudioPortConfig;)I", @@ -3325,6 +3375,10 @@ int register_android_media_AudioSystem(JNIEnv *env) gArrayListMethods.add = GetMethodIDOrDie(env, arrayListClass, "add", "(Ljava/lang/Object;)Z"); gArrayListMethods.toArray = GetMethodIDOrDie(env, arrayListClass, "toArray", "()[Ljava/lang/Object;"); + jclass intArrayClass = FindClassOrDie(env, "android/util/IntArray"); + gIntArrayClass = MakeGlobalRefOrDie(env, intArrayClass); + gIntArrayMethods.add = GetMethodIDOrDie(env, gIntArrayClass, "add", "(I)V"); + jclass booleanClass = FindClassOrDie(env, "java/lang/Boolean"); gBooleanClass = MakeGlobalRefOrDie(env, booleanClass); gBooleanCstor = GetMethodIDOrDie(env, booleanClass, "<init>", "(Z)V"); diff --git a/core/jni/android_opengl_EGL14.cpp b/core/jni/android_opengl_EGL14.cpp index 2f29cae42d07..917d28348d04 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 b9c36b9ed9aa..447b8ec58ca7 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 cdc985273472..ffd75ea053d5 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 @@ nativeClassInit(JNIEnv *_env, jclass glImplClass) 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 @@ fromEGLHandle(JNIEnv *_env, jmethodID mid, jobject obj) { 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 @@ android_eglPresentationTimeANDROID 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 d65b498404fa..2d921ad59458 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 3638b87e201f..35a9a68cd8ab 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 9724e6c2a5dd..e04b56e5fa44 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 1ffa4ec67ae1..bccbda6d3f08 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 d832558aa368..165262e0893d 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 719c6b32fec6..d3fe439eafb7 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 afe7c63b6d47..b123f9d403df 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 81274331ffa4..1e4049b8671d 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 7ed754850ea3..e0175f079ab7 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 239c6260800b..aae0da9006a2 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 @@ static struct { jmethodID ctor; jmethodID addMotionRange; + jmethodID setShouldSmoothScroll; } gInputDeviceClassInfo; jobject android_view_InputDevice_create(JNIEnv* env, const InputDeviceInfo& deviceInfo) { @@ -103,6 +103,18 @@ jobject android_view_InputDevice_create(JNIEnv* env, const InputDeviceInfo& devi } } + 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 @@ int register_android_view_InputDevice(JNIEnv* env) 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/com_google_android_gles_jni_GLImpl.cpp b/core/jni/com_google_android_gles_jni_GLImpl.cpp index 21de72397384..ef29c880a8a1 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/res/AndroidManifest.xml b/core/res/AndroidManifest.xml index 0171f584a838..c71a8420ae88 100644 --- a/core/res/AndroidManifest.xml +++ b/core/res/AndroidManifest.xml @@ -881,6 +881,19 @@ android:description="@string/permdesc_writeContacts" android:protectionLevel="dangerous" /> + <!-- Allows an app to update the verification status of E2EE contact keys owned by other apps. + <p>This permission is only granted to system apps. + <p>Protection level: signature|privileged + @SystemApi + @hide + @FlaggedApi("android.provider.user_keys") + --> + <permission android:name="android.permission.WRITE_VERIFICATION_STATE_E2EE_CONTACT_KEYS" + android:permissionGroup="android.permission-group.UNDEFINED" + android:label="@string/permlab_writeVerificationStateE2eeContactKeys" + android:description="@string/permdesc_writeVerificationStateE2eeContactKeys" + android:protectionLevel="signature|privileged" /> + <!-- Allows an application to set default account for new contacts. <p> This permission is only granted to system applications fulfilling the Contacts app role. <p>Protection level: internal|role @@ -2632,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 --> <!-- ======================================== --> @@ -3188,6 +3208,14 @@ <permission android:name="android.permission.INTERACT_ACROSS_PROFILES" android:protectionLevel="signature|appop" /> + <!-- Allows applications to access profiles with ACCESS_HIDDEN_PROFILES user property + <p>Protection level: normal + @FlaggedApi("android.multiuser.enable_permission_to_access_hidden_profiles") --> + <permission android:name="android.permission.ACCESS_HIDDEN_PROFILES" + android:label="@string/permlab_accessHiddenProfile" + android:description="@string/permdesc_accessHiddenProfile" + android:protectionLevel="normal" /> + <!-- @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" /> @@ -5563,6 +5591,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" /> @@ -7828,7 +7857,7 @@ android:protectionLevel="normal"/> <!-- @FlaggedApi("android.app.job.backup_jobs_exemption") - Gives applications whose <b>primary use case</b> is to backup or sync content increased + Gives applications with a <b>major use case</b> of backing-up or syncing content increased job execution allowance in order to complete the related work. The jobs must have a valid content URI trigger and network constraint set. <p>This is a special access permission that can be revoked by the system or the user. diff --git a/core/res/assets/geoid_height_map/README.md b/core/res/assets/geoid_map/README.md index 849d32ec34aa..5d480c18a70d 100644 --- a/core/res/assets/geoid_height_map/README.md +++ b/core/res/assets/geoid_map/README.md @@ -1,2 +1,2 @@ -These binary protos are generated at runtime from the text protos in ../../geoid_height_map_assets -and using aprotoc.
\ No newline at end of file +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 Binary files differnew file mode 100644 index 000000000000..c2ec5d977edd --- /dev/null +++ b/core/res/assets/geoid_map/expiration-distance-disk-tile-1.pb 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 Binary files differnew file mode 100644 index 000000000000..e4b3b0c89246 --- /dev/null +++ b/core/res/assets/geoid_map/expiration-distance-disk-tile-3.pb 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 Binary files differnew file mode 100644 index 000000000000..95f54c224cbb --- /dev/null +++ b/core/res/assets/geoid_map/expiration-distance-disk-tile-5.pb 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 Binary files differnew file mode 100644 index 000000000000..896bbc1948ec --- /dev/null +++ b/core/res/assets/geoid_map/expiration-distance-disk-tile-7.pb 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 Binary files differnew file mode 100644 index 000000000000..ddc687d8d1f5 --- /dev/null +++ b/core/res/assets/geoid_map/expiration-distance-disk-tile-9.pb 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 Binary files differnew file mode 100644 index 000000000000..eb405f6116dd --- /dev/null +++ b/core/res/assets/geoid_map/expiration-distance-disk-tile-b.pb diff --git a/core/res/assets/geoid_map/expiration-distance-params.pb b/core/res/assets/geoid_map/expiration-distance-params.pb Binary files differnew file mode 100644 index 000000000000..e160120fa2a5 --- /dev/null +++ b/core/res/assets/geoid_map/expiration-distance-params.pb diff --git a/core/res/assets/geoid_height_map/tile-1.pb b/core/res/assets/geoid_map/geoid-height-disk-tile-1.pb Binary files differindex c0f1242de216..c0f1242de216 100644 --- a/core/res/assets/geoid_height_map/tile-1.pb +++ b/core/res/assets/geoid_map/geoid-height-disk-tile-1.pb diff --git a/core/res/assets/geoid_height_map/tile-3.pb b/core/res/assets/geoid_map/geoid-height-disk-tile-3.pb Binary files differindex cc304712db8c..cc304712db8c 100644 --- a/core/res/assets/geoid_height_map/tile-3.pb +++ b/core/res/assets/geoid_map/geoid-height-disk-tile-3.pb diff --git a/core/res/assets/geoid_height_map/tile-5.pb b/core/res/assets/geoid_map/geoid-height-disk-tile-5.pb Binary files differindex 7e1f0086fff7..7e1f0086fff7 100644 --- a/core/res/assets/geoid_height_map/tile-5.pb +++ b/core/res/assets/geoid_map/geoid-height-disk-tile-5.pb diff --git a/core/res/assets/geoid_height_map/tile-7.pb b/core/res/assets/geoid_map/geoid-height-disk-tile-7.pb Binary files differindex 3bcdaac5c9fc..3bcdaac5c9fc 100644 --- a/core/res/assets/geoid_height_map/tile-7.pb +++ b/core/res/assets/geoid_map/geoid-height-disk-tile-7.pb diff --git a/core/res/assets/geoid_height_map/tile-9.pb b/core/res/assets/geoid_map/geoid-height-disk-tile-9.pb Binary files differindex 558970d4b8ef..558970d4b8ef 100644 --- a/core/res/assets/geoid_height_map/tile-9.pb +++ b/core/res/assets/geoid_map/geoid-height-disk-tile-9.pb diff --git a/core/res/assets/geoid_height_map/tile-b.pb b/core/res/assets/geoid_map/geoid-height-disk-tile-b.pb Binary files differindex fbe02daaae93..fbe02daaae93 100644 --- a/core/res/assets/geoid_height_map/tile-b.pb +++ b/core/res/assets/geoid_map/geoid-height-disk-tile-b.pb diff --git a/core/res/assets/geoid_height_map/map-params.pb b/core/res/assets/geoid_map/geoid-height-params.pb Binary files differindex 6414557040ec..6414557040ec 100644 --- a/core/res/assets/geoid_height_map/map-params.pb +++ b/core/res/assets/geoid_map/geoid-height-params.pb diff --git a/core/res/geoid_height_map_assets/README.md b/core/res/geoid_map_assets/README.md index 37a57b8d3a81..37a57b8d3a81 100644 --- a/core/res/geoid_height_map_assets/README.md +++ b/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 000000000000..a4170e68f9ab --- /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 000000000000..684f84852b02 --- /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 000000000000..387bbdf01536 --- /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 000000000000..371e9ecaf96f --- /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 000000000000..59f386c59f13 --- /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 000000000000..e65c4335493c --- /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 000000000000..b3bbddbef84a --- /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 index b0c804455c50..b0c804455c50 100644 --- a/core/res/geoid_height_map_assets/tile-1.textpb +++ b/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 index 9abaaaa38bf4..9abaaaa38bf4 100644 --- a/core/res/geoid_height_map_assets/tile-3.textpb +++ b/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 index 0e43c84f484f..0e43c84f484f 100644 --- a/core/res/geoid_height_map_assets/tile-5.textpb +++ b/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 index 9667e6400e1a..9667e6400e1a 100644 --- a/core/res/geoid_height_map_assets/tile-7.textpb +++ b/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 index f556a35f4699..f556a35f4699 100644 --- a/core/res/geoid_height_map_assets/tile-9.textpb +++ b/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 index b9b5bfcef02c..b9b5bfcef02c 100644 --- a/core/res/geoid_height_map_assets/tile-b.textpb +++ b/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 index 170e73b76e29..170e73b76e29 100644 --- a/core/res/geoid_height_map_assets/map-params.textpb +++ b/core/res/geoid_map_assets/geoid-height-params.textpb diff --git a/core/res/res/drawable/ic_satellite_alt_24px.xml b/core/res/res/drawable/ic_satellite_alt_24px.xml new file mode 100644 index 000000000000..f9ca7dc66798 --- /dev/null +++ b/core/res/res/drawable/ic_satellite_alt_24px.xml @@ -0,0 +1,25 @@ +<!-- + 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. +--> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="960" + android:viewportHeight="960" + android:tint="?android:attr/colorControlNormal"> + <path + android:fillColor="@android:color/white" + android:pathData="M560,928L560,848Q677,848 758.5,766.5Q840,685 840,568L920,568Q920,643 891.5,708.5Q863,774 814.5,822.5Q766,871 700.5,899.5Q635,928 560,928ZM560,768L560,688Q610,688 645,653Q680,618 680,568L760,568Q760,651 701.5,709.5Q643,768 560,768ZM222,903Q207,903 192,897Q177,891 165,880L23,738Q12,726 6,711Q0,696 0,681Q0,665 6,650.5Q12,636 23,625L150,498Q173,475 207,474.5Q241,474 264,497L314,547L342,519L292,469Q269,446 269,413Q269,380 292,357L349,300Q372,277 405.5,277Q439,277 462,300L512,350L540,322L490,272Q467,249 467,215.5Q467,182 490,159L617,32Q629,20 644,14Q659,8 674,8Q689,8 703.5,14Q718,20 730,32L872,174Q884,185 889.5,199.5Q895,214 895,230Q895,245 889.5,260Q884,275 872,287L745,414Q722,437 688.5,437Q655,437 632,414L582,364L554,392L604,442Q627,465 626.5,498.5Q626,532 603,555L547,611Q524,634 490.5,634Q457,634 434,611L384,561L356,589L406,639Q429,662 428.5,696Q428,730 405,753L278,880Q267,891 252.5,897Q238,903 222,903ZM222,824Q222,824 222,824Q222,824 222,824L264,782L122,640L80,682Q80,682 80,682Q80,682 80,682L222,824ZM307,739L349,697Q349,697 349,697Q349,697 349,697L207,555Q207,555 207,555Q207,555 207,555L165,597L307,739ZM491,555Q491,555 491,555Q491,555 491,555L547,499Q547,499 547,499Q547,499 547,499L405,357Q405,357 405,357Q405,357 405,357L349,413Q349,413 349,413Q349,413 349,413L491,555ZM689,357Q689,357 689,357Q689,357 689,357L731,315L589,173L547,215Q547,215 547,215Q547,215 547,215L689,357ZM774,272L816,230Q816,230 816,230Q816,230 816,230L674,88Q674,88 674,88Q674,88 674,88L632,130L774,272ZM448,456L448,456Q448,456 448,456Q448,456 448,456L448,456Q448,456 448,456Q448,456 448,456L448,456Q448,456 448,456Q448,456 448,456L448,456Q448,456 448,456Q448,456 448,456Z"/> +</vector> diff --git a/core/res/res/values/attrs.xml b/core/res/res/values/attrs.xml index 2eb28eb555e8..e7b1d09d6ab0 100644 --- a/core/res/res/values/attrs.xml +++ b/core/res/res/values/attrs.xml @@ -4375,9 +4375,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 +5788,17 @@ <enum name="none" value="0" /> <!-- Justification by stretching word spacing. --> <enum name="inter_word" value = "1" /> - </attr> + <!-- 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 6884fc0057d9..65c4d9fdab78 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 @@ -2793,6 +2799,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 diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml index 238f242537cb..951625ea844d 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 @@ -6943,4 +6950,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/public-staging.xml b/core/res/res/values/public-staging.xml index 830e99ca907b..81a8908b37df 100644 --- a/core/res/res/values/public-staging.xml +++ b/core/res/res/values/public-staging.xml @@ -143,6 +143,10 @@ <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"/> </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 558bae727415..be96cc2d6362 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> @@ -1572,6 +1593,11 @@ <string name="permdesc_setWallpaper">Allows the app to set the system wallpaper.</string> <!-- Title of an application permission, listed so the user can choose whether they want to allow the application to do this. --> + <string name="permlab_accessHiddenProfile">Access hidden profiles</string> + <!-- Description of an application permission, listed so the user can choose whether they want to allow the application to do this. --> + <string name="permdesc_accessHiddenProfile">Allows the app to access hidden profiles.</string> + + <!-- Title of an application permission, listed so the user can choose whether they want to allow the application to do this. --> <string name="permlab_setWallpaperHints">adjust your wallpaper size</string> <!-- Description of an application permission, listed so the user can choose whether they want to allow the application to do this. --> <string name="permdesc_setWallpaperHints">Allows the app to set the system wallpaper size hints.</string> @@ -2222,6 +2248,11 @@ <!-- Description of an application permission, listed so the user can choose whether they want to allow the application to do this.[CHAR_LIMIT=NONE] --> <string name="permdesc_updatePackagesWithoutUserAction">Allows the holder to update the app it previously installed without user action</string> + <!-- Title of an application permission, listed so the user can choose whether they want to allow the application to do this. [CHAR_LIMIT=NONE] --> + <string name="permlab_writeVerificationStateE2eeContactKeys">update the verification states of E2EE contact keys owned by other apps</string> + <!-- Description of an application permission, listed so the user can choose whether they want to allow the application to do this.[CHAR_LIMIT=NONE] --> + <string name="permdesc_writeVerificationStateE2eeContactKeys">Allows the app to update the verification states of E2EE contact keys owned by other apps</string> + <!-- Policy administration --> <!-- Title of policy access to limiting the user's password choices --> @@ -6386,4 +6417,14 @@ ul.</string> <string name="redacted_notification_message"></string> <!-- Notification action title used instead of a notification's normal title sensitive [CHAR_LIMIT=NOTIF_BODY] --> <string name="redacted_notification_action_title"></string> + + <!-- Satellite related messages --> + <!-- Notification title when satellite service is connected. --> + <string name="satellite_notification_title">Auto connected to satellite</string> + <!-- Notification summary when satellite service is connected. [CHAR LIMIT=NONE] --> + <string name="satellite_notification_summary">You can send and receive messages without a mobile or Wi-Fi network</string> + <!-- Invoke "What to expect" dialog of messaging application --> + <string name="satellite_notification_open_message">Open Messages</string> + <!-- Invoke Satellite setting activity of Settings --> + <string name="satellite_notification_how_it_works">How it works</string> </resources> diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index 8b74cbfce5e3..b8a399b9f77b 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" /> @@ -5329,4 +5339,13 @@ <java-symbol type="bool" name="config_showPercentageTextDuringRebootToUpdate" /> <java-symbol type="integer" name="config_minMillisBetweenInputUserActivityEvents" /> + + <!-- System notification for satellite service --> + <java-symbol type="string" name="satellite_notification_title" /> + <java-symbol type="string" name="satellite_notification_summary" /> + <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/tests/BroadcastRadioTests/OWNERS b/core/tests/BroadcastRadioTests/OWNERS index d2bdd643b0a2..51a85e48832e 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/coretests/Android.bp b/core/tests/coretests/Android.bp index f47679991418..1b25d7fa32a4 100644 --- a/core/tests/coretests/Android.bp +++ b/core/tests/coretests/Android.bp @@ -203,12 +203,12 @@ android_ravenwood_test { "androidx.test.uiautomator_uiautomator", "compatibility-device-util-axt", "flag-junit", - "mockito_ravenwood", "platform-test-annotations", "flag-junit", "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/core/tests/coretests/src/android/app/ActivityManagerTest.java b/core/tests/coretests/src/android/app/ActivityManagerTest.java new file mode 100644 index 000000000000..d930e4d79a3a --- /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 000000000000..93253915f7f3 --- /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 d8305f054d11..56ab03419b66 100644 --- a/core/tests/coretests/src/android/app/NotificationChannelTest.java +++ b/core/tests/coretests/src/android/app/NotificationChannelTest.java @@ -19,6 +19,9 @@ package android.app; 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.Bundle; 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.android.modules.utils.TypedXmlSerializer; 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.BufferedOutputStream; 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 @@ public class NotificationChannelTest { 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 @@ public class NotificationChannelTest { 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/ddm/OWNERS b/core/tests/coretests/src/android/ddm/OWNERS deleted file mode 100644 index c8be1919fb93..000000000000 --- a/core/tests/coretests/src/android/ddm/OWNERS +++ /dev/null @@ -1 +0,0 @@ -michschn@google.com diff --git a/core/tests/coretests/src/android/os/HandlerThreadTest.java b/core/tests/coretests/src/android/os/HandlerThreadTest.java index 0bac1c728f3b..32378127d897 100644 --- a/core/tests/coretests/src/android/os/HandlerThreadTest.java +++ b/core/tests/coretests/src/android/os/HandlerThreadTest.java @@ -28,15 +28,20 @@ import android.platform.test.ravenwood.RavenwoodRule; 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 @@ public class HandlerThreadTest { 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/VibrationAttributesTest.java b/core/tests/coretests/src/android/os/VibrationAttributesTest.java new file mode 100644 index 000000000000..f5a81c582b28 --- /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/tracing/perfetto/DataSourceTest.java b/core/tests/coretests/src/android/tracing/perfetto/DataSourceTest.java index bd2f36fb5198..d57f1fcc6da1 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 @@ public class DataSourceTest { 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 index 7248983c741c..45228422b97b 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 @@ import java.util.Arrays; @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 cf3eb12498ca..52e996cab3ed 100644 --- a/core/tests/coretests/src/android/view/ViewRootImplTest.java +++ b/core/tests/coretests/src/android/view/ViewRootImplTest.java @@ -18,7 +18,9 @@ package android.view; 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 @@ public class ViewRootImplTest { * 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 @@ public class ViewRootImplTest { * <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 @@ public class ViewRootImplTest { * >=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 @@ public class ViewRootImplTest { } /** + * 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 @@ public class ViewRootImplTest { 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 @@ public class ViewRootImplTest { }); } + /** + * 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/window/WindowOnBackInvokedDispatcherTest.java b/core/tests/coretests/src/android/window/WindowOnBackInvokedDispatcherTest.java index 52ff0d4037e8..a709d7be898b 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_DEFAULT; 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 @@ public class WindowOnBackInvokedDispatcherTest { } @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 @@ public class WindowOnBackInvokedDispatcherTest { 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 145aa60dc495..75b0d4a159d9 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_DIALOG_SHO 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 androidx.test.ext.junit.runners.AndroidJUnit4; 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 @@ public class AccessibilityShortcutControllerTest { 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 @@ public class AccessibilityShortcutControllerTest { (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 @@ public class AccessibilityShortcutControllerTest { 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 2ea044ccfb52..69b6a9b7aa87 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.platform.app.InstrumentationRegistry; 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 @@ public class InvisibleToggleAccessibilityServiceTargetTest { mSut = new InvisibleToggleAccessibilityServiceTarget( mContextSpy, - ShortcutConstants.UserShortcutType.HARDWARE, accessibilityServiceInfo); + AccessibilityManager.ACCESSIBILITY_SHORTCUT_KEY, accessibilityServiceInfo); } @Test 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 000000000000..08333ecd99a3 --- /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/utiltests/Android.bp b/core/tests/utiltests/Android.bp index 2ccee71c8ff7..f5563a710563 100644 --- a/core/tests/utiltests/Android.bp +++ b/core/tests/utiltests/Android.bp @@ -60,7 +60,6 @@ android_ravenwood_test { static_libs: [ "androidx.annotation_annotation", "androidx.test.rules", - "mockito_ravenwood", "frameworks-base-testutils", "servicestests-utils", ], diff --git a/data/etc/Android.bp b/data/etc/Android.bp index ade20d282acc..1fd10031a129 100644 --- a/data/etc/Android.bp +++ b/data/etc/Android.bp @@ -66,6 +66,12 @@ prebuilt_etc { 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 000000000000..4a9dd2fe665a --- /dev/null +++ b/data/etc/enhanced-confirmation.xml @@ -0,0 +1,32 @@ +<?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-installer + package="com.example.app" + signature="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"/> + +This indicates that "com.example.app" should be exempt from ECM, and that, if "com.example.app" is +an installer, all packages installed via "com.example.app" will also be exempt from ECM. +--> + +<config></config> diff --git a/data/etc/privapp-permissions-platform.xml b/data/etc/privapp-permissions-platform.xml index 28734283e9b7..91e620cd4b83 100644 --- a/data/etc/privapp-permissions-platform.xml +++ b/data/etc/privapp-permissions-platform.xml @@ -185,6 +185,7 @@ applications that come with the platform <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.SEND_SAFETY_CENTER_UPDATE" /> </privapp-permissions> <privapp-permissions package="com.android.providers.calendar"> @@ -571,6 +572,8 @@ applications that come with the platform <!-- Permission required for BinaryTransparencyService shell API and host test --> <permission name="android.permission.GET_BACKGROUND_INSTALLED_PACKAGES" /> <permission name="android.permission.READ_SYSTEM_GRAMMATICAL_GENDER"/> + <!-- Permissions required for CTS test - CtsContactKeysProviderPrivilegedApp --> + <permission name="android.permission.WRITE_VERIFICATION_STATE_E2EE_CONTACT_KEYS"/> </privapp-permissions> <privapp-permissions package="com.android.statementservice"> diff --git a/graphics/java/android/graphics/Canvas.java b/graphics/java/android/graphics/Canvas.java index d1aceafc4e2c..b33a5d2dcef8 100644 --- a/graphics/java/android/graphics/Canvas.java +++ b/graphics/java/android/graphics/Canvas.java @@ -18,6 +18,7 @@ package android.graphics; import android.annotation.ColorInt; import android.annotation.ColorLong; +import android.annotation.FlaggedApi; import android.annotation.IntDef; import android.annotation.IntRange; import android.annotation.NonNull; @@ -30,6 +31,8 @@ import android.graphics.text.TextRunShaper; import android.os.Build; import android.text.TextShaper; +import com.android.graphics.hwui.flags.Flags; + import dalvik.annotation.optimization.CriticalNative; import dalvik.annotation.optimization.FastNative; @@ -766,6 +769,21 @@ public class Canvas extends BaseCanvas { } /** + * Preconcat the current matrix with the specified matrix. If the specified + * matrix is null, this method does nothing. If the canvas's matrix is changed in the z-axis + * through this function, the deprecated {@link #getMatrix()} method will return a 3x3 with + * z-axis info stripped away. + * + * @param m The 4x4 matrix to preconcatenate with the current matrix + */ + @FlaggedApi(Flags.FLAG_MATRIX_44) + public void concat44(@Nullable Matrix44 m) { + if (m != null) { + nConcat(mNativeCanvasWrapper, m.mBackingArray); + } + } + + /** * Completely replace the current matrix with the specified matrix. If the * matrix parameter is null, then the current matrix is reset to identity. * @@ -1444,6 +1462,8 @@ public class Canvas extends BaseCanvas { private static native void nSkew(long canvasHandle, float sx, float sy); @CriticalNative private static native void nConcat(long nativeCanvas, long nativeMatrix); + @FastNative + private static native void nConcat(long nativeCanvas, float[] mat); @CriticalNative private static native void nSetMatrix(long nativeCanvas, long nativeMatrix); @CriticalNative diff --git a/graphics/java/android/graphics/Matrix44.java b/graphics/java/android/graphics/Matrix44.java new file mode 100644 index 000000000000..7cc0eb7a6728 --- /dev/null +++ b/graphics/java/android/graphics/Matrix44.java @@ -0,0 +1,472 @@ +/* + * 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 android.graphics; + +import android.annotation.FlaggedApi; +import android.annotation.NonNull; + +import com.android.graphics.hwui.flags.Flags; + +import java.util.Arrays; + +/** + * The Matrix44 class holds a 4x4 matrix for transforming coordinates. It is similar to + * {@link Matrix}, and should be used when you want to manipulate the canvas in 3D. Values are kept + * in row-major order. The values and operations are treated as column vectors. + */ +@FlaggedApi(Flags.FLAG_MATRIX_44) +public class Matrix44 { + final float[] mBackingArray; + /** + * The default Matrix44 constructor will instantiate an identity matrix. + */ + @FlaggedApi(Flags.FLAG_MATRIX_44) + public Matrix44() { + mBackingArray = new float[]{1.0f, 0.0f, 0.0f, 0.0f, + 0.0f, 1.0f, 0.0f, 0.0f, + 0.0f, 0.0f, 1.0f, 0.0f, + 0.0f, 0.0f, 0.0f, 1.0f}; + } + + /** + * Creates and returns a Matrix44 by taking the 3x3 Matrix and placing it on the 0 of the z-axis + * by setting row {@code 2} and column {@code 2} to the identity as seen in the following + * operation: + * <pre class="prettyprint"> + * [ a b c ] [ a b 0 c ] + * [ d e f ] -> [ d e 0 f ] + * [ g h i ] [ 0 0 1 0 ] + * [ g h 0 i ] + * </pre> + * + * @param mat A 3x3 Matrix to be converted (original Matrix will not be changed) + */ + @FlaggedApi(Flags.FLAG_MATRIX_44) + public Matrix44(@NonNull Matrix mat) { + float[] m = new float[9]; + mat.getValues(m); + mBackingArray = new float[]{m[0], m[1], 0.0f, m[2], + m[3], m[4], 0.0f, m[5], + 0.0f, 0.0f, 1.0f, 0.0f, + m[6], m[7], 0.0f, m[8]}; + } + + /** + * Copies matrix values into the provided array in row-major order. + * + * @param dst The float array where values will be copied, must be of length 16 + * @throws IllegalArgumentException if the destination float array is not of length 16 + */ + @FlaggedApi(Flags.FLAG_MATRIX_44) + public void getValues(@NonNull float [] dst) { + if (dst.length == 16) { + System.arraycopy(mBackingArray, 0, dst, 0, mBackingArray.length); + } else { + throw new IllegalArgumentException("Dst array must be of length 16"); + } + } + + /** + * Replaces the Matrix's values with the values in the provided array. + * + * @param src A float array of length 16. Floats are treated in row-major order + * @throws IllegalArgumentException if the destination float array is not of length 16 + */ + @FlaggedApi(Flags.FLAG_MATRIX_44) + public void setValues(@NonNull float[] src) { + if (src.length == 16) { + System.arraycopy(src, 0, mBackingArray, 0, mBackingArray.length); + } else { + throw new IllegalArgumentException("Src array must be of length 16"); + } + } + + /** + * Gets the value at the matrix's row and column. + * + * @param row An integer from 0 to 4 indicating the row of the value to get + * @param col An integer from 0 to 4 indicating the column of the value to get + */ + @FlaggedApi(Flags.FLAG_MATRIX_44) + public float get(int row, int col) { + if (row >= 0 && row < 4 && col >= 0 && col < 4) { + return mBackingArray[row * 4 + col]; + } + throw new IllegalArgumentException("invalid row and column values"); + } + + /** + * Sets the value at the matrix's row and column to the provided value. + * + * @param row An integer from 0 to 4 indicating the row of the value to change + * @param col An integer from 0 to 4 indicating the column of the value to change + * @param val The value the element at the specified index will be set to + */ + @FlaggedApi(Flags.FLAG_MATRIX_44) + public void set(int row, int col, float val) { + if (row >= 0 && row < 4 && col >= 0 && col < 4) { + mBackingArray[row * 4 + col] = val; + } else { + throw new IllegalArgumentException("invalid row and column values"); + } + } + + /** + * Sets the Matrix44 to the identity matrix. + */ + @FlaggedApi(Flags.FLAG_MATRIX_44) + public void reset() { + for (int i = 0; i < mBackingArray.length; i++) { + mBackingArray[i] = (i % 4 == i / 4) ? 1.0f : 0.0f; + } + } + + /** + * Inverts the Matrix44, then return true if successful, false if unable to invert. + * + * @return {@code true} on success, {@code false} otherwise + */ + @FlaggedApi(Flags.FLAG_MATRIX_44) + public boolean invert() { + float a00 = mBackingArray[0]; + float a01 = mBackingArray[1]; + float a02 = mBackingArray[2]; + float a03 = mBackingArray[3]; + float a10 = mBackingArray[4]; + float a11 = mBackingArray[5]; + float a12 = mBackingArray[6]; + float a13 = mBackingArray[7]; + float a20 = mBackingArray[8]; + float a21 = mBackingArray[9]; + float a22 = mBackingArray[10]; + float a23 = mBackingArray[11]; + float a30 = mBackingArray[12]; + float a31 = mBackingArray[13]; + float a32 = mBackingArray[14]; + float a33 = mBackingArray[15]; + float b00 = a00 * a11 - a01 * a10; + float b01 = a00 * a12 - a02 * a10; + float b02 = a00 * a13 - a03 * a10; + float b03 = a01 * a12 - a02 * a11; + float b04 = a01 * a13 - a03 * a11; + float b05 = a02 * a13 - a03 * a12; + float b06 = a20 * a31 - a21 * a30; + float b07 = a20 * a32 - a22 * a30; + float b08 = a20 * a33 - a23 * a30; + float b09 = a21 * a32 - a22 * a31; + float b10 = a21 * a33 - a23 * a31; + float b11 = a22 * a33 - a23 * a32; + float det = (b00 * b11 - b01 * b10 + b02 * b09 + b03 * b08 - b04 * b07 + b05 * b06); + if (det == 0.0f) { + return false; + } + float invDet = 1.0f / det; + mBackingArray[0] = ((a11 * b11 - a12 * b10 + a13 * b09) * invDet); + mBackingArray[1] = ((-a01 * b11 + a02 * b10 - a03 * b09) * invDet); + mBackingArray[2] = ((a31 * b05 - a32 * b04 + a33 * b03) * invDet); + mBackingArray[3] = ((-a21 * b05 + a22 * b04 - a23 * b03) * invDet); + mBackingArray[4] = ((-a10 * b11 + a12 * b08 - a13 * b07) * invDet); + mBackingArray[5] = ((a00 * b11 - a02 * b08 + a03 * b07) * invDet); + mBackingArray[6] = ((-a30 * b05 + a32 * b02 - a33 * b01) * invDet); + mBackingArray[7] = ((a20 * b05 - a22 * b02 + a23 * b01) * invDet); + mBackingArray[8] = ((a10 * b10 - a11 * b08 + a13 * b06) * invDet); + mBackingArray[9] = ((-a00 * b10 + a01 * b08 - a03 * b06) * invDet); + mBackingArray[10] = ((a30 * b04 - a31 * b02 + a33 * b00) * invDet); + mBackingArray[11] = ((-a20 * b04 + a21 * b02 - a23 * b00) * invDet); + mBackingArray[12] = ((-a10 * b09 + a11 * b07 - a12 * b06) * invDet); + mBackingArray[13] = ((a00 * b09 - a01 * b07 + a02 * b06) * invDet); + mBackingArray[14] = ((-a30 * b03 + a31 * b01 - a32 * b00) * invDet); + mBackingArray[15] = ((a20 * b03 - a21 * b01 + a22 * b00) * invDet); + return true; + } + + /** + * Returns true if Matrix44 is equal to identity matrix. + */ + @FlaggedApi(Flags.FLAG_MATRIX_44) + public boolean isIdentity() { + for (int i = 0; i < mBackingArray.length; i++) { + float expected = (i % 4 == i / 4) ? 1.0f : 0.0f; + if (expected != mBackingArray[i]) return false; + } + return true; + } + + @FlaggedApi(Flags.FLAG_MATRIX_44) + private static float dot(Matrix44 a, Matrix44 b, int row, int col) { + return (a.get(row, 0) * b.get(0, col)) + + (a.get(row, 1) * b.get(1, col)) + + (a.get(row, 2) * b.get(2, col)) + + (a.get(row, 3) * b.get(3, col)); + } + + @FlaggedApi(Flags.FLAG_MATRIX_44) + private static float dot(float r0, float r1, float r2, float r3, + float c0, float c1, float c2, float c3) { + return (r0 * c0) + (r1 * c1) + (r2 * c2) + (r3 * c3); + } + + /** + * Multiplies (x, y, z, w) vector by the Matrix44, then returns the new (x, y, z, w). Users + * should set {@code w} to 1 to indicate the coordinates are normalized. + * + * @return An array of length 4 that represents the x, y, z, w (where w is perspective) value + * after multiplying x, y, z, 1 by the matrix + */ + @FlaggedApi(Flags.FLAG_MATRIX_44) + public @NonNull float[] map(float x, float y, float z, float w) { + float[] dst = new float[4]; + this.map(x, y, z, w, dst); + return dst; + } + + /** + * Multiplies (x, y, z, w) vector by the Matrix44, then returns the new (x, y, z, w). Users + * should set {@code w} to 1 to indicate the coordinates are normalized. + */ + @FlaggedApi(Flags.FLAG_MATRIX_44) + public void map(float x, float y, float z, float w, @NonNull float[] dst) { + if (dst.length != 4) { + throw new IllegalArgumentException("Dst array must be of length 4"); + } + dst[0] = x * mBackingArray[0] + y * mBackingArray[1] + + z * mBackingArray[2] + w * mBackingArray[3]; + dst[1] = x * mBackingArray[4] + y * mBackingArray[5] + + z * mBackingArray[6] + w * mBackingArray[7]; + dst[2] = x * mBackingArray[8] + y * mBackingArray[9] + + z * mBackingArray[10] + w * mBackingArray[11]; + dst[3] = x * mBackingArray[12] + y * mBackingArray[13] + + z * mBackingArray[14] + w * mBackingArray[15]; + } + + /** + * Multiplies `this` matrix (A) and provided Matrix (B) in the order of A * B. + * The result is saved in `this` Matrix. + * + * @param b The second Matrix in the concatenation operation + * @return A reference to this Matrix, which can be used to chain Matrix operations + */ + @FlaggedApi(Flags.FLAG_MATRIX_44) + public @NonNull Matrix44 concat(@NonNull Matrix44 b) { + float val00 = dot(this, b, 0, 0); + float val01 = dot(this, b, 0, 1); + float val02 = dot(this, b, 0, 2); + float val03 = dot(this, b, 0, 3); + float val10 = dot(this, b, 1, 0); + float val11 = dot(this, b, 1, 1); + float val12 = dot(this, b, 1, 2); + float val13 = dot(this, b, 1, 3); + float val20 = dot(this, b, 2, 0); + float val21 = dot(this, b, 2, 1); + float val22 = dot(this, b, 2, 2); + float val23 = dot(this, b, 2, 3); + float val30 = dot(this, b, 3, 0); + float val31 = dot(this, b, 3, 1); + float val32 = dot(this, b, 3, 2); + float val33 = dot(this, b, 3, 3); + + mBackingArray[0] = val00; + mBackingArray[1] = val01; + mBackingArray[2] = val02; + mBackingArray[3] = val03; + mBackingArray[4] = val10; + mBackingArray[5] = val11; + mBackingArray[6] = val12; + mBackingArray[7] = val13; + mBackingArray[8] = val20; + mBackingArray[9] = val21; + mBackingArray[10] = val22; + mBackingArray[11] = val23; + mBackingArray[12] = val30; + mBackingArray[13] = val31; + mBackingArray[14] = val32; + mBackingArray[15] = val33; + + return this; + } + + /** + * Applies a rotation around a given axis, then returns self. + * {@code x}, {@code y}, {@code z} represent the axis by which to rotate around. + * For example, pass in {@code 1, 0, 0} to rotate around the x-axis. + * The axis provided will be normalized. + * + * @param deg Amount in degrees to rotate the matrix about the x-axis + * @param xComp X component of the rotation axis + * @param yComp Y component of the rotation axis + * @param zComp Z component of the rotation axis + * @return A reference to this Matrix, which can be used to chain Matrix operations + */ + @FlaggedApi(Flags.FLAG_MATRIX_44) + public @NonNull Matrix44 rotate(float deg, float xComp, float yComp, float zComp) { + float sum = xComp + yComp + zComp; + float x = xComp / sum; + float y = yComp / sum; + float z = zComp / sum; + + float c = (float) Math.cos(deg * Math.PI / 180.0f); + float s = (float) Math.sin(deg * Math.PI / 180.0f); + float t = 1 - c; + + float rotVals00 = t * x * x + c; + float rotVals01 = t * x * y - s * z; + float rotVals02 = t * x * z + s * y; + float rotVals10 = t * x * y + s * z; + float rotVals11 = t * y * y + c; + float rotVals12 = t * y * z - s * x; + float rotVals20 = t * x * z - s * y; + float rotVals21 = t * y * z + s * x; + float rotVals22 = t * z * z + c; + + float v00 = dot(mBackingArray[0], mBackingArray[1], mBackingArray[2], mBackingArray[3], + rotVals00, rotVals10, rotVals20, 0); + float v01 = dot(mBackingArray[0], mBackingArray[1], mBackingArray[2], mBackingArray[3], + rotVals01, rotVals11, rotVals21, 0); + float v02 = dot(mBackingArray[0], mBackingArray[1], mBackingArray[2], mBackingArray[3], + rotVals02, rotVals12, rotVals22, 0); + float v03 = dot(mBackingArray[0], mBackingArray[1], mBackingArray[2], mBackingArray[3], + 0, 0, 0, 1); + float v10 = dot(mBackingArray[4], mBackingArray[5], mBackingArray[6], mBackingArray[7], + rotVals00, rotVals10, rotVals20, 0); + float v11 = dot(mBackingArray[4], mBackingArray[5], mBackingArray[6], mBackingArray[7], + rotVals01, rotVals11, rotVals21, 0); + float v12 = dot(mBackingArray[4], mBackingArray[5], mBackingArray[6], mBackingArray[7], + rotVals02, rotVals12, rotVals22, 0); + float v13 = dot(mBackingArray[4], mBackingArray[5], mBackingArray[6], mBackingArray[7], + 0, 0, 0, 1); + float v20 = dot(mBackingArray[8], mBackingArray[9], mBackingArray[10], mBackingArray[11], + rotVals00, rotVals10, rotVals20, 0); + float v21 = dot(mBackingArray[8], mBackingArray[9], mBackingArray[10], mBackingArray[11], + rotVals01, rotVals11, rotVals21, 0); + float v22 = dot(mBackingArray[8], mBackingArray[9], mBackingArray[10], mBackingArray[11], + rotVals02, rotVals12, rotVals22, 0); + float v23 = dot(mBackingArray[8], mBackingArray[9], mBackingArray[10], mBackingArray[11], + 0, 0, 0, 1); + float v30 = dot(mBackingArray[12], mBackingArray[13], mBackingArray[14], mBackingArray[15], + rotVals00, rotVals10, rotVals20, 0); + float v31 = dot(mBackingArray[12], mBackingArray[13], mBackingArray[14], mBackingArray[15], + rotVals01, rotVals11, rotVals21, 0); + float v32 = dot(mBackingArray[12], mBackingArray[13], mBackingArray[14], mBackingArray[15], + rotVals02, rotVals12, rotVals22, 0); + float v33 = dot(mBackingArray[12], mBackingArray[13], mBackingArray[14], mBackingArray[15], + 0, 0, 0, 1); + + mBackingArray[0] = v00; + mBackingArray[1] = v01; + mBackingArray[2] = v02; + mBackingArray[3] = v03; + mBackingArray[4] = v10; + mBackingArray[5] = v11; + mBackingArray[6] = v12; + mBackingArray[7] = v13; + mBackingArray[8] = v20; + mBackingArray[9] = v21; + mBackingArray[10] = v22; + mBackingArray[11] = v23; + mBackingArray[12] = v30; + mBackingArray[13] = v31; + mBackingArray[14] = v32; + mBackingArray[15] = v33; + + return this; + } + + /** + * Applies scaling factors to `this` Matrix44, then returns self. Pass 1s for no change. + * + * @param x Scaling factor for the x-axis + * @param y Scaling factor for the y-axis + * @param z Scaling factor for the z-axis + * @return A reference to this Matrix, which can be used to chain Matrix operations + */ + @FlaggedApi(Flags.FLAG_MATRIX_44) + public @NonNull Matrix44 scale(float x, float y, float z) { + mBackingArray[0] *= x; + mBackingArray[4] *= x; + mBackingArray[8] *= x; + mBackingArray[12] *= x; + mBackingArray[1] *= y; + mBackingArray[5] *= y; + mBackingArray[9] *= y; + mBackingArray[13] *= y; + mBackingArray[2] *= z; + mBackingArray[6] *= z; + mBackingArray[10] *= z; + mBackingArray[14] *= z; + + return this; + } + + /** + * Applies a translation to `this` Matrix44, then returns self. + * + * @param x Translation for the x-axis + * @param y Translation for the y-axis + * @param z Translation for the z-axis + * @return A reference to this Matrix, which can be used to chain Matrix operations + */ + @FlaggedApi(Flags.FLAG_MATRIX_44) + public @NonNull Matrix44 translate(float x, float y, float z) { + float newX = x * mBackingArray[0] + y * mBackingArray[1] + + z * mBackingArray[2] + mBackingArray[3]; + float newY = x * mBackingArray[4] + y * mBackingArray[5] + + z * mBackingArray[6] + mBackingArray[7]; + float newZ = x * mBackingArray[8] + y * mBackingArray[9] + + z * mBackingArray[10] + mBackingArray[11]; + float newW = x * mBackingArray[12] + y * mBackingArray[13] + + z * mBackingArray[14] + mBackingArray[15]; + + mBackingArray[3] = newX; + mBackingArray[7] = newY; + mBackingArray[11] = newZ; + mBackingArray[15] = newW; + + return this; + } + + @Override + public String toString() { + return String.format(""" + | %f %f %f %f | + | %f %f %f %f | + | %f %f %f %f | + | %f %f %f %f | + """, mBackingArray[0], mBackingArray[1], mBackingArray[2], mBackingArray[3], + mBackingArray[4], mBackingArray[5], mBackingArray[6], mBackingArray[7], + mBackingArray[8], mBackingArray[9], mBackingArray[10], mBackingArray[11], + mBackingArray[12], mBackingArray[13], mBackingArray[14], mBackingArray[15]); + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof Matrix44) { + return Arrays.equals(mBackingArray, ((Matrix44) obj).mBackingArray); + } + return false; + } + + @Override + public int hashCode() { + return (int) mBackingArray[0] + (int) mBackingArray[1] + (int) mBackingArray[2] + + (int) mBackingArray[3] + (int) mBackingArray[4] + (int) mBackingArray[5] + + (int) mBackingArray[6] + (int) mBackingArray[7] + (int) mBackingArray[8] + + (int) mBackingArray[9] + (int) mBackingArray[10] + (int) mBackingArray[11] + + (int) mBackingArray[12] + (int) mBackingArray[13] + (int) mBackingArray[14] + + (int) mBackingArray[15]; + } + +} diff --git a/graphics/java/android/graphics/SurfaceTexture.java b/graphics/java/android/graphics/SurfaceTexture.java index dd82fed03087..50b167ef9f46 100644 --- a/graphics/java/android/graphics/SurfaceTexture.java +++ b/graphics/java/android/graphics/SurfaceTexture.java @@ -489,7 +489,7 @@ public class SurfaceTexture { @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/libs/WindowManager/Shell/aconfig/multitasking.aconfig b/libs/WindowManager/Shell/aconfig/multitasking.aconfig index 2b95f30a5e8a..9a66c0fa9eb9 100644 --- a/libs/WindowManager/Shell/aconfig/multitasking.aconfig +++ b/libs/WindowManager/Shell/aconfig/multitasking.aconfig @@ -57,3 +57,10 @@ flag { description: "Enables new UMO experience for PiP menu" bug: "307998712" } + +flag { + name: "enable_bubble_bar" + namespace: "multitasking" + description: "Enables the new bubble bar UI for tablets" + bug: "286246694" +} 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 f32f030ff06e..50a58daa6363 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 @@ -465,7 +465,7 @@ public class Bubble implements BubbleViewProvider { /** * Call when all the views should be removed/cleaned up. */ - void cleanupViews() { + public void cleanupViews() { cleanupViews(true); } 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 1a6bf2849410..a43a951b4574 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 @@ -259,7 +259,7 @@ public class BubbleController implements ConfigurationChangeListener, /** 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 +285,7 @@ public class BubbleController implements ConfigurationChangeListener, BubblePositioner positioner, DisplayController displayController, Optional<OneHandedController> oneHandedOptional, - Optional<DragAndDropController> dragAndDropController, + DragAndDropController dragAndDropController, @ShellMainThread ShellExecutor mainExecutor, @ShellMainThread Handler mainHandler, @ShellBackgroundThread ShellExecutor bgExecutor, @@ -463,7 +463,7 @@ public class BubbleController implements ConfigurationChangeListener, }); 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(); @@ -730,7 +730,7 @@ public class BubbleController implements ConfigurationChangeListener, // 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 { @@ -1276,9 +1276,17 @@ public class BubbleController implements ConfigurationChangeListener, mBubbleData.setExpanded(true); } } else { - // App bubble does not exist, lets add and expand it - Log.i(TAG, " showOrHideAppBubble, creating and expanding app bubble"); - Bubble b = Bubble.createAppBubble(intent, user, icon, mMainExecutor); + // Check if it exists in the overflow + Bubble b = mBubbleData.getOverflowBubbleWithKey(appBubbleKey); + if (b != null) { + // It's in the overflow, so remove it & reinflate + Log.i(TAG, " showOrHideAppBubble, expanding app bubble from overflow"); + mBubbleData.removeOverflowBubble(b); + } else { + // App bubble does not exist, lets add and expand it + Log.i(TAG, " showOrHideAppBubble, creating and expanding app bubble"); + b = Bubble.createAppBubble(intent, user, icon, mMainExecutor); + } b.setShouldAutoExpand(true); inflateAndAdd(b, /* suppressFlyout= */ true, /* showInShade= */ false); } @@ -1706,8 +1714,7 @@ public class BubbleController implements ConfigurationChangeListener, @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); } } 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 6e0c804f0676..dbfa2606ea06 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,7 +15,6 @@ */ 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; @@ -41,6 +40,7 @@ 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; @@ -427,7 +427,7 @@ public class BubbleData { /** * 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) { @@ -491,6 +491,19 @@ public class BubbleData { } /** + * Explicitly removes a bubble from the overflow, if it exists. + * + * @param bubble the bubble to remove. + */ + public void removeOverflowBubble(Bubble bubble) { + if (bubble == null) return; + if (mOverflowBubbles.remove(bubble)) { + mStateChange.removedOverflowBubble = bubble; + dispatchPendingChanges(); + } + } + + /** * Adds a group key indicating that the summary for this group should be suppressed. * * @param groupKey the group key of the group whose summary should be suppressed. @@ -1056,7 +1069,6 @@ public class BubbleData { /** * The set of bubbles in row. */ - @VisibleForTesting(visibility = PACKAGE) public List<Bubble> getBubbles() { return Collections.unmodifiableList(mBubbles); } @@ -1145,7 +1157,6 @@ public class BubbleData { return null; } - @VisibleForTesting(visibility = PRIVATE) public Bubble getOverflowBubbleWithKey(String key) { for (int i = 0; i < mOverflowBubbles.size(); i++) { Bubble bubble = mOverflowBubbles.get(i); 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 5fc67d7b5f00..dc271331c8f9 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 @@ -31,6 +31,7 @@ import android.content.Intent; import android.graphics.Rect; import android.util.Log; import android.view.View; +import android.view.ViewGroup; import androidx.annotation.Nullable; @@ -186,6 +187,7 @@ public class BubbleTaskViewHelper { } if (mTaskView != null) { mTaskView.release(); + ((ViewGroup) mParentView).removeView(mTaskView); mTaskView = null; } } 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 00d683e861e0..73a9cf456a6e 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 @@ public class BubbleBarExpandedView extends FrameLayout implements BubbleTaskView 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 bd8ce803c591..b95d258da6fd 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.view.WindowManager; 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 @@ public class BubbleBarLayerView extends FrameLayout 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 @@ public class BubbleBarLayerView extends FrameLayout 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,44 @@ public class BubbleBarLayerView extends FrameLayout 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 (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 e1dea3babbc2..33b61b164988 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/dagger/TvWMShellModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/TvWMShellModule.java index b52a118c7f1e..d4ed0170dca8 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 @@ public class TvWMShellModule { DisplayController displayController, DisplayImeController displayImeController, DisplayInsetsController displayInsetsController, - Optional<DragAndDropController> dragAndDropController, Transitions transitions, TransactionPool transactionPool, IconProvider iconProvider, @@ -94,8 +93,8 @@ public class TvWMShellModule { 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 fc97c7988a0f..0d6a85271fd1 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.DesktopModeTaskRepository; 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 @@ public abstract class WMShellBaseModule { @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 @@ public abstract class WMShellBaseModule { 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 36f06e8bdb3b..ead5ad23c8f3 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 @@ public abstract class WMShellModule { 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 @@ public abstract class WMShellModule { DisplayController displayController, DisplayImeController displayImeController, DisplayInsetsController displayInsetsController, - Optional<DragAndDropController> dragAndDropController, + DragAndDropController dragAndDropController, Transitions transitions, TransactionPool transactionPool, IconProvider iconProvider, @@ -553,6 +553,24 @@ public abstract class WMShellModule { } // + // 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 @@ public abstract class WMShellModule { @ShellCreateTriggerOverride @Provides static Object provideIndependentShellComponentsToCreate( + DragAndDropController dragAndDropController, DefaultMixedHandler defaultMixedHandler) { return new Object(); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeShellCommandHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeShellCommandHandler.kt new file mode 100644 index 000000000000..fd91ac0affc5 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeShellCommandHandler.kt @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.desktopmode + +import android.window.WindowContainerTransaction +import com.android.wm.shell.sysui.ShellCommandHandler +import java.io.PrintWriter + +/** + * Handles the shell commands for the DesktopTasksController. + */ +class DesktopModeShellCommandHandler(private val controller: DesktopTasksController) : + ShellCommandHandler.ShellCommandActionHandler { + + override fun onShellCommand(args: Array<String>, pw: PrintWriter): Boolean { + return when (args[0]) { + "moveToDesktop" -> { + if (!runMoveToDesktop(args, pw)) { + pw.println("Task not found. Please enter a valid taskId.") + false + } else { + true + } + } + + else -> { + pw.println("Invalid command: ${args[0]}") + false + } + } + } + + private fun runMoveToDesktop(args: Array<String>, pw: PrintWriter): Boolean { + if (args.size < 2) { + // First argument is the action name. + pw.println("Error: task id should be provided as arguments") + return false + } + + val taskId = try { + args[1].toInt() + } catch (e: NumberFormatException) { + pw.println("Error: task id should be an integer") + return false + } + + return controller.moveToDesktopWithoutDecor(taskId, WindowContainerTransaction()) + } + + override fun printShellCommandHelp(pw: PrintWriter, prefix: String) { + pw.println("$prefix moveToDesktop <taskId> ") + pw.println("$prefix Move a task with given id to desktop mode.") + } +}
\ No newline at end of file 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 f82212d97bd3..7c8fcbb16711 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 @@ class DesktopModeTaskRepository { // 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 @@ class DesktopModeTaskRepository { * 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 @@ class DesktopModeTaskRepository { 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/desktopmode/DesktopTasksController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt index a089e81ff6dd..e8728498ad64 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt @@ -100,6 +100,9 @@ class DesktopTasksController( private val desktopMode: DesktopModeImpl private var visualIndicator: DesktopModeVisualIndicator? = null + private val desktopModeShellCommandHandler: DesktopModeShellCommandHandler = + DesktopModeShellCommandHandler(this) + private val mOnAnimationFinishedCallback = Consumer<SurfaceControl.Transaction> { t: SurfaceControl.Transaction -> visualIndicator?.releaseVisualIndicator(t) @@ -148,6 +151,8 @@ class DesktopTasksController( private fun onInit() { KtProtoLog.d(WM_SHELL_DESKTOP_MODE, "Initialize DesktopTasksController") shellCommandHandler.addDumpCallback(this::dump, this) + shellCommandHandler.addCommandCallback("desktopmode", desktopModeShellCommandHandler, + this) shellController.addExternalInterface( ShellSharedConstants.KEY_EXTRA_SHELL_DESKTOP_MODE, { createExternalInterface() }, @@ -240,6 +245,40 @@ class DesktopTasksController( } } + /** Move a task with given `taskId` to desktop without decor */ + fun moveToDesktopWithoutDecor( + taskId: Int, + wct: WindowContainerTransaction + ): Boolean { + val task = shellTaskOrganizer.getRunningTaskInfo(taskId) ?: return false + moveToDesktopWithoutDecor(task, wct) + return true + } + + /** + * Move a task to desktop without decor + */ + private fun moveToDesktopWithoutDecor( + task: RunningTaskInfo, + wct: WindowContainerTransaction + ) { + KtProtoLog.v( + WM_SHELL_DESKTOP_MODE, + "DesktopTasksController: moveToDesktopWithoutDecor taskId=%d", + task.taskId + ) + exitSplitIfApplicable(wct, task) + // Bring other apps to front first + bringDesktopAppsToFront(task.displayId, wct) + addMoveToDesktopChanges(wct, task) + + if (Transitions.ENABLE_SHELL_TRANSITIONS) { + transitions.startTransition(TRANSIT_CHANGE, wct, null /* handler */) + } else { + shellTaskOrganizer.applyTransaction(wct) + } + } + /** * Move a task to desktop */ diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/docs/README.md b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/README.md index 73a7348d5aca..3fad28ad232f 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/docs/README.md +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/README.md @@ -15,4 +15,4 @@ particular order): Todo - Per-feature docs - Feature flagging -- Best practices
\ No newline at end of file +- Best practices & patterns
\ No newline at end of file diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/docs/changes.md b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/changes.md index fbf326eadcd5..9aa5f4ffcd78 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/docs/changes.md +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/changes.md @@ -102,5 +102,5 @@ AIDL interfaces and constants. Currently, all AIDL files, and classes under the Launcher uses. If the new code doesn't fall into those categories, they can be added explicitly in the Shell's -[Android.bp](frameworks/base/libs/WindowManager/Shell/Android.bp) file under the +[Android.bp](/libs/WindowManager/Shell/Android.bp) file under the `wm_shell_util-sources` filegroup.
\ No newline at end of file diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/docs/dagger.md b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/dagger.md index 6c01d962adc9..7070dead9957 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/docs/dagger.md +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/dagger.md @@ -21,16 +21,16 @@ developers to jump into a few select files and understand how different componen (especially as products override components). The module dependency tree looks a bit like: -- [WMShellConcurrencyModule](frameworks/base/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellConcurrencyModule.java) +- [WMShellConcurrencyModule](/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellConcurrencyModule.java) (provides threading-related components) - - [WMShellBaseModule](frameworks/base/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java) + - [WMShellBaseModule](/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java) (provides components that are likely common to all products, ie. DisplayController, Transactions, etc.) - - [WMShellModule](frameworks/base/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java) + - [WMShellModule](/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java) (phone/tablet specific components only) - - [TvPipModule](frameworks/base/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/TvPipModule.java) + - [TvPipModule](/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/TvPipModule.java) (PIP specific components for TV) - - [TvWMShellModule](frameworks/base/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/TvWMShellModule.java) + - [TvWMShellModule](/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/TvWMShellModule.java) (TV specific components only) - etc. @@ -43,7 +43,7 @@ In some rare cases, there are base components that can change behavior depending product it runs on. If there are hooks that can be added to the component, that is the preferable approach. -The alternative is to use the [@DynamicOverride](frameworks/base/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/DynamicOverride.java) +The alternative is to use the [@DynamicOverride](/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/DynamicOverride.java) annotation to allow the product module to provide an implementation that the base module can reference. This is most useful if the existence of the entire component is controlled by the product and the override implementation is optional (there is a default implementation). More diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/docs/debugging.md b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/debugging.md index f9ea1d4e2a07..438aa768165e 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/docs/debugging.md +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/debugging.md @@ -29,30 +29,78 @@ building to check the log state (is enabled) before printing the print format st ### Kotlin Protolog tool does not yet have support for Kotlin code (see [b/168581922](https://b.corp.google.com/issues/168581922)). -For logging in Kotlin, use the [KtProtoLog](frameworks/base/libs/WindowManager/Shell/src/com/android/wm/shell/util/KtProtoLog.kt) +For logging in Kotlin, use the [KtProtoLog](/libs/WindowManager/Shell/src/com/android/wm/shell/util/KtProtoLog.kt) class which has a similar API to the Java ProtoLog class. ### Enabling ProtoLog command line logging -Run these commands to enable protologs for both WM Core and WM Shell to print to logcat. +Run these commands to enable protologs (in logcat) for WM Core ([list of all core tags](/core/java/com/android/internal/protolog/ProtoLogGroup.java)): ```shell -adb shell wm logging enable-text NEW_FEATURE -adb shell wm logging disable-text NEW_FEATURE +adb shell wm logging enable-text TAG +adb shell wm logging disable-text TAG +``` + +And these commands to enable protologs (in logcat) for WM Shell ([list of all shell tags](/libs/WindowManager/Shell/src/com/android/wm/shell/protolog/ShellProtoLogGroup.java)): +```shell +adb shell dumpsys activity service SystemUIService WMShell protolog enable-text TAG +adb shell dumpsys activity service SystemUIService WMShell protolog enable-text TAG ``` ## Winscope Tracing The Winscope tool is extremely useful in determining what is happening on-screen in both WindowManager and SurfaceFlinger. Follow [go/winscope](http://go/winscope-help) to learn how to -use the tool. +use the tool. This trace will contain all the information about the windows/activities/surfaces on +screen. + +## WindowManager/SurfaceFlinger hierarchy dump + +A quick way to view the WindowManager hierarchy without a winscope trace is via the wm dumps: +```shell +adb shell dumpsys activity containers +``` + +Likewise, the SurfaceFlinger hierarchy can be dumped for inspection by running: +```shell +adb shell dumpsys SurfaceFlinger +# Search output for "Layer Hierarchy" +``` + +## Tracing global SurfaceControl transaction updates -In addition, there is limited preliminary support for Winscope tracing componetns in the Shell, -which involves adding trace fields to [wm_shell_trace.proto](frameworks/base/libs/WindowManager/Shell/proto/wm_shell_trace.proto) -file and ensure it is updated as a part of `WMShell#writeToProto`. +While Winscope traces are very useful, it sometimes doesn't give you enough information about which +part of the code is initiating the transaction updates. In such cases, it can be helpful to get +stack traces when specific surface transaction calls are made, which is possible by enabling the +following system properties for example: +```shell +# Enabling +adb shell setprop persist.wm.debug.sc.tx.log_match_call setAlpha # matches the name of the SurfaceControlTransaction method +adb shell setprop persist.wm.debug.sc.tx.log_match_name com.android.systemui # matches the name of the surface +adb reboot +adb logcat -s "SurfaceControlRegistry" + +# Disabling logging +adb shell setprop persist.wm.debug.sc.tx.log_match_call \"\" +adb shell setprop persist.wm.debug.sc.tx.log_match_name \"\" +adb reboot +``` + +It is not necessary to set both `log_match_call` and `log_match_name`, but note logs can be quite +noisy if unfiltered. -Tracing can be started via the shell command (to be added to the Winscope tool as needed): +## Tracing activity starts in the app process + +It's sometimes useful to know when to see a stack trace of when an activity starts in the app code +(ie. if you are repro'ing a bug related to activity starts). You can enable this system property to +get this trace: ```shell -adb shell cmd statusbar tracing start -adb shell cmd statusbar tracing stop +# Enabling +adb shell setprop persist.wm.debug.start_activity true +adb reboot +adb logcat -s "Instrumentation" + +# Disabling +adb shell setprop persist.wm.debug.start_activity \"\" +adb reboot ``` ## Dumps @@ -69,6 +117,21 @@ If information should be added to the dump, either: - Update `WMShell` if you are dumping SysUI state - Inject `ShellCommandHandler` into your Shell class, and add a dump callback +## Shell commands + +It can be useful to add additional shell commands to drive and test specific interactions. + +To add a new command for your feature, inject a `ShellCommandHandler` into your class and add a +shell command handler in your controller. + +```shell +# List all available commands +adb shell dumpsys activity service SystemUIService WMShell help + +# Run a specific command +adb shell dumpsys activity service SystemUIService WMShell <cmd> <args> ... +``` + ## Debugging in Android Studio If you are using the [go/sysui-studio](http://go/sysui-studio) project, then you can debug Shell diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/docs/overview.md b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/overview.md index a88ef6aea2ec..b489fe8ea1a9 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/docs/overview.md +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/overview.md @@ -19,25 +19,24 @@ Currently, the WMShell library is used to drive the windowing experience on hand ## Where does the code live -The core WMShell library code is currently located in the [frameworks/base/libs/WindowManager/Shell](frameworks/base/libs/WindowManager/Shell) +The core WMShell library code is currently located in the [frameworks/base/libs/WindowManager/Shell](/libs/WindowManager/Shell) directory and is included as a part dependency of the host SystemUI apk. ## How do I build the Shell library -The library can be built directly by running (using [go/makepush](http://go/makepush)): +The library can be built directly by running: ```shell -mp :WindowManager-Shell +m WindowManager-Shell ``` But this is mainly useful for inspecting the contents of the library or verifying it builds. The -various targets can be found in the Shell library's [Android.bp](frameworks/base/libs/WindowManager/Shell/Android.bp) +various targets can be found in the Shell library's [Android.bp](/libs/WindowManager/Shell/Android.bp) file. Normally, you would build it as a part of the host SystemUI, for example via commandline: ```shell # Phone SystemUI variant -mp sysuig -# Building Shell & SysUI changes along w/ framework changes -mp core services sysuig +m SystemUI +adevice update ``` Or preferably, if you are making WMShell/SysUI only changes (no other framework changes), then diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/docs/patterns/TEMPLATE.md b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/patterns/TEMPLATE.md new file mode 100644 index 000000000000..0e20a8ec275c --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/patterns/TEMPLATE.md @@ -0,0 +1,30 @@ +# Pattern (one line description) + +### What this pattern solves + +Give detailed information about the problem that this pattern addresses, include when it +should/should not be used. + +### How it works + +Give a high level overview of how this pattern works technically with sufficient links for the +relevant dependencies for folks to read more. + +### Code examples + +Explain how this pattern is used in code. + +File.kt: +```kotlin +fun someFunction() { + // Use this code +} +``` + +### Relevant links + +Add relevant links to other files that implement this pattern, design docs, or other important +info. + +Link 1: [More info](some_example_link) \ +Link 2: [More info](some_example_link)
\ No newline at end of file diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/docs/sysui.md b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/sysui.md index d6302e640ba7..30ff6691f503 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/docs/sysui.md +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/sysui.md @@ -80,4 +80,6 @@ adb shell dumpsys activity service SystemUIService WMShell # Run a specific command adb shell dumpsys activity service SystemUIService WMShell help adb shell dumpsys activity service SystemUIService WMShell <cmd> <args> ... -```
\ No newline at end of file +``` + +More detail can be found in [Debugging in the Shell](debugging.md) section.
\ No newline at end of file diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/docs/testing.md b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/testing.md index 8a80333facc4..98af930c4486 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/docs/testing.md +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/testing.md @@ -5,7 +5,7 @@ ## Unit tests New WM Shell unit tests can be added to the -[Shell/tests/unittest](frameworks/base/libs/WindowManager/Shell/tests/unittest) directory, and can +[Shell/tests/unittest](/libs/WindowManager/Shell/tests/unittest) directory, and can be run via command line using `atest`: ```shell atest WMShellUnitTests @@ -25,10 +25,24 @@ Flicker tests are tests that perform actions and make assertions on the state in and SurfaceFlinger traces captured during the run. New WM Shell Flicker tests can be added to the -[Shell/tests/flicker](frameworks/base/libs/WindowManager/Shell/tests/flicker) directory, and can -be run via command line using `atest`: +[Shell/tests/flicker](/libs/WindowManager/Shell/tests/flicker) directory, and can be run via command line using `atest`: ```shell -atest WMShellFlickerTests +# Bubbles +atest WMShellFlickerTestsBubbles + +# PIP +atest WMShellFlickerTestsPip1 +atest WMShellFlickerTestsPip2 +atest WMShellFlickerTestsPip3 +atest WMShellFlickerTestsPipApps +atest WMShellFlickerTestsPipAppsCSuite + +# Splitscreen +atest WMShellFlickerTestsSplitScreenGroup1 +atest WMShellFlickerTestsSplitScreenGroup2 + +# Other +atest WMShellFlickerTestsOther ``` **Note**: Currently Flicker tests can only be run from the commandline and not via SysUI Studio diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/docs/threading.md b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/threading.md index eac748894432..9d015357b60b 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/docs/threading.md +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/threading.md @@ -43,7 +43,7 @@ the product. ## Dagger setup -The threading-related components are provided by the [WMShellConcurrencyModule](frameworks/base/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellConcurrencyModule.java), +The threading-related components are provided by the [WMShellConcurrencyModule](/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellConcurrencyModule.java), for example, the Executors and Handlers for the various threads that are used. You can request an executor of the necessary type by using the appropriate annotation for each of the threads (ie. `@ShellMainThread Executor`) when injecting into your Shell component. @@ -76,7 +76,7 @@ To get the SysUI main thread, you can use the `@Main` annotation. want to dedupe multiple messages - In such cases inject `@ShellMainThread Handler` or use view.getHandler() which should be OK assuming that the view root was initialized on the main Shell thread -- **Never use Looper.getMainLooper()** +- <u>**Never</u> use Looper.getMainLooper()** - It's likely going to be wrong, you can inject `@Main ShellExecutor` to get the SysUI main thread ### Testing 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 fdfb6f3680b2..269c3699ac0a 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 @@ public class DragAndDropController implements RemoteCallable<DragAndDropControll 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/phone/PipController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java index 238e6b5bf2af..c5a01025dcdd 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 @@ -797,14 +797,20 @@ public class PipController implements PipTransitionController.PipTransitionCallb mPipBoundsAlgorithm.getMovementBounds(postChangeBounds), mPipBoundsState.getStashedState()); - updateDisplayLayout.run(); + // 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)); + } - // 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())); + updateDisplayLayout.run(); // Calculate the PiP bounds in the new orientation based on same fraction along the // rotated movement bounds. @@ -821,10 +827,6 @@ public class PipController implements PipTransitionController.PipTransitionCallb 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. 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 70cb2fc6d52c..1b124c2168df 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 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, 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 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, DisplayController displayController, DisplayImeController displayImeController, DisplayInsetsController displayInsetsController, - Optional<DragAndDropController> dragAndDropController, + DragAndDropController dragAndDropController, Transitions transitions, TransactionPool transactionPool, IconProvider iconProvider, @@ -290,7 +290,7 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, mDisplayController = displayController; mDisplayImeController = displayImeController; mDisplayInsetsController = displayInsetsController; - mDragAndDropController = Optional.of(dragAndDropController); + mDragAndDropController = dragAndDropController; mTransitions = transitions; mTransactionPool = transactionPool; mIconProvider = iconProvider; @@ -328,7 +328,9 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, // 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 e8894a836663..b60e361caad8 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 @@ class SplitScreenTransitions { 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 c10142588bde..aec4d1176dc0 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 @@ public class TvSplitScreenController extends SplitScreenController { DisplayController displayController, DisplayImeController displayImeController, DisplayInsetsController displayInsetsController, - Optional<DragAndDropController> dragAndDropController, Transitions transitions, TransactionPool transactionPool, IconProvider iconProvider, @@ -85,7 +84,7 @@ public class TvSplitScreenController extends SplitScreenController { 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/transition/OneShotRemoteHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/OneShotRemoteHandler.java index 4355ed2bd3bf..94519a0d118c 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 @@ public class OneShotRemoteHandler implements Transitions.TransitionHandler { @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 @@ public class OneShotRemoteHandler implements Transitions.TransitionHandler { } mMainExecutor.execute(() -> { finishCallback.onTransitionFinished(wct); + mRemote = null; }); - mRemote = null; } }; Transitions.setRunningRemoteTransitionDelegate(mRemote.getAppThread()); @@ -115,17 +118,24 @@ public class OneShotRemoteHandler implements Transitions.TransitionHandler { 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/PerfettoTransitionTracer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/tracing/PerfettoTransitionTracer.java index 99df6a31beac..0c25f27854bd 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,6 +18,7 @@ package com.android.wm.shell.transition.tracing; import android.internal.perfetto.protos.PerfettoTrace; import android.os.SystemClock; +import android.os.Trace; import android.tracing.perfetto.DataSourceInstance; import android.tracing.perfetto.DataSourceParams; import android.tracing.perfetto.InitArguments; @@ -58,6 +59,15 @@ public class PerfettoTransitionTracer implements TransitionTracer { 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); @@ -97,6 +107,15 @@ public class PerfettoTransitionTracer implements TransitionTracer { 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 +139,19 @@ public class PerfettoTransitionTracer implements TransitionTracer { 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 +170,15 @@ public class PerfettoTransitionTracer implements TransitionTracer { 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); @@ -157,6 +194,15 @@ public class PerfettoTransitionTracer implements TransitionTracer { } private void onFlush() { + Trace.traceBegin(Trace.TRACE_TAG_WINDOW_MANAGER, "onFlush"); + try { + doOnFlush(); + } finally { + Trace.traceEnd(Trace.TRACE_TAG_WINDOW_MANAGER); + } + } + + private void doOnFlush() { mDataSource.trace(ctx -> { final ProtoOutputStream os = ctx.newTracePacket(); 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 a8b39c41e6bb..891eea072c0d 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 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { 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/unittest/src/com/android/wm/shell/bubbles/BubbleDataTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleDataTest.java index dab762f233e2..fa0aba5a6ee9 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleDataTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleDataTest.java @@ -1190,6 +1190,23 @@ public class BubbleDataTest extends ShellTestCase { assertThat(mBubbleData.getBubbleInStackWithKey(appBubbleKey)).isNull(); } + @Test + public void test_removeOverflowBubble() { + sendUpdatedEntryAtTime(mEntryA1, 2000); + mBubbleData.setListener(mListener); + + mBubbleData.dismissBubbleWithKey(mEntryA1.getKey(), Bubbles.DISMISS_USER_GESTURE); + verifyUpdateReceived(); + assertOverflowChangedTo(ImmutableList.of(mBubbleA1)); + + mBubbleData.removeOverflowBubble(mBubbleA1); + verifyUpdateReceived(); + + BubbleData.Update update = mUpdateCaptor.getValue(); + assertThat(update.removedOverflowBubble).isEqualTo(mBubbleA1); + assertOverflowChangedTo(ImmutableList.of()); + } + private void verifyUpdateReceived() { verify(mListener).applyUpdate(mUpdateCaptor.capture()); reset(mListener); 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 75965d6d68d9..1668e3712462 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 @@ class BubbleViewInfoTest : ShellTestCase() { bubbleController, mainExecutor ) - bubbleBarLayerView = BubbleBarLayerView(context, bubbleController) + bubbleBarLayerView = BubbleBarLayerView(context, bubbleController, bubbleData) } @Test diff --git a/libs/hwui/SkiaCanvas.cpp b/libs/hwui/SkiaCanvas.cpp index 008ea3aaa2e7..14b8d8d0aa12 100644 --- a/libs/hwui/SkiaCanvas.cpp +++ b/libs/hwui/SkiaCanvas.cpp @@ -341,6 +341,10 @@ void SkiaCanvas::concat(const SkMatrix& matrix) { mCanvas->concat(matrix); } +void SkiaCanvas::concat(const SkM44& matrix) { + mCanvas->concat(matrix); +} + void SkiaCanvas::rotate(float degrees) { mCanvas->rotate(degrees); } diff --git a/libs/hwui/SkiaCanvas.h b/libs/hwui/SkiaCanvas.h index 4bf1790e2415..5e3553bbbbb4 100644 --- a/libs/hwui/SkiaCanvas.h +++ b/libs/hwui/SkiaCanvas.h @@ -86,6 +86,7 @@ public: virtual void getMatrix(SkMatrix* outMatrix) const override; virtual void setMatrix(const SkMatrix& matrix) override; virtual void concat(const SkMatrix& matrix) override; + virtual void concat(const SkM44& matrix) override; virtual void rotate(float degrees) override; virtual void scale(float sx, float sy) override; virtual void skew(float sx, float sy) override; diff --git a/libs/hwui/aconfig/hwui_flags.aconfig b/libs/hwui/aconfig/hwui_flags.aconfig index 72ddeccd26b2..3d7e559bebe0 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/Canvas.h b/libs/hwui/hwui/Canvas.h index 9ec023b2c36f..20e3ad2c8202 100644 --- a/libs/hwui/hwui/Canvas.h +++ b/libs/hwui/hwui/Canvas.h @@ -175,6 +175,7 @@ public: virtual void setMatrix(const SkMatrix& matrix) = 0; virtual void concat(const SkMatrix& matrix) = 0; + virtual void concat(const SkM44& matrix) = 0; virtual void rotate(float degrees) = 0; virtual void scale(float sx, float sy) = 0; virtual void skew(float sx, float sy) = 0; diff --git a/libs/hwui/jni/android_graphics_Canvas.cpp b/libs/hwui/jni/android_graphics_Canvas.cpp index d5725935551a..e5bdeeea75be 100644 --- a/libs/hwui/jni/android_graphics_Canvas.cpp +++ b/libs/hwui/jni/android_graphics_Canvas.cpp @@ -158,6 +158,13 @@ static void concat(CRITICAL_JNI_PARAMS_COMMA jlong canvasHandle, jlong matrixHan get_canvas(canvasHandle)->concat(*matrix); } +static void concat44(JNIEnv* env, jobject obj, jlong canvasHandle, jfloatArray arr) { + jfloat* matVals = env->GetFloatArrayElements(arr, 0); + const SkM44 matrix = SkM44::RowMajor(matVals); + get_canvas(canvasHandle)->concat(matrix); + env->ReleaseFloatArrayElements(arr, matVals, 0); +} + static void rotate(CRITICAL_JNI_PARAMS_COMMA jlong canvasHandle, jfloat degrees) { get_canvas(canvasHandle)->rotate(degrees); } @@ -756,40 +763,41 @@ static void punchHole(JNIEnv* env, jobject, jlong canvasPtr, jfloat left, jfloat }; // 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}, - - // ------------ @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}, - {"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}, + {"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}, + + // ------------ @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/libs/hwui/renderthread/VulkanManager.cpp b/libs/hwui/renderthread/VulkanManager.cpp index d55d28d469d0..b5f7caaf1b5b 100644 --- a/libs/hwui/renderthread/VulkanManager.cpp +++ b/libs/hwui/renderthread/VulkanManager.cpp @@ -31,6 +31,8 @@ #include <vk/GrVkExtensions.h> #include <vk/GrVkTypes.h> +#include <sstream> + #include "Properties.h" #include "RenderThread.h" #include "pipeline/skia/ShaderCache.h" @@ -40,7 +42,8 @@ namespace android { namespace uirenderer { namespace renderthread { -static std::array<std::string_view, 20> sEnableExtensions{ +// Not all of these are strictly required, but are all enabled if present. +static std::array<std::string_view, 21> sEnableExtensions{ VK_KHR_BIND_MEMORY_2_EXTENSION_NAME, VK_KHR_DEDICATED_ALLOCATION_EXTENSION_NAME, VK_KHR_EXTERNAL_MEMORY_CAPABILITIES_EXTENSION_NAME, @@ -61,6 +64,7 @@ static std::array<std::string_view, 20> sEnableExtensions{ VK_KHR_EXTERNAL_SEMAPHORE_FD_EXTENSION_NAME, VK_KHR_ANDROID_SURFACE_EXTENSION_NAME, VK_EXT_GLOBAL_PRIORITY_EXTENSION_NAME, + VK_EXT_DEVICE_FAULT_EXTENSION_NAME, }; static bool shouldEnableExtension(const std::string_view& extension) { @@ -303,6 +307,15 @@ void VulkanManager::setupDevice(GrVkExtensions& grExtensions, VkPhysicalDeviceFe *tailPNext = ycbcrFeature; tailPNext = &ycbcrFeature->pNext; + if (grExtensions.hasExtension(VK_EXT_DEVICE_FAULT_EXTENSION_NAME, 1)) { + VkPhysicalDeviceFaultFeaturesEXT* deviceFaultFeatures = + new VkPhysicalDeviceFaultFeaturesEXT; + deviceFaultFeatures->sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FAULT_FEATURES_EXT; + deviceFaultFeatures->pNext = nullptr; + *tailPNext = deviceFaultFeatures; + tailPNext = &deviceFaultFeatures->pNext; + } + // query to get the physical device features mGetPhysicalDeviceFeatures2(mPhysicalDevice, &features); // this looks like it would slow things down, @@ -405,6 +418,79 @@ void VulkanManager::initialize() { }); } +namespace { +void onVkDeviceFault(const std::string& contextLabel, const std::string& description, + const std::vector<VkDeviceFaultAddressInfoEXT>& addressInfos, + const std::vector<VkDeviceFaultVendorInfoEXT>& vendorInfos, + const std::vector<std::byte>& vendorBinaryData) { + // The final crash string should contain as much differentiating info as possible, up to 1024 + // bytes. As this final message is constructed, the same information is also dumped to the logs + // but in a more verbose format. Building the crash string is unsightly, so the clearer logging + // statement is always placed first to give context. + ALOGE("VK_ERROR_DEVICE_LOST (%s context): %s", contextLabel.c_str(), description.c_str()); + std::stringstream crashMsg; + crashMsg << "VK_ERROR_DEVICE_LOST (" << contextLabel; + + if (!addressInfos.empty()) { + ALOGE("%zu VkDeviceFaultAddressInfoEXT:", addressInfos.size()); + crashMsg << ", " << addressInfos.size() << " address info ("; + for (VkDeviceFaultAddressInfoEXT addressInfo : addressInfos) { + ALOGE(" addressType: %d", (int)addressInfo.addressType); + ALOGE(" reportedAddress: %" PRIu64, addressInfo.reportedAddress); + ALOGE(" addressPrecision: %" PRIu64, addressInfo.addressPrecision); + crashMsg << addressInfo.addressType << ":" + << addressInfo.reportedAddress << ":" + << addressInfo.addressPrecision << ", "; + } + crashMsg.seekp(-2, crashMsg.cur); // Move back to overwrite trailing ", " + crashMsg << ")"; + } + + if (!vendorInfos.empty()) { + ALOGE("%zu VkDeviceFaultVendorInfoEXT:", vendorInfos.size()); + crashMsg << ", " << vendorInfos.size() << " vendor info ("; + for (VkDeviceFaultVendorInfoEXT vendorInfo : vendorInfos) { + ALOGE(" description: %s", vendorInfo.description); + ALOGE(" vendorFaultCode: %" PRIu64, vendorInfo.vendorFaultCode); + ALOGE(" vendorFaultData: %" PRIu64, vendorInfo.vendorFaultData); + // Omit descriptions for individual vendor info structs in the crash string, as the + // fault code and fault data fields should be enough for clustering, and the verbosity + // isn't worth it. Additionally, vendors may just set the general description field of + // the overall fault to the description of the first element in this list, and that + // overall description will be placed at the end of the crash string. + crashMsg << vendorInfo.vendorFaultCode << ":" + << vendorInfo.vendorFaultData << ", "; + } + crashMsg.seekp(-2, crashMsg.cur); // Move back to overwrite trailing ", " + crashMsg << ")"; + } + + if (!vendorBinaryData.empty()) { + // TODO: b/322830575 - Log in base64, or dump directly to a file that gets put in bugreports + ALOGE("%zu bytes of vendor-specific binary data (please notify Android's Core Graphics" + " Stack team if you observe this message).", + vendorBinaryData.size()); + crashMsg << ", " << vendorBinaryData.size() << " bytes binary"; + } + + crashMsg << "): " << description; + LOG_ALWAYS_FATAL("%s", crashMsg.str().c_str()); +} + +void deviceLostProcRenderThread(void* callbackContext, const std::string& description, + const std::vector<VkDeviceFaultAddressInfoEXT>& addressInfos, + const std::vector<VkDeviceFaultVendorInfoEXT>& vendorInfos, + const std::vector<std::byte>& vendorBinaryData) { + onVkDeviceFault("RenderThread", description, addressInfos, vendorInfos, vendorBinaryData); +} +void deviceLostProcUploadThread(void* callbackContext, const std::string& description, + const std::vector<VkDeviceFaultAddressInfoEXT>& addressInfos, + const std::vector<VkDeviceFaultVendorInfoEXT>& vendorInfos, + const std::vector<std::byte>& vendorBinaryData) { + onVkDeviceFault("UploadThread", description, addressInfos, vendorInfos, vendorBinaryData); +} +} // anonymous namespace + static void onGrContextReleased(void* context) { VulkanManager* manager = (VulkanManager*)context; manager->decStrong((void*)onGrContextReleased); @@ -430,6 +516,10 @@ sk_sp<GrDirectContext> VulkanManager::createContext(GrContextOptions& options, backendContext.fVkExtensions = &mExtensions; backendContext.fDeviceFeatures2 = &mPhysicalDeviceFeatures2; backendContext.fGetProc = std::move(getProc); + backendContext.fDeviceLostContext = nullptr; + backendContext.fDeviceLostProc = (contextType == ContextType::kRenderThread) + ? deviceLostProcRenderThread + : deviceLostProcUploadThread; LOG_ALWAYS_FATAL_IF(options.fContextDeleteProc != nullptr, "Conflicting fContextDeleteProcs!"); this->incStrong((void*)onGrContextReleased); diff --git a/location/java/android/location/altitude/AltitudeConverter.java b/location/java/android/location/altitude/AltitudeConverter.java index 461dafb91916..ec1edb8943af 100644 --- a/location/java/android/location/altitude/AltitudeConverter.java +++ b/location/java/android/location/altitude/AltitudeConverter.java @@ -219,6 +219,27 @@ public final class AltitudeConverter { * called on the main thread as data will not be loaded from raw assets. Returns true if a Mean * Sea Level altitude is added to the {@code location}; otherwise, returns false and leaves the * {@code location} unchanged. + * + * <p>Prior calls to {@link #addMslAltitudeToLocation(Context, Location)} off the main thread + * are necessary to load data from raw assets. Example code on the main thread is as follows: + * + * <pre>{@code + * if (!mAltitudeConverter.addMslAltitudeToLocation(location)) { + * // Queue up only one call off the main thread. + * if (mIsAltitudeConverterIdle) { + * mIsAltitudeConverterIdle = false; + * executeOffMainThread(() -> { + * try { + * // Load raw assets for next call attempt on main thread. + * mAltitudeConverter.addMslAltitudeToLocation(mContext, location); + * } catch (IOException e) { + * Log.e(TAG, "Not loading raw assets: " + e); + * } + * mIsAltitudeConverterIdle = true; + * }); + * } + * } + * }</pre> */ @FlaggedApi(Flags.FLAG_GEOID_HEIGHTS_VIA_ALTITUDE_HAL) public boolean addMslAltitudeToLocation(@NonNull Location location) { diff --git a/location/java/com/android/internal/location/altitude/GeoidMap.java b/location/java/com/android/internal/location/altitude/GeoidMap.java index 9bf5689c1028..df9ca97817f7 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 @@ import java.util.Objects; */ 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 final class GeoidMap { 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 @@ public final class GeoidMap { 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 @@ public final class GeoidMap { @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 @@ public final class GeoidMap { @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 @@ public final class GeoidMap { */ @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 @@ public final class GeoidMap { @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 @@ public final class GeoidMap { 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/java/android/media/AudioManager.java b/media/java/android/media/AudioManager.java index 4918289e8b5c..69708ecde281 100644 --- a/media/java/android/media/AudioManager.java +++ b/media/java/android/media/AudioManager.java @@ -23,6 +23,7 @@ import static android.media.audio.Flags.autoPublicVolumeApiHardening; import static android.media.audio.Flags.automaticBtDeviceType; import static android.media.audio.Flags.FLAG_FOCUS_EXCLUSIVE_WITH_RECORDING; import static android.media.audio.Flags.FLAG_FOCUS_FREEZE_TEST_API; +import static android.media.audio.Flags.FLAG_SUPPORTED_DEVICE_TYPES_API; import static android.media.audiopolicy.Flags.FLAG_ENABLE_FADE_MANAGER_CONFIGURATION; import android.Manifest; @@ -80,6 +81,7 @@ import android.os.UserHandle; import android.provider.Settings; import android.text.TextUtils; import android.util.ArrayMap; +import android.util.IntArray; import android.util.Log; import android.util.Pair; import android.view.KeyEvent; @@ -100,6 +102,7 @@ import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Set; import java.util.TreeMap; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.Executor; @@ -7838,6 +7841,51 @@ public class AudioManager { } /** + * Returns a Set of unique Integers corresponding to audio device type identifiers that can + * <i>potentially</i> be connected to the system and meeting the criteria specified in the + * <code>direction</code> parameter. + * Note that this set contains {@link AudioDeviceInfo} device type identifiers for both devices + * currently available <i>and</i> those that can be available if the user connects an audio + * peripheral. Examples include TYPE_WIRED_HEADSET if the Android device supports an analog + * headset jack or TYPE_USB_DEVICE if the Android device supports a USB host-mode port. + * These are generally a superset of device type identifiers associated with the + * AudioDeviceInfo objects returned from AudioManager.getDevices(). + * @param direction The constant specifying whether input or output devices are queried. + * @see #GET_DEVICES_OUTPUTS + * @see #GET_DEVICES_INPUTS + * @return A (possibly zero-length) Set of Integer objects corresponding to the audio + * device types of devices supported by the implementation. + * @throws IllegalArgumentException If an invalid direction constant is specified. + */ + @FlaggedApi(FLAG_SUPPORTED_DEVICE_TYPES_API) + public @NonNull Set<Integer> + getSupportedDeviceTypes(int direction) { + if (direction != GET_DEVICES_OUTPUTS && direction != GET_DEVICES_INPUTS) { + throw new IllegalArgumentException("AudioManager.getSupportedDeviceTypes(" + + Integer.toHexString(direction) + ") - Invalid."); + } + + IntArray internalDeviceTypes = new IntArray(); + int status = AudioSystem.getSupportedDeviceTypes(direction, internalDeviceTypes); + if (status != AudioManager.SUCCESS) { + Log.e(TAG, "AudioManager.getSupportedDeviceTypes(" + direction + ") failed. status:" + + status); + } + + // convert to external (AudioDeviceInfo.getType()) device IDs + HashSet<Integer> externalDeviceTypes = new HashSet<Integer>(); + for (int index = 0; index < internalDeviceTypes.size(); index++) { + // Set will eliminate any duplicates which AudioSystem.getSupportedDeviceTypes() + // returns + externalDeviceTypes.add( + AudioDeviceInfo.convertInternalDeviceToDeviceType( + internalDeviceTypes.get(index))); + } + + return externalDeviceTypes; + } + + /** * Returns an array of {@link AudioDeviceInfo} objects corresponding to the audio devices * currently connected to the system and meeting the criteria specified in the * <code>flags</code> parameter. diff --git a/media/java/android/media/AudioSystem.java b/media/java/android/media/AudioSystem.java index 0f6cbffef300..f73be35fa130 100644 --- a/media/java/android/media/AudioSystem.java +++ b/media/java/android/media/AudioSystem.java @@ -36,6 +36,7 @@ import android.os.IBinder; import android.os.Parcel; import android.os.Vibrator; import android.telephony.TelephonyManager; +import android.util.IntArray; import android.util.Log; import android.util.Pair; @@ -1947,6 +1948,8 @@ public class AudioSystem /** @hide */ public static native int listAudioPorts(ArrayList<AudioPort> ports, int[] generation); /** @hide */ + public static native int getSupportedDeviceTypes(int flags, IntArray internalDeviceTypes); + /** @hide */ public static native int createAudioPatch(AudioPatch[] patch, AudioPortConfig[] sources, AudioPortConfig[] sinks); /** @hide */ diff --git a/media/java/android/media/BluetoothProfileConnectionInfo.java b/media/java/android/media/BluetoothProfileConnectionInfo.java index e4dc1521ae70..0613fc655521 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 final class BluetoothProfileConnectionInfo implements Parcelable { 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/tv/ITvInputManager.aidl b/media/java/android/media/tv/ITvInputManager.aidl index 84c197d9e20f..be93abb15b41 100644 --- a/media/java/android/media/tv/ITvInputManager.aidl +++ b/media/java/android/media/tv/ITvInputManager.aidl @@ -111,7 +111,7 @@ interface ITvInputManager { 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 7b20c3976fd6..6b247cc27a2f 100644 --- a/media/java/android/media/tv/ITvInputSession.aidl +++ b/media/java/android/media/tv/ITvInputSession.aidl @@ -63,7 +63,7 @@ oneway interface ITvInputSession { 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 999e2cfbcede..d1453975f801 100644 --- a/media/java/android/media/tv/ITvInputSessionWrapper.java +++ b/media/java/android/media/tv/ITvInputSessionWrapper.java @@ -80,7 +80,7 @@ public class ITvInputSessionWrapper extends ITvInputSession.Stub implements Hand 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 @@ public class ITvInputSessionWrapper extends ITvInputSession.Stub implements Hand 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 @@ public class ITvInputSessionWrapper extends ITvInputSession.Stub implements Hand } @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 8720bfef1e49..672f58b576f2 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.AudioFormat.Encoding; 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 @@ public final class TvInputManager { 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 @@ public final class TvInputManager { /** * 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 */ @@ -3616,13 +3619,13 @@ public final class TvInputManager { } } - 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 a022b1c7cf64..6b030412df7d 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.hardware.hdmi.HdmiDeviceInfo; 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; @@ -1615,20 +1617,20 @@ public abstract class TvInputService extends Service { * <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 @@ public abstract class TvInputService extends Service { } /** - * 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 cb45661a01ac..ffc121e6be0e 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.PlaybackParams; 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 @@ public class TvView extends ViewGroup { * <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 @@ public class TvView extends ViewGroup { } /** - * 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(); } } diff --git a/media/java/android/media/tv/ad/TvAdManager.java b/media/java/android/media/tv/ad/TvAdManager.java index 02c0d75dc2b6..f373bed37170 100644 --- a/media/java/android/media/tv/ad/TvAdManager.java +++ b/media/java/android/media/tv/ad/TvAdManager.java @@ -437,11 +437,10 @@ public class TvAdManager { } /** - * 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() { @@ -1174,8 +1173,7 @@ public class TvAdManager { } /** - * 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 4d8f5c8b640b..953b5cfd0e27 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.graphics.Rect; 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 @@ import java.util.List; /** * 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 @@ public abstract class TvAdService extends Service { * 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 @@ public abstract class TvAdService extends Service { @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) { @@ -398,6 +399,7 @@ public abstract class TvAdService extends Service { * @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 +423,22 @@ public abstract class TvAdService extends Service { } @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 +486,7 @@ public abstract class TvAdService extends Service { * @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, diff --git a/media/java/android/media/tv/ad/TvAdServiceInfo.java b/media/java/android/media/tv/ad/TvAdServiceInfo.java index 45dc89d838d8..bac14e704e51 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.pm.ServiceInfo; 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 @@ import java.util.List; /** * 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 @@ public final class TvAdServiceInfo implements Parcelable { 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 604dbd5a3399..be88506a4920 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.TvInputManager; 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.List; 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; @@ -182,14 +184,12 @@ public class TvAdView extends ViewGroup { return true; } - /** @hide */ @Override public void onAttachedToWindow() { super.onAttachedToWindow(); createSessionMediaView(); } - /** @hide */ @Override public void onDetachedFromWindow() { removeSessionMediaView(); @@ -381,6 +381,7 @@ public class TvAdView extends ViewGroup { * @param event The input event. * @return If you handled the event, return {@code true}. If you want to allow the event to be * handled by the next receiver, return {@code false}. + * @hide */ public boolean onUnhandledInputEvent(@NonNull InputEvent event) { return false; @@ -422,7 +423,7 @@ public class TvAdView extends ViewGroup { } @Override - public boolean dispatchKeyEvent(@NonNull KeyEvent event) { + public boolean dispatchKeyEvent(@Nullable KeyEvent event) { if (super.dispatchKeyEvent(event)) { return true; } @@ -465,6 +466,7 @@ public class TvAdView extends ViewGroup { /** * Stops the AD service. + * @hide */ public void stopAdService() { if (DEBUG) { @@ -479,6 +481,7 @@ public class TvAdView extends ViewGroup { * Resets the AD service. * * <p>This releases the resources of the corresponding {@link TvAdService.Session}. + * @hide */ public void resetAdService() { if (DEBUG) { @@ -493,6 +496,7 @@ public class TvAdView extends ViewGroup { * 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 +512,7 @@ public class TvAdView extends ViewGroup { * * @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 +525,7 @@ public class TvAdView extends ViewGroup { /** * Sends track info list to related TV AD service. + * @hide */ public void sendTrackInfoList(@Nullable List<TvTrackInfo> tracks) { if (DEBUG) { @@ -536,6 +542,7 @@ public class TvAdView extends ViewGroup { * @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 +584,7 @@ public class TvAdView extends ViewGroup { * 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 +607,7 @@ public class TvAdView extends ViewGroup { * {@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) { diff --git a/media/java/android/media/tv/flags/media_tv.aconfig b/media/java/android/media/tv/flags/media_tv.aconfig index 018eaf6dc32f..f1107059111c 100644 --- a/media/java/android/media/tv/flags/media_tv.aconfig +++ b/media/java/android/media/tv/flags/media_tv.aconfig @@ -12,4 +12,11 @@ flag { namespace: "media_tv" description: "Enable the TV client-side AD framework." bug: "303506816" +} + +flag { + name: "tiaf_v_apis" + namespace: "media_tv" + description: "TIAF V3.0 APIs for Android V" + bug: "303323657" }
\ No newline at end of file diff --git a/media/java/android/media/tv/interactive/TvInteractiveAppService.java b/media/java/android/media/tv/interactive/TvInteractiveAppService.java index 7b6dc38fe22c..6b0620c58d31 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.TvInputManager; 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; @@ -959,12 +961,12 @@ public abstract class TvInteractiveAppService extends Service { /** * 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 @@ public abstract class TvInteractiveAppService extends Service { } /** - * 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(() -> { diff --git a/media/java/android/media/tv/interactive/TvInteractiveAppView.java b/media/java/android/media/tv/interactive/TvInteractiveAppView.java index 3b295742c244..584ea841b18c 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.TvInputManager; 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 @@ public class TvInteractiveAppView extends ViewGroup { /** * 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"); @@ -1248,8 +1251,8 @@ public class TvInteractiveAppView extends ViewGroup { * 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) { } diff --git a/media/tests/EffectsTest/src/com/android/effectstest/EffectsTest.java b/media/tests/EffectsTest/src/com/android/effectstest/EffectsTest.java index 70202463a79e..c18a2de44573 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 class EffectsTest extends Activity { public EffectsTest() { - Log.d(TAG, "contructor"); + Log.d(TAG, "constructor"); } @Override diff --git a/nfc/api/current.txt b/nfc/api/current.txt index 1046d8e9aebb..28cf250c40be 100644 --- a/nfc/api/current.txt +++ b/nfc/api/current.txt @@ -64,10 +64,8 @@ package android.nfc { } 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 @@ package android.nfc { 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 @@ package android.nfc { 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 @@ package android.nfc { 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 diff --git a/nfc/api/system-current.txt b/nfc/api/system-current.txt index 3524f8cce04c..768013644b12 100644 --- a/nfc/api/system-current.txt +++ b/nfc/api/system-current.txt @@ -14,15 +14,23 @@ package android.nfc { 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 setReaderMode(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.enable_nfc_mainline") @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 @@ package android.nfc { 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 63c3414acc36..c444740a5b1b 100644 --- a/nfc/java/android/nfc/INfcAdapter.aidl +++ b/nfc/java/android/nfc/INfcAdapter.aidl @@ -24,6 +24,7 @@ import android.nfc.TechListParcel; 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 @@ interface INfcAdapter @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 @@ interface INfcAdapter 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/nfc/java/android/nfc/INfcVendorNciCallback.aidl b/nfc/java/android/nfc/INfcVendorNciCallback.aidl new file mode 100644 index 000000000000..821dc6f6c868 --- /dev/null +++ b/nfc/java/android/nfc/INfcVendorNciCallback.aidl @@ -0,0 +1,24 @@ +/* + * 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; + +/** + * @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 11eb97b440bf..782af5f3fe19 100644 --- a/nfc/java/android/nfc/NfcAdapter.java +++ b/nfc/java/android/nfc/NfcAdapter.java @@ -19,6 +19,7 @@ package android.nfc; 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 @@ public final class NfcAdapter { 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 @@ public final class NfcAdapter { /** * 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 @@ public final class NfcAdapter { mLock = new Object(); mControllerAlwaysOnListener = new NfcControllerAlwaysOnListener(getService()); mNfcWlcStateListener = new NfcWlcStateListener(getService()); + mNfcVendorNciCallbackListener = new NfcVendorNciCallbackListener(getService()); } /** @@ -1204,18 +1207,16 @@ public final class NfcAdapter { } } - /** - * 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 @@ public final class NfcAdapter { } /** - * 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; @@ -1764,7 +1767,9 @@ public final class NfcAdapter { private static final int ENABLE_POLLING_FLAGS = 0x0000; /** - * Privileged API to enable disable reader polling. + * Privileged API to enable or disable reader polling. + * Unlike {@link #enableReaderMode(Activity, ReaderCallback, int, Bundle)}, this API does not + * need a foreground activity to control reader mode parameters * Note: Use with caution! The app is responsible for ensuring that the polling state is * returned to normal. * @@ -1778,14 +1783,14 @@ public final class NfcAdapter { @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) @FlaggedApi(Flags.FLAG_ENABLE_NFC_MAINLINE) @SuppressLint("VisiblySynchronized") - public void setReaderMode(boolean enablePolling) { + public void setReaderModePollingEnabled(boolean enable) { synchronized (sLock) { if (!sHasNfcFeature) { throw new UnsupportedOperationException(); } } Binder token = new Binder(); - int flags = enablePolling ? ENABLE_POLLING_FLAGS : DISABLE_POLLING_FLAGS; + int flags = enable ? ENABLE_POLLING_FLAGS : DISABLE_POLLING_FLAGS; try { NfcAdapter.sService.setReaderMode(token, null, flags, null); } catch (RemoteException e) { @@ -1799,6 +1804,8 @@ public final class NfcAdapter { * * 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. @@ -1830,6 +1837,15 @@ public final class NfcAdapter { @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) { @@ -1856,10 +1872,10 @@ public final class NfcAdapter { } /** - * 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) @@ -2965,4 +2981,163 @@ public final class NfcAdapter { 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_ENABLE_NFC_MAINLINE) + @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 000000000000..742d75fe4bc3 --- /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 426c5aac487b..3254a394c25a 100644 --- a/nfc/java/android/nfc/cardemulation/ApduServiceInfo.java +++ b/nfc/java/android/nfc/cardemulation/ApduServiceInfo.java @@ -105,6 +105,8 @@ public final class ApduServiceInfo implements Parcelable { 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 @@ public final class ApduServiceInfo implements Parcelable { 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 @@ public final class ApduServiceInfo implements Parcelable { 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 @@ public final class ApduServiceInfo implements Parcelable { 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 @@ public final class ApduServiceInfo implements Parcelable { } /** + * 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 @@ public final class ApduServiceInfo implements Parcelable { } /** + * 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/flags.aconfig b/nfc/java/android/nfc/flags.aconfig index 01a45708fddf..0a2619c8a19a 100644 --- a/nfc/java/android/nfc/flags.aconfig +++ b/nfc/java/android/nfc/flags.aconfig @@ -69,3 +69,10 @@ flag { 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 1570e0e22f50..31104a0d0a08 100644 --- a/opengl/java/android/opengl/EGLExt.java +++ b/opengl/java/android/opengl/EGLExt.java @@ -46,14 +46,6 @@ public class EGLExt { _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 @@ public class EGLExt { } 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/CrashRecovery/aconfig/flags.aconfig b/packages/CrashRecovery/aconfig/flags.aconfig index 563626634068..572a66922ea3 100644 --- a/packages/CrashRecovery/aconfig/flags.aconfig +++ b/packages/CrashRecovery/aconfig/flags.aconfig @@ -6,4 +6,11 @@ flag { 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 b2af315ef2c9..c0d93531a1e6 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 27ddff93247e..ab10b5a23676 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 dd543349fc4d..dffe4e239ec9 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 android.util.Slog; 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,7 @@ public class RescueParty { return; } - FrameworkStatsLog.write(FrameworkStatsLog.RESCUE_PARTY_RESET_REPORTED, level); + CrashRecoveryStatsLog.write(CrashRecoveryStatsLog.RESCUE_PARTY_RESET_REPORTED, 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 2007079ea5ca..50322f09640f 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.Slog; 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 class RollbackPackageHealthObserver implements PackageHealthObserver { 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 @@ final class RollbackPackageHealthObserver implements PackageHealthObserver { 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 f9ef994a523a..898c5439a293 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.ArraySet; 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 @@ public final class WatchdogRollbackLogger { + " 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 @@ public final class WatchdogRollbackLogger { } 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/drawable/more_horiz_24px.xml b/packages/CredentialManager/res/drawable/more_horiz_24px.xml index 7b235f84f0fa..0100718ffd08 100644 --- a/packages/CredentialManager/res/drawable/more_horiz_24px.xml +++ b/packages/CredentialManager/res/drawable/more_horiz_24px.xml @@ -3,6 +3,7 @@ android:height="24dp" android:viewportWidth="960" android:viewportHeight="960" + android:contentDescription="@string/more_options_content_description" android:tint="?attr/colorControlNormal"> <path android:fillColor="@android:color/white" diff --git a/packages/CredentialManager/res/layout/credman_dropdown_bottom_sheet.xml b/packages/CredentialManager/res/layout/credman_dropdown_bottom_sheet.xml index b97e9927e0ed..fdda9ea06ab9 100644 --- a/packages/CredentialManager/res/layout/credman_dropdown_bottom_sheet.xml +++ b/packages/CredentialManager/res/layout/credman_dropdown_bottom_sheet.xml @@ -28,7 +28,7 @@ android:layout_height="wrap_content" android:layout_centerVertical="true" android:layout_alignParentStart="true" - android:contentDescription="@string/provider_icon_content_description" + android:contentDescription="@string/more_options_content_description" android:background="@null"/> <TextView android:id="@android:id/text1" diff --git a/packages/CredentialManager/res/layout/credman_dropdown_presentation_layout.xml b/packages/CredentialManager/res/layout/credman_dropdown_presentation_layout.xml index 261154fe9792..c7c2fda6a489 100644 --- a/packages/CredentialManager/res/layout/credman_dropdown_presentation_layout.xml +++ b/packages/CredentialManager/res/layout/credman_dropdown_presentation_layout.xml @@ -25,7 +25,6 @@ android:id="@android:id/icon1" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:contentDescription="@string/dropdown_presentation_more_sign_in_options_text" android:layout_centerVertical="true" android:layout_alignParentStart="true" android:background="@null"/> diff --git a/packages/CredentialManager/res/values/strings.xml b/packages/CredentialManager/res/values/strings.xml index f98164b8788c..82b47a964204 100644 --- a/packages/CredentialManager/res/values/strings.xml +++ b/packages/CredentialManager/res/values/strings.xml @@ -172,5 +172,5 @@ <!-- Strings for dropdown presentation. --> <!-- Text shown in the dropdown presentation to select more sign in options. [CHAR LIMIT=120] --> <string name="dropdown_presentation_more_sign_in_options_text">Sign-in options</string> - <string name="provider_icon_content_description">Credential provider icon</string> + <string name="more_options_content_description">More</string> </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 0fa248de4465..9a2cf61d2b86 100644 --- a/packages/CredentialManager/shared/src/com/android/credentialmanager/IntentParser.kt +++ b/packages/CredentialManager/shared/src/com/android/credentialmanager/IntentParser.kt @@ -36,7 +36,7 @@ fun Intent.parse( 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) { 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 4155b0398ce0..9242141cfd63 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.ProviderData 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 6cafcf7cd976..30681f39d929 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt @@ -18,7 +18,7 @@ package com.android.credentialmanager 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.IBinder 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 @@ -80,9 +81,10 @@ class CredentialManagerRepo( 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( @@ -238,6 +240,24 @@ class CredentialManagerRepo( ) } + 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( @@ -275,10 +295,10 @@ class CredentialManagerRepo( } /** 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 fa975aabc867..05aa5489ff36 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorActivity.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorActivity.kt @@ -131,7 +131,7 @@ class CredentialSelectorActivity : ComponentActivity() { // 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") diff --git a/packages/CredentialManager/src/com/android/credentialmanager/autofill/CredentialAutofillService.kt b/packages/CredentialManager/src/com/android/credentialmanager/autofill/CredentialAutofillService.kt index 1f1d236f2108..07f1fa34580b 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/autofill/CredentialAutofillService.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/autofill/CredentialAutofillService.kt @@ -16,22 +16,24 @@ 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.credentials.GetCredentialRequest -import android.credentials.GetCandidateCredentialsResponse -import android.credentials.GetCandidateCredentialsException +import android.credentials.Credential +import android.credentials.CredentialManager import android.credentials.CredentialOption +import android.credentials.GetCandidateCredentialsException +import android.credentials.GetCandidateCredentialsResponse +import android.credentials.GetCredentialRequest +import android.credentials.GetCredentialResponse +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 +46,11 @@ import android.service.autofill.SaveCallback import android.service.autofill.SaveRequest import android.service.credentials.CredentialProviderService import android.util.Log +import android.view.autofill.AutofillId 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.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 +62,9 @@ import com.android.credentialmanager.getflow.toProviderDisplayInfo 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() { @@ -153,8 +153,7 @@ class CredentialAutofillService : AutofillService() { return } - val fillResponse = convertToFillResponse(result, request, - this@CredentialAutofillService) + val fillResponse = convertToFillResponse(result, request) if (fillResponse != null) { callback.onSuccess(fillResponse) } else { @@ -231,7 +230,7 @@ class CredentialAutofillService : AutofillService() { } private fun getEntryToIconMap( - candidateProviderDataList: MutableList<GetCredentialProviderData> + candidateProviderDataList: List<GetCredentialProviderData> ): Map<String, Icon> { val entryIconMap: MutableMap<String, Icon> = mutableMapOf() candidateProviderDataList.forEach { provider -> @@ -261,20 +260,16 @@ class CredentialAutofillService : AutofillService() { private fun convertToFillResponse( getCredResponse: GetCandidateCredentialsResponse, - filLRequest: FillRequest, - context: Context + filLRequest: FillRequest ): 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() var validFillResponse = false autofillIdToProvidersMap.forEach { (autofillId, providers) -> @@ -292,11 +287,14 @@ class CredentialAutofillService : AutofillService() { 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 } @@ -340,7 +338,7 @@ class CredentialAutofillService : AutofillService() { 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. @@ -398,19 +396,20 @@ class CredentialAutofillService : AutofillService() { 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,7 +429,8 @@ class CredentialAutofillService : AutofillService() { private fun addDropdownMoreOptionsPresentation( bottomSheetPendingIntent: PendingIntent, autofillId: AutofillId, - fillResponseBuilder: FillResponse.Builder) { + fillResponseBuilder: FillResponse.Builder + ) { val presentationBuilder = Presentations.Builder() .setMenuPresentation(RemoteViewsFactory.createMoreSignInOptionsPresentation(this)) @@ -460,7 +460,8 @@ class CredentialAutofillService : AutofillService() { bottomSheetPendingIntent: PendingIntent, spec: InlinePresentationSpec, autofillId: AutofillId, - fillResponseBuilder: FillResponse.Builder + fillResponseBuilder: FillResponse.Builder, + providerDataList: ArrayList<GetCredentialProviderData> ) { val dataSetBuilder = Dataset.Builder() val sliceBuilder = InlineSuggestionUi @@ -471,6 +472,10 @@ class CredentialAutofillService : AutofillService() { .setInlinePresentation(InlinePresentation( sliceBuilder.build().slice, spec, /* pinned= */ true)) + val extraBundle = Bundle() + extraBundle.putParcelableArrayList( + ProviderData.EXTRA_ENABLED_PROVIDER_DATA_LIST, providerDataList) + fillResponseBuilder.addDataset( dataSetBuilder .setField( @@ -479,6 +484,7 @@ class CredentialAutofillService : AutofillService() { presentationBuilder.build()) .build()) .setAuthentication(bottomSheetPendingIntent.intentSender) + .setAuthenticationExtras(extraBundle) .build() ) } @@ -514,16 +520,16 @@ class CredentialAutofillService : AutofillService() { * } */ 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 +537,13 @@ class CredentialAutofillService : AutofillService() { } 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 +552,22 @@ class CredentialAutofillService : AutofillService() { " 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 ) } diff --git a/packages/CredentialManager/src/com/android/credentialmanager/common/ui/RemoteViewsFactory.kt b/packages/CredentialManager/src/com/android/credentialmanager/common/ui/RemoteViewsFactory.kt index 68f1c861d51b..02afc547e3f8 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/common/ui/RemoteViewsFactory.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/common/ui/RemoteViewsFactory.kt @@ -74,6 +74,8 @@ class RemoteViewsFactory { setMaxHeightMethodName, context.resources.getDimensionPixelSize( com.android.credentialmanager.R.dimen.autofill_icon_size)); + remoteViews.setContentDescription(android.R.id.icon1, credentialEntryInfo + .providerDisplayName); val drawableId = com.android.credentialmanager.R.drawable.fill_dialog_dynamic_list_item_one remoteViews.setInt( diff --git a/packages/CredentialManager/wear/res/values/strings.xml b/packages/CredentialManager/wear/res/values/strings.xml index 109644f46b10..be7e4487efde 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/components/CredentialsScreenChip.kt b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/components/CredentialsScreenChip.kt new file mode 100644 index 000000000000..3297991e2504 --- /dev/null +++ b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/components/CredentialsScreenChip.kt @@ -0,0 +1,156 @@ +/* + * 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, + 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 + .fillMaxWidth() + .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 + .fillMaxWidth() + .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 + .fillMaxWidth() + .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 5cb3c1590dfd..000000000000 --- 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/screens/single/SingleAccountScreen.kt b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/single/SingleAccountScreen.kt index 85327833429d..e94dd6829792 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 @@ fun SingleAccountScreen( 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/passkey/SinglePasskeyScreen.kt b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/single/passkey/SinglePasskeyScreen.kt index c9b0230e74b9..9558bb0c3ff9 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 @@ -25,20 +25,15 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp 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.android.credentialmanager.ui.screens.single.SingleAccountScreen 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 @Composable fun SinglePasskeyScreen( name: String, email: String, - onCancelClick: () -> Unit, - onOKClick: () -> Unit, columnState: ScalingLazyColumnState, modifier: Modifier = Modifier, ) { @@ -60,24 +55,6 @@ fun SinglePasskeyScreen( modifier = modifier.padding(horizontal = 10.dp) ) { item { - DialogButtonsRow( - onCancelClick = onCancelClick, - onOKClick = onOKClick, - modifier = Modifier.padding(top = 10.dp) - ) } } } - -@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/password/SinglePasswordScreen.kt b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/single/password/SinglePasswordScreen.kt index 9f971ae1e327..54636122397f 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 @@ -33,15 +33,12 @@ import com.android.credentialmanager.CredentialSelectorUiState.Get.SingleEntry 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.SignInHeader import com.android.credentialmanager.ui.model.PasswordUiModel import com.android.credentialmanager.ui.screens.single.SingleAccountScreen 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 @Composable fun SinglePasswordScreen( @@ -63,8 +60,6 @@ fun SinglePasswordScreen( is SinglePasswordScreenUiState.Loaded -> { SinglePasswordScreen( passwordUiModel = state.passwordUiModel, - onCancelClick = viewModel::onCancelClick, - onOKClick = viewModel::onOKClick, columnState = columnState, modifier = modifier ) @@ -102,8 +97,6 @@ fun SinglePasswordScreen( @Composable fun SinglePasswordScreen( passwordUiModel: PasswordUiModel, - onCancelClick: () -> Unit, - onOKClick: () -> Unit, columnState: ScalingLazyColumnState, modifier: Modifier = Modifier, ) { @@ -124,23 +117,6 @@ fun SinglePasswordScreen( modifier = modifier.padding(horizontal = 10.dp) ) { item { - DialogButtonsRow( - onCancelClick = onCancelClick, - onOKClick = onOKClick, - modifier = Modifier.padding(top = 10.dp) - ) } } } - -@WearPreview -@Composable -fun SinglePasswordScreenPreview() { - SinglePasswordScreen( - passwordUiModel = PasswordUiModel(email = "beckett_bakery@gmail.com"), - onCancelClick = {}, - onOKClick = {}, - columnState = belowTimeTextPreview(), - ) -} - diff --git a/packages/PackageInstaller/res/values/strings.xml b/packages/PackageInstaller/res/values/strings.xml index f4641b9930cb..f425f52804c5 100644 --- a/packages/PackageInstaller/res/values/strings.xml +++ b/packages/PackageInstaller/res/values/strings.xml @@ -272,7 +272,7 @@ <!-- The title of a dialog which asks the user to restore (i.e. re-install, re-download) an app after parts of the app have been previously moved into the cloud for temporary storage. "installername" is the app that will facilitate the download of the app. [CHAR LIMIT=50] --> - <string name="unarchive_application_title">Restore <xliff:g id="appname" example="Bird Game">%1$s</xliff:g> from <xliff:g id="installername" example="App Store">%1$s</xliff:g>?</string> + <string name="unarchive_application_title">Restore <xliff:g id="appname" example="Bird Game">%1$s</xliff:g> from <xliff:g id="installername" example="App Store">%2$s</xliff:g>?</string> <!-- After the user confirms the dialog, a download will start. [CHAR LIMIT=none] --> <string name="unarchive_body_text">This app will begin to download in the background</string> <!-- The action to restore (i.e. re-install, re-download) an app after parts of the app have been previously moved diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/UnarchiveActivity.java b/packages/PackageInstaller/src/com/android/packageinstaller/UnarchiveActivity.java index 9af799c37e8f..b20117d78230 100644 --- a/packages/PackageInstaller/src/com/android/packageinstaller/UnarchiveActivity.java +++ b/packages/PackageInstaller/src/com/android/packageinstaller/UnarchiveActivity.java @@ -25,10 +25,12 @@ import android.app.DialogFragment; import android.app.Fragment; import android.app.FragmentTransaction; import android.content.IntentSender; +import android.content.pm.InstallSourceInfo; import android.content.pm.PackageInstaller; import android.content.pm.PackageManager; import android.os.Bundle; import android.os.Process; +import android.text.TextUtils; import android.util.Log; import androidx.annotation.NonNull; @@ -97,14 +99,30 @@ public class UnarchiveActivity extends Activity { String appTitle = pm.getApplicationInfo(mPackageName, PackageManager.ApplicationInfoFlags.of( MATCH_ARCHIVED_PACKAGES)).loadLabel(pm).toString(); - // TODO(ag/25387215) Get the real installer title here after fixing getInstallSource for - // archived apps. - showDialogFragment(appTitle, "installerTitle"); + String installerTitle = getResponsibleInstallerTitle(pm, + pm.getInstallSourceInfo(mPackageName)); + showDialogFragment(appTitle, installerTitle); } catch (PackageManager.NameNotFoundException e) { Log.e(TAG, "Invalid packageName: " + e.getMessage()); } } + private String getResponsibleInstallerTitle(PackageManager pm, + InstallSourceInfo installSource) + throws PackageManager.NameNotFoundException { + String packageName = TextUtils.isEmpty(installSource.getUpdateOwnerPackageName()) + ? installSource.getInstallingPackageName() + : installSource.getUpdateOwnerPackageName(); + if (packageName == null) { + // Should be unreachable. + Log.e(TAG, "Installer not found."); + setResult(Activity.RESULT_FIRST_USER); + finish(); + return ""; + } + return pm.getApplicationInfo(packageName, /* flags= */ 0).loadLabel(pm).toString(); + } + @NonNull private String[] getRequestedPermissions(String callingPackage) { String[] requestedPermissions = null; diff --git a/packages/SettingsLib/Spa/build.gradle.kts b/packages/SettingsLib/Spa/build.gradle.kts index 6f9556f91bd6..ec519ca61021 100644 --- a/packages/SettingsLib/Spa/build.gradle.kts +++ b/packages/SettingsLib/Spa/build.gradle.kts @@ -29,7 +29,7 @@ val androidTop: String = File(rootDir, "../../../../..").canonicalPath 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 1f78a9c3ac07..f6fbc02de3b0 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 618dc37037aa..08a87973468b 100644 --- a/packages/SettingsLib/Spa/spa/build.gradle.kts +++ b/packages/SettingsLib/Spa/spa/build.gradle.kts @@ -57,13 +57,13 @@ dependencies { 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/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 000000000000..1d3eb51ebaa4 --- /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/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 000000000000..de915ef662cf --- /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/AppOpsController.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppOpsController.kt index 9f33fcb0052b..6e9bde45e061 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.Context 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 @@ class AppOpsController( ) : 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 @@ class AppOpsController( 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 000000000000..0b5604eb74c1 --- /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 25c3bc541249..5db5eae22745 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.app.AppOpsManager 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 @@ internal fun isAllowed( 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/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 000000000000..97c74411d945 --- /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 d158a2414f85..bb25cf3b6d48 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.Context 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 @@ class AppOpPermissionAppListTest { 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 fd2f9bd345d2..bab678178ebf 100644 --- a/packages/SettingsLib/aconfig/settingslib.aconfig +++ b/packages/SettingsLib/aconfig/settingslib.aconfig @@ -7,3 +7,13 @@ flag { 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/values/strings.xml b/packages/SettingsLib/res/values/strings.xml index a4b3af990c17..2e64212d298a 100644 --- a/packages/SettingsLib/res/values/strings.xml +++ b/packages/SettingsLib/res/values/strings.xml @@ -1340,11 +1340,11 @@ <string name="notice_header" translatable="false"></string> <!-- Name of the phone device. [CHAR LIMIT=30] --> - <string name="media_transfer_this_device_name" product="default">This phone</string> + <string name="media_transfer_this_device_name">This phone</string> <!-- Name of the tablet device. [CHAR LIMIT=30] --> - <string name="media_transfer_this_device_name" product="tablet">This tablet</string> + <string name="media_transfer_this_device_name_tablet">This tablet</string> <!-- Name of the default media output of the TV. [CHAR LIMIT=30] --> - <string name="media_transfer_this_device_name" product="tv">@string/tv_media_transfer_default</string> + <string name="media_transfer_this_device_name_tv">@string/tv_media_transfer_default</string> <!-- Name of the dock device. [CHAR LIMIT=30] --> <string name="media_transfer_dock_speaker_device_name">Dock speaker</string> <!-- Default name of the external device. [CHAR LIMIT=30] --> diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothEventManager.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothEventManager.java index c97445f04eea..647fcb9f67fa 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 @@ public class BluetoothEventManager { 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 f7f06739d4b2..09b1eafe4d78 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.BluetoothDevice; 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 android.util.Pair; 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.R; 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 class BluetoothUtils { 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 @@ public class BluetoothUtils { 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 560bc467ca8e..bcdb64d17636 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 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice> 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 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice> 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 89fe268b3258..32eec7e709af 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 @@ public class CachedBluetoothDeviceManager { } } + 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 1d2f7902b781..6ee403d50751 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 static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_FORBIDDEN; 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.ConcurrentHashMap; 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 @@ public class LocalBluetoothLeBroadcast implements LocalBluetoothProfile { 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 @@ public class LocalBluetoothLeBroadcast implements LocalBluetoothProfile { 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 @@ public class LocalBluetoothLeBroadcast implements LocalBluetoothProfile { + ", sourceId = " + sourceId); } + updateFallbackActiveDeviceIfNeeded(); } @Override @@ -348,7 +364,9 @@ public class LocalBluetoothLeBroadcast implements LocalBluetoothProfile { } } - 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 @@ public class LocalBluetoothLeBroadcast implements LocalBluetoothProfile { 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 @@ public class LocalBluetoothLeBroadcast implements LocalBluetoothProfile { } } } + + /** 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 000000000000..5dc0237e508c --- /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 119aef6da12e..79e4c374667e 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 @@ public class LocalBluetoothProfileManager { 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/PhoneMediaDevice.java b/packages/SettingsLib/src/com/android/settingslib/media/PhoneMediaDevice.java index 15f33d2cff42..ba9180db0887 100644 --- a/packages/SettingsLib/src/com/android/settingslib/media/PhoneMediaDevice.java +++ b/packages/SettingsLib/src/com/android/settingslib/media/PhoneMediaDevice.java @@ -37,6 +37,7 @@ import android.hardware.hdmi.HdmiDeviceInfo; import android.hardware.hdmi.HdmiPortInfo; import android.media.MediaRoute2Info; import android.media.RouteListingPreference; +import android.os.SystemProperties; import android.util.Log; import androidx.annotation.NonNull; @@ -45,6 +46,7 @@ import androidx.annotation.VisibleForTesting; import com.android.settingslib.R; import com.android.settingslib.media.flags.Flags; +import java.util.Arrays; import java.util.List; /** @@ -63,6 +65,17 @@ public class PhoneMediaDevice extends MediaDevice { private final DeviceIconUtil mDeviceIconUtil; + /** Returns this device name for media transfer. */ + public static @NonNull String getMediaTransferThisDeviceName(@NonNull Context context) { + if (isTv(context)) { + return context.getString(R.string.media_transfer_this_device_name_tv); + } else if (isTablet()) { + return context.getString(R.string.media_transfer_this_device_name_tablet); + } else { + return context.getString(R.string.media_transfer_this_device_name); + } + } + /** Returns the device name for the given {@code routeInfo}. */ public static String getSystemRouteNameFromType( @NonNull Context context, @NonNull MediaRoute2Info routeInfo) { @@ -80,7 +93,7 @@ public class PhoneMediaDevice extends MediaDevice { name = context.getString(R.string.media_transfer_dock_speaker_device_name); break; case TYPE_BUILTIN_SPEAKER: - name = context.getString(R.string.media_transfer_this_device_name); + name = getMediaTransferThisDeviceName(context); break; case TYPE_HDMI: name = context.getString(isTv ? R.string.tv_media_transfer_default : @@ -135,6 +148,11 @@ public class PhoneMediaDevice extends MediaDevice { && Flags.enableTvMediaOutputDialog(); } + static boolean isTablet() { + return Arrays.asList(SystemProperties.get("ro.build.characteristics").split(",")) + .contains("tablet"); + } + // MediaRoute2Info.getType was made public on API 34, but exists since API 30. @SuppressWarnings("NewApi") @Override 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 3355fb395ca0..6761aa7a1006 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,29 +16,71 @@ 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.model.AudioStream +import com.android.settingslib.volume.shared.model.AudioStreamModel +import com.android.settingslib.volume.shared.model.RingerMode import kotlin.coroutines.CoroutineContext 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 +import kotlinx.coroutines.flow.filter +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 -/** Provides audio managing functionality and data. */ +/** Provides audio streams state and managing functionality. */ interface AudioRepository { /** Current [AudioManager.getMode]. */ val mode: StateFlow<Int> + + /** + * Ringtone mode. + * + * @see AudioManager.getRingerModeInternal + */ + val ringerMode: StateFlow<RingerMode> + + /** + * Communication device. Emits null when there is no communication device available. + * + * @see AudioDeviceInfo.getType + */ + val communicationDevice: StateFlow<AudioDeviceInfo?> + + /** State of the [AudioStream]. */ + suspend fun getAudioStream(audioStream: AudioStream): Flow<AudioStreamModel> + + /** Current state of the [AudioStream]. */ + suspend fun getCurrentAudioStream(audioStream: AudioStream): AudioStreamModel + + suspend fun setVolume(audioStream: AudioStream, volume: Int) + + suspend fun setMuted(audioStream: AudioStream, isMuted: Boolean) } class AudioRepositoryImpl( + private val context: Context, private val audioManager: AudioManager, - backgroundCoroutineContext: CoroutineContext, - coroutineScope: CoroutineScope, + private val backgroundCoroutineContext: CoroutineContext, + private val coroutineScope: CoroutineScope, ) : AudioRepository { override val mode: StateFlow<Int> = @@ -50,4 +92,117 @@ class AudioRepositoryImpl( } .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) } + .map { RingerMode(audioManager.ringerModeInternal) } + .flowOn(backgroundCoroutineContext) + .stateIn( + coroutineScope, + SharingStarted.WhileSubscribed(), + RingerMode(audioManager.ringerModeInternal), + ) + + override val communicationDevice: StateFlow<AudioDeviceInfo?> + get() = + callbackFlow { + val listener = OnCommunicationDeviceChangedListener { trySend(Unit) } + audioManager.addOnCommunicationDeviceChangedListener( + DirectExecutor.INSTANCE, + listener + ) + + awaitClose { audioManager.removeOnCommunicationDeviceChangedListener(listener) } + } + .filterNotNull() + .map { audioManager.communicationDevice } + .flowOn(backgroundCoroutineContext) + .stateIn( + coroutineScope, + SharingStarted.WhileSubscribed(), + audioManager.communicationDevice, + ) + + override suspend fun getAudioStream(audioStream: AudioStream): Flow<AudioStreamModel> { + return audioManagerIntents + .filter { modelActions.contains(it) } + .map { getCurrentAudioStream(audioStream) } + .flowOn(backgroundCoroutineContext) + } + + override suspend fun getCurrentAudioStream(audioStream: AudioStream): AudioStreamModel { + return withContext(backgroundCoroutineContext) { + AudioStreamModel( + audioStream = audioStream, + minVolume = getMinVolume(audioStream), + maxVolume = audioManager.getStreamMaxVolume(audioStream.value), + volume = audioManager.getStreamVolume(audioStream.value), + isAffectedByRingerMode = + audioManager.isStreamAffectedByRingerMode(audioStream.value), + isMuted = audioManager.isStreamMute(audioStream.value) + ) + } + } + + override suspend fun setVolume(audioStream: AudioStream, volume: Int) = + withContext(backgroundCoroutineContext) { + audioManager.setStreamVolume(audioStream.value, volume, 0) + } + + override suspend fun setMuted(audioStream: AudioStream, isMuted: Boolean) = + withContext(backgroundCoroutineContext) { + if (isMuted) { + audioManager.adjustStreamVolume(audioStream.value, 0, AudioManager.ADJUST_MUTE) + } else { + audioManager.adjustStreamVolume(audioStream.value, 0, AudioManager.ADJUST_UNMUTE) + } + } + + private fun getMinVolume(stream: AudioStream): Int = + try { + audioManager.getStreamMinVolume(stream.value) + } catch (e: IllegalArgumentException) { + // Fallback to STREAM_VOICE_CALL because + // CallVolumePreferenceController.java default + // 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/AudioSystemRepository.kt b/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/AudioSystemRepository.kt new file mode 100644 index 000000000000..2b12936e8118 --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/AudioSystemRepository.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.volume.data.repository + +import android.content.Context +import android.media.AudioSystem + +/** Provides the current state of the audio system. */ +interface AudioSystemRepository { + + val isSingleVolume: Boolean +} + +class AudioSystemRepositoryImpl(private val context: Context) : AudioSystemRepository { + + override val isSingleVolume: Boolean + get() = AudioSystem.isSingleVolume(context) +} 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 000000000000..1597b7712863 --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/LocalMediaRepository.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.settingslib.volume.data.repository + +import com.android.settingslib.media.LocalMediaManager +import com.android.settingslib.media.MediaDevice +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.flowOn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.mapNotNull +import kotlinx.coroutines.flow.stateIn + +/** 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?> +} + +class LocalMediaRepositoryImpl( + private val localMediaManager: LocalMediaManager, + coroutineScope: CoroutineScope, + backgroundContext: CoroutineContext, +) : LocalMediaRepository { + + private val deviceUpdates: 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) + } + } + + override val mediaDevices: StateFlow<Collection<MediaDevice>> = + deviceUpdates + .mapNotNull { + if (it is DevicesUpdate.DeviceListUpdate) { + it.newDevices ?: emptyList() + } else { + null + } + } + .flowOn(backgroundContext) + .stateIn(coroutineScope, SharingStarted.WhileSubscribed(), emptyList()) + + override val currentConnectedDevice: StateFlow<MediaDevice?> = + deviceUpdates + .map { localMediaManager.currentConnectedDevice } + .stateIn( + coroutineScope, + SharingStarted.WhileSubscribed(), + localMediaManager.currentConnectedDevice + ) + + 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 000000000000..93aa90d11678 --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/MediaControllerRepository.kt @@ -0,0 +1,124 @@ +/* + * 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.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +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 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.combine +import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.flow.shareIn +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch + +/** 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( + private val context: Context, + private val mediaSessionManager: MediaSessionManager, + localBluetoothManager: LocalBluetoothManager?, + coroutineScope: CoroutineScope, + backgroundContext: CoroutineContext, +) : MediaControllerRepository { + + private val devicesChanges: Flow<Unit> = + callbackFlow { + val receiver = + object : BroadcastReceiver() { + override fun onReceive(context: Context?, intent: Intent?) { + if (AudioManager.STREAM_DEVICES_CHANGED_ACTION == intent?.action) { + launch { send(Unit) } + } + } + } + context.registerReceiver( + receiver, + IntentFilter(AudioManager.STREAM_DEVICES_CHANGED_ACTION) + ) + + awaitClose { context.unregisterReceiver(receiver) } + } + .shareIn(coroutineScope, SharingStarted.WhileSubscribed(), replay = 0) + + override val activeMediaController: StateFlow<MediaController?> = + combine( + localBluetoothManager?.headsetAudioModeChanges?.onStart { emit(Unit) } + ?: emptyFlow(), + devicesChanges.onStart { emit(Unit) }, + ) { _, _ -> + 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/shared/model/AudioStream.kt b/packages/SettingsLib/src/com/android/settingslib/volume/shared/model/AudioStream.kt new file mode 100644 index 000000000000..58f3c2d61f3b --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/volume/shared/model/AudioStream.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.volume.shared.model + +import android.media.AudioManager + +/** Type-safe wrapper for [AudioManager] audio stream. */ +@JvmInline +value class AudioStream(val value: Int) { + init { + require(value in supportedStreamTypes) { "Unsupported stream=$value" } + } + + private companion object { + val supportedStreamTypes = + setOf( + AudioManager.STREAM_VOICE_CALL, + AudioManager.STREAM_SYSTEM, + AudioManager.STREAM_RING, + AudioManager.STREAM_MUSIC, + AudioManager.STREAM_ALARM, + AudioManager.STREAM_NOTIFICATION, + AudioManager.STREAM_BLUETOOTH_SCO, + AudioManager.STREAM_SYSTEM_ENFORCED, + AudioManager.STREAM_DTMF, + AudioManager.STREAM_TTS, + AudioManager.STREAM_ACCESSIBILITY, + AudioManager.STREAM_ASSISTANT, + ) + } +} diff --git a/packages/SettingsLib/src/com/android/settingslib/volume/shared/model/AudioStreamModel.kt b/packages/SettingsLib/src/com/android/settingslib/volume/shared/model/AudioStreamModel.kt new file mode 100644 index 000000000000..c1be1ee020f2 --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/volume/shared/model/AudioStreamModel.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.settingslib.volume.shared.model + +/** Current state of the audio stream. */ +data class AudioStreamModel( + val audioStream: AudioStream, + val volume: Int, + val minVolume: Int, + val maxVolume: Int, + val isAffectedByRingerMode: Boolean, + val isMuted: Boolean, +) diff --git a/packages/SettingsLib/src/com/android/settingslib/volume/shared/model/RingerMode.kt b/packages/SettingsLib/src/com/android/settingslib/volume/shared/model/RingerMode.kt new file mode 100644 index 000000000000..9f03927264c1 --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/volume/shared/model/RingerMode.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.settingslib.volume.shared.model + +import android.media.AudioManager + +/** Type-safe wrapper for [AudioManager] ringer mode. */ +@JvmInline +value class RingerMode(val value: Int) { + + init { + require(value in supportedRingerModes) { "Unsupported stream=$value" } + } + + private companion object { + val supportedRingerModes = + setOf( + AudioManager.RINGER_MODE_SILENT, + AudioManager.RINGER_MODE_VIBRATE, + AudioManager.RINGER_MODE_NORMAL, + AudioManager.RINGER_MODE_MAX, + ) + } +} diff --git a/packages/SettingsLib/tests/integ/Android.bp b/packages/SettingsLib/tests/integ/Android.bp index ce3a7baf6be6..f303ab5fc54b 100644 --- a/packages/SettingsLib/tests/integ/Android.bp +++ b/packages/SettingsLib/tests/integ/Android.bp @@ -40,6 +40,8 @@ android_test { "android.test.runner", "telephony-common", "android.test.base", + "android.test.mock", + "truth", ], platform_apis: true, @@ -49,16 +51,23 @@ android_test { "androidx.test.core", "androidx.test.rules", "androidx.test.espresso.core", + "androidx.test.ext.junit", "flag-junit", - "mockito-target-minus-junit4", + "kotlinx_coroutines_test", + "mockito-target-extended-minus-junit4", "platform-test-annotations", "truth", "SettingsLibDeviceStateRotationLock", "SettingsLibSettingsSpinner", "SettingsLibUsageProgressBarPreference", "settingslib_media_flags_lib", - "kotlinx_coroutines_test", ], + jni_libs: [ + "libdexmakerjvmtiagent", + "libmultiplejvmtiagentsinterferenceagent", + "libstaticjvmtiagent", + ], dxflags: ["--multi-dex"], + manifest: "AndroidManifest.xml", } diff --git a/packages/SettingsLib/tests/integ/AndroidManifest.xml b/packages/SettingsLib/tests/integ/AndroidManifest.xml index 32048ca6344c..9fb1c1f65961 100644 --- a/packages/SettingsLib/tests/integ/AndroidManifest.xml +++ b/packages/SettingsLib/tests/integ/AndroidManifest.xml @@ -25,7 +25,7 @@ <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" /> <uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS" /> - <application> + <application android:debuggable="true" android:testOnly="true"> <uses-library android:name="android.test.runner" /> <activity android:name=".drawer.SettingsDrawerActivityTest$TestActivity"/> diff --git a/packages/SettingsLib/tests/integ/AndroidTest.xml b/packages/SettingsLib/tests/integ/AndroidTest.xml index d0aee8822a72..9de601907a62 100644 --- a/packages/SettingsLib/tests/integ/AndroidTest.xml +++ b/packages/SettingsLib/tests/integ/AndroidTest.xml @@ -16,6 +16,7 @@ <configuration description="Runs Tests for SettingsLib."> <target_preparer class="com.android.tradefed.targetprep.TestAppInstallSetup"> <option name="test-file-name" value="SettingsLibTests.apk" /> + <option name="install-arg" value="-t" /> </target_preparer> <option name="test-suite-tag" value="apct" /> 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 new file mode 100644 index 000000000000..7b70c64f349b --- /dev/null +++ b/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/AudioRepositoryTest.kt @@ -0,0 +1,281 @@ +/* + * 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.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.model.AudioStream +import com.android.settingslib.volume.shared.model.AudioStreamModel +import com.android.settingslib.volume.shared.model.RingerMode +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.ArgumentMatchers.any +import org.mockito.ArgumentMatchers.anyInt +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 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 volumeByStream: MutableMap<Int, Int> = mutableMapOf() + private val isAffectedByRingerModeByStream: MutableMap<Int, Boolean> = mutableMapOf() + private val isMuteByStream: MutableMap<Int, Boolean> = mutableMapOf() + private val testScope = TestScope() + + private lateinit var underTest: AudioRepository + + @Before + fun setup() { + MockitoAnnotations.initMocks(this) + + `when`(audioManager.mode).thenReturn(AudioManager.MODE_RINGTONE) + `when`(audioManager.communicationDevice).thenReturn(communicationDevice) + `when`(audioManager.getStreamMinVolume(anyInt())).thenReturn(MIN_VOLUME) + `when`(audioManager.getStreamMaxVolume(anyInt())).thenReturn(MAX_VOLUME) + `when`(audioManager.ringerModeInternal).thenReturn(AudioManager.RINGER_MODE_NORMAL) + `when`(audioManager.setStreamVolume(anyInt(), anyInt(), anyInt())).then { + volumeByStream[it.arguments[0] as Int] = it.arguments[1] as Int + triggerIntent(AudioManager.ACTION_VOLUME_CHANGED) + } + `when`(audioManager.adjustStreamVolume(anyInt(), anyInt(), anyInt())).then { + isMuteByStream[it.arguments[0] as Int] = it.arguments[2] == AudioManager.ADJUST_MUTE + triggerIntent(AudioManager.STREAM_MUTE_CHANGED_ACTION) + } + `when`(audioManager.getStreamVolume(anyInt())).thenAnswer { + volumeByStream.getOrDefault(it.arguments[0] as Int, 0) + } + `when`(audioManager.isStreamAffectedByRingerMode(anyInt())).thenAnswer { + isAffectedByRingerModeByStream.getOrDefault(it.arguments[0] as Int, false) + } + `when`(audioManager.isStreamMute(anyInt())).thenAnswer { + isMuteByStream.getOrDefault(it.arguments[0] as Int, false) + } + + underTest = + AudioRepositoryImpl( + context, + audioManager, + testScope.testScheduler, + testScope.backgroundScope, + ) + } + + @Test + fun audioModeChanges_repositoryEmits() { + testScope.runTest { + val modes = mutableListOf<Int>() + underTest.mode.onEach { modes.add(it) }.launchIn(backgroundScope) + runCurrent() + + triggerModeChange(AudioManager.MODE_IN_CALL) + runCurrent() + + assertThat(modes).containsExactly(AudioManager.MODE_RINGTONE, AudioManager.MODE_IN_CALL) + } + } + + @Test + fun ringerModeChanges_repositoryEmits() { + testScope.runTest { + val modes = mutableListOf<RingerMode>() + underTest.ringerMode.onEach { modes.add(it) }.launchIn(backgroundScope) + runCurrent() + + `when`(audioManager.ringerModeInternal).thenReturn(AudioManager.RINGER_MODE_SILENT) + triggerIntent(AudioManager.INTERNAL_RINGER_MODE_CHANGED_ACTION) + runCurrent() + + assertThat(modes) + .containsExactly( + RingerMode(AudioManager.RINGER_MODE_NORMAL), + RingerMode(AudioManager.RINGER_MODE_SILENT), + ) + } + } + + @Test + fun communicationDeviceChanges_repositoryEmits() { + testScope.runTest { + var device: AudioDeviceInfo? = null + underTest.communicationDevice.onEach { device = it }.launchIn(backgroundScope) + runCurrent() + + triggerConnectedDeviceChange(communicationDevice) + runCurrent() + + assertThat(device).isSameInstanceAs(communicationDevice) + } + } + + @Test + fun adjustingVolume_changesTheStream() { + testScope.runTest { + val audioStream = AudioStream(AudioManager.STREAM_SYSTEM) + var streamModel: AudioStreamModel? = null + underTest + .getAudioStream(audioStream) + .onEach { streamModel = it } + .launchIn(backgroundScope) + runCurrent() + + underTest.setVolume(audioStream, 50) + runCurrent() + + assertThat(streamModel) + .isEqualTo( + AudioStreamModel( + audioStream = audioStream, + volume = 50, + minVolume = MIN_VOLUME, + maxVolume = MAX_VOLUME, + isAffectedByRingerMode = false, + isMuted = false, + ) + ) + } + } + + @Test + fun adjustingVolume_currentModeIsUpToDate() { + testScope.runTest { + val audioStream = AudioStream(AudioManager.STREAM_SYSTEM) + var streamModel: AudioStreamModel? = null + underTest + .getAudioStream(audioStream) + .onEach { streamModel = it } + .launchIn(backgroundScope) + runCurrent() + + underTest.setVolume(audioStream, 50) + runCurrent() + + assertThat(underTest.getCurrentAudioStream(audioStream)).isEqualTo(streamModel) + } + } + + @Test + fun muteStream_mutesTheStream() { + testScope.runTest { + val audioStream = AudioStream(AudioManager.STREAM_SYSTEM) + var streamModel: AudioStreamModel? = null + underTest + .getAudioStream(audioStream) + .onEach { streamModel = it } + .launchIn(backgroundScope) + runCurrent() + + underTest.setMuted(audioStream, true) + runCurrent() + + assertThat(streamModel) + .isEqualTo( + AudioStreamModel( + audioStream = audioStream, + volume = 0, + minVolume = MIN_VOLUME, + maxVolume = MAX_VOLUME, + isAffectedByRingerMode = false, + isMuted = true, + ) + ) + } + } + + @Test + fun unmuteStream_unmutesTheStream() { + testScope.runTest { + val audioStream = AudioStream(AudioManager.STREAM_SYSTEM) + isMuteByStream[audioStream.value] = true + var streamModel: AudioStreamModel? = null + underTest + .getAudioStream(audioStream) + .onEach { streamModel = it } + .launchIn(backgroundScope) + runCurrent() + + underTest.setMuted(audioStream, false) + runCurrent() + + assertThat(streamModel) + .isEqualTo( + AudioStreamModel( + audioStream = audioStream, + volume = 0, + minVolume = MIN_VOLUME, + maxVolume = MAX_VOLUME, + isAffectedByRingerMode = false, + isMuted = false, + ) + ) + } + } + + private fun triggerConnectedDeviceChange(communicationDevice: AudioDeviceInfo?) { + verify(audioManager) + .addOnCommunicationDeviceChangedListener( + any(), + communicationDeviceListenerCaptor.capture(), + ) + communicationDeviceListenerCaptor.value.onCommunicationDeviceChanged(communicationDevice) + } + + private fun triggerModeChange(mode: Int) { + verify(audioManager).addOnModeChangedListener(any(), modeListenerCaptor.capture()) + modeListenerCaptor.value.onModeChanged(mode) + } + + private fun triggerIntent(action: String) { + verify(context).registerReceiver(receiverCaptor.capture(), any()) + receiverCaptor.value.onReceive(context, Intent(action)) + } + + private companion object { + const val MIN_VOLUME = 0 + const val MAX_VOLUME = 100 + } +} diff --git a/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/FakeAudioRepository.kt b/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/FakeAudioRepository.kt index 686362fadf02..dddf8e82d5f7 100644 --- a/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/FakeAudioRepository.kt +++ b/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/FakeAudioRepository.kt @@ -16,9 +16,15 @@ package com.android.settingslib.volume.data.repository +import android.media.AudioDeviceInfo +import com.android.settingslib.volume.shared.model.AudioStream +import com.android.settingslib.volume.shared.model.AudioStreamModel +import com.android.settingslib.volume.shared.model.RingerMode +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update class FakeAudioRepository : AudioRepository { @@ -26,7 +32,59 @@ class FakeAudioRepository : AudioRepository { override val mode: StateFlow<Int> get() = mutableMode.asStateFlow() + private val mutableRingerMode = MutableStateFlow(RingerMode(0)) + override val ringerMode: StateFlow<RingerMode> + get() = mutableRingerMode.asStateFlow() + + private val mutableCommunicationDevice = MutableStateFlow<AudioDeviceInfo?>(null) + override val communicationDevice: StateFlow<AudioDeviceInfo?> + get() = mutableCommunicationDevice.asStateFlow() + + private val models: MutableMap<AudioStream, MutableStateFlow<AudioStreamModel>> = mutableMapOf() + + private fun getAudioStreamModelState( + audioStream: AudioStream + ): MutableStateFlow<AudioStreamModel> = + models.getOrPut(audioStream) { + MutableStateFlow( + AudioStreamModel( + audioStream = audioStream, + volume = 0, + minVolume = 0, + maxVolume = 0, + isAffectedByRingerMode = false, + isMuted = false, + ) + ) + } + + override suspend fun getAudioStream(audioStream: AudioStream): Flow<AudioStreamModel> = + getAudioStreamModelState(audioStream).asStateFlow() + + override suspend fun getCurrentAudioStream(audioStream: AudioStream): AudioStreamModel = + getAudioStreamModelState(audioStream).value + + override suspend fun setVolume(audioStream: AudioStream, volume: Int) { + getAudioStreamModelState(audioStream).update { it.copy(volume = volume) } + } + + override suspend fun setMuted(audioStream: AudioStream, isMuted: Boolean) { + getAudioStreamModelState(audioStream).update { it.copy(isMuted = isMuted) } + } + fun setMode(newMode: Int) { mutableMode.value = newMode } + + fun setRingerMode(newRingerMode: RingerMode) { + mutableRingerMode.value = newRingerMode + } + + fun setCommunicationDevice(device: AudioDeviceInfo?) { + mutableCommunicationDevice.value = device + } + + fun setAudioStreamModel(model: AudioStreamModel) { + getAudioStreamModelState(model.audioStream).update { model } + } } 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 000000000000..d106bceb6b85 --- /dev/null +++ b/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/LocalMediaRepositoryImplTest.kt @@ -0,0 +1,100 @@ +/* + * 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 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.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) +@RunWith(AndroidJUnit4::class) +@SmallTest +class LocalMediaRepositoryImplTest { + + @Mock private lateinit var localMediaManager: LocalMediaManager + @Mock private lateinit var mediaDevice1: MediaDevice + @Mock private lateinit var mediaDevice2: MediaDevice + + @Captor + private lateinit var deviceCallbackCaptor: ArgumentCaptor<LocalMediaManager.DeviceCallback> + + private val testScope = TestScope() + + private lateinit var underTest: LocalMediaRepository + + @Before + fun setup() { + MockitoAnnotations.initMocks(this) + + underTest = + LocalMediaRepositoryImpl( + localMediaManager, + 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) + } + } +} 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 000000000000..f07b1bff0f31 --- /dev/null +++ b/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/MediaControllerRepositoryImplTest.kt @@ -0,0 +1,178 @@ +/* + * 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.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +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.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 receiverCaptor: ArgumentCaptor<BroadcastReceiver> + @Captor private lateinit var callbackCaptor: ArgumentCaptor<BluetoothCallback> + + @Mock private lateinit var context: Context + @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 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( + context, + 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() + + triggerDevicesChange() + 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() + + triggerDevicesChange() + triggerOnAudioModeChanged() + runCurrent() + + assertThat(mediaController).isNull() + } + } + + private fun triggerDevicesChange() { + verify(context).registerReceiver(receiverCaptor.capture(), any()) + receiverCaptor.value.onReceive(context, Intent(AudioManager.STREAM_DEVICES_CHANGED_ACTION)) + } + + private fun triggerOnAudioModeChanged() { + verify(eventManager).registerCallback(callbackCaptor.capture()) + callbackCaptor.value.onAudioModeChanged() + } + + private companion object { + val statePlaying = + PlaybackState.Builder().setState(PlaybackState.STATE_PLAYING, 0, 0f).build() + val stateError = PlaybackState.Builder().setState(PlaybackState.STATE_ERROR, 0, 0f).build() + val stateStopped = + PlaybackState.Builder().setState(PlaybackState.STATE_STOPPED, 0, 0f).build() + val stateNone = PlaybackState.Builder().setState(PlaybackState.STATE_NONE, 0, 0f).build() + } +} diff --git a/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/domain/interactor/AudioModeInteractorTest.kt b/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/domain/interactor/AudioModeInteractorTest.kt index 3bc1edc9c944..4dbf865475a4 100644 --- a/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/domain/interactor/AudioModeInteractorTest.kt +++ b/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/domain/interactor/AudioModeInteractorTest.kt @@ -17,8 +17,9 @@ package com.android.settingslib.volume.domain.interactor import android.media.AudioManager +import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest -import androidx.test.runner.AndroidJUnit4 +import com.android.settingslib.BaseTest import com.android.settingslib.volume.data.repository.FakeAudioRepository import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -33,7 +34,7 @@ import org.junit.runner.RunWith @OptIn(ExperimentalCoroutinesApi::class) @RunWith(AndroidJUnit4::class) @SmallTest -class AudioModeInteractorTest { +class AudioModeInteractorTest : BaseTest() { private val testScope = TestScope() private val fakeAudioRepository = FakeAudioRepository() 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 f7ec80b041e9..475a6d65a845 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 @@ package com.android.settingslib.bluetooth; 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 static org.mockito.Mockito.when; 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 @@ public class BluetoothUtilsTest { 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 @@ public class BluetoothUtilsTest { 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 @@ public class BluetoothUtilsTest { 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 9db8b47e9644..461ecf5d3c84 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 class CachedBluetoothDeviceTest { 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 @@ public class CachedBluetoothDeviceTest { 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/robotests/src/com/android/settingslib/media/PhoneMediaDeviceTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/PhoneMediaDeviceTest.java index ceba9be70487..e2d58d660fd5 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/PhoneMediaDeviceTest.java +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/PhoneMediaDeviceTest.java @@ -24,6 +24,7 @@ import static android.media.MediaRoute2Info.TYPE_WIRED_HEADSET; import static com.android.settingslib.media.PhoneMediaDevice.PHONE_ID; import static com.android.settingslib.media.PhoneMediaDevice.USB_HEADSET_ID; import static com.android.settingslib.media.PhoneMediaDevice.WIRED_HEADSET_ID; +import static com.android.settingslib.media.PhoneMediaDevice.getMediaTransferThisDeviceName; import static com.google.common.truth.Truth.assertThat; @@ -114,7 +115,7 @@ public class PhoneMediaDeviceTest { when(mInfo.getType()).thenReturn(TYPE_BUILTIN_SPEAKER); assertThat(mPhoneMediaDevice.getName()) - .isEqualTo(mContext.getString(R.string.media_transfer_this_device_name)); + .isEqualTo(getMediaTransferThisDeviceName(mContext)); } @EnableFlags(Flags.FLAG_ENABLE_AUDIO_POLICIES_DEVICE_AND_BLUETOOTH_CONTROLLER) diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsState.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsState.java index 6f3c88fc8706..ae71cece803f 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 @@ final class SettingsState { 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.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 edbc0b391b27..ecac5ee18582 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 @@ flag { 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/Shell/AndroidManifest.xml b/packages/Shell/AndroidManifest.xml index 95e0e1b0fa09..e99fcc92dea7 100644 --- a/packages/Shell/AndroidManifest.xml +++ b/packages/Shell/AndroidManifest.xml @@ -908,6 +908,12 @@ <!-- Permissions required for CTS test - CtsPermissionUiTestCases --> <uses-permission android:name="android.permission.MANAGE_ENHANCED_CONFIRMATION_STATES" /> + <!-- 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" /> + <application android:label="@string/app_label" android:theme="@android:style/Theme.DeviceDefault.DayNight" diff --git a/packages/SystemUI/Android.bp b/packages/SystemUI/Android.bp index 900a2f830783..d1a3571a87c1 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/accessibility/accessibilitymenu/Android.bp b/packages/SystemUI/accessibility/accessibilitymenu/Android.bp index 0df9bac295fb..d674b6c96320 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 3fc351c32ec1..64dcf6e8f573 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 0ea2b1fed968..2ad7192a3248 100644 --- a/packages/SystemUI/aconfig/systemui.aconfig +++ b/packages/SystemUI/aconfig/systemui.aconfig @@ -67,6 +67,16 @@ flag { } 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 @@ flag { " 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 @@ flag { "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 { } 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,8 +371,37 @@ flag { } 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" } + +flag { + name: "enable_contextual_tip_for_power_off" + namespace: "systemui" + description: "Enables on-screen contextual tip about how to power off or restart phone" + bug: "322891421" +} + +flag { + name: "enable_contextual_tip_for_take_screenshot" + namespace: "systemui" + 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 872187abb9db..c1125f0b9e92 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/GhostedViewLaunchAnimatorController.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/GhostedViewLaunchAnimatorController.kt index efdbfdb83c70..055252bbef14 100644 --- a/packages/SystemUI/animation/src/com/android/systemui/animation/GhostedViewLaunchAnimatorController.kt +++ b/packages/SystemUI/animation/src/com/android/systemui/animation/GhostedViewLaunchAnimatorController.kt @@ -353,7 +353,7 @@ constructor( /** * 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 @@ constructor( 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/checks/Android.bp b/packages/SystemUI/checks/Android.bp index d3f66e333e96..4cbc18c3a295 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 482776a37327..6fc13d7e0ddc 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 9a4347d2afe4..4f7a43e7cb1c 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/tests/Android.bp b/packages/SystemUI/compose/core/tests/Android.bp index 8e9c5864ce70..6e7a1425ef90 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 3c325940c325..9a34d6f25551 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.bouncer.ui.viewmodel.BouncerViewModel 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 @@ object ComposeFacade : BaseComposeFacade { throwComposeUnavailableError() } - override fun createStickyKeysDialog( - dialogFactory: SystemUIDialogFactory, + override fun createStickyKeysIndicatorContent( + context: Context, viewModel: StickyKeysIndicatorViewModel - ): Dialog { + ): View { throwComposeUnavailableError() } @@ -114,6 +114,12 @@ object ComposeFacade : BaseComposeFacade { 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 725aef26cb86..fc3912e2aa52 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 afb860e62261..51d2a03342b7 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.shared.model.SceneKey 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 @@ object ComposeFacade : BaseComposeFacade { 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 @@ object ComposeFacade : BaseComposeFacade { } } - 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 @@ object ComposeFacade : BaseComposeFacade { 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 cbf249653577..f5dc15485ee8 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 android.view.View 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 @@ interface LockscreenSceneModule { ): () -> 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 5ab22357713d..c12084dac9a9 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/windowinsets/DisplayCutoutProvider.kt b/packages/SystemUI/compose/features/src/com/android/systemui/common/ui/compose/windowinsets/ScreenDecorProvider.kt index ed393c0a9cb5..76bd4ec2778a 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.CompositionLocalProvider 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/communal/ui/compose/CommunalHub.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt index 576596fa67fe..4e72dfef82e1 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt @@ -160,14 +160,23 @@ fun CommunalHub( gridCoordinates ) { detectLongPressGesture { offset -> - isButtonToEditWidgetsShowing = true - // Deduct both grid offset relative to its container and content offset. val adjustedOffset = gridCoordinates?.let { offset - it.positionInWindow() - contentOffset } val index = adjustedOffset?.let { firstIndexAtOffset(gridState, it) } + // Display the button only when the gesture initiates from widgets, + // the CTA tile, or an empty area on the screen. UMO/smartspace have + // their own long-press handlers. To prevent user confusion, we should + // not display this button. + if ( + index == null || + communalContent[index].isWidget() || + communalContent[index] is CommunalContentModel.CtaTileInViewMode + ) { + isButtonToEditWidgetsShowing = true + } val key = index?.let { keyAtIndexIfEditable(communalContent, index) } viewModel.setSelectedKey(key) } 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 68e57b5d51b8..dd8664696973 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 @@ fun StickyKeysIndicator(stickyKeys: Map<ModifierKey, Locked>, modifier: Modifier 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 2cb00342cba5..b5499b7bce35 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 androidx.compose.ui.Modifier 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 @@ class LockscreenContent @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/blueprint/CommunalBlueprint.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/CommunalBlueprint.kt index 86124c635684..6b210af1308d 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 @@ class CommunalBlueprint @Inject constructor( private val viewModel: LockscreenContentViewModel, -) : LockscreenSceneBlueprint { +) : ComposableLockscreenSceneBlueprint { override val id: String = "communal" @@ -59,5 +59,5 @@ constructor( @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 index 6d9cba4acb39..cb739830a24b 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 @@ package com.android.systemui.keyguard.ui.composable.blueprint 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 bf02d8abf73c..a07ab4a11731 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.Column 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.SettingsMenuSection 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 @@ constructor( private val bottomAreaSection: BottomAreaSection, private val settingsMenuSection: SettingsMenuSection, private val clockInteractor: KeyguardClockInteractor, -) : LockscreenSceneBlueprint { +) : ComposableLockscreenSceneBlueprint { override val id: String = "default" @@ -84,6 +87,7 @@ constructor( with(statusBarSection) { StatusBar(modifier = Modifier.fillMaxWidth()) } with(clockSection) { SmallClock( + burnInParams = burnIn.parameters, onTopChanged = burnIn.onSmallClockTopChanged, modifier = Modifier.fillMaxWidth(), ) @@ -95,7 +99,13 @@ constructor( 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 @@ constructor( @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 d0aa4443d487..b035e4220a5b 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.Column 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.SettingsMenuSection 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 @@ constructor( 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 @@ constructor( SmallClock( onTopChanged = burnIn.onSmallClockTopChanged, modifier = Modifier.fillMaxWidth(), + burnInParams = burnIn.parameters, ) } with(smartSpaceSection) { @@ -96,6 +100,12 @@ constructor( Modifier.fillMaxWidth() .padding( top = { viewModel.getSmartSpacePaddingTop(resources) } + ) + .padding( + bottom = + dimensionResource( + R.dimen.keyguard_status_view_bottom_margin + ) ), ) } @@ -222,5 +232,5 @@ constructor( 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 616a7b4752a0..660fc5a98c03 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 @@ constructor( private val settingsMenuSection: SettingsMenuSection, private val clockInteractor: KeyguardClockInteractor, private val largeScreenHeaderHelper: LargeScreenHeaderHelper, -) : LockscreenSceneBlueprint { +) : ComposableLockscreenSceneBlueprint { override val id: String = "split-shade" @@ -109,7 +109,14 @@ constructor( .padding( top = { viewModel.getSmartSpacePaddingTop(resources) - } + }, + ) + .padding( + bottom = + dimensionResource( + R.dimen + .keyguard_status_view_bottom_margin + ) ), ) } @@ -237,5 +244,7 @@ constructor( @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 8f218792ee32..fa07bafda82c 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.compose.modifiers.padding 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 @@ class ClockSection 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 @@ constructor( 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 5e27d8299c16..6a8da10944d8 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 @@ -19,6 +19,11 @@ package com.android.systemui.keyguard.ui.composable.section import android.content.Context import android.view.ViewGroup import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import com.android.compose.animation.scene.SceneScope import com.android.systemui.dagger.SysUISingleton @@ -40,56 +45,82 @@ import com.android.systemui.statusbar.notification.stack.ui.viewmodel.Notificati import com.android.systemui.statusbar.notification.stack.ui.viewmodel.SharedNotificationContainerViewModel import javax.inject.Inject import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.DisposableHandle @SysUISingleton class NotificationSection @Inject constructor( - @Application context: Context, + @Application private val context: Context, private val viewModel: NotificationsPlaceholderViewModel, - controller: NotificationStackScrollLayoutController, - sceneContainerFlags: SceneContainerFlags, - sharedNotificationContainer: SharedNotificationContainer, - sharedNotificationContainerViewModel: SharedNotificationContainerViewModel, - stackScrollLayout: NotificationStackScrollLayout, - notificationStackAppearanceViewModel: NotificationStackAppearanceViewModel, - ambientState: AmbientState, - notificationStackSizeCalculator: NotificationStackSizeCalculator, - @Main mainDispatcher: CoroutineDispatcher, + private val controller: NotificationStackScrollLayoutController, + private val sceneContainerFlags: SceneContainerFlags, + private val sharedNotificationContainer: SharedNotificationContainer, + private val sharedNotificationContainerViewModel: SharedNotificationContainerViewModel, + private val stackScrollLayout: NotificationStackScrollLayout, + private val notificationStackAppearanceViewModel: NotificationStackAppearanceViewModel, + private val ambientState: AmbientState, + private val notificationStackSizeCalculator: NotificationStackSizeCalculator, + @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) + @Composable + fun SceneScope.Notifications(modifier: Modifier = Modifier) { + 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. + return + } - SharedNotificationContainerBinder.bind( - sharedNotificationContainer, - sharedNotificationContainerViewModel, - sceneContainerFlags, - controller, - notificationStackSizeCalculator, - mainDispatcher, - ) + var isBound by remember { mutableStateOf(false) } - if (sceneContainerFlags.flexiNotifsEnabled()) { - NotificationStackAppearanceViewBinder.bind( - context, + DisposableEffect(Unit) { + val disposableHandles: MutableList<DisposableHandle> = mutableListOf() + + // Ensure stackScrollLayout is a child of sharedNotificationContainer. + if (stackScrollLayout.parent != sharedNotificationContainer) { + (stackScrollLayout.parent as? ViewGroup)?.removeView(stackScrollLayout) + sharedNotificationContainer.addNotificationStackScrollLayout(stackScrollLayout) + } + + disposableHandles.add( + SharedNotificationContainerBinder.bind( sharedNotificationContainer, - notificationStackAppearanceViewModel, - ambientState, + sharedNotificationContainerViewModel, + sceneContainerFlags, controller, + notificationStackSizeCalculator, + mainDispatcher, + ) + ) + + if (sceneContainerFlags.flexiNotifsEnabled()) { + disposableHandles.add( + NotificationStackAppearanceViewBinder.bind( + context, + sharedNotificationContainer, + notificationStackAppearanceViewModel, + ambientState, + controller, + ) ) } + + isBound = true + + onDispose { + disposableHandles.forEach { it.dispose() } + disposableHandles.clear() + isBound = false + } + } + + if (!isBound) { + return } - } - @Composable - fun SceneScope.Notifications(modifier: Modifier = Modifier) { NotificationStack( viewModel = viewModel, modifier = modifier, 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 0e08a198c71e..3fb825471c57 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 @@ -51,11 +51,13 @@ import androidx.compose.ui.graphics.graphicsLayer 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 @@ -63,10 +65,15 @@ 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.compose.ui.util.lerp +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 +84,13 @@ object Notifications { 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 +139,19 @@ fun SceneScope.NotificationScrollingStack( modifier: Modifier = Modifier, ) { val density = LocalDensity.current - val cornerRadius by viewModel.cornerRadiusDp.collectAsState() + val screenCornerRadius = LocalScreenCornerRadius.current 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() @@ -171,26 +185,53 @@ fun SceneScope.NotificationScrollingStack( .collect { shouldCollapse -> if (shouldCollapse) scrimOffset.value = 0f } } - Box(modifier = modifier.element(Notifications.Elements.NotificationScrim)) { + 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) @@ -278,10 +319,10 @@ private fun SceneScope.NotificationPlaceholder( .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 +351,23 @@ private fun SceneScope.NotificationPlaceholder( } } +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/QuickSettingsScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt index 5531f9cc5589..969dec31971c 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.height 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.getValue 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.battery.BatteryMeterViewController 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 @@ private fun SceneScope.QuickSettingsScene( 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 @@ private fun SceneScope.QuickSettingsScene( // 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, @@ -242,32 +233,5 @@ private fun SceneScope.QuickSettingsScene( } } } - // 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 770d654a4c88..736ee1f235c6 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 @@ constructor( 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/transitions/FromGoneToShadeTransition.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromGoneToShadeTransition.kt index 0c2c5195becc..1223ace6957b 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 @@ package com.android.systemui.scene.ui.composable.transitions 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 @@ fun TransitionBuilder.goneToShadeTransition() { 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 ebc343dc6d76..2d5cf5c752bf 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 @@ import com.android.systemui.shade.ui.composable.Shade 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 497fe873e87d..677df7ead2da 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.platform.LocalDensity 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 Shade { 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 @@ private fun SceneScope.ShadeScene( Box( modifier = - modifier.element(Shade.Elements.Scrim).background(MaterialTheme.colorScheme.scrim), + modifier + .element(Shade.Elements.BackgroundScrim) + .background(MaterialTheme.colorScheme.scrim), ) Box { Layout( @@ -236,17 +239,7 @@ private fun SceneScope.ShadeScene( 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 dcd22febbc86..a7ec93f5e81d 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 @@ package com.android.systemui.volume.panel.ui.composable 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 @@ fun VolumePanelComposeScope.VerticalVolumePanelContent( 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 c85cd7bae41b..69b18c45aed6 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/Element.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt index a910bca078e8..e40f6b66eff4 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.Modifier 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 @@ -473,7 +474,8 @@ private fun IntermediateMeasureScope.place( 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/SceneTransitionLayout.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt index 5258078e0e9a..d904c8b770bf 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 @@ -504,6 +504,7 @@ internal fun SceneTransitionLayoutForTesting( layoutImpl.density = density layoutImpl.swipeSourceDetector = swipeSourceDetector + layoutImpl.transitionInterceptionThreshold = transitionInterceptionThreshold } layoutImpl.Content(modifier) 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 e8cc0eca0d8b..2dc94a405ae3 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 @@ -38,6 +38,8 @@ import com.android.compose.test.MonotonicClockTestScope import com.android.compose.test.runMonotonicClockTest import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertWithMessage +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch import org.junit.Test import org.junit.runner.RunWith @@ -47,7 +49,7 @@ private val LAYOUT_SIZE = IntSize(SCREEN_SIZE.toInt(), SCREEN_SIZE.toInt()) @RunWith(AndroidJUnit4::class) class SceneGestureHandlerTest { private class TestGestureScope( - val coroutineScope: MonotonicClockTestScope, + private val testScope: MonotonicClockTestScope, ) { private val layoutState = MutableSceneTransitionLayoutStateImpl(SceneA, EmptyTestTransitions) @@ -92,17 +94,16 @@ class SceneGestureHandlerTest { swipeSourceDetector = DefaultEdgeDetector, transitionInterceptionThreshold = transitionInterceptionThreshold, builder = scenesBuilder, - coroutineScope = coroutineScope, + coroutineScope = testScope, ) .apply { setScenesTargetSizeForTest(LAYOUT_SIZE) } val sceneGestureHandler = layoutImpl.gestureHandler(Orientation.Vertical) val horizontalSceneGestureHandler = layoutImpl.gestureHandler(Orientation.Horizontal) - val draggable = sceneGestureHandler.draggable fun nestedScrollConnection(nestedScrollBehavior: NestedScrollBehavior) = SceneNestedScrollHandler( - layoutImpl, + layoutImpl = layoutImpl, orientation = sceneGestureHandler.orientation, topOrLeftBehavior = nestedScrollBehavior, bottomOrRightBehavior = nestedScrollBehavior, @@ -117,8 +118,12 @@ class SceneGestureHandlerTest { fun up(fractionOfScreen: Float) = if (fractionOfScreen < 0f) error("use down()") else -down(fractionOfScreen) - // Offset y: 10% of the screen - val offsetY10 = Offset(x = 0f, y = down(0.1f)) + fun downOffset(fractionOfScreen: Float) = + if (fractionOfScreen < 0f) { + error("upOffset() is required, not implemented yet") + } else { + Offset(x = 0f, y = down(fractionOfScreen)) + } val transitionState: TransitionState get() = layoutState.transitionState @@ -127,15 +132,15 @@ class SceneGestureHandlerTest { get() = (transitionState as Transition).progress fun advanceUntilIdle() { - coroutineScope.testScheduler.advanceUntilIdle() + testScope.testScheduler.advanceUntilIdle() } fun runCurrent() { - coroutineScope.testScheduler.runCurrent() + testScope.testScheduler.runCurrent() } fun assertIdle(currentScene: SceneKey) { - assertWithMessage("transitionState must be Idle").that(transitionState is Idle).isTrue() + assertThat(transitionState).isInstanceOf(Idle::class.java) assertWithMessage("currentScene does not match") .that(transitionState.currentScene) .isEqualTo(currentScene) @@ -148,11 +153,8 @@ class SceneGestureHandlerTest { progress: Float? = null, isUserInputOngoing: Boolean? = null ) { - val transition = transitionState - assertWithMessage("transitionState must be Transition") - .that(transition is Transition) - .isTrue() - transition as Transition + assertThat(transitionState).isInstanceOf(Transition::class.java) + val transition = transitionState as Transition if (currentScene != null) assertWithMessage("currentScene does not match") @@ -180,47 +182,115 @@ class SceneGestureHandlerTest { .that(transition.isUserInputOngoing) .isEqualTo(isUserInputOngoing) } + + fun onDragStarted( + startedPosition: Offset = Offset.Zero, + overSlop: Float, + pointersDown: Int = 1 + ) { + // overSlop should be 0f only if the drag gesture starts with startDragImmediately + if (overSlop == 0f) error("Consider using onDragStartedImmediately()") + onDragStarted(sceneGestureHandler.draggable, startedPosition, overSlop, pointersDown) + } + + fun onDragStartedImmediately(startedPosition: Offset = Offset.Zero, pointersDown: Int = 1) { + onDragStarted( + sceneGestureHandler.draggable, + startedPosition, + overSlop = 0f, + pointersDown + ) + } + + fun onDragStarted( + draggableHandler: DraggableHandler, + startedPosition: Offset = Offset.Zero, + overSlop: Float = 0f, + pointersDown: Int = 1 + ) { + draggableHandler.onDragStarted( + startedPosition = startedPosition, + overSlop = overSlop, + pointersDown = pointersDown, + ) + + // MultiPointerDraggable will always call onDelta with the initial overSlop right after + onDelta(pixels = overSlop) + } + + fun onDelta(pixels: Float) { + sceneGestureHandler.draggable.onDelta(pixels = pixels) + } + + fun onDragStopped(velocity: Float) { + sceneGestureHandler.draggable.onDragStopped(velocity = velocity) + runCurrent() + } + + fun NestedScrollConnection.scroll( + available: Offset, + consumedByScroll: Offset = Offset.Zero, + ) { + val consumedByPreScroll = + onPreScroll( + available = available, + source = NestedScrollSource.Drag, + ) + val consumed = consumedByPreScroll + consumedByScroll + + onPostScroll( + consumed = consumed, + available = available - consumed, + source = NestedScrollSource.Drag + ) + } + + fun NestedScrollConnection.preFling( + available: Velocity, + coroutineScope: CoroutineScope = testScope, + ) { + // onPreFling is a suspend function that returns the consumed velocity once it finishes + // consuming it. In the current scenario, it returns after completing the animation. + // To return immediately, we can initiate a job that allows us to check the status + // before the animation starts. + coroutineScope.launch { onPreFling(available = available) } + runCurrent() + } } private fun runGestureTest(block: suspend TestGestureScope.() -> Unit) { - runMonotonicClockTest { TestGestureScope(coroutineScope = this).block() } - } + runMonotonicClockTest { + val testGestureScope = TestGestureScope(testScope = this) - private fun DraggableHandler.onDragStarted( - overSlop: Float = 0f, - startedPosition: Offset = Offset.Zero, - ) { - onDragStarted(startedPosition, overSlop) - // MultiPointerDraggable will always call onDelta with the initial overSlop right after - onDelta(overSlop) + // run the test + testGestureScope.block() + } } @Test fun testPreconditions() = runGestureTest { assertIdle(currentScene = SceneA) } @Test fun onDragStarted_shouldStartATransition() = runGestureTest { - draggable.onDragStarted(overSlop = down(0.1f)) + onDragStarted(overSlop = down(fractionOfScreen = 0.1f)) assertTransition(currentScene = SceneA) } @Test fun afterSceneTransitionIsStarted_interceptDragEvents() = runGestureTest { - draggable.onDragStarted(overSlop = down(0.1f)) + onDragStarted(overSlop = down(fractionOfScreen = 0.1f)) assertTransition(currentScene = SceneA) assertThat(progress).isEqualTo(0.1f) - draggable.onDelta(pixels = down(0.1f)) + onDelta(pixels = down(fractionOfScreen = 0.1f)) assertThat(progress).isEqualTo(0.2f) } @Test fun onDragStoppedAfterDrag_velocityLowerThanThreshold_remainSameScene() = runGestureTest { - draggable.onDragStarted(overSlop = down(0.1f)) + onDragStarted(overSlop = down(fractionOfScreen = 0.1f)) assertTransition(currentScene = SceneA) - draggable.onDragStopped( - velocity = velocityThreshold - 0.01f, - ) + onDragStopped(velocity = velocityThreshold - 0.01f) assertTransition(currentScene = SceneA) // wait for the stop animation @@ -230,11 +300,10 @@ class SceneGestureHandlerTest { @Test fun onDragStoppedAfterDrag_velocityAtLeastThreshold_goToNextScene() = runGestureTest { - draggable.onDragStarted(overSlop = down(0.1f)) + onDragStarted(overSlop = down(fractionOfScreen = 0.1f)) assertTransition(currentScene = SceneA) - draggable.onDragStopped(velocity = velocityThreshold) - + onDragStopped(velocity = velocityThreshold) assertTransition(currentScene = SceneC) // wait for the stop animation @@ -244,10 +313,10 @@ class SceneGestureHandlerTest { @Test fun onDragStoppedAfterStarted_returnToIdle() = runGestureTest { - draggable.onDragStarted(overSlop = down(0.1f)) + onDragStarted(overSlop = down(fractionOfScreen = 0.1f)) assertTransition(currentScene = SceneA) - draggable.onDragStopped(velocity = 0f) + onDragStopped(velocity = 0f) advanceUntilIdle() assertIdle(currentScene = SceneA) } @@ -255,7 +324,7 @@ class SceneGestureHandlerTest { @Test fun onDragReversedDirection_changeToScene() = runGestureTest { // Drag A -> B with progress 0.6 - draggable.onDragStarted(overSlop = -60f) + onDragStarted(overSlop = -60f) assertTransition( currentScene = SceneA, fromScene = SceneA, @@ -264,7 +333,7 @@ class SceneGestureHandlerTest { ) // Reverse direction such that A -> C now with 0.4 - draggable.onDelta(100f) + onDelta(pixels = 100f) assertTransition( currentScene = SceneA, fromScene = SceneA, @@ -273,7 +342,7 @@ class SceneGestureHandlerTest { ) // After the drag stopped scene C should be committed - draggable.onDragStopped(velocity = velocityThreshold) + onDragStopped(velocity = velocityThreshold) assertTransition(currentScene = SceneC, fromScene = SceneA, toScene = SceneC) // wait for the stop animation @@ -283,9 +352,12 @@ class SceneGestureHandlerTest { @Test fun onDragStartedWithoutActionsInBothDirections_stayIdle() = runGestureTest { - horizontalSceneGestureHandler.draggable.onDragStarted(up(0.3f)) + val horizontalDraggableHandler = horizontalSceneGestureHandler.draggable + + onDragStarted(horizontalDraggableHandler, overSlop = up(fractionOfScreen = 0.3f)) assertIdle(currentScene = SceneA) - horizontalSceneGestureHandler.draggable.onDragStarted(down(0.3f)) + + onDragStarted(horizontalDraggableHandler, overSlop = down(fractionOfScreen = 0.3f)) assertIdle(currentScene = SceneA) } @@ -294,7 +366,7 @@ class SceneGestureHandlerTest { navigateToSceneC() // We are on SceneC which has no action in Down direction - draggable.onDragStarted(10f) + onDragStarted(overSlop = 10f) assertTransition( currentScene = SceneC, fromScene = SceneC, @@ -303,7 +375,7 @@ class SceneGestureHandlerTest { ) // Reverse drag direction, it will consume the previous drag - draggable.onDelta(-10f) + onDelta(pixels = -10f) assertTransition( currentScene = SceneC, fromScene = SceneC, @@ -312,7 +384,7 @@ class SceneGestureHandlerTest { ) // Continue reverse drag direction, it should record progress to Scene B - draggable.onDelta(-10f) + onDelta(pixels = -10f) assertTransition( currentScene = SceneC, fromScene = SceneC, @@ -326,7 +398,10 @@ class SceneGestureHandlerTest { navigateToSceneC() // Start dragging from the bottom - draggable.onDragStarted(up(0.1f), Offset(SCREEN_SIZE * 0.5f, SCREEN_SIZE)) + onDragStarted( + startedPosition = Offset(SCREEN_SIZE * 0.5f, SCREEN_SIZE), + overSlop = up(fractionOfScreen = 0.1f) + ) assertTransition( currentScene = SceneC, fromScene = SceneC, @@ -337,14 +412,14 @@ class SceneGestureHandlerTest { @Test fun onDragToExactlyZero_toSceneIsSet() = runGestureTest { - draggable.onDragStarted(down(0.3f)) + onDragStarted(overSlop = down(fractionOfScreen = 0.3f)) assertTransition( currentScene = SceneA, fromScene = SceneA, toScene = SceneC, progress = 0.3f ) - draggable.onDelta(up(0.3f)) + onDelta(pixels = up(fractionOfScreen = 0.3f)) assertTransition( currentScene = SceneA, fromScene = SceneA, @@ -355,8 +430,8 @@ class SceneGestureHandlerTest { private fun TestGestureScope.navigateToSceneC() { assertIdle(currentScene = SceneA) - draggable.onDragStarted(down(1f)) - draggable.onDragStopped(0f) + onDragStarted(overSlop = down(fractionOfScreen = 1f)) + onDragStopped(velocity = 0f) advanceUntilIdle() assertIdle(currentScene = SceneC) } @@ -364,7 +439,7 @@ class SceneGestureHandlerTest { @Test fun onAccelaratedScroll_scrollToThirdScene() = runGestureTest { // Drag A -> B with progress 0.2 - draggable.onDragStarted(overSlop = up(0.2f)) + onDragStarted(overSlop = up(fractionOfScreen = 0.2f)) assertTransition( currentScene = SceneA, fromScene = SceneA, @@ -373,13 +448,13 @@ class SceneGestureHandlerTest { ) // Start animation A -> B with progress 0.2 -> 1.0 - draggable.onDragStopped(velocity = -velocityThreshold) + onDragStopped(velocity = -velocityThreshold) assertTransition(currentScene = SceneB, fromScene = SceneA, toScene = SceneB) // While at A -> B do a 100% screen drag (progress 1.2). This should go past B and change // the transition to B -> C with progress 0.2 - draggable.onDragStarted() - draggable.onDelta(up(1f)) + onDragStartedImmediately() + onDelta(pixels = up(fractionOfScreen = 1f)) assertTransition( currentScene = SceneB, fromScene = SceneB, @@ -388,7 +463,7 @@ class SceneGestureHandlerTest { ) // After the drag stopped scene C should be committed - draggable.onDragStopped(velocity = -velocityThreshold) + onDragStopped(velocity = -velocityThreshold) assertTransition(currentScene = SceneC, fromScene = SceneB, toScene = SceneC) // wait for the stop animation @@ -398,9 +473,9 @@ class SceneGestureHandlerTest { @Test fun onAccelaratedScrollBothTargetsBecomeNull_settlesToIdle() = runGestureTest { - draggable.onDragStarted(overSlop = up(0.2f)) - draggable.onDelta(up(0.2f)) - draggable.onDragStopped(velocity = -velocityThreshold) + onDragStarted(overSlop = up(fractionOfScreen = 0.2f)) + onDelta(pixels = up(fractionOfScreen = 0.2f)) + onDragStopped(velocity = -velocityThreshold) assertTransition(currentScene = SceneB, fromScene = SceneA, toScene = SceneB) mutableUserActionsA.remove(Swipe.Up) @@ -409,83 +484,79 @@ class SceneGestureHandlerTest { mutableUserActionsB.remove(Swipe.Down) // start accelaratedScroll and scroll over to B -> null - draggable.onDragStarted() - draggable.onDelta(up(0.5f)) - draggable.onDelta(up(0.5f)) + onDragStartedImmediately() + onDelta(pixels = up(fractionOfScreen = 0.5f)) + onDelta(pixels = up(fractionOfScreen = 0.5f)) // here onDragStopped is already triggered, but subsequent onDelta/onDragStopped calls may // still be called. Make sure that they don't crash or change the scene - draggable.onDelta(up(0.5f)) - draggable.onDragStopped(0f) + onDelta(pixels = up(fractionOfScreen = 0.5f)) + onDragStopped(velocity = 0f) advanceUntilIdle() assertIdle(SceneB) // These events can still come in after the animation has settled - draggable.onDelta(up(0.5f)) - draggable.onDragStopped(0f) + onDelta(pixels = up(fractionOfScreen = 0.5f)) + onDragStopped(velocity = 0f) assertIdle(SceneB) } @Test fun onDragTargetsChanged_targetStaysTheSame() = runGestureTest { - draggable.onDragStarted(up(0.1f)) + onDragStarted(overSlop = up(fractionOfScreen = 0.1f)) assertTransition(fromScene = SceneA, toScene = SceneB, progress = 0.1f) mutableUserActionsA[Swipe.Up] = SceneC - draggable.onDelta(up(0.1f)) + onDelta(pixels = up(fractionOfScreen = 0.1f)) // target stays B even though UserActions changed assertTransition(fromScene = SceneA, toScene = SceneB, progress = 0.2f) - draggable.onDragStopped(down(0.1f)) + onDragStopped(velocity = down(fractionOfScreen = 0.1f)) advanceUntilIdle() // now target changed to C for new drag - draggable.onDragStarted(up(0.1f)) + onDragStarted(overSlop = up(fractionOfScreen = 0.1f)) assertTransition(fromScene = SceneA, toScene = SceneC, progress = 0.1f) } @Test fun onDragTargetsChanged_targetsChangeWhenStartingNewDrag() = runGestureTest { - draggable.onDragStarted(up(0.1f)) + onDragStarted(overSlop = up(fractionOfScreen = 0.1f)) assertTransition(fromScene = SceneA, toScene = SceneB, progress = 0.1f) mutableUserActionsA[Swipe.Up] = SceneC - draggable.onDelta(up(0.1f)) - draggable.onDragStopped(down(0.1f)) + onDelta(pixels = up(fractionOfScreen = 0.1f)) + onDragStopped(velocity = down(fractionOfScreen = 0.1f)) // now target changed to C for new drag that started before previous drag settled to Idle - draggable.onDragStarted(overSlop = 0f) - draggable.onDelta(up(0.1f)) + onDragStartedImmediately() + onDelta(pixels = up(fractionOfScreen = 0.1f)) assertTransition(fromScene = SceneA, toScene = SceneC, progress = 0.3f) } @Test fun startGestureDuringAnimatingOffset_shouldImmediatelyStopTheAnimation() = runGestureTest { - draggable.onDragStarted(overSlop = down(0.1f)) + onDragStarted(overSlop = down(fractionOfScreen = 0.1f)) assertTransition(currentScene = SceneA) - draggable.onDragStopped( - velocity = velocityThreshold, - ) - - // The stop animation is not started yet - assertThat(sceneGestureHandler.swipeTransition.isAnimatingOffset).isFalse() - - runCurrent() + onDragStopped(velocity = velocityThreshold) - assertThat(sceneGestureHandler.swipeTransition.isAnimatingOffset).isTrue() - assertThat(sceneGestureHandler.isDrivingTransition).isTrue() assertTransition(currentScene = SceneC) + assertThat(sceneGestureHandler.isDrivingTransition).isTrue() + assertThat(sceneGestureHandler.swipeTransition.isAnimatingOffset).isTrue() // Start a new gesture while the offset is animating - draggable.onDragStarted() + onDragStartedImmediately() assertThat(sceneGestureHandler.swipeTransition.isAnimatingOffset).isFalse() } @Test fun onInitialPreScroll_EdgeWithOverscroll_doNotChangeState() = runGestureTest { val nestedScroll = nestedScrollConnection(nestedScrollBehavior = EdgeWithPreview) - nestedScroll.onPreScroll(available = offsetY10, source = NestedScrollSource.Drag) + nestedScroll.onPreScroll( + available = downOffset(fractionOfScreen = 0.1f), + source = NestedScrollSource.Drag + ) assertIdle(currentScene = SceneA) } @@ -509,40 +580,29 @@ class SceneGestureHandlerTest { val consumed = nestedScroll.onPostScroll( consumed = Offset.Zero, - available = offsetY10, + available = downOffset(fractionOfScreen = 0.1f), source = NestedScrollSource.Drag ) assertTransition(currentScene = SceneA) assertThat(progress).isEqualTo(0.1f) - assertThat(consumed).isEqualTo(offsetY10) - } - - private fun NestedScrollConnection.scroll( - available: Offset, - consumedByScroll: Offset = Offset.Zero, - ) { - val consumedByPreScroll = - onPreScroll(available = available, source = NestedScrollSource.Drag) - val consumed = consumedByPreScroll + consumedByScroll - onPostScroll( - consumed = consumed, - available = available - consumed, - source = NestedScrollSource.Drag - ) + assertThat(consumed).isEqualTo(downOffset(fractionOfScreen = 0.1f)) } @Test fun afterSceneTransitionIsStarted_interceptPreScrollEvents() = runGestureTest { val nestedScroll = nestedScrollConnection(nestedScrollBehavior = EdgeWithPreview) - nestedScroll.scroll(available = offsetY10) + nestedScroll.scroll(available = downOffset(fractionOfScreen = 0.1f)) assertTransition(currentScene = SceneA) assertThat(progress).isEqualTo(0.1f) // start intercept preScroll val consumed = - nestedScroll.onPreScroll(available = offsetY10, source = NestedScrollSource.Drag) + nestedScroll.onPreScroll( + available = downOffset(fractionOfScreen = 0.1f), + source = NestedScrollSource.Drag + ) assertThat(progress).isEqualTo(0.2f) // do nothing on postScroll @@ -553,12 +613,12 @@ class SceneGestureHandlerTest { ) assertThat(progress).isEqualTo(0.2f) - nestedScroll.scroll(available = offsetY10) + nestedScroll.scroll(available = downOffset(fractionOfScreen = 0.1f)) assertThat(progress).isEqualTo(0.3f) assertTransition(currentScene = SceneA) } - private suspend fun TestGestureScope.preScrollAfterSceneTransition( + private fun TestGestureScope.preScrollAfterSceneTransition( firstScroll: Float, secondScroll: Float ) { @@ -567,10 +627,13 @@ class SceneGestureHandlerTest { nestedScroll.scroll(available = Offset(0f, firstScroll)) // stop scene transition (start the "stop animation") - nestedScroll.onPreFling(available = Velocity.Zero) + nestedScroll.preFling(available = Velocity.Zero) // a pre scroll event, that could be intercepted by SceneGestureHandler - nestedScroll.onPreScroll(Offset(0f, secondScroll), NestedScrollSource.Drag) + nestedScroll.onPreScroll( + available = Offset(0f, secondScroll), + source = NestedScrollSource.Drag + ) } @Test @@ -610,16 +673,17 @@ class SceneGestureHandlerTest { preScrollAfterSceneTransition(firstScroll = firstScroll, secondScroll = secondScroll) + advanceUntilIdle() assertIdle(SceneB) } @Test fun onPreFling_velocityLowerThanThreshold_remainSameScene() = runGestureTest { val nestedScroll = nestedScrollConnection(nestedScrollBehavior = EdgeWithPreview) - nestedScroll.scroll(available = offsetY10) + nestedScroll.scroll(available = downOffset(fractionOfScreen = 0.1f)) assertTransition(currentScene = SceneA) - nestedScroll.onPreFling(available = Velocity.Zero) + nestedScroll.preFling(available = Velocity.Zero) assertTransition(currentScene = SceneA) // wait for the stop animation @@ -627,15 +691,15 @@ class SceneGestureHandlerTest { assertIdle(currentScene = SceneA) } - private suspend fun TestGestureScope.flingAfterScroll( + private fun TestGestureScope.flingAfterScroll( use: NestedScrollBehavior, idleAfterScroll: Boolean, ) { val nestedScroll = nestedScrollConnection(nestedScrollBehavior = use) - nestedScroll.scroll(available = offsetY10) + nestedScroll.scroll(available = downOffset(fractionOfScreen = 0.1f)) if (idleAfterScroll) assertIdle(SceneA) else assertTransition(SceneA) - nestedScroll.onPreFling(available = Velocity(0f, velocityThreshold)) + nestedScroll.preFling(available = Velocity(0f, velocityThreshold)) } @Test @@ -679,19 +743,22 @@ class SceneGestureHandlerTest { } /** we started the scroll in the scene, then fling with the velocityThreshold */ - private suspend fun TestGestureScope.flingAfterScrollStartedInScene( + private fun TestGestureScope.flingAfterScrollStartedInScene( use: NestedScrollBehavior, idleAfterScroll: Boolean, ) { val nestedScroll = nestedScrollConnection(nestedScrollBehavior = use) // scroll consumed in child - nestedScroll.scroll(available = offsetY10, consumedByScroll = offsetY10) + nestedScroll.scroll( + available = downOffset(fractionOfScreen = 0.1f), + consumedByScroll = downOffset(fractionOfScreen = 0.1f) + ) // scroll offsetY10 is all available for parents - nestedScroll.scroll(available = offsetY10) + nestedScroll.scroll(available = downOffset(fractionOfScreen = 0.1f)) if (idleAfterScroll) assertIdle(SceneA) else assertTransition(SceneA) - nestedScroll.onPreFling(available = Velocity(0f, velocityThreshold)) + nestedScroll.preFling(available = Velocity(0f, velocityThreshold)) } @Test @@ -732,20 +799,20 @@ class SceneGestureHandlerTest { @Test fun beforeDraggableStart_drag_shouldBeIgnored() = runGestureTest { - draggable.onDelta(down(0.1f)) + onDelta(pixels = down(fractionOfScreen = 0.1f)) assertIdle(currentScene = SceneA) } @Test fun beforeDraggableStart_stop_shouldBeIgnored() = runGestureTest { - draggable.onDragStopped(velocityThreshold) + onDragStopped(velocity = velocityThreshold) assertIdle(currentScene = SceneA) } @Test fun beforeNestedScrollStart_stop_shouldBeIgnored() = runGestureTest { val nestedScroll = nestedScrollConnection(nestedScrollBehavior = EdgeWithPreview) - nestedScroll.onPreFling(Velocity(0f, velocityThreshold)) + nestedScroll.preFling(available = Velocity(0f, velocityThreshold)) assertIdle(currentScene = SceneA) } @@ -753,8 +820,10 @@ class SceneGestureHandlerTest { fun startNestedScrollWhileDragging() = runGestureTest { val nestedScroll = nestedScrollConnection(nestedScrollBehavior = EdgeAlways) + val offsetY10 = downOffset(fractionOfScreen = 0.1f) + // Start a drag and then stop it, given that - draggable.onDragStarted(overSlop = up(0.1f)) + onDragStarted(overSlop = up(0.1f)) assertTransition(currentScene = SceneA) assertThat(progress).isEqualTo(0.1f) @@ -764,7 +833,7 @@ class SceneGestureHandlerTest { assertThat(progress).isEqualTo(0.2f) // this should be ignored, we are scrolling now! - draggable.onDragStopped(-velocityThreshold) + onDragStopped(-velocityThreshold) assertTransition(currentScene = SceneA) nestedScroll.scroll(available = -offsetY10) @@ -773,7 +842,7 @@ class SceneGestureHandlerTest { nestedScroll.scroll(available = -offsetY10) assertThat(progress).isEqualTo(0.4f) - nestedScroll.onPreFling(available = Velocity(0f, -velocityThreshold)) + nestedScroll.preFling(available = Velocity(0f, -velocityThreshold)) assertTransition(currentScene = SceneB) // wait for the stop animation @@ -788,7 +857,7 @@ class SceneGestureHandlerTest { // Swipe up from the middle to transition to scene B. val middle = Offset(SCREEN_SIZE / 2f, SCREEN_SIZE / 2f) - draggable.onDragStarted(startedPosition = middle, overSlop = up(0.1f)) + onDragStarted(startedPosition = middle, overSlop = up(0.1f)) assertTransition( currentScene = SceneC, fromScene = SceneC, @@ -802,7 +871,7 @@ class SceneGestureHandlerTest { // should intercept it. Because it is intercepted, the overSlop passed to onDragStarted() // should be 0f. assertThat(sceneGestureHandler.shouldImmediatelyIntercept(middle)).isTrue() - draggable.onDragStarted(startedPosition = middle, overSlop = 0f) + onDragStartedImmediately(startedPosition = middle) // We should have intercepted the transition, so the transition should be the same object. assertTransition(currentScene = SceneC, fromScene = SceneC, toScene = SceneB) @@ -813,7 +882,7 @@ class SceneGestureHandlerTest { // instead animate from C to A. val bottom = Offset(SCREEN_SIZE / 2, SCREEN_SIZE) assertThat(sceneGestureHandler.shouldImmediatelyIntercept(bottom)).isFalse() - draggable.onDragStarted(startedPosition = bottom, overSlop = up(0.1f)) + onDragStarted(startedPosition = bottom, overSlop = up(0.1f)) assertTransition( currentScene = SceneC, diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/test/RunMonotonicClockTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/test/RunMonotonicClockTest.kt index fbcd5b27836e..0245cf2a26b8 100644 --- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/test/RunMonotonicClockTest.kt +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/test/RunMonotonicClockTest.kt @@ -3,7 +3,9 @@ package com.android.compose.test import androidx.compose.ui.test.ExperimentalTestApi import androidx.compose.ui.test.TestMonotonicFrameClock import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.TestCoroutineScheduler +import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runTest import kotlinx.coroutines.withContext @@ -12,16 +14,38 @@ import kotlinx.coroutines.withContext * function. * * The [TestCoroutineScheduler] is passed to provide the functionality to wait for idle. + * + * Note: Please refer to the documentation for [runTest], as this feature utilizes it. This will + * provide a comprehensive understanding of all its behaviors. */ -@OptIn(ExperimentalTestApi::class) +@OptIn(ExperimentalTestApi::class, ExperimentalCoroutinesApi::class) fun runMonotonicClockTest(block: suspend MonotonicClockTestScope.() -> Unit) = runTest { - // We need a CoroutineScope (like a TestScope) to create a TestMonotonicFrameClock. - withContext(TestMonotonicFrameClock(this)) { - MonotonicClockTestScope(coroutineScope = this, testScheduler = testScheduler).block() + val testScope: TestScope = this + + withContext(TestMonotonicFrameClock(coroutineScope = testScope)) { + val testScopeWithMonotonicFrameClock: CoroutineScope = this + + val scope = + MonotonicClockTestScope( + testScope = testScopeWithMonotonicFrameClock, + testScheduler = testScope.testScheduler, + backgroundScope = backgroundScope, + ) + + // Run the test + scope.block() } } +/** + * A coroutine scope that for launching test coroutines for Compose. + * + * @param testScheduler The delay-skipping scheduler used by the test dispatchers running the code + * in this scope (see [TestScope.testScheduler]). + * @param backgroundScope A scope for background work (see [TestScope.backgroundScope]). + */ class MonotonicClockTestScope( - coroutineScope: CoroutineScope, - val testScheduler: TestCoroutineScheduler -) : CoroutineScope by coroutineScope + testScope: CoroutineScope, + val testScheduler: TestCoroutineScheduler, + val backgroundScope: CoroutineScope, +) : CoroutineScope by testScope diff --git a/packages/SystemUI/customization/Android.bp b/packages/SystemUI/customization/Android.bp index 81b5bd43bdbd..c399abc7d81d 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/notifications/data/repository/NotificationSettingsRepository.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/notifications/data/repository/NotificationSettingsRepository.kt index 4b21105a20a0..e39d7edddcfd 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 android.provider.Settings 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 @@ class NotificationSettingsRepository( 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 9ec6ec8d3811..04e8090e3ae2 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 @@ import kotlinx.coroutines.flow.StateFlow 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/log/Android.bp b/packages/SystemUI/log/Android.bp index 2be22a65676b..2f1d354c3b3e 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 507ea25083e1..98f7aced7522 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/systemui/accessibility/data/repository/ColorCorrectionRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/ColorCorrectionRepositoryImplTest.kt index b6605ed9d007..fa47a02d78c9 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/ColorCorrectionRepositoryImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/ColorCorrectionRepositoryImplTest.kt @@ -59,15 +59,21 @@ class ColorCorrectionRepositoryImplTest : SysuiTestCase() { } @Test + fun isEnabled_settingNotInitialized_returnsFalseByDefault() = + scope.runTest { + val actualValue by collectLastValue(underTest.isEnabled(testUser1)) + + runCurrent() + + Truth.assertThat(actualValue).isFalse() + } + + @Test fun isEnabled_initiallyGetsSettingsValue() = scope.runTest { val actualValue by collectLastValue(underTest.isEnabled(testUser1)) - settings.putIntForUser( - SETTING_NAME, - ENABLED, - testUser1.identifier - ) + settings.putIntForUser(SETTING_NAME, ENABLED, testUser1.identifier) runCurrent() Truth.assertThat(actualValue).isTrue() @@ -78,25 +84,13 @@ class ColorCorrectionRepositoryImplTest : SysuiTestCase() { scope.runTest { val flowValues: List<Boolean> by collectValues(underTest.isEnabled(testUser1)) - settings.putIntForUser( - SETTING_NAME, - DISABLED, - testUser1.identifier - ) + settings.putIntForUser(SETTING_NAME, DISABLED, testUser1.identifier) runCurrent() - settings.putIntForUser( - SETTING_NAME, - ENABLED, - testUser1.identifier - ) + settings.putIntForUser(SETTING_NAME, ENABLED, testUser1.identifier) runCurrent() - settings.putIntForUser( - SETTING_NAME, - DISABLED, - testUser1.identifier - ) + settings.putIntForUser(SETTING_NAME, DISABLED, testUser1.identifier) runCurrent() Truth.assertThat(flowValues.size).isEqualTo(3) @@ -109,26 +103,14 @@ class ColorCorrectionRepositoryImplTest : SysuiTestCase() { val lastValueUser1 by collectLastValue(underTest.isEnabled(testUser1)) val lastValueUser2 by collectLastValue(underTest.isEnabled(testUser2)) - settings.putIntForUser( - SETTING_NAME, - DISABLED, - testUser1.identifier - ) - settings.putIntForUser( - SETTING_NAME, - DISABLED, - testUser2.identifier - ) + settings.putIntForUser(SETTING_NAME, DISABLED, testUser1.identifier) + settings.putIntForUser(SETTING_NAME, DISABLED, testUser2.identifier) runCurrent() Truth.assertThat(lastValueUser1).isFalse() Truth.assertThat(lastValueUser2).isFalse() - settings.putIntForUser( - SETTING_NAME, - ENABLED, - testUser1.identifier - ) + settings.putIntForUser(SETTING_NAME, ENABLED, testUser1.identifier) runCurrent() Truth.assertThat(lastValueUser1).isTrue() @@ -142,11 +124,7 @@ class ColorCorrectionRepositoryImplTest : SysuiTestCase() { runCurrent() Truth.assertThat(success).isTrue() - val actualValue = - settings.getIntForUser( - SETTING_NAME, - testUser1.identifier - ) + val actualValue = settings.getIntForUser(SETTING_NAME, testUser1.identifier) Truth.assertThat(actualValue).isEqualTo(ENABLED) } @@ -157,11 +135,7 @@ class ColorCorrectionRepositoryImplTest : SysuiTestCase() { runCurrent() Truth.assertThat(success).isTrue() - val actualValue = - settings.getIntForUser( - SETTING_NAME, - testUser1.identifier - ) + val actualValue = settings.getIntForUser(SETTING_NAME, testUser1.identifier) Truth.assertThat(actualValue).isEqualTo(DISABLED) } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/ColorInversionRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/ColorInversionRepositoryImplTest.kt index 30eb782e5938..3d8159e70061 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/ColorInversionRepositoryImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/ColorInversionRepositoryImplTest.kt @@ -24,6 +24,7 @@ import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectLastValue import com.android.systemui.coroutines.collectValues import com.android.systemui.util.settings.FakeSettings +import com.google.common.truth.Truth import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.StandardTestDispatcher @@ -59,6 +60,16 @@ class ColorInversionRepositoryImplTest : SysuiTestCase() { } @Test + fun isEnabled_settingNotInitialized_returnsFalseByDefault() = + scope.runTest { + val actualValue by collectLastValue(underTest.isEnabled(testUser1)) + + runCurrent() + + Truth.assertThat(actualValue).isFalse() + } + + @Test fun isEnabled_initiallyGetsSettingsValue() = scope.runTest { val actualValue by collectLastValue(underTest.isEnabled(testUser1)) @@ -72,8 +83,7 @@ class ColorInversionRepositoryImplTest : SysuiTestCase() { @Test fun isEnabled_settingUpdated_valueUpdated() = scope.runTest { - val flowValues: List<Boolean> by - collectValues(underTest.isEnabled(testUser1)) + val flowValues: List<Boolean> by collectValues(underTest.isEnabled(testUser1)) settings.putIntForUser(SETTING_NAME, DISABLED, testUser1.identifier) runCurrent() 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 6380ace7ba4f..23869576a7a4 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 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.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.kosmos.testScope 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 @@ class PackageChangeRepositoryTest : SysuiTestCase() { 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 d610925edd8a..35d9d3f21c8f 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 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.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.kosmos.testScope 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 @@ class PackageUpdateMonitorTest : SysuiTestCase() { bgHandler = handler, context = context, scope = applicationCoroutineScope, - logger = PackageUpdateLogger(logcatLogBuffer()) + logger = PackageUpdateLogger(logcatLogBuffer()), + systemClock = fakeSystemClock, ) + + fakeSystemClock.setCurrentTimeMillis(0) } @Test @@ -96,11 +100,16 @@ class PackageUpdateMonitorTest : SysuiTestCase() { 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 @@ class PackageUpdateMonitorTest : SysuiTestCase() { 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 @@ class PackageUpdateMonitorTest : SysuiTestCase() { 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 @@ class PackageUpdateMonitorTest : SysuiTestCase() { 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 @@ class PackageUpdateMonitorTest : SysuiTestCase() { 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 000000000000..a164e7cbc0ec --- /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 76b0d4aaa8ca..45f98be2ca12 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 @@ -20,6 +20,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.log.table.TableLogBuffer import com.android.systemui.media.controls.models.player.MediaData import com.android.systemui.media.controls.pipeline.MediaDataManager import com.android.systemui.util.mockito.KotlinArgumentCaptor @@ -44,78 +45,79 @@ import org.mockito.MockitoAnnotations class CommunalMediaRepositoryImplTest : SysuiTestCase() { @Mock private lateinit var mediaDataManager: MediaDataManager @Mock private lateinit var mediaData: MediaData + @Mock private lateinit var tableLogBuffer: TableLogBuffer + + private lateinit var underTest: CommunalMediaRepositoryImpl private val mediaDataListenerCaptor: KotlinArgumentCaptor<MediaDataManager.Listener> by lazy { KotlinArgumentCaptor(MediaDataManager.Listener::class.java) } - private lateinit var mediaRepository: CommunalMediaRepository - private val testDispatcher = StandardTestDispatcher() private val testScope = TestScope(testDispatcher) @Before fun setUp() { MockitoAnnotations.initMocks(this) + + underTest = + CommunalMediaRepositoryImpl( + mediaDataManager, + tableLogBuffer, + ) } @Test fun hasAnyMediaOrRecommendation_defaultsToFalse() = testScope.runTest { - mediaRepository = CommunalMediaRepositoryImpl(mediaDataManager) - - val mediaModel = collectLastValue(mediaRepository.mediaModel) + val mediaModel = collectLastValue(underTest.mediaModel) runCurrent() - assertThat(mediaModel()?.hasAnyMediaOrRecommendation).isFalse() + assertThat(mediaModel()?.hasActiveMediaOrRecommendation).isFalse() } @Test fun mediaModel_updatesWhenMediaDataLoaded() = testScope.runTest { - mediaRepository = CommunalMediaRepositoryImpl(mediaDataManager) - // Listener is added verify(mediaDataManager).addListener(mediaDataListenerCaptor.capture()) // Initial value is false. - val mediaModel = collectLastValue(mediaRepository.mediaModel) + 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) } @Test fun mediaModel_updatesWhenMediaDataRemoved() = testScope.runTest { - mediaRepository = CommunalMediaRepositoryImpl(mediaDataManager) - // Listener is added 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(mediaRepository.mediaModel) - assertThat(mediaModel()?.hasAnyMediaOrRecommendation).isTrue() + val mediaModel = collectLastValue(underTest.mediaModel) + 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/CommunalPrefsRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalPrefsRepositoryImplTest.kt index d15e15e179fb..6bff0dc7bd9e 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalPrefsRepositoryImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalPrefsRepositoryImplTest.kt @@ -25,6 +25,8 @@ import com.android.systemui.communal.data.repository.CommunalPrefsRepositoryImpl import com.android.systemui.coroutines.collectLastValue import com.android.systemui.kosmos.testDispatcher import com.android.systemui.kosmos.testScope +import com.android.systemui.log.logcatLogBuffer +import com.android.systemui.log.table.TableLogBuffer import com.android.systemui.settings.UserFileManager import com.android.systemui.testKosmos import com.android.systemui.user.data.repository.FakeUserRepository @@ -36,11 +38,15 @@ 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 @SmallTest @RunWith(AndroidJUnit4::class) @android.platform.test.annotations.EnabledOnRavenwood class CommunalPrefsRepositoryImplTest : SysuiTestCase() { + @Mock private lateinit var tableLogBuffer: TableLogBuffer + private lateinit var underTest: CommunalPrefsRepositoryImpl private val kosmos = testKosmos() @@ -51,6 +57,8 @@ class CommunalPrefsRepositoryImplTest : SysuiTestCase() { @Before fun setUp() { + MockitoAnnotations.initMocks(this) + userRepository = kosmos.fakeUserRepository userRepository.setUserInfos(USER_INFOS) @@ -67,6 +75,8 @@ class CommunalPrefsRepositoryImplTest : SysuiTestCase() { kosmos.testDispatcher, userRepository, userFileManager, + logcatLogBuffer("CommunalPrefsRepositoryImplTest"), + tableLogBuffer, ) } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalTutorialRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalTutorialRepositoryImplTest.kt index 0c66bbb63439..2911a50c2737 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalTutorialRepositoryImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalTutorialRepositoryImplTest.kt @@ -26,6 +26,7 @@ import com.android.systemui.kosmos.applicationCoroutineScope import com.android.systemui.kosmos.testDispatcher import com.android.systemui.kosmos.testScope import com.android.systemui.log.logcatLogBuffer +import com.android.systemui.log.table.TableLogBuffer import com.android.systemui.testKosmos import com.android.systemui.user.data.repository.FakeUserRepository import com.android.systemui.util.settings.FakeSettings @@ -34,12 +35,15 @@ 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 @SmallTest @RunWith(AndroidJUnit4::class) @android.platform.test.annotations.EnabledOnRavenwood class CommunalTutorialRepositoryImplTest : SysuiTestCase() { + @Mock private lateinit var tableLogBuffer: TableLogBuffer + private val kosmos = testKosmos() private val testScope = kosmos.testScope @@ -64,6 +68,7 @@ class CommunalTutorialRepositoryImplTest : SysuiTestCase() { userRepository, secureSettings, logcatLogBuffer("CommunalTutorialRepositoryImplTest"), + tableLogBuffer, ) } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryImplTest.kt index c979ca63950a..475179dfdb72 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryImplTest.kt @@ -198,13 +198,22 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { } @Test - fun deleteWidget_removeWidgetId_andDeleteFromDb() = + fun deleteWidgetFromDb() = testScope.runTest { val id = 1 - underTest.deleteWidget(id) + underTest.deleteWidgetFromDb(id) runCurrent() verify(communalWidgetDao).deleteWidgetById(id) + } + + @Test + fun deleteWidgetFromHost() = + testScope.runTest { + val id = 1 + underTest.deleteWidgetFromHost(id) + runCurrent() + verify(appWidgetHost).deleteAppWidgetId(id) } 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 ee01bf9c26e4..c5485c5c5c33 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 @@ -642,6 +642,53 @@ class CommunalInteractorTest : SysuiTestCase() { } @Test + fun isCommunalVisible() = + testScope.runTest { + val transitionState = + MutableStateFlow<ObservableCommunalTransitionState>( + ObservableCommunalTransitionState.Idle(CommunalSceneKey.Blank) + ) + communalRepository.setTransitionState(transitionState) + + // isCommunalVisible is false when not on communal. + val isCommunalVisible by collectLastValue(underTest.isCommunalVisible) + assertThat(isCommunalVisible).isEqualTo(false) + + // Start transition to communal. + transitionState.value = + ObservableCommunalTransitionState.Transition( + fromScene = CommunalSceneKey.Blank, + toScene = CommunalSceneKey.Communal, + progress = flowOf(0f), + isInitiatedByUserInput = false, + isUserInputOngoing = flowOf(false), + ) + + // isCommunalVisible is true once transition starts. + assertThat(isCommunalVisible).isEqualTo(true) + + // Finish transition to communal + transitionState.value = + ObservableCommunalTransitionState.Idle(CommunalSceneKey.Communal) + + // isCommunalVisible is true since we're on communal. + assertThat(isCommunalVisible).isEqualTo(true) + + // Start transition away from communal. + transitionState.value = + ObservableCommunalTransitionState.Transition( + fromScene = CommunalSceneKey.Communal, + toScene = CommunalSceneKey.Blank, + progress = flowOf(1.0f), + isInitiatedByUserInput = false, + isUserInputOngoing = flowOf(false), + ) + + // isCommunalVisible is still true as the false as soon as transition away runs. + assertThat(isCommunalVisible).isEqualTo(true) + } + + @Test fun testShowWidgetEditorStartsActivity() = testScope.runTest { underTest.showWidgetEditor() diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt index 273d1cd55626..cf727cf5e180 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt @@ -36,6 +36,7 @@ import com.android.systemui.communal.shared.model.CommunalWidgetContentModel import com.android.systemui.communal.ui.viewmodel.CommunalEditModeViewModel import com.android.systemui.coroutines.collectLastValue import com.android.systemui.kosmos.testScope +import com.android.systemui.log.logcatLogBuffer import com.android.systemui.media.controls.ui.MediaHost import com.android.systemui.smartspace.data.repository.FakeSmartspaceRepository import com.android.systemui.smartspace.data.repository.fakeSmartspaceRepository @@ -82,6 +83,7 @@ class CommunalEditModeViewModelTest : SysuiTestCase() { kosmos.communalInteractor, mediaHost, uiEventLogger, + logcatLogBuffer("CommunalEditModeViewModelTest"), ) } @@ -142,6 +144,49 @@ class CommunalEditModeViewModelTest : SysuiTestCase() { } @Test + fun deleteWidget() = + testScope.runTest { + tutorialRepository.setTutorialSettingState(Settings.Secure.HUB_MODE_TUTORIAL_COMPLETED) + + // Widgets available. + val widgets = + listOf( + CommunalWidgetContentModel( + appWidgetId = 0, + priority = 30, + providerInfo = mock(), + ), + CommunalWidgetContentModel( + appWidgetId = 1, + priority = 20, + providerInfo = mock(), + ), + ) + widgetRepository.setCommunalWidgets(widgets) + + val communalContent by collectLastValue(underTest.communalContent) + + // Widgets and CTA tile are shown. + assertThat(communalContent?.size).isEqualTo(3) + assertThat(communalContent?.get(0)) + .isInstanceOf(CommunalContentModel.Widget::class.java) + assertThat(communalContent?.get(1)) + .isInstanceOf(CommunalContentModel.Widget::class.java) + assertThat(communalContent?.get(2)) + .isInstanceOf(CommunalContentModel.CtaTileInEditMode::class.java) + + underTest.onDeleteWidget(widgets.get(0).appWidgetId) + + // Only one widget and CTA tile remain. + assertThat(communalContent?.size).isEqualTo(2) + val item = communalContent?.get(0) + val appWidgetId = if (item is CommunalContentModel.Widget) item.appWidgetId else null + assertThat(appWidgetId).isEqualTo(widgets.get(1).appWidgetId) + assertThat(communalContent?.get(1)) + .isInstanceOf(CommunalContentModel.CtaTileInEditMode::class.java) + } + + @Test fun reorderWidget_uiEventLogging_start() { underTest.onReorderWidgetStart() verify(uiEventLogger).log(CommunalUiEvent.COMMUNAL_HUB_REORDER_WIDGET_START) 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 0723e8306f71..73d309118548 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 @@ -40,6 +40,7 @@ import com.android.systemui.coroutines.collectLastValue import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository import com.android.systemui.kosmos.testScope +import com.android.systemui.log.logcatLogBuffer import com.android.systemui.media.controls.ui.MediaHierarchyManager import com.android.systemui.media.controls.ui.MediaHost import com.android.systemui.smartspace.data.repository.FakeSmartspaceRepository @@ -99,6 +100,7 @@ class CommunalViewModelTest : SysuiTestCase() { kosmos.communalInteractor, kosmos.communalTutorialInteractor, mediaHost, + logcatLogBuffer("CommunalViewModelTest"), ) } 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 efccf7a81ccd..2c890f4e6c42 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 ce74a905ef66..298ce7029f0a 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.Context 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)) - - 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, - ) - } + fun setUp() = + with(kosmos) { + fakeSystemClock.setCurrentTimeMillis(0) + fakeUserRepository.setUserInfos(listOf(PRIMARY_USER, ANOTHER_USER)) + whenever(controlsComponent.getControlsListingController()) + .thenReturn(Optional.of(controlsListingController)) + + 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 @@ class HomeControlsComponentInteractorTest : SysuiTestCase() { 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 @@ class HomeControlsComponentInteractorTest : SysuiTestCase() { 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 @@ class HomeControlsComponentInteractorTest : SysuiTestCase() { 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 @@ class HomeControlsComponentInteractorTest : SysuiTestCase() { 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 @@ class HomeControlsComponentInteractorTest : SysuiTestCase() { 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 @@ class HomeControlsComponentInteractorTest : SysuiTestCase() { ) 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 d28b6bf39f30..0a3aea767733 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 @@ import org.mockito.MockitoAnnotations 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 - - whenever(controlsComponent.getControlsListingController()) - .thenReturn(Optional.of(controlsListingController)) - - homeControlsComponentInteractor = kosmos.homeControlsComponentInteractor - - fakeDreamActivityProvider = DreamActivityProvider { activity } - underTest = - HomeControlsDreamService( - controlsSettingsRepository, - taskFragmentComponentFactory, - homeControlsComponentInteractor, - fakeDreamActivityProvider, - logBuffer - ) - } + underTest = buildService { activity } + } @Test - fun testOnAttachedToWindowCreatesTaskFragmentComponent() { - underTest.onAttachedToWindow() - verify(taskFragmentComponentFactory).create(any(), any(), any(), any()) - } + fun testOnAttachedToWindowCreatesTaskFragmentComponent() = + testScope.runTest { + underTest.onAttachedToWindow() + verify(taskFragmentComponentFactory).create(any(), any(), any(), any()) + } @Test - fun testOnDetachedFromWindowDestroyTaskFragmentComponent() { - underTest.onAttachedToWindow() - underTest.onDetachedFromWindow() - verify(taskFragmentComponent).destroy() - } + fun testOnDetachedFromWindowDestroyTaskFragmentComponent() = + testScope.runTest { + underTest.onAttachedToWindow() + underTest.onDetachedFromWindow() + verify(taskFragmentComponent).destroy() + } @Test - fun testNotCreatingTaskFragmentComponentWhenActivityIsNull() { - fakeDreamActivityProvider = DreamActivityProvider { null } - underTest = - HomeControlsDreamService( - controlsSettingsRepository, - taskFragmentComponentFactory, - homeControlsComponentInteractor, - fakeDreamActivityProvider, - logBuffer + 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") ) - - 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 6610e7007eb2..87b1bbb9ff70 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.dagger.ControlsComponent 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 @@ class HomeControlsDreamStartableTest : SysuiTestCase() { 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/dreams/touch/CommunalTouchHandlerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/touch/CommunalTouchHandlerTest.java index 74c197075461..2a9bc4a1d80e 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/touch/CommunalTouchHandlerTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/touch/CommunalTouchHandlerTest.java @@ -28,7 +28,6 @@ import androidx.test.filters.SmallTest; import com.android.systemui.SysuiTestCase; import com.android.systemui.shared.system.InputChannelCompat; -import com.android.systemui.statusbar.NotificationShadeWindowController; import com.android.systemui.statusbar.phone.CentralSurfaces; import org.junit.Before; @@ -47,8 +46,6 @@ public class CommunalTouchHandlerTest extends SysuiTestCase { @Mock CentralSurfaces mCentralSurfaces; @Mock - NotificationShadeWindowController mNotificationShadeWindowController; - @Mock DreamTouchHandler.TouchSession mTouchSession; CommunalTouchHandler mTouchHandler; @@ -59,17 +56,10 @@ public class CommunalTouchHandlerTest extends SysuiTestCase { MockitoAnnotations.initMocks(this); mTouchHandler = new CommunalTouchHandler( Optional.of(mCentralSurfaces), - mNotificationShadeWindowController, INITIATION_WIDTH); } @Test - public void testSessionStartForcesShadeOpen() { - mTouchHandler.onSessionStart(mTouchSession); - verify(mNotificationShadeWindowController).setForcePluginOpen(true, mTouchHandler); - } - - @Test public void testEventPropagation() { final MotionEvent motionEvent = Mockito.mock(MotionEvent.class); 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 2fe4ef78bfdc..f400cb18295c 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 androidx.test.filters.SmallTest 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 @@ class SideFpsProgressBarViewModelTest : SysuiTestCase() { kosmos.fakeKeyguardRepository.setIsDozing(false) kosmos.fakeDeviceEntryFingerprintAuthRepository.setAuthenticationStatus( AcquiredFingerprintAuthenticationStatus( + AuthenticationReason.DeviceEntryAuthentication, BiometricFingerprintConstants.FINGERPRINT_ACQUIRED_START ) ) @@ -165,6 +168,7 @@ class SideFpsProgressBarViewModelTest : SysuiTestCase() { kosmos.fakeKeyguardRepository.setIsDozing(true) kosmos.fakeDeviceEntryFingerprintAuthRepository.setAuthenticationStatus( AcquiredFingerprintAuthenticationStatus( + AuthenticationReason.DeviceEntryAuthentication, BiometricFingerprintConstants.FINGERPRINT_ACQUIRED_START ) ) @@ -177,6 +181,7 @@ class SideFpsProgressBarViewModelTest : SysuiTestCase() { 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 d9f24b36dac2..9c0674dfbd6e 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 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.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/fontscaling/domain/FontScalingTileMapperTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/fontscaling/domain/FontScalingTileMapperTest.kt new file mode 100644 index 000000000000..b7b3fdbed955 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/fontscaling/domain/FontScalingTileMapperTest.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.systemui.qs.tiles.impl.fontscaling.domain + +import android.graphics.drawable.TestStubDrawable +import android.widget.Switch +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.common.shared.model.Icon +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.qs.tiles.impl.custom.QSTileStateSubject +import com.android.systemui.qs.tiles.impl.fontscaling.domain.model.FontScalingTileModel +import com.android.systemui.qs.tiles.impl.fontscaling.qsFontScalingTileConfig +import com.android.systemui.qs.tiles.viewmodel.QSTileState +import com.android.systemui.res.R +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidJUnit4::class) +class FontScalingTileMapperTest : SysuiTestCase() { + private val kosmos = Kosmos() + private val fontScalingTileConfig = kosmos.qsFontScalingTileConfig + + private val mapper by lazy { + FontScalingTileMapper( + context.orCreateTestableResources + .apply { addOverride(R.drawable.ic_qs_font_scaling, TestStubDrawable()) } + .resources, + context.theme + ) + } + + @Test + fun activeStateMatchesEnabledModel() { + val inputModel = FontScalingTileModel + + val outputState = mapper.map(fontScalingTileConfig, inputModel) + + val expectedState = createFontScalingTileState() + QSTileStateSubject.assertThat(outputState).isEqualTo(expectedState) + } + + private fun createFontScalingTileState(): QSTileState = + QSTileState( + { + Icon.Loaded( + context.getDrawable( + R.drawable.ic_qs_font_scaling, + )!!, + null + ) + }, + context.getString(R.string.quick_settings_font_scaling_label), + QSTileState.ActivationState.ACTIVE, + null, + setOf(QSTileState.UserAction.CLICK, QSTileState.UserAction.LONG_CLICK), + context.getString(R.string.quick_settings_font_scaling_label), + null, + QSTileState.SideViewIcon.Chevron, + QSTileState.EnabledState.ENABLED, + Switch::class.qualifiedName + ) +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/fontscaling/domain/interactor/FontScalingTileDataInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/fontscaling/domain/interactor/FontScalingTileDataInteractorTest.kt new file mode 100644 index 000000000000..39bc8a6fa6a2 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/fontscaling/domain/interactor/FontScalingTileDataInteractorTest.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.qs.tiles.impl.fontscaling.domain.interactor + +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.coroutines.collectLastValue +import com.android.systemui.coroutines.collectValues +import com.android.systemui.qs.tiles.base.interactor.DataUpdateTrigger +import com.google.common.truth.Truth +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.junit.runner.RunWith + +@OptIn(ExperimentalCoroutinesApi::class) +@SmallTest +@RunWith(AndroidJUnit4::class) +class FontScalingTileDataInteractorTest : SysuiTestCase() { + private val underTest: FontScalingTileDataInteractor = FontScalingTileDataInteractor() + private val testUser = UserHandle.of(1) + + @Test + fun collectsExactlyOneValue() = runTest { + val flowValues by + collectValues(underTest.tileData(testUser, flowOf(DataUpdateTrigger.InitialRequest))) + + runCurrent() + + Truth.assertThat(flowValues.size).isEqualTo(1) + } + + @Test + fun lastValueIsNotEmpty() = runTest { + val flowValue by + collectLastValue(underTest.tileData(testUser, flowOf(DataUpdateTrigger.InitialRequest))) + + runCurrent() + + Truth.assertThat(flowValue).isNotNull() + } + + @Test + fun isAvailable() = runTest { + val availability by collectLastValue(underTest.availability(testUser)) + + Truth.assertThat(availability).isTrue() + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/fontscaling/domain/interactor/FontScalingUserActionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/fontscaling/domain/interactor/FontScalingUserActionInteractorTest.kt new file mode 100644 index 000000000000..2384915c8703 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/fontscaling/domain/interactor/FontScalingUserActionInteractorTest.kt @@ -0,0 +1,142 @@ +/* + * 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.qs.tiles.impl.fontscaling.domain.interactor + +import android.provider.Settings +import android.view.View +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.accessibility.fontscaling.FontScalingDialogDelegate +import com.android.systemui.animation.DialogLaunchAnimator +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.testScope +import com.android.systemui.plugins.ActivityStarter +import com.android.systemui.qs.tiles.base.actions.FakeQSTileIntentUserInputHandler +import com.android.systemui.qs.tiles.base.actions.intentInputs +import com.android.systemui.qs.tiles.base.interactor.QSTileInputTestKtx +import com.android.systemui.qs.tiles.base.interactor.QSTileInputTestKtx.click +import com.android.systemui.qs.tiles.impl.fontscaling.domain.model.FontScalingTileModel +import com.android.systemui.statusbar.phone.FakeKeyguardStateController +import com.android.systemui.statusbar.phone.SystemUIDialog +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.nullable +import com.android.systemui.util.mockito.whenever +import com.google.common.truth.Truth +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.ArgumentMatchers.anyBoolean +import org.mockito.Captor +import org.mockito.Mock +import org.mockito.Mockito.never +import org.mockito.Mockito.verify + +@SmallTest +@RunWith(AndroidJUnit4::class) +class FontScalingUserActionInteractorTest : SysuiTestCase() { + private val kosmos = Kosmos() + private val qsTileIntentUserActionHandler = FakeQSTileIntentUserInputHandler() + private val keyguardStateController = FakeKeyguardStateController() + + private lateinit var underTest: FontScalingTileUserActionInteractor + + @Mock private lateinit var fontScalingDialogDelegate: FontScalingDialogDelegate + @Mock private lateinit var dialogLaunchAnimator: DialogLaunchAnimator + @Mock private lateinit var dialog: SystemUIDialog + @Mock private lateinit var activityStarter: ActivityStarter + + @Captor private lateinit var argumentCaptor: ArgumentCaptor<Runnable> + + @Before + fun setup() { + activityStarter = mock<ActivityStarter>() + dialogLaunchAnimator = mock<DialogLaunchAnimator>() + dialog = mock<SystemUIDialog>() + fontScalingDialogDelegate = + mock<FontScalingDialogDelegate> { whenever(createDialog()).thenReturn(dialog) } + argumentCaptor = ArgumentCaptor.forClass(Runnable::class.java) + + underTest = + FontScalingTileUserActionInteractor( + kosmos.testScope.coroutineContext, + qsTileIntentUserActionHandler, + { fontScalingDialogDelegate }, + keyguardStateController, + dialogLaunchAnimator, + activityStarter + ) + } + + @Test + fun clickTile_screenUnlocked_showDialogAnimationFromView() = + kosmos.testScope.runTest { + keyguardStateController.isShowing = false + val testView = View(context) + + underTest.handleInput(click(FontScalingTileModel, view = testView)) + + verify(activityStarter) + .executeRunnableDismissingKeyguard( + argumentCaptor.capture(), + eq(null), + eq(true), + eq(true), + eq(false) + ) + argumentCaptor.value.run() + verify(dialogLaunchAnimator).showFromView(any(), eq(testView), nullable(), anyBoolean()) + } + + @Test + fun clickTile_onLockScreen_neverShowDialogAnimationFromView_butShowsDialog() = + kosmos.testScope.runTest { + keyguardStateController.isShowing = true + val testView = View(context) + + underTest.handleInput(click(FontScalingTileModel, view = testView)) + + verify(activityStarter) + .executeRunnableDismissingKeyguard( + argumentCaptor.capture(), + eq(null), + eq(true), + eq(true), + eq(false) + ) + argumentCaptor.value.run() + verify(dialogLaunchAnimator, never()) + .showFromView(any(), eq(testView), nullable(), anyBoolean()) + verify(dialog).show() + } + + @Test + fun handleLongClick() = + kosmos.testScope.runTest { + underTest.handleInput(QSTileInputTestKtx.longClick(FontScalingTileModel)) + + Truth.assertThat(qsTileIntentUserActionHandler.handledInputs).hasSize(1) + val intentInput = qsTileIntentUserActionHandler.intentInputs.last() + val actualIntentAction = intentInput.intent.action + val expectedIntentAction = Settings.ACTION_TEXT_READING_SETTINGS + Truth.assertThat(actualIntentAction).isEqualTo(expectedIntentAction) + } +} 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 504ded30a06c..189ba7b1965a 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt @@ -76,6 +76,8 @@ import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.FakeMobi import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconsViewModel import com.android.systemui.statusbar.pipeline.mobile.util.FakeMobileMappingsProxy import com.android.systemui.statusbar.pipeline.shared.data.repository.FakeConnectivityRepository +import com.android.systemui.statusbar.policy.data.repository.fakeDeviceProvisioningRepository +import com.android.systemui.statusbar.policy.domain.interactor.deviceProvisioningInteractor import com.android.systemui.telephony.data.repository.fakeTelephonyRepository import com.android.systemui.testKosmos import com.android.systemui.util.mockito.any @@ -262,6 +264,7 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() { simBouncerInteractor = dagger.Lazy { kosmos.simBouncerInteractor }, authenticationInteractor = dagger.Lazy { kosmos.authenticationInteractor }, windowController = mock(), + deviceProvisioningInteractor = kosmos.deviceProvisioningInteractor, ) startable.start() @@ -518,6 +521,21 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() { assertCurrentScene(SceneKey.Lockscreen) } + @Test + fun deviceProvisioningAndFactoryResetProtection() = + testScope.runTest { + val isVisible by collectLastValue(sceneContainerViewModel.isVisible) + 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() + } + /** * Asserts that the current scene in the view-model matches what's expected. * 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 16cb6231a872..12dbf11255b6 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 @@ -49,6 +49,8 @@ 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.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 import com.google.common.truth.Truth.assertThat @@ -112,6 +114,7 @@ class SceneContainerStartableTest : SysuiTestCase() { simBouncerInteractor = dagger.Lazy { kosmos.simBouncerInteractor }, authenticationInteractor = dagger.Lazy { authenticationInteractor }, windowController = windowController, + deviceProvisioningInteractor = kosmos.deviceProvisioningInteractor, ) } @@ -162,6 +165,30 @@ class SceneContainerStartableTest : SysuiTestCase() { } @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 }) @@ -743,6 +770,8 @@ class SceneContainerStartableTest : SysuiTestCase() { authenticationMethod: AuthenticationMethodModel? = null, isLockscreenEnabled: Boolean = true, startsAwake: Boolean = true, + isDeviceProvisioned: Boolean = true, + isFrpActive: Boolean = false, ): MutableStateFlow<ObservableTransitionState> { if (authenticationMethod?.isSecure == true) { assert(isLockscreenEnabled) { @@ -779,6 +808,10 @@ class SceneContainerStartableTest : SysuiTestCase() { } else { powerInteractor.setAsleepForTest() } + + kosmos.fakeDeviceProvisioningRepository.setDeviceProvisioned(isDeviceProvisioned) + kosmos.fakeDeviceProvisioningRepository.setFactoryResetProtectionActive(isFrpActive) + runCurrent() return transitionStateFlow 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 000000000000..693de55211f8 --- /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/FakeKeyguardStateController.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/FakeKeyguardStateController.java index c669c6f6fb1c..1f6ba29c5fc1 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/FakeKeyguardStateController.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/FakeKeyguardStateController.java @@ -33,6 +33,10 @@ public class FakeKeyguardStateController implements KeyguardStateController { mOccluded = occluded; } + public void setShowing(boolean isShowing) { + mShowing = isShowing; + } + @Override public boolean isShowing() { return mShowing; diff --git a/packages/SystemUI/plugin/Android.bp b/packages/SystemUI/plugin/Android.bp index 9063a02ee885..682a68fdf846 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 66951b5d9640..f6fa6a599881 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_core/Android.bp b/packages/SystemUI/plugin_core/Android.bp index b7e154583531..521c019d74f3 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/layout/biometric_prompt_constraint_layout.xml b/packages/SystemUI/res/layout/biometric_prompt_constraint_layout.xml new file mode 100644 index 000000000000..a877853eaec8 --- /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/values-xxhdpi/dimens.xml b/packages/SystemUI/res/values-xxhdpi/dimens.xml index 26c84371c445..9bff422aec12 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/dimens.xml b/packages/SystemUI/res/values/dimens.xml index fa89fcd17797..51012a4f3a21 100644 --- a/packages/SystemUI/res/values/dimens.xml +++ b/packages/SystemUI/res/values/dimens.xml @@ -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> @@ -903,6 +900,10 @@ Keep it the same as in Launcher--> <dimen name="communal_enforced_rounded_corner_max_radius">16dp</dimen> + <!-- Width and height used to filter widgets displayed in the communal widget picker --> + <dimen name="communal_widget_picker_desired_width">464dp</dimen> + <dimen name="communal_widget_picker_desired_height">307dp</dimen> + <!-- The width/height of the unlock icon view on keyguard. --> <dimen name="keyguard_lock_height">42dp</dimen> <dimen name="keyguard_lock_padding">20dp</dimen> diff --git a/packages/SystemUI/shared/Android.bp b/packages/SystemUI/shared/Android.bp index 4e04af6d00bd..6e611fe51e8d 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 @@ android_library { "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 dfeb1f318081..d821f1982e58 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardAbsKeyInputView.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardAbsKeyInputView.java @@ -33,7 +33,7 @@ import com.android.systemui.res.R; 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/KeyguardViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardViewController.java index bc12aeebd84c..ce03072e2f0b 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardViewController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardViewController.java @@ -132,7 +132,7 @@ public interface KeyguardViewController { 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/systemui/CameraAvailabilityListener.kt b/packages/SystemUI/src/com/android/systemui/CameraAvailabilityListener.kt index 5f5cca86cce6..e8499d3ca9a9 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 @@ class CameraAvailabilityListener( } 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/SystemUI/src/com/android/systemui/CameraProtectionInfo.kt b/packages/SystemUI/src/com/android/systemui/CameraProtectionInfo.kt new file mode 100644 index 000000000000..bbab4ded3fa2 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/CameraProtectionInfo.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 + +import android.graphics.Path +import android.graphics.Rect + +data class CameraProtectionInfo( + val logicalCameraId: String, + val physicalCameraId: String?, + val cutoutProtectionPath: Path, + val cutoutBounds: Rect, +) 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 000000000000..8fe9389b7b1d --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/CameraProtectionLoader.kt @@ -0,0 +1,88 @@ +/* + * 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 + +class CameraProtectionLoader @Inject constructor(private val context: Context) { + + fun loadCameraProtectionInfoList(): List<CameraProtectionInfo> { + val list = mutableListOf<CameraProtectionInfo>() + val front = + loadCameraProtectionInfo( + R.string.config_protectedCameraId, + R.string.config_protectedPhysicalCameraId, + R.string.config_frontBuiltInDisplayCutoutProtection + ) + if (front != null) { + list.add(front) + } + val inner = + loadCameraProtectionInfo( + R.string.config_protectedInnerCameraId, + R.string.config_protectedInnerPhysicalCameraId, + R.string.config_innerBuiltInDisplayCutoutProtection + ) + if (inner != null) { + list.add(inner) + } + return list + } + + private fun loadCameraProtectionInfo( + cameraIdRes: Int, + physicalCameraIdRes: Int, + pathRes: 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() + ) + return CameraProtectionInfo( + logicalCameraId, + physicalCameraId, + protectionPath, + protectionBounds + ) + } + + 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/src/com/android/systemui/ScreenDecorations.java b/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java index d6d5c2631e14..3e03fb8dfaff 100644 --- a/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java +++ b/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java @@ -146,6 +146,7 @@ public class ScreenDecorations implements 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 @@ public class ScreenDecorations implements FaceScanningProviderFactory faceScanningFactory, ScreenDecorationsLogger logger, FacePropertyRepository facePropertyRepository, - JavaAdapter javaAdapter) { + JavaAdapter javaAdapter, + CameraProtectionLoader cameraProtectionLoader) { mContext = context; mSecureSettings = secureSettings; mCommandRegistry = commandRegistry; @@ -343,6 +345,7 @@ public class ScreenDecorations implements 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 @@ public class ScreenDecorations implements 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/src/com/android/systemui/accessibility/data/repository/ColorCorrectionRepository.kt b/packages/SystemUI/src/com/android/systemui/accessibility/data/repository/ColorCorrectionRepository.kt index c7e5b645db5f..c23a05128df5 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/data/repository/ColorCorrectionRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/accessibility/data/repository/ColorCorrectionRepository.kt @@ -61,7 +61,8 @@ constructor( .observerFlow(userHandle.identifier, SETTING_NAME) .onStart { emit(Unit) } .map { - secureSettings.getIntForUser(SETTING_NAME, userHandle.identifier) == ENABLED + secureSettings.getIntForUser(SETTING_NAME, DISABLED, userHandle.identifier) == + ENABLED } .distinctUntilChanged() .flowOn(bgCoroutineContext) diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/data/repository/ColorInversionRepository.kt b/packages/SystemUI/src/com/android/systemui/accessibility/data/repository/ColorInversionRepository.kt index 419eada91f87..852579794a27 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/data/repository/ColorInversionRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/accessibility/data/repository/ColorInversionRepository.kt @@ -61,7 +61,8 @@ constructor( .observerFlow(userHandle.identifier, SETTING_NAME) .onStart { emit(Unit) } .map { - secureSettings.getIntForUser(SETTING_NAME, userHandle.identifier) == ENABLED + secureSettings.getIntForUser(SETTING_NAME, DISABLED, userHandle.identifier) == + ENABLED } .distinctUntilChanged() .flowOn(bgCoroutineContext) 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 4a06ae9523fa..0538e7d55bfd 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_MIGRA 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 android.view.accessibility.AccessibilityManager; 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 @@ class MenuInfoRepository { } 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 @@ class MenuInfoRepository { 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/MenuViewLayer.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayer.java index f2e9531666e3..a883c009269c 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.lifecycle.Observer; 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 @@ class MenuViewLayer extends FrameLayout implements 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/accessibility/qs/QSAccessibilityModule.kt b/packages/SystemUI/src/com/android/systemui/accessibility/qs/QSAccessibilityModule.kt index 135ab35a4681..4047623a4b2f 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/qs/QSAccessibilityModule.kt +++ b/packages/SystemUI/src/com/android/systemui/accessibility/qs/QSAccessibilityModule.kt @@ -31,6 +31,10 @@ import com.android.systemui.qs.tiles.impl.colorcorrection.domain.ColorCorrection import com.android.systemui.qs.tiles.impl.colorcorrection.domain.interactor.ColorCorrectionTileDataInteractor import com.android.systemui.qs.tiles.impl.colorcorrection.domain.interactor.ColorCorrectionUserActionInteractor import com.android.systemui.qs.tiles.impl.colorcorrection.domain.model.ColorCorrectionTileModel +import com.android.systemui.qs.tiles.impl.fontscaling.domain.FontScalingTileMapper +import com.android.systemui.qs.tiles.impl.fontscaling.domain.interactor.FontScalingTileDataInteractor +import com.android.systemui.qs.tiles.impl.fontscaling.domain.interactor.FontScalingTileUserActionInteractor +import com.android.systemui.qs.tiles.impl.fontscaling.domain.model.FontScalingTileModel import com.android.systemui.qs.tiles.impl.inversion.domain.ColorInversionTileMapper import com.android.systemui.qs.tiles.impl.inversion.domain.interactor.ColorInversionTileDataInteractor import com.android.systemui.qs.tiles.impl.inversion.domain.interactor.ColorInversionUserActionInteractor @@ -93,6 +97,7 @@ interface QSAccessibilityModule { companion object { const val COLOR_CORRECTION_TILE_SPEC = "color_correction" const val COLOR_INVERSION_TILE_SPEC = "inversion" + const val FONT_SCALING_TILE_SPEC = "font_scaling" @Provides @IntoMap @@ -155,5 +160,36 @@ interface QSAccessibilityModule { stateInteractor, mapper, ) + + @Provides + @IntoMap + @StringKey(FONT_SCALING_TILE_SPEC) + fun provideFontScalingTileConfig(uiEventLogger: QsEventLogger): QSTileConfig = + QSTileConfig( + tileSpec = TileSpec.create(FONT_SCALING_TILE_SPEC), + uiConfig = + QSTileUIConfig.Resource( + iconRes = R.drawable.ic_qs_font_scaling, + labelRes = R.string.quick_settings_font_scaling_label, + ), + instanceId = uiEventLogger.getNewInstanceId(), + ) + + /** Inject FontScaling Tile into tileViewModelMap in QSModule */ + @Provides + @IntoMap + @StringKey(FONT_SCALING_TILE_SPEC) + fun provideFontScalingTileViewModel( + factory: QSTileViewModelFactory.Static<FontScalingTileModel>, + mapper: FontScalingTileMapper, + stateInteractor: FontScalingTileDataInteractor, + userActionInteractor: FontScalingTileUserActionInteractor + ): QSTileViewModel = + factory.create( + TileSpec.create(FONT_SCALING_TILE_SPEC), + userActionInteractor, + stateInteractor, + mapper, + ) } } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java index 57e308ff16e8..3397906aa6ea 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.hardware.biometrics.BiometricAuthenticator.TYPE_FACE; 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.widget.ScrollView; 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 @@ public class AuthContainerView extends LinearLayout @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 @@ public class AuthContainerView extends LinearLayout 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 @@ public class AuthContainerView extends LinearLayout } }); - 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 @@ public class AuthContainerView extends LinearLayout 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); - - // TODO(b/251476085): migrate these dependencies - if (fpProps != null && fpProps.isAnyUdfpsType()) { - view.setUdfpsAdapter(new UdfpsDialogMeasureAdapter(view, fpProps), - config.mScaleProvider); + 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); + } } + } else if (constraintBp() && Utils.isDeviceCredentialAllowed(mConfig.mPromptInfo)) { + addCredentialView(true, false); } else { mPromptSelectorInteractorProvider.get().resetPrompt(); } @@ -477,7 +495,7 @@ public class AuthContainerView extends LinearLayout vm.setAnimateContents(animateContents); ((CredentialView) mCredentialView).init(vm, this, mPanelController, animatePanel); - mFrameLayout.addView(mCredentialView); + mLayout.addView(mCredentialView); } @Override @@ -488,7 +506,9 @@ public class AuthContainerView extends LinearLayout @Override public void onOrientationChanged() { - maybeUpdatePositionForUdfps(true /* invalidate */); + if (!constraintBp()) { + maybeUpdatePositionForUdfps(true /* invalidate */); + } } @Override @@ -502,8 +522,9 @@ public class AuthContainerView extends LinearLayout 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 @@ public class AuthContainerView extends LinearLayout + 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/UdfpsControllerOverlay.kt b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt index 417608336974..4ea5f4c7af0f 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt @@ -198,11 +198,13 @@ class UdfpsControllerOverlay @JvmOverloads constructor( UdfpsTouchOverlayBinder.bind( view = this, viewModel = deviceEntryUdfpsTouchOverlayViewModel.get(), + udfpsOverlayInteractor = udfpsOverlayInteractor, ) else -> UdfpsTouchOverlayBinder.bind( view = this, viewModel = defaultUdfpsTouchOverlayViewModel.get(), + udfpsOverlayInteractor = udfpsOverlayInteractor, ) } } 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 d28dbc0ae06f..27bb023e3366 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_OTHER 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 @@ interface BiometricStatusRepository { * [NotRunning]. */ val fingerprintAuthenticationReason: Flow<AuthenticationReason> + + /** The current status of an acquired fingerprint. */ + val fingerprintAcquiredStatus: Flow<FingerprintAuthenticationStatus> } @SysUISingleton @@ -54,53 +64,53 @@ constructor( 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 @@ constructor( } .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/domain/interactor/BiometricStatusInteractor.kt b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/BiometricStatusInteractor.kt index 55a2d3d7563e..ed1557cccd01 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 android.app.ActivityTaskManager 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 @@ interface BiometricStatusInteractor { * 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 @@ constructor( } } + 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 000000000000..77cf8406725f --- /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 285ab4a800b6..efad21bda380 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.lifecycleScope 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 @@ object BiometricViewBinder { @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 @@ object BiometricViewBinder { 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 d5695f31f121..2417fe9cd333 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 @@ package com.android.systemui.biometrics.ui.binder 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 @@ object BiometricViewSizeBinder { /** 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 @@ object BiometricViewSizeBinder { } } - 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 - } + 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) + + 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) + + // 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) + } + } - // prepare for animated size transitions - for (v in viewsToHideWhenSmall) { - v.showContentOrHide(forceHide = size.isSmall) + 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 } - if (currentSize == null && size.isSmall) { - iconHolderView.alpha = 0f + 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 + } } - if ((currentSize.isSmall && size.isMedium) || size.isSmall) { - viewsToFadeInOnSizeChange.forEach { it.alpha = 0f } + + // 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 } + 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 + } + } - // 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(), - ), + 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) + } + } + + 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 */ - ) + + TransitionManager.beginDelayedTransition( + view, + autoTransition + ) + mediumConstraintSet.applyTo(view) + } + size.isLarge -> { + val autoTransition = AutoTransition() + autoTransition.setDuration( + ANIMATE_MEDIUM_TO_LARGE_DURATION_MS.toLong() + ) + + TransitionManager.beginDelayedTransition( + view, + autoTransition + ) + largeConstraintSet.applyTo(view) + } } - 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, + + 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 + } + + // 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(), ), + ) ) - ) - // TODO(b/251476085): clean up (copied from legacy) - if (view.isAttachedToWindow) { - val parent = view.parent as? ViewGroup - parent?.removeView(view) + } + 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() + currentSize = size + view.visibility = View.VISIBLE + viewModel.setIsIconViewLoaded(false) + notifyAccessibilityChanged() + } } } } @@ -244,6 +454,20 @@ private fun View.showContentOrHide(forceHide: Boolean = false) { } } +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 6e3bcf575072..2e47375b69fe 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 @@ object PromptIconViewBinder { } 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 80d37b4741e4..7b4be0220ff2 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 com.android.systemui.util.kotlin.sample 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/binder/UdfpsTouchOverlayBinder.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/UdfpsTouchOverlayBinder.kt index bb6a68b7aa2c..2e29c3b59c4c 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/UdfpsTouchOverlayBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/UdfpsTouchOverlayBinder.kt @@ -16,9 +16,11 @@ package com.android.systemui.biometrics.ui.binder +import android.util.Log import androidx.core.view.isInvisible import androidx.lifecycle.Lifecycle import androidx.lifecycle.repeatOnLifecycle +import com.android.systemui.biometrics.domain.interactor.UdfpsOverlayInteractor import com.android.systemui.biometrics.ui.view.UdfpsTouchOverlay import com.android.systemui.biometrics.ui.viewmodel.UdfpsTouchOverlayViewModel import com.android.systemui.deviceentry.shared.DeviceEntryUdfpsRefactor @@ -31,19 +33,26 @@ object UdfpsTouchOverlayBinder { /** * Updates visibility for the UdfpsTouchOverlay which controls whether the view will receive - * touches or not. + * touches or not. For some devices, this is instead handled by UdfpsOverlayInteractor, so this + * viewBinder will send the information to the interactor. */ @JvmStatic fun bind( view: UdfpsTouchOverlay, viewModel: UdfpsTouchOverlayViewModel, + udfpsOverlayInteractor: UdfpsOverlayInteractor, ) { if (DeviceEntryUdfpsRefactor.isUnexpectedlyInLegacyMode()) return view.repeatWhenAttached { repeatOnLifecycle(Lifecycle.State.CREATED) { launch { viewModel.shouldHandleTouches.collect { shouldHandleTouches -> + Log.d( + "UdfpsTouchOverlayBinder", + "[$view]: update shouldHandleTouches=$shouldHandleTouches" + ) view.isInvisible = !shouldHandleTouches + udfpsOverlayInteractor.setHandleTouches(shouldHandleTouches) } } } 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 3defec5ca48d..b7cffaf2bcde 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 @@ package com.android.systemui.biometrics.ui.viewmodel 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 @@ class PromptIconViewModel constructor( promptViewModel: PromptViewModel, private val displayStateInteractor: DisplayStateInteractor, - promptSelectorInteractor: PromptSelectorInteractor + promptSelectorInteractor: PromptSelectorInteractor, + udfpsOverlayInteractor: UdfpsOverlayInteractor, ) { /** Auth types for the UI to display. */ @@ -71,7 +75,40 @@ constructor( } 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 000000000000..d45dad616e99 --- /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 0f1340a63032..ef5c37eaa953 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 @@ constructor( 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 @@ constructor( /** 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 @@ constructor( .distinctUntilChanged() val iconViewModel: PromptIconViewModel = - PromptIconViewModel(this, displayStateInteractor, promptSelectorInteractor) + PromptIconViewModel( + this, + displayStateInteractor, + promptSelectorInteractor, + udfpsOverlayInteractor + ) private val _isIconViewLoaded = MutableStateFlow(false) @@ -244,7 +278,13 @@ constructor( !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 ce726034913f..cfda75c7851a 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.domain.interactor.DeviceEntrySideFpsOverlay 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 @@ constructor( 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 11c7a3196913..ecbd3f97f4e5 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 @@ class CameraGestureHelper @Inject constructor( } val resolveInfo: ResolveInfo? = packageManager.resolveActivityAsUser( - getStartCameraIntent(), + getStartCameraIntent(selectedUserInteractor.getSelectedUserId()), PackageManager.MATCH_DEFAULT_ONLY, selectedUserInteractor.getSelectedUserId() ) @@ -85,7 +85,7 @@ class CameraGestureHelper @Inject constructor( * @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 @@ class CameraGestureHelper @Inject constructor( * 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 1e17059d20e4..11375862142c 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 @@ package com.android.systemui.camera 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 @@ class CameraIntents { 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 @@ class CameraIntents { /** 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 a434617f2da7..b65c0d49fd96 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 @@ constructor( /** * 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 7c7b3db53b51..5c64dc645283 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 b1b348c1600a..712a3527b7c9 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 @@ constructor( 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 4184ef7c7922..cba2b47d9d01 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 f7cc34457079..3fed69e2c7f7 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.content.Context 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 @@ constructor( @Application private val context: Context, @Application private val scope: CoroutineScope, private val logger: PackageUpdateLogger, + private val systemClock: SystemClock, ) : PackageMonitor() { @AssistedFactory @@ -88,12 +90,24 @@ constructor( 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 @@ constructor( 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/domain/interactor/PackageChangeInteractor.kt b/packages/SystemUI/src/com/android/systemui/common/domain/interactor/PackageChangeInteractor.kt new file mode 100644 index 000000000000..59c19235ab02 --- /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/data/shared/model/PackageChangeModel.kt b/packages/SystemUI/src/com/android/systemui/common/shared/model/PackageChangeModel.kt index 3ae87c327e50..bcb00fde3e16 100644 --- a/packages/SystemUI/src/com/android/systemui/common/data/shared/model/PackageChangeModel.kt +++ b/packages/SystemUI/src/com/android/systemui/common/shared/model/PackageChangeModel.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,21 +14,45 @@ * limitations under the License. */ -package com.android.systemui.common.data.shared.model +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 } /** @@ -37,8 +61,11 @@ sealed interface PackageChangeModel { * 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 + 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. @@ -46,8 +73,11 @@ sealed interface PackageChangeModel { * 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 + 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 @@ -56,8 +86,11 @@ sealed interface PackageChangeModel { * 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 + 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. @@ -65,8 +98,11 @@ sealed interface PackageChangeModel { * 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 + 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 @@ -74,6 +110,12 @@ sealed interface PackageChangeModel { * * Equivalent to receiving the [Intent.ACTION_PACKAGE_CHANGED] broadcast. */ - data class Changed(override val packageName: String, override val packageUid: Int) : - PackageChangeModel + 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/communal/data/db/CommunalWidgetDao.kt b/packages/SystemUI/src/com/android/systemui/communal/data/db/CommunalWidgetDao.kt index 779446d3cd37..3b727d2a7ade 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/data/db/CommunalWidgetDao.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/data/db/CommunalWidgetDao.kt @@ -97,7 +97,7 @@ interface CommunalWidgetDao { fun getWidgets(): Flow<Map<CommunalItemRank, CommunalWidgetItem>> @Query("SELECT * FROM communal_widget_table WHERE widget_id = :id") - fun getWidgetByIdNow(id: Int): CommunalWidgetItem + fun getWidgetByIdNow(id: Int): CommunalWidgetItem? @Delete fun deleteWidgets(vararg widgets: CommunalWidgetItem) @@ -120,7 +120,9 @@ interface CommunalWidgetDao { fun updateWidgetOrder(widgetIdToPriorityMap: Map<Int, Int>) { widgetIdToPriorityMap.forEach { (id, priority) -> val widget = getWidgetByIdNow(id) - updateItemRank(widget.itemId, priority) + if (widget != null) { + updateItemRank(widget.itemId, priority) + } } } @@ -134,9 +136,13 @@ interface CommunalWidgetDao { } @Transaction - fun deleteWidgetById(widgetId: Int) { - val widget = getWidgetByIdNow(widgetId) + fun deleteWidgetById(widgetId: Int): Boolean { + val widget = + getWidgetByIdNow(widgetId) ?: // no entry to delete from db + return false + deleteItemRankById(widget.itemId) deleteWidgets(widget) + return true } } 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 cf2e33ce1df5..33edb800756d 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 @@ -16,15 +16,34 @@ package com.android.systemui.communal.data.model +import com.android.systemui.log.table.Diffable +import com.android.systemui.log.table.TableRowLogger + /** 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 (hasActiveMediaOrRecommendation != prevVal.hasActiveMediaOrRecommendation) { + row.logChange( + columnName = "isMediaActive", + value = hasActiveMediaOrRecommendation, + ) + } + + if (createdTimestampMillis != prevVal.createdTimestampMillis) { + row.logChange( + columnName = "mediaCreationTimestamp", + value = createdTimestampMillis.toString(), ) + } } } 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 e8a561b37d20..201be51b873c 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 @@ -18,6 +18,9 @@ package com.android.systemui.communal.data.repository import com.android.systemui.communal.data.model.CommunalMediaModel import com.android.systemui.dagger.SysUISingleton +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.media.controls.models.player.MediaData import com.android.systemui.media.controls.pipeline.MediaDataManager import javax.inject.Inject @@ -34,6 +37,7 @@ class CommunalMediaRepositoryImpl @Inject constructor( private val mediaDataManager: MediaDataManager, + @CommunalTableLog tableLogBuffer: TableLogBuffer, ) : CommunalMediaRepository { private val mediaDataListener = @@ -61,13 +65,18 @@ constructor( private val _mediaModel: MutableStateFlow<CommunalMediaModel> = MutableStateFlow(CommunalMediaModel.INACTIVE) - override val mediaModel: Flow<CommunalMediaModel> = _mediaModel + override val mediaModel: Flow<CommunalMediaModel> = + _mediaModel.logDiffsForTable( + tableLogBuffer = tableLogBuffer, + columnPrefix = "", + initialValue = CommunalMediaModel.INACTIVE, + ) 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 c2ea2e93ce58..0e9b32ffd12a 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 @@ -21,9 +21,15 @@ import android.content.SharedPreferences import android.content.pm.UserInfo import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.log.LogBuffer +import com.android.systemui.log.core.Logger +import com.android.systemui.log.dagger.CommunalLog +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.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 @@ -59,11 +65,21 @@ constructor( @Background private val bgDispatcher: CoroutineDispatcher, private val userRepository: UserRepository, private val userFileManager: UserFileManager, + @CommunalLog logBuffer: LogBuffer, + @CommunalTableLog tableLogBuffer: TableLogBuffer, ) : CommunalPrefsRepository { + private val logger = Logger(logBuffer, "CommunalPrefsRepositoryImpl") + override val isCtaDismissed: Flow<Boolean> = userRepository.selectedUserInfo .flatMapLatest(::observeCtaDismissState) + .logDiffsForTable( + tableLogBuffer = tableLogBuffer, + columnPrefix = "", + columnName = "isCtaDismissed", + initialValue = false, + ) .stateIn( scope = backgroundScope, started = SharingStarted.WhileSubscribed(), @@ -76,11 +92,13 @@ constructor( .edit() .putBoolean(CTA_DISMISSED_STATE, true) .apply() + + logger.i("Dismissed CTA tile") } 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/CommunalTutorialRepository.kt b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalTutorialRepository.kt index 046aaaa33342..4c06e97c89b0 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalTutorialRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalTutorialRepository.kt @@ -25,6 +25,9 @@ import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.log.LogBuffer import com.android.systemui.log.core.Logger import com.android.systemui.log.dagger.CommunalLog +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.data.repository.UserRepository import com.android.systemui.util.settings.SecureSettings import com.android.systemui.util.settings.SettingsProxyExt.observerFlow @@ -66,6 +69,7 @@ constructor( private val userRepository: UserRepository, private val secureSettings: SecureSettings, @CommunalLog logBuffer: LogBuffer, + @CommunalTableLog tableLogBuffer: TableLogBuffer, ) : CommunalTutorialRepository { companion object { @@ -94,6 +98,12 @@ constructor( settingsState .map { it.hubModeTutorialState } .filterNotNull() + .logDiffsForTable( + tableLogBuffer = tableLogBuffer, + columnPrefix = "", + columnName = "tutorialSettingState", + initialValue = HUB_MODE_TUTORIAL_NOT_STARTED, + ) .stateIn( scope = applicationScope, started = SharingStarted.WhileSubscribed(), diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalWidgetRepository.kt b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalWidgetRepository.kt index f36547b01802..2ac9d053f8d0 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalWidgetRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalWidgetRepository.kt @@ -54,8 +54,11 @@ interface CommunalWidgetRepository { configurator: WidgetConfigurator? = null ) {} - /** Delete a widget by id from app widget service and the database. */ - fun deleteWidget(widgetId: Int) {} + /** Delete a widget by id from the database. */ + fun deleteWidgetFromDb(widgetId: Int) {} + + /** Delete a widget by id from app widget host. */ + fun deleteWidgetFromHost(widgetId: Int) {} /** * Update the order of widgets in the database. @@ -143,9 +146,18 @@ constructor( } } - override fun deleteWidget(widgetId: Int) { + override fun deleteWidgetFromDb(widgetId: Int) { + bgScope.launch { + if (communalWidgetDao.deleteWidgetById(widgetId)) { + logger.i("Deleted widget with id $widgetId from DB .") + } else { + logger.w("Widget with id $widgetId cannot be deleted from DB.") + } + } + } + + override fun deleteWidgetFromHost(widgetId: Int) { bgScope.launch { - communalWidgetDao.deleteWidgetById(widgetId) appWidgetHost.deleteAppWidgetId(widgetId) logger.i("Deleted widget with id $widgetId.") } 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 80fee640974d..950ac3c3aae6 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 @@ -33,16 +33,25 @@ import com.android.systemui.communal.widgets.CommunalAppWidgetHost import com.android.systemui.communal.widgets.EditWidgetsActivityStarter import com.android.systemui.communal.widgets.WidgetConfigurator 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.log.LogBuffer +import com.android.systemui.log.core.Logger +import com.android.systemui.log.dagger.CommunalLog +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.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 import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.combine @@ -50,6 +59,8 @@ import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.shareIn /** Encapsulates business-logic related to communal mode. */ @OptIn(ExperimentalCoroutinesApi::class) @@ -57,6 +68,7 @@ import kotlinx.coroutines.flow.map class CommunalInteractor @Inject constructor( + @Application applicationScope: CoroutineScope, private val communalRepository: CommunalRepository, private val widgetRepository: CommunalWidgetRepository, private val communalPrefsRepository: CommunalPrefsRepository, @@ -65,8 +77,12 @@ constructor( userRepository: UserRepository, keyguardInteractor: KeyguardInteractor, private val appWidgetHost: CommunalAppWidgetHost, - private val editWidgetsActivityStarter: EditWidgetsActivityStarter + private val editWidgetsActivityStarter: EditWidgetsActivityStarter, + @CommunalLog logBuffer: LogBuffer, + @CommunalTableLog tableLogBuffer: TableLogBuffer, ) { + private val logger = Logger(logBuffer, "CommunalInteractor") + private val _editModeOpen = MutableStateFlow(false) /** Whether edit mode is currently open. */ @@ -85,6 +101,22 @@ constructor( or(keyguardInteractor.isKeyguardVisible, keyguardInteractor.isDreaming) ) .distinctUntilChanged() + .onEach { available -> + logger.i({ "Communal is ${if (bool1) "" else "un"}available" }) { + bool1 = available + } + } + .logDiffsForTable( + tableLogBuffer = tableLogBuffer, + columnPrefix = "", + columnName = "isCommunalAvailable", + initialValue = false, + ) + .shareIn( + scope = applicationScope, + started = SharingStarted.WhileSubscribed(), + replay = 1, + ) /** * Target scene as requested by the underlying [SceneTransitionLayout] or through @@ -129,11 +161,36 @@ constructor( .distinctUntilChanged() /** - * Flow that emits a boolean if the communal UI is showing, ie. the [desiredScene] is the - * [CommunalSceneKey.Communal]. + * Flow that emits a boolean if the communal UI is the target scene, ie. the [desiredScene] is + * the [CommunalSceneKey.Communal]. + * + * This will be true as soon as the desired scene is set programmatically or at whatever point + * during a fling that SceneTransitionLayout determines that the end state will be the communal + * scene. The value also does not change while flinging away until the target scene is no longer + * communal. + * + * If you need a flow that is only true when communal is fully showing and not in transition, + * use [isIdleOnCommunal]. */ + // TODO(b/323215860): rename to something more appropriate after cleaning up usages val isCommunalShowing: Flow<Boolean> = - communalRepository.desiredScene.map { it == CommunalSceneKey.Communal } + communalRepository.desiredScene + .map { it == CommunalSceneKey.Communal } + .distinctUntilChanged() + .onEach { showing -> + logger.i({ "Communal is ${if (bool1) "showing" else "gone"}" }) { bool1 = showing } + } + .logDiffsForTable( + tableLogBuffer = tableLogBuffer, + columnPrefix = "", + columnName = "isCommunalShowing", + initialValue = false, + ) + .shareIn( + scope = applicationScope, + started = SharingStarted.WhileSubscribed(), + replay = 1, + ) /** * Flow that emits a boolean if the communal UI is fully visible and not in transition. @@ -146,6 +203,16 @@ constructor( it is ObservableCommunalTransitionState.Idle && it.scene == CommunalSceneKey.Communal } + /** + * Flow that emits a boolean if any portion of the communal UI is visible at all. + * + * This flow will be true during any transition and when idle on the communal scene. + */ + val isCommunalVisible: Flow<Boolean> = + communalRepository.transitionState.map { + !(it is ObservableCommunalTransitionState.Idle && it.scene == CommunalSceneKey.Blank) + } + /** Callback received whenever the [SceneTransitionLayout] finishes a scene transition. */ fun onSceneChanged(newScene: CommunalSceneKey) { communalRepository.setDesiredScene(newScene) @@ -170,9 +237,15 @@ constructor( configurator: WidgetConfigurator?, ) = widgetRepository.addWidget(componentName, priority, configurator) - /** Delete a widget by id. */ - fun deleteWidget(id: Int) = widgetRepository.deleteWidget(id) + /** + * Delete a widget by id from the database. [CommunalAppWidgetHostStartable] invokes this + * function to manage the deletion from the database for uninstalled or user-deleted widgets, + * following the removal of a widget from the host. + */ + fun deleteWidgetFromDb(id: Int) = widgetRepository.deleteWidgetFromDb(id) + /** Delete a widget by id from AppWidgetHost. Called when user deletes a widget from the hub */ + fun deleteWidgetFromHost(id: Int) = widgetRepository.deleteWidgetFromHost(id) /** * Reorder the widgets. * @@ -245,7 +318,7 @@ constructor( ) // 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/CommunalTutorialInteractor.kt b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalTutorialInteractor.kt index 309c84e4e955..1404ee20cb72 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 @@ -22,6 +22,9 @@ import com.android.systemui.communal.data.repository.CommunalTutorialRepository 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.log.dagger.CommunalTableLog +import com.android.systemui.log.table.TableLogBuffer +import com.android.systemui.log.table.logDiffsForTable import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -49,6 +52,7 @@ constructor( keyguardInteractor: KeyguardInteractor, private val communalRepository: CommunalRepository, communalInteractor: CommunalInteractor, + @CommunalTableLog tableLogBuffer: TableLogBuffer, ) { /** An observable for whether the tutorial is available. */ val isTutorialAvailable: StateFlow<Boolean> = @@ -61,6 +65,12 @@ constructor( isKeyguardVisible && tutorialSettingState != Settings.Secure.HUB_MODE_TUTORIAL_COMPLETED } + .logDiffsForTable( + tableLogBuffer = tableLogBuffer, + columnPrefix = "", + columnName = "isTutorialAvailable", + initialValue = false, + ) .stateIn( scope = scope, started = SharingStarted.WhileSubscribed(), 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 c5dac775c8a8..80db5353893f 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 @@ package com.android.systemui.communal.smartspace 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 @@ constructor( // 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 @@ constructor( } private fun hasActiveSessionListeners(): Boolean { - return smartspaceViews.isNotEmpty() || - listeners.isNotEmpty() || - unfilteredListeners.isNotEmpty() + return listeners.isNotEmpty() } private fun connectSession() { @@ -188,8 +179,4 @@ constructor( 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/CommunalEditModeViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt index ebcfb8bb8209..69d55815fe01 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt @@ -21,6 +21,9 @@ import com.android.systemui.communal.domain.interactor.CommunalInteractor import com.android.systemui.communal.domain.model.CommunalContentModel import com.android.systemui.communal.shared.log.CommunalUiEvent import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.log.LogBuffer +import com.android.systemui.log.core.Logger +import com.android.systemui.log.dagger.CommunalLog import com.android.systemui.media.controls.ui.MediaHost import com.android.systemui.media.dagger.MediaModule import javax.inject.Inject @@ -29,6 +32,7 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach /** The view model for communal hub in edit mode. */ @SysUISingleton @@ -38,21 +42,27 @@ constructor( private val communalInteractor: CommunalInteractor, @Named(MediaModule.COMMUNAL_HUB) mediaHost: MediaHost, private val uiEventLogger: UiEventLogger, + @CommunalLog logBuffer: LogBuffer, ) : BaseCommunalViewModel(communalInteractor, mediaHost) { + + private val logger = Logger(logBuffer, "CommunalEditModeViewModel") + override val isEditMode = true // Only widgets are editable. The CTA tile comes last in the list and remains visible. override val communalContent: Flow<List<CommunalContentModel>> = - communalInteractor.widgetContent.map { widgets -> - widgets + listOf(CommunalContentModel.CtaTileInEditMode()) - } + communalInteractor.widgetContent + .map { widgets -> widgets + listOf(CommunalContentModel.CtaTileInEditMode()) } + .onEach { models -> + logger.d({ "Content updated: $str1" }) { str1 = models.joinToString { it.key } } + } private val _reorderingWidgets = MutableStateFlow(false) override val reorderingWidgets: StateFlow<Boolean> get() = _reorderingWidgets - override fun onDeleteWidget(id: Int) = communalInteractor.deleteWidget(id) + override fun onDeleteWidget(id: Int) = communalInteractor.deleteWidgetFromHost(id) override fun onReorderWidgets(widgetIdToPriorityMap: Map<Int, Int>) = communalInteractor.updateWidgetOrder(widgetIdToPriorityMap) 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 d7a3705248ff..40d2d1656fbc 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 @@ -21,6 +21,9 @@ import com.android.systemui.communal.domain.interactor.CommunalTutorialInteracto import com.android.systemui.communal.domain.model.CommunalContentModel import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.log.LogBuffer +import com.android.systemui.log.core.Logger +import com.android.systemui.log.dagger.CommunalLog import com.android.systemui.media.controls.ui.MediaHierarchyManager import com.android.systemui.media.controls.ui.MediaHost import com.android.systemui.media.controls.ui.MediaHostState @@ -37,6 +40,7 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch /** The default view model used for showing the communal hub. */ @@ -48,22 +52,30 @@ constructor( private val communalInteractor: CommunalInteractor, tutorialInteractor: CommunalTutorialInteractor, @Named(MediaModule.COMMUNAL_HUB) mediaHost: MediaHost, + @CommunalLog logBuffer: LogBuffer, ) : BaseCommunalViewModel(communalInteractor, mediaHost) { + + private val logger = Logger(logBuffer, "CommunalViewModel") + @OptIn(ExperimentalCoroutinesApi::class) override val communalContent: Flow<List<CommunalContentModel>> = - tutorialInteractor.isTutorialAvailable.flatMapLatest { isTutorialMode -> - if (isTutorialMode) { - return@flatMapLatest flowOf(communalInteractor.tutorialContent) + tutorialInteractor.isTutorialAvailable + .flatMapLatest { isTutorialMode -> + if (isTutorialMode) { + return@flatMapLatest flowOf(communalInteractor.tutorialContent) + } + combine( + communalInteractor.ongoingContent, + communalInteractor.widgetContent, + communalInteractor.ctaTileContent, + ) { ongoing, widgets, ctaTile, + -> + ongoing + widgets + ctaTile + } } - combine( - communalInteractor.ongoingContent, - communalInteractor.widgetContent, - communalInteractor.ctaTileContent, - ) { ongoing, widgets, ctaTile, - -> - ongoing + widgets + ctaTile + .onEach { models -> + logger.d({ "Content updated: $str1" }) { str1 = models.joinToString { it.key } } } - } private val _isPopupOnDismissCtaShowing: MutableStateFlow<Boolean> = MutableStateFlow(false) override val isPopupOnDismissCtaShowing: Flow<Boolean> = @@ -75,7 +87,7 @@ constructor( 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/CommunalAppWidgetHostStartable.kt b/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartable.kt index fb9abeb4fbcf..6fd0fbe80fd7 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartable.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartable.kt @@ -50,7 +50,7 @@ constructor( .launchIn(bgScope) appWidgetHost.appWidgetIdToRemove - .onEach { appWidgetId -> communalInteractor.deleteWidget(appWidgetId) } + .onEach { appWidgetId -> communalInteractor.deleteWidgetFromDb(appWidgetId) } .launchIn(bgScope) } diff --git a/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivity.kt b/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivity.kt index 54c709d4f053..92e8153840eb 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivity.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivity.kt @@ -32,6 +32,10 @@ import com.android.systemui.communal.shared.log.CommunalUiEvent import com.android.systemui.communal.shared.model.CommunalSceneKey import com.android.systemui.communal.ui.viewmodel.CommunalEditModeViewModel import com.android.systemui.compose.ComposeFacade.setCommunalEditWidgetActivityContent +import com.android.systemui.log.LogBuffer +import com.android.systemui.log.core.Logger +import com.android.systemui.log.dagger.CommunalLog +import com.android.systemui.res.R import javax.inject.Inject /** An Activity for editing the widgets that appear in hub mode. */ @@ -41,16 +45,21 @@ constructor( private val communalViewModel: CommunalEditModeViewModel, private var windowManagerService: IWindowManager? = null, private val uiEventLogger: UiEventLogger, - private val widgetConfiguratorFactory: WidgetConfigurationController.Factory + private val widgetConfiguratorFactory: WidgetConfigurationController.Factory, + @CommunalLog logBuffer: LogBuffer, ) : ComponentActivity() { companion object { private const val EXTRA_IS_PENDING_WIDGET_DRAG = "is_pending_widget_drag" - private const val EXTRA_FILTER_STRATEGY = "filter_strategy" - private const val FILTER_STRATEGY_GLANCEABLE_HUB = 1 + private const val EXTRA_DESIRED_WIDGET_WIDTH = "desired_widget_width" + + private const val EXTRA_DESIRED_WIDGET_HEIGHT = "desired_widget_height" + private const val TAG = "EditWidgetsActivity" const val EXTRA_PRESELECTED_KEY = "preselected_key" } + private val logger = Logger(logBuffer, "EditWidgetsActivity") + private val widgetConfigurator by lazy { widgetConfiguratorFactory.create(this) } private val addWidgetActivityLauncher: ActivityResultLauncher<Intent> = @@ -111,11 +120,19 @@ constructor( ?.let { packageName -> try { addWidgetActivityLauncher.launch( - Intent(Intent.ACTION_PICK).also { - it.setPackage(packageName) - it.putExtra( - EXTRA_FILTER_STRATEGY, - FILTER_STRATEGY_GLANCEABLE_HUB + Intent(Intent.ACTION_PICK).apply { + setPackage(packageName) + putExtra( + EXTRA_DESIRED_WIDGET_WIDTH, + resources.getDimensionPixelSize( + R.dimen.communal_widget_picker_desired_width + ) + ) + putExtra( + EXTRA_DESIRED_WIDGET_HEIGHT, + resources.getDimensionPixelSize( + R.dimen.communal_widget_picker_desired_height + ) ) } ) @@ -146,11 +163,15 @@ constructor( override fun onStart() { super.onStart() + + logger.i("Starting the communal widget editor activity") uiEventLogger.log(CommunalUiEvent.COMMUNAL_HUB_EDIT_MODE_SHOWN) } override fun onStop() { super.onStop() + + logger.i("Stopping the communal widget editor activity") uiEventLogger.log(CommunalUiEvent.COMMUNAL_HUB_EDIT_MODE_GONE) } diff --git a/packages/SystemUI/src/com/android/systemui/compose/BaseComposeFacade.kt b/packages/SystemUI/src/com/android/systemui/compose/BaseComposeFacade.kt index 947cb024f7fe..9a4dfdd5d1f6 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.bouncer.ui.viewmodel.BouncerViewModel 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 @@ interface BaseComposeFacade { 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 @@ interface BaseComposeFacade { /** 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 ae9c37aa0e7b..b35bec4eead1 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 4e935df12ac1..7c2dae34707b 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 @@ package com.android.systemui.controls.panels 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 @@ constructor( 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 @@ constructor( 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 0baa81a12e4f..9be049400962 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.ComponentName 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 @@ class SelectedComponentRepositoryImpl 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 @@ constructor( 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 20bfbc9c2ab8..3458d3ef3686 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 android.os.UserManager 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.CoroutineDispatcher 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 @@ constructor( 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 @@ constructor( 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/ReferenceSystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java index e9d1e94a63b1..dd186d624c84 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java @@ -42,7 +42,7 @@ import com.android.systemui.recents.RecentsImplementation; 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; diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt b/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt index 5ee2045865a6..a3d6ad456e54 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.MediaTttCommandLineHelper 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/display/data/repository/DisplayRepository.kt b/packages/SystemUI/src/com/android/systemui/display/data/repository/DisplayRepository.kt index c93b8e1a48f2..1230156953ee 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 @@ constructor( 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/doze/DozeDockHandler.java b/packages/SystemUI/src/com/android/systemui/doze/DozeDockHandler.java index c331164f001b..537cacd13a96 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 @@ public class DozeDockHandler implements DozeMachine.Part { } 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 e04a5052199c..e74814adb1b0 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.controls.ControlsProviderService 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 @@ constructor( 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 @@ constructor( 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 @@ constructor( if (taskFragmentInfo.isEmpty) { logger.d("Finishing dream due to TaskFragment being empty") finish() + homeControlsComponentInteractor.onTaskFragmentEmpty() } } @@ -84,5 +101,19 @@ constructor( 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 91e0547ff93d..f0067dcb7fe2 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.SysUISingleton 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 @@ class HomeControlsComponentInteractor @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 @@ constructor( 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/dreams/touch/CommunalTouchHandler.java b/packages/SystemUI/src/com/android/systemui/dreams/touch/CommunalTouchHandler.java index c9b56a2ebd9a..05279fcdf51c 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/touch/CommunalTouchHandler.java +++ b/packages/SystemUI/src/com/android/systemui/dreams/touch/CommunalTouchHandler.java @@ -23,7 +23,6 @@ import android.graphics.Region; import android.view.GestureDetector; import android.view.MotionEvent; -import com.android.systemui.statusbar.NotificationShadeWindowController; import com.android.systemui.statusbar.phone.CentralSurfaces; import java.util.Optional; @@ -34,17 +33,14 @@ import javax.inject.Named; /** {@link DreamTouchHandler} responsible for handling touches to open communal hub. **/ public class CommunalTouchHandler implements DreamTouchHandler { private final int mInitiationWidth; - private final NotificationShadeWindowController mNotificationShadeWindowController; private final Optional<CentralSurfaces> mCentralSurfaces; @Inject public CommunalTouchHandler( Optional<CentralSurfaces> centralSurfaces, - NotificationShadeWindowController notificationShadeWindowController, @Named(COMMUNAL_GESTURE_INITIATION_WIDTH) int initiationWidth) { mInitiationWidth = initiationWidth; mCentralSurfaces = centralSurfaces; - mNotificationShadeWindowController = notificationShadeWindowController; } @Override @@ -60,9 +56,8 @@ public class CommunalTouchHandler implements DreamTouchHandler { } private void handleSessionStart(CentralSurfaces surfaces, TouchSession session) { - // Force the notification shade window open (otherwise the hub won't show while swiping). - mNotificationShadeWindowController.setForcePluginOpen(true, this); - + // Notification shade window has its own logic to be visible if the hub is open, no need to + // do anything here other than send touch events over. session.registerInputListener(ev -> { surfaces.handleDreamTouch((MotionEvent) ev); if (ev != null && ((MotionEvent) ev).getAction() == MotionEvent.ACTION_UP) { diff --git a/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt b/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt index df0566e246a8..41ce3fd11e8a 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.crossAppPoliteNotifications 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 @@ class FlagDependencies @Inject constructor(featureFlags: FeatureFlagsClassic, ha // 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 @@ class FlagDependencies @Inject constructor(featureFlags: FeatureFlagsClassic, ha 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/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt index c69c9ef93761..6eff79284847 100644 --- a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt +++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt @@ -585,10 +585,6 @@ object Flags { @JvmField val SHARE_WIFI_QS_BUTTON = releasedFlag("share_wifi_qs_button") - // TODO(b/287205379): Tracking bug - @JvmField - val QS_CONTAINER_GRAPH_OPTIMIZER = releasedFlag( "qs_container_graph_optimizer") - /** Enable showing a dialog when clicking on Quick Settings bluetooth tile. */ @JvmField val BLUETOOTH_QS_TILE_DIALOG = releasedFlag("bluetooth_qs_tile_dialog") 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 d5f082a2566f..72a81cbac9d5 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 @@ package com.android.systemui.keyboard.stickykeys.shared.model @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 000000000000..3ed58a7fe5ae --- /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 c3a618d29c76..842fd04bfcc5 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 @@ package com.android.systemui.keyboard.stickykeys.ui 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 @@ class StickyKeysIndicatorCoordinator @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 @@ constructor( 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 9b83b75cec22..ee3706a3ba62 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 @@ public class KeyguardIndication { } /** - * 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 b5f9c69a71b0..4cabd70cb142 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.RemoteAnimationAdapter; 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 @@ public class KeyguardService extends Service { 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 class KeyguardService extends Service { 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 @@ public class KeyguardService extends Service { 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 @@ public class KeyguardService extends Service { } private void finish(IBinder transition) throws RemoteException { - IRemoteTransitionFinishedCallback finishCallback = null; + final IRemoteTransitionFinishedCallback finishCallback; SurfaceControl.Transaction finishTransaction = null; synchronized (mLeashMap) { diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt index 7f43fac575cb..abe49eefda99 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 @@ package com.android.systemui.keyguard 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.keyguard.dagger.KeyguardStatusViewComponent 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.KeyguardBlueprintViewModel 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 @@ constructor( 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 @@ constructor( 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 794befa3725d..4766a84b97ee 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java @@ -45,12 +45,12 @@ import static com.android.systemui.util.kotlin.JavaAdapterKt.collectFlow; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.ValueAnimator; +import android.annotation.SuppressLint; import android.app.AlarmManager; import android.app.BroadcastOptions; import android.app.IActivityTaskManager; import android.app.PendingIntent; import android.app.StatusBarManager; -import android.app.WallpaperManager; import android.app.WindowConfiguration; import android.app.trust.TrustManager; import android.content.BroadcastReceiver; @@ -174,6 +174,8 @@ import com.android.systemui.util.time.SystemClock; import com.android.systemui.wallpapers.data.repository.WallpaperRepository; import com.android.wm.shell.keyguard.KeyguardTransitions; +import dagger.Lazy; + import java.io.PrintWriter; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -183,7 +185,6 @@ import java.util.Objects; import java.util.concurrent.Executor; import java.util.function.Consumer; -import dagger.Lazy; import kotlinx.coroutines.CoroutineDispatcher; /** @@ -326,7 +327,6 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, private AlarmManager mAlarmManager; private AudioManager mAudioManager; private StatusBarManager mStatusBarManager; - private WallpaperManager mWallpaperManager; private final IStatusBarService mStatusBarService; private final IBinder mStatusBarDisableToken = new Binder(); private final UserTracker mUserTracker; @@ -356,13 +356,13 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, private final SecureSettings mSecureSettings; private final SystemSettings mSystemSettings; private final SystemClock mSystemClock; - private SystemPropertiesHelper mSystemPropertiesHelper; + private final SystemPropertiesHelper mSystemPropertiesHelper; /** * Used to keep the device awake while to ensure the keyguard finishes opening before * we sleep. */ - private PowerManager.WakeLock mShowKeyguardWakeLock; + private final PowerManager.WakeLock mShowKeyguardWakeLock; private final Lazy<KeyguardViewController> mKeyguardViewControllerLazy; @@ -405,13 +405,13 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, private boolean mWakeAndUnlocking = false; /** - * Helps remember whether the screen has turned on since the last time - * it turned off due to timeout. see {@link #onScreenTurnedOff(int)} + * Helps remember whether the screen has turned on since the last time it turned off due to + * timeout. See {@link #onScreenTurnedOff} */ private int mDelayedShowingSequence; /** - * Similar to {@link #mDelayedProfileShowingSequence}, but it is for profile case. + * Similar to {@link #mDelayedShowingSequence}, but it is for profile case. */ private int mDelayedProfileShowingSequence; @@ -439,12 +439,12 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, private boolean mGoingToSleep; // last known state of the cellular connection - private String mPhoneState = TelephonyManager.EXTRA_STATE_IDLE; + private final String mPhoneState = TelephonyManager.EXTRA_STATE_IDLE; /** * Whether a hide is pending and we are just waiting for #startKeyguardExitAnimation to be * called. - * */ + */ private boolean mHiding; /** @@ -1088,8 +1088,7 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, public void onAnimationStart(int transit, RemoteAnimationTarget[] apps, RemoteAnimationTarget[] wallpapers, RemoteAnimationTarget[] nonApps, IRemoteAnimationFinishedCallback finishedCallback) throws RemoteException { - if (!handleOnAnimationStart( - transit, apps, wallpapers, nonApps, finishedCallback)) { + if (!handleOnAnimationStart(apps, finishedCallback)) { // Usually we rely on animation completion to synchronize occluded status, // but there was no animation to play, so just update it now. setOccluded(true /* isOccluded */, false /* animate */); @@ -1097,9 +1096,8 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, } } - private boolean handleOnAnimationStart(int transit, RemoteAnimationTarget[] apps, - RemoteAnimationTarget[] wallpapers, RemoteAnimationTarget[] nonApps, - IRemoteAnimationFinishedCallback finishedCallback) throws RemoteException { + private boolean handleOnAnimationStart(RemoteAnimationTarget[] apps, + IRemoteAnimationFinishedCallback finishedCallback) { if (apps == null || apps.length == 0 || apps[0] == null) { Log.d(TAG, "No apps provided to the OccludeByDream runner; " + "skipping occluding animation."); @@ -1107,8 +1105,8 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, } final RemoteAnimationTarget primary = apps[0]; - final boolean isDream = (apps[0].taskInfo != null - && apps[0].taskInfo.topActivityType + final boolean isDream = (primary.taskInfo != null + && primary.taskInfo.topActivityType == WindowConfiguration.ACTIVITY_TYPE_DREAM); if (!isDream) { Log.w(TAG, "The occluding app isn't Dream; " @@ -1322,9 +1320,9 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, } private DeviceConfigProxy mDeviceConfig; - private DozeParameters mDozeParameters; - private SelectedUserInteractor mSelectedUserInteractor; - private KeyguardInteractor mKeyguardInteractor; + private final DozeParameters mDozeParameters; + private final SelectedUserInteractor mSelectedUserInteractor; + private final KeyguardInteractor mKeyguardInteractor; @VisibleForTesting protected FoldGracePeriodProvider mFoldGracePeriodProvider = new FoldGracePeriodProvider(); @@ -1346,14 +1344,12 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, private final Lazy<KeyguardUnlockAnimationController> mKeyguardUnlockAnimationControllerLazy; private final InteractionJankMonitor mInteractionJankMonitor; private boolean mWallpaperSupportsAmbientMode; - private ScreenOnCoordinator mScreenOnCoordinator; private final KeyguardTransitions mKeyguardTransitions; - private Lazy<ActivityLaunchAnimator> mActivityLaunchAnimator; - private Lazy<ScrimController> mScrimControllerLazy; - private IActivityTaskManager mActivityTaskManagerService; + private final Lazy<ActivityLaunchAnimator> mActivityLaunchAnimator; + private final Lazy<ScrimController> mScrimControllerLazy; + private final IActivityTaskManager mActivityTaskManagerService; - private FeatureFlags mFeatureFlags; private final UiEventLogger mUiEventLogger; private final SessionTracker mSessionTracker; private final CoroutineDispatcher mMainDispatcher; @@ -1361,7 +1357,7 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, mDreamingToLockscreenTransitionViewModel; private RemoteAnimationTarget mRemoteAnimationTarget; - private Lazy<WindowManagerLockscreenVisibilityManager> mWmLockscreenVisibilityManager; + private final Lazy<WindowManagerLockscreenVisibilityManager> mWmLockscreenVisibilityManager; /** * Injected constructor. See {@link KeyguardModule}. @@ -1433,7 +1429,6 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, mShadeController = shadeControllerLazy; dumpManager.registerDumpable(this); mDeviceConfig = deviceConfig; - mScreenOnCoordinator = screenOnCoordinator; mKeyguardTransitions = keyguardTransitions; mNotificationShadeWindowControllerLazy = notificationShadeWindowControllerLazy; mShowHomeOverLockscreen = mDeviceConfig.getBoolean( @@ -1445,9 +1440,8 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, mHandler::post, mOnPropertiesChangedListener); mInGestureNavigationMode = - QuickStepContract.isGesturalMode(navigationModeController.addListener(mode -> { - mInGestureNavigationMode = QuickStepContract.isGesturalMode(mode); - })); + QuickStepContract.isGesturalMode(navigationModeController.addListener(mode -> + mInGestureNavigationMode = QuickStepContract.isGesturalMode(mode))); mDozeParameters = dozeParameters; mSelectedUserInteractor = selectedUserInteractor; mKeyguardInteractor = keyguardInteractor; @@ -1474,7 +1468,6 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, mDreamOpenAnimationDuration = (int) DREAMING_ANIMATION_DURATION_MS; - mFeatureFlags = featureFlags; mUiEventLogger = uiEventLogger; mSessionTracker = sessionTracker; @@ -1578,14 +1571,6 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, this::setWallpaperSupportsAmbientMode); } - // TODO(b/273443374) remove, temporary util to get a feature flag - private WallpaperManager getWallpaperManager() { - if (mWallpaperManager == null) { - mWallpaperManager = mContext.getSystemService(WallpaperManager.class); - } - return mWallpaperManager; - } - @Override public void start() { synchronized (this) { @@ -1611,11 +1596,11 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, ViewRootImpl viewRootImpl = mKeyguardViewControllerLazy.get().getViewRootImpl(); if (viewRootImpl != null) { - collectFlow(viewRootImpl.getView(), - mDreamingToLockscreenTransitionViewModel.get().getDreamOverlayAlpha(), + DreamingToLockscreenTransitionViewModel viewModel = + mDreamingToLockscreenTransitionViewModel.get(); + collectFlow(viewRootImpl.getView(), viewModel.getDreamOverlayAlpha(), getRemoteSurfaceAlphaApplier(), mMainDispatcher); - collectFlow(viewRootImpl.getView(), - mDreamingToLockscreenTransitionViewModel.get().getTransitionEnded(), + collectFlow(viewRootImpl.getView(), viewModel.getTransitionEnded(), getFinishedCallbackConsumer(), mMainDispatcher); } } @@ -2304,6 +2289,7 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, showKeyguard(options); } + @SuppressLint("MissingPermission") private void lockProfile(int userId) { mTrustManager.setDeviceLockedForUser(userId, true); } @@ -2497,13 +2483,12 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, }; /** - * This handler will be associated with the policy thread, which will also - * be the UI thread of the keyguard. Since the apis of the policy, and therefore - * this class, can be called by other threads, any action that directly - * interacts with the keyguard ui should be posted to this handler, rather - * than called directly. + * This handler will be associated with the policy thread, which will also be the UI thread of + * the keyguard. Since the apis of the policy, and therefore this class, can be called by other + * threads, any action that directly interacts with the keyguard ui should be posted to this + * handler, rather than called directly. */ - private Handler mHandler = new Handler(Looper.myLooper(), null, true /*async*/) { + private final Handler mHandler = new Handler(Looper.myLooper(), null, true /*async*/) { @Override public void handleMessage(Message msg) { String message = ""; @@ -2766,7 +2751,7 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, try { mActivityTaskManagerService.setLockScreenShown(showing, aodShowing); - } catch (RemoteException e) { + } catch (RemoteException ignored) { } }); } @@ -2790,9 +2775,8 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, if (!mSystemReady) { if (DEBUG) Log.d(TAG, "ignoring handleShow because system is not ready."); return; - } else { - if (DEBUG) Log.d(TAG, "handleShow"); } + if (DEBUG) Log.d(TAG, "handleShow"); mKeyguardExitAnimationRunner = null; mWakeAndUnlocking = false; @@ -2851,6 +2835,7 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, } private final Runnable mKeyguardGoingAwayRunnable = new Runnable() { + @SuppressLint("MissingPermission") @Override public void run() { Trace.beginSection("KeyguardViewMediator.mKeyGuardGoingAwayRunnable"); @@ -2925,24 +2910,13 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, return; } - final String reasonDescription; - - switch(reason) { - case WakeAndUnlockUpdateReason.FULFILL: - reasonDescription = "fulfilling existing request"; - break; - case WakeAndUnlockUpdateReason.HIDE: - reasonDescription = "hiding keyguard"; - break; - case WakeAndUnlockUpdateReason.SHOW: - reasonDescription = "showing keyguard"; - break; - case WakeAndUnlockUpdateReason.WAKE_AND_UNLOCK: - reasonDescription = "waking to unlock"; - break; - default: - throw new IllegalStateException("Unexpected value: " + reason); - } + final String reasonDescription = switch (reason) { + case WakeAndUnlockUpdateReason.FULFILL -> "fulfilling existing request"; + case WakeAndUnlockUpdateReason.HIDE -> "hiding keyguard"; + case WakeAndUnlockUpdateReason.SHOW -> "showing keyguard"; + case WakeAndUnlockUpdateReason.WAKE_AND_UNLOCK -> "waking to unlock"; + default -> throw new IllegalStateException("Unexpected value: " + reason); + }; final boolean unsetUnfulfilled = !updatedValue && reason != WakeAndUnlockUpdateReason.FULFILL; @@ -3057,7 +3031,7 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, IRemoteAnimationFinishedCallback callback = new IRemoteAnimationFinishedCallback() { @Override - public void onAnimationFinished() throws RemoteException { + public void onAnimationFinished() { if (!KeyguardWmStateRefactor.isEnabled()) { try { finishedCallback.onAnimationFinished(); @@ -3542,11 +3516,6 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, /** * Registers the CentralSurfaces to which the Keyguard View is mounted. * - * @param centralSurfaces - * @param panelView - * @param biometricUnlockController - * @param notificationContainer - * @param bypassController * @return the View Controller for the Keyguard View this class is mediating. */ public KeyguardViewController registerCentralSurfaces(CentralSurfaces centralSurfaces, @@ -3773,9 +3742,7 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, } }); updateInputRestrictedLocked(); - mUiBgExecutor.execute(() -> { - mTrustManager.reportKeyguardShowingChanged(); - }); + mUiBgExecutor.execute(mTrustManager::reportKeyguardShowingChanged); } private void notifyTrustedChangedLocked(boolean trusted) { 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 e16f8dcbb00e..70da3e7ad1fa 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java +++ b/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java @@ -90,10 +90,12 @@ import dagger.multibindings.IntoMap; import java.util.concurrent.Executor; import kotlinx.coroutines.CoroutineDispatcher; +import kotlinx.coroutines.ExperimentalCoroutinesApi; /** * Dagger Module providing keyguard. */ +@ExperimentalCoroutinesApi @Module(subcomponents = { KeyguardQsUserSwitchComponent.class, KeyguardStatusBarViewComponent.class, @@ -115,7 +117,7 @@ public interface KeyguardModule { */ @Provides @SysUISingleton - public static KeyguardViewMediator newKeyguardViewMediator( + static KeyguardViewMediator newKeyguardViewMediator( Context context, UiEventLogger uiEventLogger, SessionTracker sessionTracker, 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 bbdd90375ece..3e6e3b79a820 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 @@ constructor( @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 9a13558d3327..b152eea63028 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 android.hardware.biometrics.BiometricSourceType 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 @@ constructor( 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 @@ constructor( sendUpdateIfFingerprint( biometricSourceType, AcquiredFingerprintAuthenticationStatus( - acquireInfo, + AuthenticationReason.DeviceEntryAuthentication, + acquireInfo ), ) } 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 b1a2297526ce..e017129bd5c5 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 @@ constructor( 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/FromLockscreenTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractor.kt index deb70b7f1086..1da0a0e5bd8c 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.flags.FeatureFlags 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.shade.data.repository.ShadeRepository 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 @@ constructor( 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 @@ constructor( 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 @@ constructor( 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 @@ constructor( 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 e7d74a5c2056..8ec831c35140 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.data.repository.KeyguardSurfaceBehindReposi 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 @@ constructor( 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 @@ constructor( } /** - * 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/KeyguardSurfaceBehindInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardSurfaceBehindInteractor.kt index 922baa3611cc..8784723a3909 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,75 @@ 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.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, ) { - - @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, + ) { startedStep, currentState -> + // If we're in transition to GONE, special unlock animation params apply. + if (startedStep.to == KeyguardState.GONE && currentState != KeyguardState.GONE) { + 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/SwipeToDismissInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/SwipeToDismissInteractor.kt new file mode 100644 index 000000000000..86e41154205e --- /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/shared/ComposeLockscreen.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/ComposeLockscreen.kt new file mode 100644 index 000000000000..7f0b483919b3 --- /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 cc385a8eea85..474de77f09ab 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_ACQ 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 @@ data class HelpFingerprintAuthenticationStatus( ) : 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 7fb5cfd8f930..aad8a4bbd77f 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 @@ data class KeyguardSurfaceBehindModel( * 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/src/com/android/systemui/keyguard/shared/model/LockscreenSceneBlueprint.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/LockscreenSceneBlueprint.kt new file mode 100644 index 000000000000..2eafb83dc605 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/LockscreenSceneBlueprint.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.keyguard.shared.model + +/** + * 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 +} 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 48092c6374e0..789d30ff1a31 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 @@ -191,6 +191,7 @@ object KeyguardRootViewBinder { .collect { y -> childViews[burnInLayerId]?.translationY = y childViews[largeClockId]?.translationY = y + childViews[aodNotificationIconContainerId]?.translationY = y } } @@ -200,6 +201,7 @@ object KeyguardRootViewBinder { .collect { x -> childViews[burnInLayerId]?.translationX = x childViews[largeClockId]?.translationX = x + childViews[aodNotificationIconContainerId]?.translationX = x } } @@ -219,6 +221,10 @@ object KeyguardRootViewBinder { // 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 } } } 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 8587022a24b7..fb6efd3310f6 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 @@ constructor( SpringAnimation(animatedTranslationY).apply { spring = SpringForce().apply { - stiffness = 200f - dampingRatio = 1f + stiffness = 275f + dampingRatio = 0.98f } addUpdateListener { _, _, _ -> applyToSurfaceBehind() } addEndListener { _, _, _, _ -> @@ -84,7 +84,7 @@ constructor( 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 @@ constructor( // 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/view/layout/sections/AodBurnInSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodBurnInSection.kt index 3d36eb03a1bc..9a1fcc1a6a51 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 @@ constructor( 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/NotificationStackScrollLayoutSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/NotificationStackScrollLayoutSection.kt index a651c10d1c35..52d94a087110 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 @@ constructor( 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 @@ constructor( if (!KeyguardShadeMigrationNssl.isEnabled) { return } - disposableHandle?.dispose() - disposableHandle = + + disposeHandles() + disposableHandles.add( SharedNotificationContainerBinder.bind( sharedNotificationContainer, sharedNotificationContainerViewModel, @@ -112,19 +113,28 @@ constructor( 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/AodAlphaViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodAlphaViewModel.kt index d4ea728bbffb..9cf3c955b35c 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodAlphaViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodAlphaViewModel.kt @@ -28,7 +28,6 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.merge -import kotlinx.coroutines.flow.onStart /** Models UI state for the alpha of the AOD (always-on display). */ @SysUISingleton @@ -43,15 +42,13 @@ constructor( /** The alpha level for the entire lockscreen while in AOD. */ val alpha: Flow<Float> = combine( - keyguardTransitionInteractor.transitionValue(KeyguardState.GONE).onStart { - emit(0f) - }, + keyguardTransitionInteractor.currentKeyguardState, merge( keyguardInteractor.keyguardAlpha, occludedToLockscreenTransitionViewModel.lockscreenAlpha, ) - ) { transitionToGone, alpha -> - if (transitionToGone == 1f) { + ) { currentKeyguardState, alpha -> + if (currentKeyguardState == KeyguardState.GONE) { // Ensures content is not visible when in GONE state 0f } else { diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryForegroundViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryForegroundViewModel.kt index 310ec95a22df..ad6a36c71e39 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryForegroundViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryForegroundViewModel.kt @@ -27,6 +27,7 @@ import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.ui.view.DeviceEntryIconView import com.android.systemui.res.R import javax.inject.Inject +import kotlin.math.roundToInt import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine @@ -74,7 +75,18 @@ constructor( isTransitionToAod && isUdfps } .distinctUntilChanged() - private val padding: Flow<Int> = udfpsOverlayInteractor.iconPadding + + private val padding: Flow<Int> = + deviceEntryUdfpsInteractor.isUdfpsSupported.flatMapLatest { udfpsSupported -> + if (udfpsSupported) { + udfpsOverlayInteractor.iconPadding + } else { + configurationInteractor.scaleForResolution.map { scale -> + (context.resources.getDimensionPixelSize(R.dimen.lock_icon_padding) * scale) + .roundToInt() + } + } + } val viewModel: Flow<ForegroundIconViewModel> = combine( 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 ba04fd3741a4..f5e61355df37 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 @@ -67,6 +67,8 @@ constructor( duration = 500.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/SideFpsProgressBarViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/SideFpsProgressBarViewModel.kt index ca9c8571f6b9..67c42f0fe343 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 android.graphics.Point 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.domain.interactor.KeyguardInteractor 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.asStateFlow 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 @@ class SideFpsProgressBarViewModel @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 @@ constructor( 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 @@ constructor( 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 @@ constructor( 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/CommunalTableLog.kt b/packages/SystemUI/src/com/android/systemui/log/dagger/CommunalTableLog.kt new file mode 100644 index 000000000000..fe7dc4bdf59f --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/log/dagger/CommunalTableLog.kt @@ -0,0 +1,25 @@ +/* + * 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 javax.inject.Qualifier + +/** A [TableLogBuffer] for communal-related logging. */ +@Qualifier +@MustBeDocumented +@Retention(AnnotationRetention.RUNTIME) +annotation class CommunalTableLog 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 3e0094081638..ac579d6d2491 100644 --- a/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java +++ b/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java @@ -579,6 +579,16 @@ public class LogModule { return factory.create("CommunalLog", 250); } + /** + * Provides a {@link TableLogBuffer} for communal-related logs. + */ + @Provides + @SysUISingleton + @CommunalTableLog + public static TableLogBuffer provideCommunalTableLogBuffer(TableLogBufferFactory factory) { + return factory.create("CommunalTableLog", 250); + } + /** Provides a {@link LogBuffer} for display metrics related logs. */ @Provides @SysUISingleton @@ -618,4 +628,13 @@ public class LogModule { 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 000000000000..939dab2ecefa --- /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/controls/pipeline/LocalMediaManagerFactory.kt b/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/LocalMediaManagerFactory.kt index 785a1e818465..1d3cfd2569aa 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 @@ constructor( 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/navigationbar/NavBarButtonClickLogger.kt b/packages/SystemUI/src/com/android/systemui/navigationbar/NavBarButtonClickLogger.kt new file mode 100644 index 000000000000..408acf3ce1a1 --- /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 7d13397b374c..dfe41eb9f7f2 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 android.view.accessibility.AccessibilityManager; 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 @@ public final class NavBarHelper implements // 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 068e5fd61ea4..95b75ac7a875 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.InsetsFrameProvider; 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 @@ public class NavigationBar extends ViewController<NavigationBarView> implements 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 @@ public class NavigationBar extends ViewController<NavigationBarView> implements UserContextProvider userContextProvider, WakefulnessLifecycle wakefulnessLifecycle, TaskStackChangeListeners taskStackChangeListeners, - DisplayTracker displayTracker) { + DisplayTracker displayTracker, + NavBarButtonClickLogger navBarButtonClickLogger) { super(navigationBarView); mFrame = navigationBarFrame; mContext = context; @@ -601,6 +599,7 @@ public class NavigationBar extends ViewController<NavigationBarView> implements mTaskStackChangeListeners = taskStackChangeListeners; mDisplayTracker = displayTracker; mEdgeBackGestureHandler = navBarHelper.getEdgeBackGestureHandler(); + mNavBarButtonClickLogger = navBarButtonClickLogger; mNavColorSampleMargin = getResources() .getDimensionPixelSize(R.dimen.navigation_handle_sample_horizontal_margin); @@ -1276,6 +1275,10 @@ public class NavigationBar extends ViewController<NavigationBarView> implements ButtonDispatcher homeButton = mView.getHomeButton(); homeButton.setOnTouchListener(this::onHomeTouch); + homeButton.setNavBarButtonClickLogger(mNavBarButtonClickLogger); + + ButtonDispatcher backButton = mView.getBackButton(); + backButton.setNavBarButtonClickLogger(mNavBarButtonClickLogger); reconfigureHomeLongClick(); @@ -1388,6 +1391,8 @@ public class NavigationBar extends ViewController<NavigationBarView> implements } private void onRecentsClick(View v) { + mNavBarButtonClickLogger.logRecentsButtonClick(); + if (LatencyTracker.isEnabled(mContext)) { LatencyTracker.getInstance(mContext).onActionStart( LatencyTracker.ACTION_TOGGLE_RECENTS); @@ -1397,6 +1402,7 @@ public class NavigationBar extends ViewController<NavigationBarView> implements } 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 @@ public class NavigationBar extends ViewController<NavigationBarView> implements } 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/buttons/ButtonDispatcher.java b/packages/SystemUI/src/com/android/systemui/navigationbar/buttons/ButtonDispatcher.java index 5739abcae7eb..fc37b9f0979d 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.AnimatorListenerAdapter; 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 @@ public class ButtonDispatcher { private boolean mVertical; private ValueAnimator mFadeAnimator; private AccessibilityDelegate mAccessibilityDelegate; + private NavBarButtonClickLogger mNavBarButtonClickLogger; private final ValueAnimator.AnimatorUpdateListener mAlphaListener = animation -> setAlpha( @@ -341,4 +345,36 @@ public class ButtonDispatcher { */ 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 df6843d31ab1..dbe87eafef7b 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.UiEventLoggerImpl; 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 @@ public class KeyButtonView extends ImageView implements ButtonInterface { 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 @@ public class KeyButtonView extends ImageView implements ButtonInterface { mOnClickListener = onClickListener; } + public void setNavBarButtonClickLogger(NavBarButtonClickLogger navBarButtonClickLogger) { + mNavBarButtonClickLogger = navBarButtonClickLogger; + } + public void loadAsync(Icon icon) { new AsyncTask<Icon, Void, Drawable>() { @Override @@ -389,11 +395,19 @@ public class KeyButtonView extends ImageView implements ButtonInterface { 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/qs/QSPanelControllerBase.java b/packages/SystemUI/src/com/android/systemui/qs/QSPanelControllerBase.java index ac0bd29e22d6..1c37510fde34 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSPanelControllerBase.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSPanelControllerBase.java @@ -77,6 +77,7 @@ public abstract class QSPanelControllerBase<T extends QSPanel> extends ViewContr private Consumer<Boolean> mMediaVisibilityChangedListener; @Orientation private int mLastOrientation; + private int mLastScreenLayout; private String mCachedSpecs = ""; @Nullable private QSTileRevealController mQsTileRevealController; @@ -93,15 +94,19 @@ public abstract class QSPanelControllerBase<T extends QSPanel> extends ViewContr public void onConfigurationChange(Configuration newConfig) { final boolean previousSplitShadeState = mShouldUseSplitNotificationShade; final int previousOrientation = mLastOrientation; + final int previousScreenLayout = mLastScreenLayout; mShouldUseSplitNotificationShade = mSplitShadeStateController .shouldUseSplitNotificationShade(getResources()); mLastOrientation = newConfig.orientation; + mLastScreenLayout = newConfig.screenLayout; mQSLogger.logOnConfigurationChanged( /* oldOrientation= */ previousOrientation, /* newOrientation= */ mLastOrientation, /* oldShouldUseSplitShade= */ previousSplitShadeState, /* newShouldUseSplitShade= */ mShouldUseSplitNotificationShade, + /* oldScreenLayout= */ previousScreenLayout, + /* newScreenLayout= */ mLastScreenLayout, /* containerName= */ mView.getDumpableTag()); switchTileLayoutIfNeeded(); @@ -198,6 +203,7 @@ public abstract class QSPanelControllerBase<T extends QSPanel> extends ViewContr mView.addOnConfigurationChangedListener(mOnConfigurationChangedListener); setTiles(); mLastOrientation = getResources().getConfiguration().orientation; + mLastScreenLayout = getResources().getConfiguration().screenLayout; mQSLogger.logOnViewAttached(mLastOrientation, mView.getDumpableTag()); switchTileLayout(true); @@ -443,11 +449,13 @@ public abstract class QSPanelControllerBase<T extends QSPanel> extends ViewContr } boolean shouldUseHorizontalLayout() { - if (mShouldUseSplitNotificationShade) { + if (mShouldUseSplitNotificationShade) { return false; } return mUsingMediaPlayer && mMediaHost.getVisible() - && mLastOrientation == Configuration.ORIENTATION_LANDSCAPE; + && mLastOrientation == Configuration.ORIENTATION_LANDSCAPE + && (mLastScreenLayout & Configuration.SCREENLAYOUT_LONG_MASK) + == Configuration.SCREENLAYOUT_LONG_YES; } private void logTiles() { diff --git a/packages/SystemUI/src/com/android/systemui/qs/logging/QSLogger.kt b/packages/SystemUI/src/com/android/systemui/qs/logging/QSLogger.kt index 38e797228d4f..b515ce07cc02 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/logging/QSLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/logging/QSLogger.kt @@ -19,6 +19,8 @@ package com.android.systemui.qs.logging import android.content.res.Configuration.ORIENTATION_LANDSCAPE import android.content.res.Configuration.ORIENTATION_PORTRAIT import android.content.res.Configuration.Orientation +import android.content.res.Configuration.SCREENLAYOUT_LONG_NO +import android.content.res.Configuration.SCREENLAYOUT_LONG_YES import android.service.quicksettings.Tile import android.view.View import com.android.systemui.log.ConstantStringsLogger @@ -266,8 +268,10 @@ constructor( fun logOnConfigurationChanged( @Orientation oldOrientation: Int, @Orientation newOrientation: Int, - newShouldUseSplitShade: Boolean, oldShouldUseSplitShade: Boolean, + newShouldUseSplitShade: Boolean, + oldScreenLayout: Int, + newScreenLayout: Int, containerName: String ) { configChangedBuffer.log( @@ -277,6 +281,8 @@ constructor( str1 = containerName int1 = oldOrientation int2 = newOrientation + long1 = oldScreenLayout.toLong() + long2 = newScreenLayout.toLong() bool1 = oldShouldUseSplitShade bool2 = newShouldUseSplitShade }, @@ -284,6 +290,8 @@ constructor( "config change: " + "$str1 orientation=${toOrientationString(int2)} " + "(was ${toOrientationString(int1)}), " + + "screen layout=${toScreenLayoutString(long1.toInt())} " + + "(was ${toScreenLayoutString(long2.toInt())}), " + "splitShade=$bool2 (was $bool1)" } ) @@ -370,3 +378,11 @@ private inline fun toOrientationString(@Orientation orientation: Int): String { else -> "undefined" } } + +private inline fun toScreenLayoutString(screenLayout: Int): String { + return when (screenLayout) { + SCREENLAYOUT_LONG_YES -> "long" + SCREENLAYOUT_LONG_NO -> "notlong" + else -> "undefined" + } +} 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 dc39c97bc9ab..93021bab606d 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.os.UserHandle 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/tiles/impl/fontscaling/domain/FontScalingTileMapper.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/fontscaling/domain/FontScalingTileMapper.kt new file mode 100644 index 000000000000..26069c774364 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/fontscaling/domain/FontScalingTileMapper.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.qs.tiles.impl.fontscaling.domain + +import android.content.res.Resources +import com.android.systemui.common.shared.model.Icon +import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.qs.tiles.base.interactor.QSTileDataToStateMapper +import com.android.systemui.qs.tiles.impl.fontscaling.domain.model.FontScalingTileModel +import com.android.systemui.qs.tiles.viewmodel.QSTileConfig +import com.android.systemui.qs.tiles.viewmodel.QSTileState +import com.android.systemui.res.R +import javax.inject.Inject + +/** Maps [FontScalingTileModel] to [QSTileState]. */ +class FontScalingTileMapper +@Inject +constructor( + @Main private val resources: Resources, + private val theme: Resources.Theme, +) : QSTileDataToStateMapper<FontScalingTileModel> { + + override fun map(config: QSTileConfig, data: FontScalingTileModel): QSTileState = + QSTileState.build(resources, theme, config.uiConfig) { + val icon = + Icon.Loaded( + resources.getDrawable( + R.drawable.ic_qs_font_scaling, + theme, + ), + contentDescription = null + ) + this.icon = { icon } + contentDescription = label + activationState = QSTileState.ActivationState.ACTIVE + sideViewIcon = QSTileState.SideViewIcon.Chevron + supportedActions = + setOf(QSTileState.UserAction.CLICK, QSTileState.UserAction.LONG_CLICK) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/fontscaling/domain/interactor/FontScalingTileDataInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/fontscaling/domain/interactor/FontScalingTileDataInteractor.kt new file mode 100644 index 000000000000..745e6a301689 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/fontscaling/domain/interactor/FontScalingTileDataInteractor.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.qs.tiles.impl.fontscaling.domain.interactor + +import android.os.UserHandle +import com.android.systemui.qs.tiles.base.interactor.DataUpdateTrigger +import com.android.systemui.qs.tiles.base.interactor.QSTileDataInteractor +import com.android.systemui.qs.tiles.impl.fontscaling.domain.model.FontScalingTileModel +import javax.inject.Inject +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOf + +/** Provides [FontScalingTileModel]. */ +class FontScalingTileDataInteractor @Inject constructor() : + QSTileDataInteractor<FontScalingTileModel> { + override fun tileData( + user: UserHandle, + triggers: Flow<DataUpdateTrigger> + ): Flow<FontScalingTileModel> = flowOf(FontScalingTileModel) + + override fun availability(user: UserHandle): Flow<Boolean> = flowOf(true) +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/fontscaling/domain/interactor/FontScalingTileUserActionInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/fontscaling/domain/interactor/FontScalingTileUserActionInteractor.kt new file mode 100644 index 000000000000..b6f4afb84259 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/fontscaling/domain/interactor/FontScalingTileUserActionInteractor.kt @@ -0,0 +1,96 @@ +/* + * 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.qs.tiles.impl.fontscaling.domain.interactor + +import android.content.Intent +import android.provider.Settings +import com.android.internal.jank.InteractionJankMonitor +import com.android.systemui.accessibility.fontscaling.FontScalingDialogDelegate +import com.android.systemui.animation.DialogCuj +import com.android.systemui.animation.DialogLaunchAnimator +import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.plugins.ActivityStarter +import com.android.systemui.qs.tiles.base.actions.QSTileIntentUserInputHandler +import com.android.systemui.qs.tiles.base.interactor.QSTileInput +import com.android.systemui.qs.tiles.base.interactor.QSTileUserActionInteractor +import com.android.systemui.qs.tiles.impl.fontscaling.domain.model.FontScalingTileModel +import com.android.systemui.qs.tiles.viewmodel.QSTileUserAction +import com.android.systemui.statusbar.phone.SystemUIDialog +import com.android.systemui.statusbar.policy.KeyguardStateController +import javax.inject.Inject +import javax.inject.Provider +import kotlin.coroutines.CoroutineContext +import kotlinx.coroutines.withContext + +/** Handles font scaling tile clicks. */ +class FontScalingTileUserActionInteractor +@Inject +constructor( + @Main private val coroutineContext: CoroutineContext, + private val qsTileIntentUserActionHandler: QSTileIntentUserInputHandler, + private val fontScalingDialogDelegateProvider: Provider<FontScalingDialogDelegate>, + private val keyguardStateController: KeyguardStateController, + private val dialogLaunchAnimator: DialogLaunchAnimator, + private val activityStarter: ActivityStarter, +) : QSTileUserActionInteractor<FontScalingTileModel> { + + override suspend fun handleInput(input: QSTileInput<FontScalingTileModel>): Unit = + with(input) { + when (action) { + is QSTileUserAction.Click -> { + // We animate from the touched view only if we are not on the keyguard + val animateFromView: Boolean = + action.view != null && !keyguardStateController.isShowing + val runnable = Runnable { + val dialog: SystemUIDialog = + fontScalingDialogDelegateProvider.get().createDialog() + if (animateFromView) { + dialogLaunchAnimator.showFromView( + dialog, + action.view!!, + DialogCuj( + InteractionJankMonitor.CUJ_SHADE_DIALOG_OPEN, + INTERACTION_JANK_TAG + ) + ) + } else { + dialog.show() + } + } + + withContext(coroutineContext) { + activityStarter.executeRunnableDismissingKeyguard( + runnable, + /* cancelAction= */ null, + /* dismissShade= */ true, + /* afterKeyguardGone= */ true, + /* deferred= */ false + ) + } + } + is QSTileUserAction.LongClick -> { + qsTileIntentUserActionHandler.handle( + action.view, + Intent(Settings.ACTION_TEXT_READING_SETTINGS) + ) + } + } + } + companion object { + private const val INTERACTION_JANK_TAG = "font_scaling" + } +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/fontscaling/domain/model/FontScalingTileModel.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/fontscaling/domain/model/FontScalingTileModel.kt new file mode 100644 index 000000000000..76042dffa47e --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/fontscaling/domain/model/FontScalingTileModel.kt @@ -0,0 +1,20 @@ +/* + * 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.qs.tiles.impl.fontscaling.domain.model + +/** FontScaling tile model. No data needed as the tile just opens a dialog. */ +data object FontScalingTileModel 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 246ccb1180b0..56c0ca910bd7 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 @@ -42,6 +42,7 @@ 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.notification.stack.shared.flexiNotifsEnabled +import com.android.systemui.statusbar.policy.domain.interactor.DeviceProvisioningInteractor import com.android.systemui.util.asIndenting import com.android.systemui.util.printSection import com.android.systemui.util.println @@ -51,10 +52,12 @@ import javax.inject.Inject 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 import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.mapNotNull import kotlinx.coroutines.launch @@ -81,6 +84,7 @@ constructor( private val simBouncerInteractor: Lazy<SimBouncerInteractor>, private val authenticationInteractor: Lazy<AuthenticationInteractor>, private val windowController: NotificationShadeWindowController, + private val deviceProvisioningInteractor: DeviceProvisioningInteractor, ) : CoreStartable { override fun start() { @@ -112,26 +116,39 @@ constructor( private fun hydrateVisibility() { applicationScope.launch { // TODO(b/296114544): Combine with some global hun state to make it visible! - sceneInteractor.transitionState - .mapNotNull { state -> - when (state) { - is ObservableTransitionState.Idle -> { - if (state.scene != SceneKey.Gone) { - true to "scene is not Gone" - } else { - false to "scene is Gone" - } - } - is ObservableTransitionState.Transition -> { - if (state.fromScene == SceneKey.Gone) { - true to "scene transitioning away from Gone" - } else { - null + combine( + deviceProvisioningInteractor.isDeviceProvisioned, + deviceProvisioningInteractor.isFactoryResetProtectionActive, + ) { isDeviceProvisioned, isFrpActive -> + isDeviceProvisioned && !isFrpActive + } + .distinctUntilChanged() + .flatMapLatest { isAllowedToBeVisible -> + if (isAllowedToBeVisible) { + sceneInteractor.transitionState + .mapNotNull { state -> + when (state) { + is ObservableTransitionState.Idle -> { + if (state.scene != SceneKey.Gone) { + true to "scene is not Gone" + } else { + false to "scene is Gone" + } + } + is ObservableTransitionState.Transition -> { + if (state.fromScene == SceneKey.Gone) { + true to "scene transitioning away from Gone" + } else { + null + } + } + } } - } + .distinctUntilChanged() + } else { + flowOf(false to "Device not provisioned or Factory Reset Protection active") } } - .distinctUntilChanged() .collect { (isVisible, loggingReason) -> sceneInteractor.setVisible(isVisible, loggingReason) } diff --git a/packages/SystemUI/src/com/android/systemui/settings/dagger/MultiUserUtilsModule.java b/packages/SystemUI/src/com/android/systemui/settings/MultiUserUtilsModule.java index a0dd924b3959..fd807dbec581 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.Application; 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 000000000000..76d1d3dd145e --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/settings/SecureSettingsRepositoryModule.kt @@ -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.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/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java index 09e4e751c5f0..e219bccfecfa 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java @@ -165,6 +165,7 @@ import com.android.systemui.plugins.statusbar.StatusBarStateController.StateList 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; @@ -2083,6 +2084,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump 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 +2201,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump 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 +2475,9 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump 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; diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java index 00534743588c..f7fed537a167 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 @@ public class NotificationShadeWindowControllerImpl implements NotificationShadeW 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; @@ -331,8 +332,8 @@ public class NotificationShadeWindowControllerImpl implements NotificationShadeW ); collectFlow( mWindowRootView, - mCommunalInteractor.get().isCommunalShowing(), - this::onCommunalShowingChanged + mCommunalInteractor.get().isCommunalVisible(), + this::onCommunalVisibleChanged ); } @@ -475,6 +476,9 @@ public class NotificationShadeWindowControllerImpl implements NotificationShadeW } visible = true; mLogger.d("Visibility forced to be true"); + } else if (state.communalVisible) { + visible = true; + mLogger.d("Visibility forced to be true by communal"); } if (mWindowRootView != null) { if (visible) { @@ -510,15 +514,15 @@ public class NotificationShadeWindowControllerImpl implements NotificationShadeW } private void applyUserActivityTimeout(NotificationShadeWindowState state) { - final Boolean communalShowing = state.isCommunalShowingAndNotOccluded(); + final Boolean communalVisible = state.isCommunalVisibleAndNotOccluded(); final Boolean keyguardShowing = state.isKeyguardShowingAndNotOccluded(); long timeout = -1; - if ((communalShowing || keyguardShowing) + if ((communalVisible || keyguardShowing) && state.statusBarState == StatusBarState.KEYGUARD && !state.qsExpanded) { if (state.bouncerShowing) { timeout = KeyguardViewMediator.AWAKE_INTERVAL_BOUNCER_MS; - } else if (communalShowing) { + } else if (communalVisible) { timeout = CommunalInteractor.AWAKE_INTERVAL_MS; } else if (keyguardShowing) { timeout = mLockScreenDisplayTimeout; @@ -624,7 +628,7 @@ public class NotificationShadeWindowControllerImpl implements NotificationShadeW state.dozing, state.scrimsVisibility, state.backgroundBlurRadius, - state.communalShowing + state.communalVisible ); } @@ -749,8 +753,8 @@ public class NotificationShadeWindowControllerImpl implements NotificationShadeW } @VisibleForTesting - void onCommunalShowingChanged(Boolean showing) { - mCurrentState.communalShowing = showing; + void onCommunalVisibleChanged(Boolean visible) { + mCurrentState.communalVisible = visible; apply(mCurrentState); } diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowState.kt b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowState.kt index f9c9d83e03aa..e0a98b3b7f9e 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowState.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowState.kt @@ -58,15 +58,15 @@ class NotificationShadeWindowState( @JvmField var dreaming: Boolean = false, @JvmField var scrimsVisibility: Int = 0, @JvmField var backgroundBlurRadius: Int = 0, - @JvmField var communalShowing: Boolean = false, + @JvmField var communalVisible: Boolean = false, ) { fun isKeyguardShowingAndNotOccluded(): Boolean { return keyguardShowing && !keyguardOccluded } - fun isCommunalShowingAndNotOccluded(): Boolean { - return communalShowing && !keyguardOccluded + fun isCommunalVisibleAndNotOccluded(): Boolean { + return communalVisible && !keyguardOccluded } /** List of [String] to be used as a [Row] with [DumpsysTableLogger]. */ @@ -99,7 +99,7 @@ class NotificationShadeWindowState( dozing.toString(), scrimsVisibility.toString(), backgroundBlurRadius.toString(), - communalShowing.toString(), + communalVisible.toString(), ) } @@ -140,7 +140,7 @@ class NotificationShadeWindowState( dozing: Boolean, scrimsVisibility: Int, backgroundBlurRadius: Int, - communalShowing: Boolean, + communalVisible: Boolean, ) { buffer.advance().apply { this.keyguardShowing = keyguardShowing @@ -172,7 +172,7 @@ class NotificationShadeWindowState( this.dozing = dozing this.scrimsVisibility = scrimsVisibility this.backgroundBlurRadius = backgroundBlurRadius - this.communalShowing = communalShowing + this.communalVisible = communalVisible } } @@ -218,7 +218,7 @@ class NotificationShadeWindowState( "dozing", "scrimsVisibility", "backgroundBlurRadius", - "communalShowing" + "communalVisible" ) } } diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationsQSContainerController.kt b/packages/SystemUI/src/com/android/systemui/shade/NotificationsQSContainerController.kt index 84cad1d16d73..c0afa32571e7 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationsQSContainerController.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationsQSContainerController.kt @@ -30,8 +30,6 @@ import androidx.lifecycle.lifecycleScope import com.android.systemui.Flags.centralizedStatusBarDimensRefactor import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Main -import com.android.systemui.flags.FeatureFlags -import com.android.systemui.flags.Flags import com.android.systemui.fragments.FragmentService import com.android.systemui.keyguard.shared.KeyguardShadeMigrationNssl import com.android.systemui.lifecycle.repeatWhenAttached @@ -59,18 +57,17 @@ internal const val INSET_DEBOUNCE_MILLIS = 500L @SysUISingleton class NotificationsQSContainerController @Inject constructor( - view: NotificationsQuickSettingsContainer, - private val navigationModeController: NavigationModeController, - private val overviewProxyService: OverviewProxyService, - private val shadeHeaderController: ShadeHeaderController, - private val shadeInteractor: ShadeInteractor, - private val fragmentService: FragmentService, - @Main private val delayableExecutor: DelayableExecutor, - private val featureFlags: FeatureFlags, - private val - notificationStackScrollLayoutController: NotificationStackScrollLayoutController, - private val splitShadeStateController: SplitShadeStateController, - private val largeScreenHeaderHelperLazy: Lazy<LargeScreenHeaderHelper>, + view: NotificationsQuickSettingsContainer, + private val navigationModeController: NavigationModeController, + private val overviewProxyService: OverviewProxyService, + private val shadeHeaderController: ShadeHeaderController, + private val shadeInteractor: ShadeInteractor, + private val fragmentService: FragmentService, + @Main private val delayableExecutor: DelayableExecutor, + private val + notificationStackScrollLayoutController: NotificationStackScrollLayoutController, + private val splitShadeStateController: SplitShadeStateController, + private val largeScreenHeaderHelperLazy: Lazy<LargeScreenHeaderHelper>, ) : ViewController<NotificationsQuickSettingsContainer>(view), QSContainerController { private var splitShadeEnabled = false @@ -133,9 +130,6 @@ class NotificationsQSContainerController @Inject constructor( isGestureNavigation = QuickStepContract.isGesturalMode(currentMode) mView.setStackScroller(notificationStackScrollLayoutController.getView()) - if (featureFlags.isEnabled(Flags.QS_CONTAINER_GRAPH_OPTIMIZER)){ - mView.enableGraphOptimization() - } } public override fun onViewAttached() { diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationsQuickSettingsContainer.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationsQuickSettingsContainer.java index de3d16a57a1f..25e558ee42dd 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationsQuickSettingsContainer.java +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationsQuickSettingsContainer.java @@ -32,10 +32,10 @@ import androidx.annotation.Nullable; import androidx.constraintlayout.widget.ConstraintLayout; import androidx.constraintlayout.widget.ConstraintSet; -import com.android.systemui.keyguard.shared.KeyguardShadeMigrationNssl; -import com.android.systemui.res.R; import com.android.systemui.fragments.FragmentHostManager.FragmentListener; +import com.android.systemui.keyguard.shared.KeyguardShadeMigrationNssl; import com.android.systemui.plugins.qs.QS; +import com.android.systemui.res.R; import com.android.systemui.statusbar.notification.AboveShelfObserver; import java.util.ArrayList; @@ -73,6 +73,7 @@ public class NotificationsQuickSettingsContainer extends ConstraintLayout public NotificationsQuickSettingsContainer(Context context, AttributeSet attrs) { super(context, attrs); + setOptimizationLevel(getOptimizationLevel() | OPTIMIZATION_GRAPH); } @Override @@ -180,10 +181,6 @@ public class NotificationsQuickSettingsContainer extends ConstraintLayout super.dispatchDraw(canvas); } - void enableGraphOptimization() { - setOptimizationLevel(getOptimizationLevel() | OPTIMIZATION_GRAPH); - } - @Override public boolean dispatchTouchEvent(MotionEvent ev) { return TouchLogger.logDispatchTouch("NotificationsQuickSettingsContainer", ev, diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerSceneImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerSceneImpl.kt index 10b9db0a349b..4e8b4039cc79 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 android.view.MotionEvent 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.notification.stack.NotificationStackScroll 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 @@ import kotlinx.coroutines.launch 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 @@ constructor( } 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/ShadeExpansionStateManager.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeExpansionStateManager.kt index d3459b109d79..e40bcd5dbdbc 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/ShadeExpansionStateManager.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeExpansionStateManager.kt @@ -46,7 +46,7 @@ class ShadeExpansionStateManager @Inject constructor() { /** * Adds a listener that will be notified when the panel expansion fraction has changed and - * returns the current state in a ShadeExpansionChangeEvent for legacy purposes (b/23035507). + * returns the current state in a ShadeExpansionChangeEvent for legacy purposes (b/281038056). * * @see #addExpansionListener */ diff --git a/packages/SystemUI/src/com/android/systemui/shade/data/repository/FlingInfo.kt b/packages/SystemUI/src/com/android/systemui/shade/data/repository/FlingInfo.kt new file mode 100644 index 000000000000..d7f96e6cfa6b --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/shade/data/repository/FlingInfo.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.systemui.shade.data.repository + +/** + * 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). + */ +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 2445bdb17955..e5ff9778fed4 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 @@ interface ShadeRepository { */ 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 @@ interface ShadeRepository { @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 @@ class ShadeRepositoryImpl @Inject constructor() : ShadeRepository { 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 @@ class ShadeRepositoryImpl @Inject constructor() : ShadeRepository { _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 @@ class ShadeRepositoryImpl @Inject constructor() : ShadeRepository { _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/ShadeInteractorImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorImpl.kt index a71cf950cbe1..e619806abc34 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorImpl.kt @@ -25,9 +25,10 @@ import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.power.domain.interactor.PowerInteractor import com.android.systemui.statusbar.disableflags.data.repository.DisableFlagsRepository import com.android.systemui.statusbar.phone.DozeParameters -import com.android.systemui.statusbar.policy.data.repository.DeviceProvisioningRepository import com.android.systemui.statusbar.policy.data.repository.UserSetupRepository +import com.android.systemui.statusbar.policy.domain.interactor.DeviceProvisioningInteractor import com.android.systemui.user.domain.interactor.UserSwitcherInteractor +import com.android.systemui.util.kotlin.combine import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow @@ -44,7 +45,7 @@ class ShadeInteractorImpl @Inject constructor( @Application val scope: CoroutineScope, - deviceProvisioningRepository: DeviceProvisioningRepository, + deviceProvisioningInteractor: DeviceProvisioningInteractor, disableFlagsRepository: DisableFlagsRepository, dozeParams: DozeParameters, keyguardRepository: KeyguardRepository, @@ -56,7 +57,7 @@ constructor( ) : ShadeInteractor, BaseShadeInteractor by baseShadeInteractor { override val isShadeEnabled: StateFlow<Boolean> = combine( - deviceProvisioningRepository.isFactoryResetProtectionActive, + deviceProvisioningInteractor.isFactoryResetProtectionActive, disableFlagsRepository.disableFlags, ) { isFrpActive, isDisabledByFlags -> isDisabledByFlags.isShadeEnabled() && !isFrpActive @@ -83,7 +84,7 @@ constructor( powerInteractor.isAsleep, keyguardTransitionInteractor.isInTransitionToStateWhere { it == KeyguardState.AOD }, keyguardRepository.dozeTransitionModel.map { it.to == DozeStateModel.DOZE_PULSING }, - deviceProvisioningRepository.isFactoryResetProtectionActive, + deviceProvisioningInteractor.isFactoryResetProtectionActive, ) { isAsleep, goingToSleep, isPulsing, isFrpActive -> when { // Touches are disabled when Factory Reset Protection is active @@ -103,7 +104,7 @@ constructor( isShadeEnabled, keyguardRepository.isDozing, userSetupRepository.isUserSetUp, - deviceProvisioningRepository.isDeviceProvisioned, + deviceProvisioningInteractor.isDeviceProvisioned, ) { disableFlags, isShadeEnabled, isDozing, isUserSetup, isDeviceProvisioned -> isDeviceProvisioned && // Disallow QS during setup if it's a simple user switcher. (The user intends to diff --git a/packages/SystemUI/src/com/android/systemui/smartspace/data/repository/SmartspaceRepository.kt b/packages/SystemUI/src/com/android/systemui/smartspace/data/repository/SmartspaceRepository.kt index 095d30ef55df..52a1c1555591 100644 --- a/packages/SystemUI/src/com/android/systemui/smartspace/data/repository/SmartspaceRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/smartspace/data/repository/SmartspaceRepository.kt @@ -21,7 +21,9 @@ import android.os.Parcelable import android.widget.RemoteViews import com.android.systemui.communal.smartspace.CommunalSmartspaceController import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.plugins.BcSmartspaceDataPlugin +import java.util.concurrent.Executor import javax.inject.Inject import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow @@ -41,6 +43,7 @@ class SmartspaceRepositoryImpl @Inject constructor( private val communalSmartspaceController: CommunalSmartspaceController, + @Main private val uiExecutor: Executor, ) : SmartspaceRepository, BcSmartspaceDataPlugin.SmartspaceTargetListener { override val isSmartspaceRemoteViewsEnabled: Boolean @@ -51,12 +54,18 @@ constructor( override val communalSmartspaceTargets: Flow<List<SmartspaceTarget>> = _communalSmartspaceTargets .onStart { - communalSmartspaceController.addListener(listener = this@SmartspaceRepositoryImpl) + uiExecutor.execute { + communalSmartspaceController.addListener( + listener = this@SmartspaceRepositoryImpl + ) + } } .onCompletion { - communalSmartspaceController.removeListener( - listener = this@SmartspaceRepositoryImpl - ) + uiExecutor.execute { + communalSmartspaceController.removeListener( + listener = this@SmartspaceRepositoryImpl + ) + } } override fun onSmartspaceTargetsUpdated(targetsNullable: MutableList<out Parcelable>?) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt index 7f8be1cc7e55..ef5026538216 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.FloatRange 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 @@ class DragDownHelper( 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 32cd56ca223f..b64e0b7d3187 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesDependenciesModule.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesDependenciesModule.java @@ -56,7 +56,6 @@ import com.android.systemui.statusbar.phone.ManagedProfileControllerImpl; 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 @@ import dagger.multibindings.IntoMap; * their own version of CentralSurfaces can include just dependencies, without injecting * CentralSurfaces itself. */ -@Module(includes = {StatusBarNotificationPresenterModule.class}) +@Module public interface CentralSurfacesDependenciesModule { /** */ 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 99d4b2e525d1..27536bce97f6 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 @@ package com.android.systemui.statusbar.dagger; 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/collection/coordinator/SensitiveContentCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/SensitiveContentCoordinator.kt index ae4ba27775b8..29627e14e4a0 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 @@ package com.android.systemui.statusbar.notification.collection.coordinator 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/inflation/NotificationRowBinderImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotificationRowBinderImpl.java index cd816aea452b..954e80505cbe 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 6bba72b2cd49..92b0c048f3fc 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.NotifGutsVi 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.KeyguardBypassController; 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 @@ import dagger.multibindings.IntoMap; 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 2cac0002f013..b187cf15cccd 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 @@ package com.android.systemui.statusbar.notification.data 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 000000000000..a7970c70e4ed --- /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/SystemUI/src/com/android/systemui/statusbar/notification/domain/NotificationDomainLayerModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/NotificationDomainLayerModule.kt new file mode 100644 index 000000000000..5c49b28f343f --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/NotificationDomainLayerModule.kt @@ -0,0 +1,23 @@ +/* + * 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.domain + +import com.android.systemui.statusbar.notification.domain.interactor.NotificationSettingsInteractorModule +import dagger.Module + +@Module(includes = [NotificationSettingsInteractorModule::class]) +object NotificationDomainLayerModule 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 000000000000..0a9e12a23b8e --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/NotificationSettingsInteractorModule.kt @@ -0,0 +1,31 @@ +/* + * 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.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 3616fd6d8cd1..16f18a3c3fb6 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 @@ public class FooterView extends StackScrollerDecorView { 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 @@ public class FooterView extends StackScrollerDecorView { 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 @@ public class FooterView extends StackScrollerDecorView { 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 @@ public class FooterView extends StackScrollerDecorView { 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 @@ public class FooterView extends StackScrollerDecorView { 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 @@ public class FooterView extends StackScrollerDecorView { /** 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 @@ public class FooterView extends StackScrollerDecorView { /** 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 @@ public class FooterView extends StackScrollerDecorView { /** Show "History" instead of "Manage" on the start button. */ public void showHistory(boolean showHistory) { + FooterViewRefactor.assertInLegacyMode(); if (mShowHistory == showHistory) { return; } @@ -260,17 +301,13 @@ public class FooterView extends StackScrollerDecorView { } 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 @@ public class FooterView extends StackScrollerDecorView { // `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 @@ public class FooterView extends StackScrollerDecorView { /** Whether the start button shows "History" (true) or "Manage" (false). */ public boolean isHistoryShown() { + FooterViewRefactor.assertInLegacyMode(); return mShowHistory; } @@ -304,7 +350,9 @@ public class FooterView extends StackScrollerDecorView { protected void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); updateColors(); - updateResources(); + if (!FooterViewRefactor.isEnabled()) { + updateResources(); + } updateContent(); } @@ -328,23 +376,22 @@ public class FooterView extends StackScrollerDecorView { } 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 e0eee96d8f0a..9fb453afb55e 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.isAnimating 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 { - // Bind the resource IDs - footer.setMessageString(viewModel.message.messageId) - footer.setMessageIcon(viewModel.message.iconId) - footer.setClearAllButtonText(viewModel.clearAllButton.labelId) - footer.setClearAllButtonDescription(viewModel.clearAllButton.accessibilityDescriptionId) + 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) } + } - // Bind the click listeners + private suspend fun bindClearAllButton( + footer: FooterView, + viewModel: FooterViewModel, + clearAllNotifications: View.OnClickListener, + ) = coroutineScope { 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, - ) + 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, + ) } } + } + } - lifecycleScope.launch { - viewModel.message.isVisible.collect { visible -> - footer.setFooterLabelVisible(visible) + 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) + + 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 244555a3d73b..691dc4297145 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 com.android.systemui.util.ui.AnimatedValue 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 e6b0abcfad65..5111c11ac584 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,30 +19,36 @@ package com.android.systemui.statusbar.notification.footer.ui.viewmodel 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( @@ -59,6 +65,22 @@ class FooterViewModel( .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 +95,7 @@ object FooterViewModelModule { @SysUISingleton fun provideOptional( activeNotificationsInteractor: Provider<ActiveNotificationsInteractor>, + notificationSettingsInteractor: Provider<NotificationSettingsInteractor>, seenNotificationsInteractor: Provider<SeenNotificationsInteractor>, shadeInteractor: Provider<ShadeInteractor>, ): Optional<FooterViewModel> { @@ -80,6 +103,7 @@ object FooterViewModelModule { Optional.of( FooterViewModel( activeNotificationsInteractor.get(), + notificationSettingsInteractor.get(), seenNotificationsInteractor.get(), shadeInteractor.get() ) 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 000000000000..c74c396741d7 --- /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 8e824423973f..20c8add6cc60 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,68 @@ class AlertKeyguardVisibilitySuppressor( 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" + + 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 = allow(entry) + val suppress = isActive && state == State.SUPPRESS + reason = "avalanche suppress=$suppress isActive=$isActive state=$state" + return suppress + } + + fun allow(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 6878a1e9062c..dabb18b5a1c4 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.VisualInterrupti 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 @@ import javax.inject.Inject 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 @@ constructor( 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/interruption/VisualInterruptionSuppressor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionSuppressor.kt index ee797274deac..2f80c5d225ff 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionSuppressor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionSuppressor.kt @@ -85,7 +85,7 @@ abstract class VisualInterruptionCondition( /** A reason why visual interruptions might be suppressed based on the notification. */ abstract class VisualInterruptionFilter( override val types: Set<VisualInterruptionType>, - override val reason: String, + override var reason: String, override val uiEventId: UiEventEnum? = null, override val eventLogData: EventLogData? = null ) : VisualInterruptionSuppressor { 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 99177c270b32..195fe785b538 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 @@ constructor( precomputedTextViewFactory: PrecomputedTextViewFactory, bigPictureLayoutInflaterFactory: BigPictureLayoutInflaterFactory, callLayoutSetDataAsyncFactory: CallLayoutSetDataAsyncFactory, + optimizedLinearLayoutFactory: NotificationOptimizedLinearLayoutFactory ) : NotifRemoteViewsFactoryContainer { override val factories: Set<NotifRemoteViewsFactory> = buildSet { add(precomputedTextViewFactory) @@ -40,5 +42,8 @@ constructor( 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 000000000000..f231fbc6e433 --- /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 b9afb1409d91..0a11eb26cc07 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 @@ -1445,12 +1445,12 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable 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); } @@ -4698,6 +4698,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable * this will return false. **/ public boolean isHistoryShown() { + FooterViewRefactor.assertInLegacyMode(); return mFooterView != null && mFooterView.isHistoryShown(); } @@ -4710,10 +4711,10 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable } 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(); @@ -4794,8 +4795,8 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable } boolean animate = mIsExpanded && mAnimationsEnabled; mFooterView.setVisible(visible, animate); - mFooterView.showHistory(showHistory); if (!FooterViewRefactor.isEnabled()) { + mFooterView.showHistory(showHistory); mFooterView.setClearAllButtonVisible(showDismissView, animate); mFooterView.setFooterLabelVisible(mHasFilteredOutSeenNotifications); } @@ -5490,6 +5491,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable * 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); 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 ed266772ac15..a2ff406b9599 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 android.service.notification.NotificationStats.DISMISS_SENTIMENT_N 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; @@ -845,11 +846,13 @@ public class NotificationStackScrollLayoutController implements Dumpable { 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); @@ -2052,7 +2055,7 @@ public class NotificationStackScrollLayoutController implements Dumpable { } 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() 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 a3e09417b34c..b38d619a4434 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 @@ -59,9 +59,6 @@ public class StackStateAnimator { 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; @@ -70,7 +67,6 @@ public class StackStateAnimator { private static final int MAX_STAGGER_COUNT = 5; private final int mGoToFullShadeAppearingTranslation; - private final int mPulsingAppearingTranslation; @VisibleForTesting float mHeadsUpAppearStartAboveScreen; private final ExpandableViewState mTmpState = new ExpandableViewState(); @@ -102,9 +98,6 @@ public class StackStateAnimator { 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); mAnimationProperties = new AnimationProperties() { 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 01972646f394..311ba83e85f1 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 @@ class NotificationStackAppearanceRepository @Inject constructor() { /** 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 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 8307397c57da..9984ba9c32ce 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 @@ -34,9 +34,6 @@ constructor( /** 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 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 44a7e7e27455..4d65b9d9c792 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 @@ -29,6 +29,7 @@ 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 @@ -45,6 +46,7 @@ import com.android.systemui.statusbar.phone.NotificationIconAreaController import com.android.systemui.util.kotlin.getOrNull import java.util.Optional import javax.inject.Inject +import javax.inject.Provider import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.flow.combine import kotlinx.coroutines.launch @@ -58,9 +60,11 @@ constructor( 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, ) { @@ -115,7 +119,7 @@ constructor( ) { footerView: FooterView -> traceSection("bind FooterView") { val disposableHandle = - FooterViewBinder.bind( + FooterViewBinder.bindWhileAttached( footerView, footerViewModel, clearAllNotifications = { @@ -124,6 +128,16 @@ constructor( ) parentView.clearAllNotifications() }, + launchNotificationSettings = { view -> + notificationActivityStarter + .get() + .startHistoryIntent(view, /* showHistory = */ false) + }, + launchNotificationHistory = { view -> + notificationActivityStarter + .get() + .startHistoryIntent(view, /* showHistory = */ true) + }, ) parentView.setFooterView(footerView) return@reinflateAndBindLatest disposableHandle 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 6c2cbbecb477..814146c329a6 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.NotificationStackScroll 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 @@ object NotificationStackAppearanceViewBinder { 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 @@ object NotificationStackAppearanceViewBinder { 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, ) } } 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 fe5bdd41a94f..f3d0d2c2e9fc 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 @@ object SharedNotificationContainerBinder { .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/NotificationStackAppearanceViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationStackAppearanceViewModel.kt index 56ff7f9e50df..bdf1a6431549 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 @@ -74,9 +74,6 @@ constructor( /** 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 a436f1783a0c..65d9c9f523f8 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.common.shared.model.NotificationContainerBounds 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 @@ constructor( 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 @@ constructor( 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 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 39ca7b219663..3669ba851005 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 @@ public class CentralSurfacesCommandQueueCallbacks implements CommandQueue.Callba } 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/CentralSurfacesImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java index 64fcef51755d..209936108f6b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java @@ -1803,10 +1803,10 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { } 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) { 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 4c83ca28b3cb..69282ae5f9eb 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.plugins.ActivityStarter.OnDismissAction; 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.FeatureFlags; 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 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb } }; private Lazy<WindowManagerLockscreenVisibilityInteractor> mWmLockscreenVisibilityInteractor; + private Lazy<KeyguardSurfaceBehindInteractor> mSurfaceBehindInteractor; private Lazy<KeyguardDismissActionInteractor> mKeyguardDismissActionInteractor; @Inject @@ -377,7 +380,8 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb @Main CoroutineDispatcher mainDispatcher, Lazy<WindowManagerLockscreenVisibilityInteractor> wmLockscreenVisibilityInteractor, Lazy<KeyguardDismissActionInteractor> keyguardDismissActionInteractorLazy, - SelectedUserInteractor selectedUserInteractor + SelectedUserInteractor selectedUserInteractor, + Lazy<KeyguardSurfaceBehindInteractor> surfaceBehindInteractor ) { mContext = context; mViewMediatorCallback = callback; @@ -410,6 +414,7 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb mWmLockscreenVisibilityInteractor = wmLockscreenVisibilityInteractor; mKeyguardDismissActionInteractor = keyguardDismissActionInteractorLazy; mSelectedUserInteractor = selectedUserInteractor; + mSurfaceBehindInteractor = surfaceBehindInteractor; } KeyguardTransitionInteractor mKeyguardTransitionInteractor; @@ -480,7 +485,16 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb 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)); } } 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 1a17e7c28410..665a5714e277 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.PropertyAnimator 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 @@ class UnlockedScreenOffAnimationController @Inject constructor( } // 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/policy/SensitiveNotificationProtectionControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionControllerImpl.java index 3c4ca4465874..11e374f24f9c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionControllerImpl.java @@ -16,12 +16,13 @@ package com.android.systemui.statusbar.policy; -import static com.android.systemui.Flags.screenshareNotificationHiding; +import static com.android.server.notification.Flags.screenshareNotificationHiding; import android.media.projection.MediaProjectionInfo; import android.media.projection.MediaProjectionManager; import android.os.Handler; import android.os.Trace; +import android.service.notification.StatusBarNotification; import com.android.internal.annotations.VisibleForTesting; import com.android.systemui.dagger.SysUISingleton; @@ -46,8 +47,9 @@ public class SensitiveNotificationProtectionControllerImpl public void onStart(MediaProjectionInfo info) { Trace.beginSection( "SNPC.onProjectionStart"); - mProjection = info; - mListeners.forEach(Runnable::run); + // 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(); } @@ -55,10 +57,22 @@ public class SensitiveNotificationProtectionControllerImpl public void onStop(MediaProjectionInfo info) { Trace.beginSection( "SNPC.onProjectionStop"); - mProjection = null; - mListeners.forEach(Runnable::run); + 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); + } + } }; @Inject @@ -86,7 +100,6 @@ public class SensitiveNotificationProtectionControllerImpl public boolean isSensitiveStateActive() { // TODO(b/316955558): Add disabled by developer option // TODO(b/316955306): Add feature exemption for sysui and bug handlers - // TODO(b/316955346): Add feature exemption for single app screen sharing return mProjection != null; } @@ -96,9 +109,18 @@ public class SensitiveNotificationProtectionControllerImpl return false; } + MediaProjectionInfo projection = mProjection; + if (projection == null) { + return false; + } + // Exempt foreground service notifications from protection in effort to keep screen share // stop actions easily accessible - // TODO(b/316955208): Exempt FGS notifications only for app that started projection - return !entry.getSbn().getNotification().isFgsOrUij(); + StatusBarNotification sbn = entry.getSbn(); + if (sbn.getNotification().isFgsOrUij()) { + return !sbn.getPackageName().equals(projection.getPackageName()); + } + + return true; } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/domain/interactor/DeviceProvisioningInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/domain/interactor/DeviceProvisioningInteractor.kt new file mode 100644 index 000000000000..32cf86d8e390 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/domain/interactor/DeviceProvisioningInteractor.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.statusbar.policy.domain.interactor + +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.statusbar.policy.data.repository.DeviceProvisioningRepository +import javax.inject.Inject +import kotlinx.coroutines.flow.Flow + +/** Encapsulates device-provisioning related business logic. */ +@SysUISingleton +class DeviceProvisioningInteractor +@Inject +constructor( + repository: DeviceProvisioningRepository, +) { + /** + * Whether this device has been provisioned. + * + * @see android.provider.Settings.Global.DEVICE_PROVISIONED + */ + val isDeviceProvisioned: Flow<Boolean> = repository.isDeviceProvisioned + + /** Whether Factory Reset Protection (FRP) is currently active, locking the device. */ + val isFactoryResetProtectionActive: Flow<Boolean> = repository.isFactoryResetProtectionActive +} diff --git a/packages/SystemUI/src/com/android/systemui/unfold/DisplaySwitchLatencyTracker.kt b/packages/SystemUI/src/com/android/systemui/unfold/DisplaySwitchLatencyTracker.kt index 92a64a618ba9..4dfd5a1bae46 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 @@ package com.android.systemui.unfold 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 @@ constructor( .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 @@ constructor( fromFoldableDeviceState: Int ): DisplaySwitchLatencyEvent { log { "fromFoldableDeviceState=$fromFoldableDeviceState" } - instantForTrack(TAG, "fromFoldableDeviceState=$fromFoldableDeviceState") + instantForTrack(TAG) { "fromFoldableDeviceState=$fromFoldableDeviceState" } return copy(fromFoldableDeviceState = fromFoldableDeviceState) } @@ -187,7 +187,7 @@ constructor( "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/settings/UserFileManagerExt.kt b/packages/SystemUI/src/com/android/systemui/util/kotlin/SharedPreferencesExt.kt index b09bfe21e014..ab6a37bccc11 100644 --- a/packages/SystemUI/src/com/android/systemui/settings/UserFileManagerExt.kt +++ b/packages/SystemUI/src/com/android/systemui/util/kotlin/SharedPreferencesExt.kt @@ -12,32 +12,28 @@ * WITHOUT WARRANTIES OR CONDITIONS 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 +package com.android.systemui.util.kotlin -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 +import kotlinx.coroutines.flow.mapNotNull -/** 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) } - } +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 6993c961fba8..fa0d0306f157 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 @@ data class Sextuple<A, B, C, D, E, F>( 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/volume/dagger/AudioModule.kt b/packages/SystemUI/src/com/android/systemui/volume/dagger/AudioModule.kt index 8d5e55a2917e..ff1daea4816e 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/dagger/AudioModule.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/dagger/AudioModule.kt @@ -16,6 +16,7 @@ package com.android.systemui.volume.dagger +import android.content.Context import android.media.AudioManager import com.android.settingslib.volume.data.repository.AudioRepository import com.android.settingslib.volume.data.repository.AudioRepositoryImpl @@ -35,10 +36,12 @@ interface AudioModule { @Provides fun provideAudioRepository( + @Application context: Context, audioManager: AudioManager, @Background coroutineContext: CoroutineContext, @Application coroutineScope: CoroutineScope, - ): AudioRepository = AudioRepositoryImpl(audioManager, coroutineContext, coroutineScope) + ): AudioRepository = + AudioRepositoryImpl(context, audioManager, coroutineContext, coroutineScope) @Provides fun provideAudioModeInteractor(repository: AudioRepository): AudioModeInteractor = 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 000000000000..2ff9af9ac3c0 --- /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.content.Context +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.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( + @Application context: Context, + mediaSessionManager: MediaSessionManager, + localBluetoothManager: LocalBluetoothManager?, + @Application coroutineScope: CoroutineScope, + @Background backgroundContext: CoroutineContext, + ): MediaControllerRepository = + MediaControllerRepositoryImpl( + context, + 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 c842e5f295b9..5cb6fa8c8046 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/dagger/VolumeModule.java +++ b/packages/SystemUI/src/com/android/systemui/volume/dagger/VolumeModule.java @@ -62,6 +62,7 @@ import kotlinx.coroutines.CoroutineScope; @Module( includes = { AudioModule.class, + MediaDevicesModule.class }, subcomponents = { VolumePanelComponent.class 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 000000000000..57ac4357b9b5 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/data/repository/LocalMediaRepositoryFactory.kt @@ -0,0 +1,41 @@ +/* + * 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 com.android.settingslib.volume.data.repository.LocalMediaRepository +import com.android.settingslib.volume.data.repository.LocalMediaRepositoryImpl +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 localMediaManagerFactory: LocalMediaManagerFactory, + @Application private val coroutineScope: CoroutineScope, + @Background private val backgroundCoroutineContext: CoroutineContext, +) { + + fun create(packageName: String?): LocalMediaRepository = + LocalMediaRepositoryImpl( + localMediaManagerFactory.create(packageName), + 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 000000000000..6c456f963f03 --- /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 000000000000..f250308802b2 --- /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/tests/Android.bp b/packages/SystemUI/tests/Android.bp index ec0414e94eba..88939a2ba67d 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/systemui/CameraAvailabilityListenerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/CameraAvailabilityListenerTest.kt index e921a59f5860..64cd5262eade 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 @@ class CameraAvailabilityListenerTest : SysuiTestCase() { } private fun createAndStartSut(): CameraAvailabilityListener { - return CameraAvailabilityListener.build(context, context.mainExecutor).also { - it.addTransitionCallback(cameraTransitionCallback) - it.startListening() - } + return CameraAvailabilityListener.build( + context, + context.mainExecutor, + CameraProtectionLoader((context)) + ) + .also { + it.addTransitionCallback(cameraTransitionCallback) + it.startListening() + } } private class TestCameraTransitionCallback : diff --git a/packages/SystemUI/tests/src/com/android/systemui/CameraProtectionLoaderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/CameraProtectionLoaderTest.kt new file mode 100644 index 000000000000..238e5e9197a3 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/CameraProtectionLoaderTest.kt @@ -0,0 +1,124 @@ +/* + * 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 CameraProtectionLoaderTest : SysuiTestCase() { + + private val loader = CameraProtectionLoader(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_protectedInnerCameraId, INNER_CAMERA_LOGICAL_ID) + overrideResource(R.string.config_protectedInnerPhysicalCameraId, INNER_CAMERA_PHYSICAL_ID) + overrideResource( + R.string.config_innerBuiltInDisplayCutoutProtection, + INNER_CAMERA_PROTECTION_PATH + ) + } + + @Test + fun loadCameraProtectionInfoList() { + val protectionInfos = loader.loadCameraProtectionInfoList().map { it.toTestableVersion() } + + assertThat(protectionInfos) + .containsExactly(OUTER_CAMERA_PROTECTION_INFO, INNER_CAMERA_PROTECTION_INFO) + } + + @Test + fun loadCameraProtectionInfoList_outerCameraIdEmpty_onlyReturnsInnerInfo() { + overrideResource(R.string.config_protectedCameraId, "") + + val protectionInfos = loader.loadCameraProtectionInfoList().map { it.toTestableVersion() } + + assertThat(protectionInfos).containsExactly(INNER_CAMERA_PROTECTION_INFO) + } + + @Test + fun loadCameraProtectionInfoList_innerCameraIdEmpty_onlyReturnsOuterInfo() { + overrideResource(R.string.config_protectedInnerCameraId, "") + + val protectionInfos = loader.loadCameraProtectionInfoList().map { it.toTestableVersion() } + + assertThat(protectionInfos).containsExactly(OUTER_CAMERA_PROTECTION_INFO) + } + + @Test + fun loadCameraProtectionInfoList_innerAndOuterCameraIdsEmpty_returnsEmpty() { + overrideResource(R.string.config_protectedCameraId, "") + overrideResource(R.string.config_protectedInnerCameraId, "") + + val protectionInfos = loader.loadCameraProtectionInfoList().map { it.toTestableVersion() } + + assertThat(protectionInfos).isEmpty() + } + + private fun CameraProtectionInfo.toTestableVersion() = + TestableProtectionInfo(logicalCameraId, physicalCameraId, cutoutBounds) + + /** + * "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, + ) + + 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 val OUTER_CAMERA_PROTECTION_INFO = + TestableProtectionInfo( + OUTER_CAMERA_LOGICAL_ID, + OUTER_CAMERA_PHYSICAL_ID, + OUTER_CAMERA_PROTECTION_BOUNDS + ) + + 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 val INNER_CAMERA_PROTECTION_INFO = + TestableProtectionInfo( + INNER_CAMERA_LOGICAL_ID, + INNER_CAMERA_PHYSICAL_ID, + INNER_CAMERA_PROTECTION_BOUNDS + ) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/ScreenDecorationsTest.java b/packages/SystemUI/tests/src/com/android/systemui/ScreenDecorationsTest.java index c07148b32cf3..1f1fa7259cd5 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 @@ public class ScreenDecorationsTest extends SysuiTestCase { private FakeFacePropertyRepository mFakeFacePropertyRepository = new FakeFacePropertyRepository(); private List<DecorProvider> mMockCutoutList; + private final CameraProtectionLoader mCameraProtectionLoader = + new CameraProtectionLoader(mContext); @Before public void setup() { @@ -247,7 +249,7 @@ public class ScreenDecorationsTest extends SysuiTestCase { mThreadFactory, mPrivacyDotDecorProviderFactory, mFaceScanningProviderFactory, new ScreenDecorationsLogger(logcatLogBuffer("TestLogBuffer")), - mFakeFacePropertyRepository, mJavaAdapter) { + mFakeFacePropertyRepository, mJavaAdapter, mCameraProtectionLoader) { @Override public void start() { super.start(); @@ -1243,7 +1245,7 @@ public class ScreenDecorationsTest extends SysuiTestCase { 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/accessibility/WindowMagnificationAnimationControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationAnimationControllerTest.java index 8299acbc2d52..375ebe86ae29 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationAnimationControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationAnimationControllerTest.java @@ -51,6 +51,7 @@ import android.view.WindowManagerGlobal; 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; @@ -79,6 +80,7 @@ import java.util.function.Supplier; @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 ca3eb3e077d4..4a1bdbcc9b48 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.DynamicAnimation; 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 @@ public class MenuViewLayerTest extends SysuiTestCase { 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/biometrics/data/repository/BiometricStatusRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/data/repository/BiometricStatusRepositoryTest.kt index 8f0e910271c7..8fbeb6f93360 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_OTHER 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 @@ class BiometricStatusRepositoryTest : SysuiTestCase() { 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/domain/interactor/BiometricStatusInteractorImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/BiometricStatusInteractorImplTest.kt index d7b7d79425c8..5c34fd9a1bc5 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 @@ package com.android.systemui.biometrics.domain.interactor 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 @@ class BiometricStatusInteractorImplTest : SysuiTestCase() { ) 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/ui/binder/SideFpsOverlayViewBinderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/binder/SideFpsOverlayViewBinderTest.kt index 3603c3c6c46a..5509c048b0da 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.domain.interactor.PrimaryBouncerInteractor 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.junit.runners.JUnit4 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 @@ class SideFpsOverlayViewBinderTest : SysuiTestCase() { sideFpsProgressBarViewModel = SideFpsProgressBarViewModel( mContext, - mock(), + biometricStatusInteractor, + kosmos.deviceEntryFingerprintAuthInteractor, sfpsSensorInteractor, kosmos.dozeServiceHost, kosmos.keyguardInteractor, @@ -426,6 +429,54 @@ class SideFpsOverlayViewBinderTest : SysuiTestCase() { } } + // 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 6a9c88151dd0..2e94d381b8dc 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 USER_ID = 4 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 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa } @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 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa 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 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa contentViewFromApp = contentView, logoResFromApp = logoRes, logoBitmapFromApp = logoBitmap, + packageName = packageName, ) // put the view model in the initial authenticating state, unless explicitly skipped @@ -1481,6 +1492,7 @@ private fun PromptSelectorInteractor.initializePrompt( contentViewFromApp: PromptContentView? = null, logoResFromApp: Int = -1, logoBitmapFromApp: Bitmap? = null, + packageName: String = OP_PACKAGE_NAME, ) { val info = PromptInfo().apply { @@ -1500,7 +1512,7 @@ private fun PromptSelectorInteractor.initializePrompt( 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 3c430316ffbe..2014755bd964 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.FingerprintInteractiveToAuthProvider 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.testKosmos 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 @@ class SideFpsOverlayViewModelTest : SysuiTestCase() { 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 @@ class SideFpsOverlayViewModelTest : SysuiTestCase() { 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 @@ class SideFpsOverlayViewModelTest : SysuiTestCase() { .thenReturn( Display(mock(DisplayManagerGlobal::class.java), 1, contextDisplayInfo, resources) ) + kosmos.biometricStatusRepository = biometricStatusRepository alternateBouncerInteractor = AlternateBouncerInteractor( @@ -197,9 +194,6 @@ class SideFpsOverlayViewModelTest : SysuiTestCase() { testScope.backgroundScope, ) - biometricStatusInteractor = - BiometricStatusInteractorImpl(activityTaskManager, biometricStatusRepository) - displayStateInteractor = DisplayStateInteractorImpl( testScope.backgroundScope, @@ -256,6 +250,7 @@ class SideFpsOverlayViewModelTest : SysuiTestCase() { sideFpsProgressBarViewModel = SideFpsProgressBarViewModel( mContext, + kosmos.biometricStatusInteractor, kosmos.deviceEntryFingerprintAuthInteractor, sfpsSensorInteractor, kosmos.dozeServiceHost, @@ -263,13 +258,13 @@ class SideFpsOverlayViewModelTest : SysuiTestCase() { 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 6d3cc4c536f1..669795bc91a8 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 @@ class CameraGestureHelperTest : SysuiTestCase() { @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/communal/data/db/CommunalWidgetDaoTest.kt b/packages/SystemUI/tests/src/com/android/systemui/communal/data/db/CommunalWidgetDaoTest.kt index 9c5cd713dffb..20dd913550d3 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/communal/data/db/CommunalWidgetDaoTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/communal/data/db/CommunalWidgetDaoTest.kt @@ -77,6 +77,18 @@ class CommunalWidgetDaoTest : SysuiTestCase() { } @Test + fun deleteWidget_notInDb_returnsFalse() = + testScope.runTest { + val (widgetId, provider, priority) = widgetInfo1 + communalWidgetDao.addWidget( + widgetId = widgetId, + provider = provider, + priority = priority, + ) + assertThat(communalWidgetDao.deleteWidgetById(widgetId = 123)).isFalse() + } + + @Test fun addWidget_emitsActiveWidgetsInDb(): Unit = testScope.runTest { val widgetsToAdd = listOf(widgetInfo1, widgetInfo2) 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 c98d5374311d..de455f6374f1 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.ControlStatus 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.verifyNoMoreInteractions 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 @@ class ControlsControllerImplTest : SysuiTestCase() { 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 @@ class ControlsControllerImplTest : SysuiTestCase() { wrapper, delayableExecutor, uiController, - preferredPanelRepository, + kosmos.selectedComponentRepository, bindingController, listingController, userFileManager, @@ -225,7 +225,7 @@ class ControlsControllerImplTest : SysuiTestCase() { mContext, delayableExecutor, uiController, - preferredPanelRepository, + kosmos.selectedComponentRepository, bindingController, listingController, userFileManager, @@ -245,7 +245,7 @@ class ControlsControllerImplTest : SysuiTestCase() { 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 4828ba37ecb5..18ce4a8e1b7a 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 @@ class AuthorizedPanelsRepositoryImplTest : SysuiTestCase() { 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 @@ class AuthorizedPanelsRepositoryImplTest : SysuiTestCase() { 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 @@ class AuthorizedPanelsRepositoryImplTest : SysuiTestCase() { 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 b463adf40a91..a7e7ba97b5e5 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 android.testing.AndroidTestingRunner 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 @@ class SelectedComponentRepositoryTest : SysuiTestCase() { @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 @@ class SelectedComponentRepositoryTest : SysuiTestCase() { ) 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 bcef67e76ea1..c44429bbbf9e 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 androidx.test.filters.SmallTest 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 @@ class ControlsStartableTest : SysuiTestCase() { @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 @@ class ControlsStartableTest : SysuiTestCase() { whenever(userTracker.userHandle).thenReturn(UserHandle.of(1)) fakeExecutor = FakeExecutor(FakeSystemClock()) - preferredPanelsRepository = FakeSelectedComponentRepository() + preferredPanelsRepository = kosmos.selectedComponentRepository } @Test @@ -444,7 +444,7 @@ class ControlsStartableTest : SysuiTestCase() { 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 36ae0c740c48..8f3813d49b8f 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.controller.StructureInfo 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.res.R 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 @@ import org.mockito.MockitoAnnotations @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 @@ class ControlsUiControllerImplTest : SysuiTestCase() { @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/doze/DozeDockHandlerTest.java b/packages/SystemUI/tests/src/com/android/systemui/doze/DozeDockHandlerTest.java index af027e871542..6d2df19b997d 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 @@ public class DozeDockHandlerTest extends SysuiTestCase { 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 @@ public class DozeDockHandlerTest extends SysuiTestCase { 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/keyboard/stickykeys/ui/StickyKeysIndicatorCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyboard/stickykeys/ui/StickyKeysIndicatorCoordinatorTest.kt index df73cc8f0212..a992956f5121 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.Locked 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.Before 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 @@ class StickyKeysIndicatorCoordinatorTest : SysuiTestCase() { 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/keyguard/domain/interactor/FromLockscreenTransitionInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractorTest.kt index 722c11d9f34c..668fb644065d 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,60 @@ 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.TransitionInfo 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 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.android.systemui.util.mockito.withArgCaptor +import com.google.common.truth.Truth.assertThat import junit.framework.Assert.assertEquals -import junit.framework.Assert.assertTrue -import junit.framework.Assert.fail 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 +78,12 @@ class FromLockscreenTransitionInteractorTest : KeyguardTransitionInteractorTestC 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 +92,69 @@ class FromLockscreenTransitionInteractorTest : KeyguardTransitionInteractorTestC to = KeyguardState.GONE, ) ) - runCurrent() - assertEquals( - listOf( + assertThat(values) + .containsExactly( null, true, // Surface is made visible immediately during LOCKSCREEN -> GONE - ), - values - ) - } - - @Test - fun testSurfaceBehindModel() = - testScope.runTest { - val values by collectValues(underTest.surfaceBehindModel) - runCurrent() - - assertEquals( - values, - listOf( - null, // We should start null ("don't care"). - ) - ) - - transitionRepository.sendTransitionStep( - TransitionStep( - transitionState = TransitionState.STARTED, - from = KeyguardState.LOCKSCREEN, - to = KeyguardState.AOD, ) - ) - runCurrent() - - assertEquals( - listOf( - null, // LOCKSCREEN -> AOD does not have specific view params. - ), - values - ) + .inOrder() transitionRepository.sendTransitionStep( TransitionStep( - transitionState = TransitionState.STARTED, + transitionState = TransitionState.FINISHED, 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, + assertThat(values) + .containsExactly( + null, + true, // Surface remains visible. ) - ) - 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.") - } + .inOrder() } @Test - fun testSurfaceBehindModel_alpha1_whenTransitioningWithInWindowAnimation() = + @EnableFlags(Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR) + fun testTransitionsToGone_whenDismissFlingWhileDismissable_flagEnabled() = testScope.runTest { - testComponent.inWindowLauncherUnlockAnimationRepository.setLauncherActivityClass( - topActivityClassName - ) - runCurrent() + underTest.start() + verify(transitionRepository, never()).startTransition(any()) - val values by collectValues(underTest.surfaceBehindModel) + keyguardRepository.setKeyguardDismissible(true) runCurrent() - - transitionRepository.sendTransitionStep( - TransitionStep( - transitionState = TransitionState.STARTED, - from = KeyguardState.LOCKSCREEN, - to = KeyguardState.GONE, - ) + shadeRepository.setCurrentFling( + FlingInfo(expand = true) // Not a dismiss fling (expand = true). ) runCurrent() - assertEquals(1f, values[values.size - 1]?.alpha) + withArgCaptor<TransitionInfo> { + verify(transitionRepository).startTransition(capture()) + } + .also { + assertEquals(KeyguardState.LOCKSCREEN, it.from) + assertEquals(KeyguardState.GONE, it.to) + } } @Test - fun testSurfaceBehindModel_alphaZero_whenNotTransitioningWithInWindowAnimation() = + @DisableFlags(Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR) + fun testDoesNotTransitionToGone_whenDismissFlingWhileDismissable_flagDisabled() = testScope.runTest { - testComponent.inWindowLauncherUnlockAnimationRepository.setLauncherActivityClass( - "not_launcher" - ) - runCurrent() + underTest.start() + verify(transitionRepository, never()).startTransition(any()) - val values by collectValues(underTest.surfaceBehindModel) + keyguardRepository.setKeyguardDismissible(true) runCurrent() - - transitionRepository.sendTransitionStep( - TransitionStep( - transitionState = TransitionState.STARTED, - from = KeyguardState.LOCKSCREEN, - to = KeyguardState.GONE, - ) + shadeRepository.setCurrentFling( + FlingInfo(expand = true) // Not a dismiss fling (expand = true). ) 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 + verify(transitionRepository, never()).startTransition(any()) } - } } 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 6092b6b351af..f33a5c9484ee 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 @@ 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.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/KeyguardSurfaceBehindInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardSurfaceBehindInteractorTest.kt index 71fcf6f9955c..f23dd557fc1f 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,176 @@ 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.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.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) - - whenever(fromLockscreenTransitionInteractor.surfaceBehindModel) - .thenReturn(flowOf(lockscreenSurfaceBehindModel)) - whenever(fromPrimaryBouncerTransitionInteractor.surfaceBehindModel) - .thenReturn(flowOf(primaryBouncerSurfaceBehindModel)) - - transitionRepository = FakeKeyguardTransitionRepository() + inWindowUnlockInteractor.setLauncherActivityClass(LAUNCHER_ACTIVITY_NAME) - 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) + runCurrent() + + assertThat(values) + .containsExactly( + // We're initialized in LOCKSCREEN. + KeyguardSurfaceBehindModel(alpha = 0f), + ) - // Start on the LOCKSCREEN. transitionRepository.sendTransitionStep( TransitionStep( + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.GONE, transitionState = TransitionState.STARTED, - from = KeyguardState.AOD, - to = KeyguardState.LOCKSCREEN, ) ) - runCurrent() + 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.AOD, - to = KeyguardState.LOCKSCREEN, + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.GONE, + transitionState = TransitionState.RUNNING, ) ) - runCurrent() - // We're on LOCKSCREEN; we should be using the default params. - assertEquals(1, values.size) - assertTrue(values[0].alpha == 0f) + 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( - transitionState = TransitionState.STARTED, 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() - // 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) + assertThat(values) + .containsExactly( + // We're initialized in LOCKSCREEN. + KeyguardSurfaceBehindModel(alpha = 0f), + ) + .inOrder() transitionRepository.sendTransitionStep( TransitionStep( - transitionState = TransitionState.STARTED, - from = KeyguardState.PRIMARY_BOUNCER, + from = KeyguardState.LOCKSCREEN, to = KeyguardState.GONE, + transitionState = TransitionState.STARTED, ) ) - 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) + 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( - transitionState = TransitionState.FINISHED, - from = KeyguardState.PRIMARY_BOUNCER, + from = KeyguardState.LOCKSCREEN, to = KeyguardState.GONE, + transitionState = TransitionState.RUNNING, ) ) + runCurrent() + transitionRepository.sendTransitionStep( + TransitionStep( + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.GONE, + transitionState = TransitionState.FINISHED, + ) + ) 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) + assertThat(values) + .containsExactly( + KeyguardSurfaceBehindModel(alpha = 0f), + // Should have remained at alpha = 1f through the entire animation. + KeyguardSurfaceBehindModel(alpha = 1f), + ) + .inOrder() } } 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 28127186706b..bb61d18f260f 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.keyguard.KeyguardSecurityModel.SecurityMode.PIN 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 @@ -47,16 +42,14 @@ 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.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 @@ -89,22 +82,26 @@ import org.mockito.MockitoAnnotations @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 +109,26 @@ class KeyguardTransitionScenariosTest : SysuiTestCase() { 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 +137,9 @@ class KeyguardTransitionScenariosTest : SysuiTestCase() { 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( 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 000000000000..d77519d4b755 --- /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/media/controls/pipeline/MediaDeviceManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDeviceManagerTest.kt index 437a35f2fab5..e3c4c2858b3d 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDeviceManagerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDeviceManagerTest.kt @@ -475,10 +475,7 @@ public class MediaDeviceManagerTest : SysuiTestCase() { // Then the device name is the PhoneMediaDevice string val data = captureDeviceData(KEY) - assertThat(data.name) - .isEqualTo( - context.getString(com.android.settingslib.R.string.media_transfer_this_device_name) - ) + assertThat(data.name).isEqualTo(PhoneMediaDevice.getMediaTransferThisDeviceName(context)) } @Test 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 239bf659b1b4..edba902d5886 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 @@ -30,6 +30,7 @@ import com.android.systemui.communal.shared.model.CommunalSceneKey import com.android.systemui.controls.controller.ControlsControllerImplTest.Companion.eq import com.android.systemui.dreams.DreamOverlayStateController import com.android.systemui.keyguard.WakefulnessLifecycle +import com.android.systemui.kosmos.testScope import com.android.systemui.media.controls.pipeline.MediaDataManager import com.android.systemui.media.controls.util.MediaFlags import com.android.systemui.media.dream.MediaDreamComplication @@ -52,8 +53,6 @@ import com.android.systemui.utils.os.FakeHandler import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.test.StandardTestDispatcher -import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Assert.assertNotNull @@ -106,8 +105,7 @@ class MediaHierarchyManagerTest : SysuiTestCase() { private lateinit var dreamOverlayCallback: ArgumentCaptor<(DreamOverlayStateController.Callback)> @JvmField @Rule val mockito = MockitoJUnit.rule() - private val testDispatcher = StandardTestDispatcher() - private val testScope = TestScope(testDispatcher) + private val testScope = kosmos.testScope private lateinit var mediaHierarchyManager: MediaHierarchyManager private lateinit var isQsBypassingShade: MutableStateFlow<Boolean> private lateinit var mediaFrame: ViewGroup 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 28d35ce24f4b..e5ba569fd1d6 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 android.view.accessibility.AccessibilityManager; 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.NotificationShadeWindowController; 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.List; import java.util.Optional; import java.util.concurrent.Executor; +import dagger.Lazy; + /** * Tests for {@link NavBarHelper}. */ @@ -271,8 +270,7 @@ public class NavBarHelperTest extends SysuiTestCase { @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 @@ public class NavBarHelperTest extends SysuiTestCase { 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 db5bd9b13dd0..0d1e87433c60 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 @@ public class NavigationBarTest extends SysuiTestCase { @Mock private UiEventLogger mUiEventLogger; @Mock + private NavBarButtonClickLogger mNavBarButtonClickLogger; + @Mock private ViewTreeObserver mViewTreeObserver; NavBarHelper mNavBarHelper; @Mock @@ -596,7 +598,8 @@ public class NavigationBarTest extends SysuiTestCase { mUserContextProvider, mWakefulnessLifecycle, mTaskStackChangeListeners, - new FakeDisplayTracker(mContext))); + new FakeDisplayTracker(mContext), + mNavBarButtonClickLogger)); } private void processAllMessages() { diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelControllerBaseTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelControllerBaseTest.java index a92111e2c270..da8d29c622d1 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelControllerBaseTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelControllerBaseTest.java @@ -246,8 +246,9 @@ public class QSPanelControllerBaseTest extends SysuiTestCase { @Test - public void testShouldUzeHorizontalLayout_falseForSplitShade() { + public void testShouldUseHorizontalLayout_falseForSplitShade() { mConfiguration.orientation = Configuration.ORIENTATION_LANDSCAPE; + mConfiguration.screenLayout = Configuration.SCREENLAYOUT_LONG_YES; when(mMediaHost.getVisible()).thenReturn(true); when(mResources.getBoolean(R.bool.config_use_split_notification_shade)).thenReturn(false); @@ -270,12 +271,13 @@ public class QSPanelControllerBaseTest extends SysuiTestCase { } @Test - public void testChangeConfiguration_shouldUseHorizontalLayout() { + public void testChangeConfiguration_shouldUseHorizontalLayoutInLandscape_true() { when(mMediaHost.getVisible()).thenReturn(true); mController.setUsingHorizontalLayoutChangeListener(mHorizontalLayoutListener); - // When device is rotated to landscape + // When device is rotated to landscape and is long mConfiguration.orientation = Configuration.ORIENTATION_LANDSCAPE; + mConfiguration.screenLayout = Configuration.SCREENLAYOUT_LONG_YES; mController.mOnConfigurationChangedListener.onConfigurationChange(mConfiguration); // Then the layout changes @@ -292,6 +294,29 @@ public class QSPanelControllerBaseTest extends SysuiTestCase { } @Test + public void testChangeConfiguration_shouldUseHorizontalLayoutInLongDevices_true() { + when(mMediaHost.getVisible()).thenReturn(true); + mController.setUsingHorizontalLayoutChangeListener(mHorizontalLayoutListener); + + // When device is rotated to landscape and is long + mConfiguration.orientation = Configuration.ORIENTATION_LANDSCAPE; + mConfiguration.screenLayout = Configuration.SCREENLAYOUT_LONG_YES; + mController.mOnConfigurationChangedListener.onConfigurationChange(mConfiguration); + + // Then the layout changes + assertThat(mController.shouldUseHorizontalLayout()).isTrue(); + verify(mHorizontalLayoutListener).run(); + + // When device changes to not-long + mConfiguration.screenLayout = Configuration.SCREENLAYOUT_LONG_NO; + mController.mOnConfigurationChangedListener.onConfigurationChange(mConfiguration); + + // Then the layout changes back + assertThat(mController.shouldUseHorizontalLayout()).isFalse(); + verify(mHorizontalLayoutListener, times(2)).run(); + } + + @Test public void testRefreshAllTilesDoesntRefreshListeningTiles() { when(mQSHost.getTiles()).thenReturn(List.of(mQSTile, mOtherTile)); mController.setTiles(); @@ -310,6 +335,7 @@ public class QSPanelControllerBaseTest extends SysuiTestCase { when(mMediaHost.getVisible()).thenReturn(true); when(mResources.getBoolean(R.bool.config_use_split_notification_shade)).thenReturn(false); mConfiguration.orientation = Configuration.ORIENTATION_LANDSCAPE; + mConfiguration.screenLayout = Configuration.SCREENLAYOUT_LONG_YES; mController.setUsingHorizontalLayoutChangeListener(mHorizontalLayoutListener); mController.mOnConfigurationChangedListener.onConfigurationChange(mConfiguration); assertThat(mController.shouldUseHorizontalLayout()).isTrue(); 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 a6e240b1b701..9517f823cf10 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,13 +35,18 @@ import com.android.systemui.communal.shared.model.CommunalSceneKey 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 @@ -51,11 +58,17 @@ import org.mockito.Mockito.times import org.mockito.Mockito.verify import org.mockito.MockitoAnnotations +@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 @@ -102,17 +115,24 @@ class GlanceableHubContainerControllerTest : SysuiTestCase() { R.dimen.communal_bottom_edge_swipe_region_height, BOTTOM_SWIPE_REGION_WIDTH ) + + initAndAttachContainerView() + } + + @After + fun tearDown() { + ViewUtils.detachView(parentView) } @Test - fun isEnabled_interactorEnabled_interceptsTouches() { + fun isEnabled_communalEnabled_returnsTrue() { communalRepository.setIsCommunalEnabled(true) assertThat(underTest.isEnabled()).isTrue() } @Test - fun isEnabled_interactorDisabled_doesNotIntercept() { + fun isEnabled_communalDisabled_returnsFalse() { communalRepository.setIsCommunalEnabled(false) assertThat(underTest.isEnabled()).isFalse() @@ -122,11 +142,29 @@ class GlanceableHubContainerControllerTest : SysuiTestCase() { fun initView_notEnabled_throwsException() { communalRepository.setIsCommunalEnabled(false) + underTest = + GlanceableHubContainerController( + communalInteractor, + communalViewModel, + keyguardTransitionInteractor, + shadeInteractor, + powerManager, + ) + assertThrows(RuntimeException::class.java) { underTest.initView(context) } } @Test fun initView_calledTwice_throwsException() { + underTest = + GlanceableHubContainerController( + communalInteractor, + communalViewModel, + keyguardTransitionInteractor, + shadeInteractor, + powerManager, + ) + // First call succeeds. underTest.initView(context) @@ -135,25 +173,20 @@ class GlanceableHubContainerControllerTest : SysuiTestCase() { } @Test - fun onTouchEvent_touchInsideGestureRegion_interceptsTouches() { - // Communal is open. - communalRepository.setDesiredScene(CommunalSceneKey.Communal) - - initAndAttachContainerView() + fun onTouchEvent_communalClosed_doesNotIntercept() { + // Communal is closed. + goToScene(CommunalSceneKey.Blank) - // 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) - - initAndAttachContainerView() + fun onTouchEvent_openGesture_interceptsTouches() { + // Communal is closed. + goToScene(CommunalSceneKey.Blank) - // 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() @@ -163,34 +196,27 @@ class GlanceableHubContainerControllerTest : SysuiTestCase() { @Test fun onTouchEvent_communalOpen_interceptsTouches() { // Communal is open. - communalRepository.setDesiredScene(CommunalSceneKey.Communal) - - initAndAttachContainerView() - testableLooper.processAllMessages() + goToScene(CommunalSceneKey.Communal) - // 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() @@ -199,9 +225,7 @@ class GlanceableHubContainerControllerTest : SysuiTestCase() { @Test fun onTouchEvent_communalAndBouncerShowing_doesNotIntercept() { // Communal is open. - communalRepository.setDesiredScene(CommunalSceneKey.Communal) - - initAndAttachContainerView() + goToScene(CommunalSceneKey.Communal) // Bouncer is visible. bouncerShowingFlow.value = true @@ -216,9 +240,7 @@ class GlanceableHubContainerControllerTest : SysuiTestCase() { @Test fun onTouchEvent_communalAndShadeShowing_doesNotIntercept() { // Communal is open. - communalRepository.setDesiredScene(CommunalSceneKey.Communal) - - initAndAttachContainerView() + goToScene(CommunalSceneKey.Communal) shadeShowingFlow.value = true testableLooper.processAllMessages() @@ -230,10 +252,7 @@ class GlanceableHubContainerControllerTest : SysuiTestCase() { @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() @@ -251,15 +270,24 @@ class GlanceableHubContainerControllerTest : SysuiTestCase() { 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 { @@ -269,13 +297,17 @@ class GlanceableHubContainerControllerTest : SysuiTestCase() { 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/NotificationPanelViewControllerBaseTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java index b3e386e69905..cc27cbd9d809 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java @@ -189,7 +189,6 @@ import com.android.systemui.statusbar.policy.KeyguardStateController; import com.android.systemui.statusbar.policy.KeyguardUserSwitcherController; import com.android.systemui.statusbar.policy.KeyguardUserSwitcherView; import com.android.systemui.statusbar.policy.ResourcesSplitShadeStateController; -import com.android.systemui.statusbar.policy.data.repository.FakeDeviceProvisioningRepository; import com.android.systemui.statusbar.policy.data.repository.FakeUserSetupRepository; import com.android.systemui.statusbar.window.StatusBarWindowStateController; import com.android.systemui.unfold.SysUIUnfoldComponent; @@ -411,7 +410,7 @@ public class NotificationPanelViewControllerBaseTest extends SysuiTestCase { mShadeInteractor = new ShadeInteractorImpl( mTestScope.getBackgroundScope(), - new FakeDeviceProvisioningRepository(), + mKosmos.getDeviceProvisioningInteractor(), new FakeDisableFlagsRepository(), mDozeParameters, mFakeKeyguardRepository, 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 461db8e6166c..c772ee288f8b 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,9 @@ import com.android.systemui.flags.FakeFeatureFlagsClassic; 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 +83,6 @@ import com.android.systemui.shade.data.repository.FakeShadeRepository; 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; @@ -99,7 +94,6 @@ import com.android.systemui.statusbar.phone.ScrimController; import com.android.systemui.statusbar.policy.ConfigurationController; import com.android.systemui.statusbar.policy.KeyguardStateController; import com.android.systemui.statusbar.policy.ResourcesSplitShadeStateController; -import com.android.systemui.statusbar.policy.data.repository.FakeDeviceProvisioningRepository; import com.android.systemui.statusbar.policy.data.repository.FakeUserSetupRepository; import com.android.systemui.user.domain.interactor.SelectedUserInteractor; import com.android.systemui.user.domain.interactor.UserSwitcherInteractor; @@ -214,45 +208,9 @@ public class NotificationShadeWindowControllerImplTest extends SysuiTestCase { () -> mFromLockscreenTransitionInteractor, () -> mFromPrimaryBouncerTransitionInteractor); - 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); @@ -260,7 +218,7 @@ public class NotificationShadeWindowControllerImplTest extends SysuiTestCase { mShadeInteractor = new ShadeInteractorImpl( mTestScope.getBackgroundScope(), - new FakeDeviceProvisioningRepository(), + mKosmos.getDeviceProvisioningInteractor(), new FakeDisableFlagsRepository(), mock(DozeParameters.class), keyguardRepository, @@ -452,11 +410,11 @@ public class NotificationShadeWindowControllerImplTest extends SysuiTestCase { } @Test - public void setCommunalShowing_userTimeout() { + public void setCommunalVisible_userTimeout() { setKeyguardShowing(); clearInvocations(mWindowManager); - mNotificationShadeWindowController.onCommunalShowingChanged(true); + mNotificationShadeWindowController.onCommunalVisibleChanged(true); verify(mWindowManager).updateViewLayout(any(), mLayoutParameters.capture()); assertThat(mLayoutParameters.getValue().userActivityTimeout) .isEqualTo(CommunalInteractor.AWAKE_INTERVAL_MS); diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationsQSContainerControllerLegacyTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationsQSContainerControllerLegacyTest.kt index 697b05aa9add..c2261211b339 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationsQSContainerControllerLegacyTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationsQSContainerControllerLegacyTest.kt @@ -28,8 +28,6 @@ import androidx.constraintlayout.widget.ConstraintSet import androidx.test.filters.SmallTest import com.android.systemui.Flags.FLAG_CENTRALIZED_STATUS_BAR_DIMENS_REFACTOR import com.android.systemui.SysuiTestCase -import com.android.systemui.flags.FakeFeatureFlags -import com.android.systemui.flags.Flags import com.android.systemui.fragments.FragmentHostManager import com.android.systemui.fragments.FragmentService import com.android.systemui.navigationbar.NavigationModeController @@ -94,7 +92,6 @@ class NotificationsQSContainerControllerLegacyTest : SysuiTestCase() { lateinit var underTest: NotificationsQSContainerController - private lateinit var featureFlags: FakeFeatureFlags private lateinit var navigationModeCallback: ModeChangedListener private lateinit var taskbarVisibilityCallback: OverviewProxyListener private lateinit var windowInsetsCallback: Consumer<WindowInsets> @@ -106,7 +103,6 @@ class NotificationsQSContainerControllerLegacyTest : SysuiTestCase() { MockitoAnnotations.initMocks(this) fakeSystemClock = FakeSystemClock() delayableExecutor = FakeExecutor(fakeSystemClock) - featureFlags = FakeFeatureFlags().apply { set(Flags.QS_CONTAINER_GRAPH_OPTIMIZER, false) } mContext.ensureTestableResources() whenever(view.context).thenReturn(mContext) whenever(view.resources).thenReturn(mContext.resources) @@ -123,7 +119,6 @@ class NotificationsQSContainerControllerLegacyTest : SysuiTestCase() { shadeInteractor, fragmentService, delayableExecutor, - featureFlags, notificationStackScrollLayoutController, ResourcesSplitShadeStateController(), largeScreenHeaderHelperLazy = { largeScreenHeaderHelper } @@ -536,7 +531,6 @@ class NotificationsQSContainerControllerLegacyTest : SysuiTestCase() { shadeInteractor, fragmentService, delayableExecutor, - featureFlags, notificationStackScrollLayoutController, ResourcesSplitShadeStateController(), largeScreenHeaderHelperLazy = { largeScreenHeaderHelper } diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationsQSContainerControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationsQSContainerControllerTest.kt index e66251a030a3..c32635020ddc 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationsQSContainerControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationsQSContainerControllerTest.kt @@ -28,8 +28,6 @@ import androidx.constraintlayout.widget.ConstraintSet import androidx.test.filters.SmallTest import com.android.systemui.Flags.FLAG_CENTRALIZED_STATUS_BAR_DIMENS_REFACTOR import com.android.systemui.SysuiTestCase -import com.android.systemui.flags.FakeFeatureFlags -import com.android.systemui.flags.Flags import com.android.systemui.fragments.FragmentHostManager import com.android.systemui.fragments.FragmentService import com.android.systemui.keyguard.shared.KeyguardShadeMigrationNssl @@ -91,7 +89,6 @@ class NotificationsQSContainerControllerTest : SysuiTestCase() { lateinit var underTest: NotificationsQSContainerController - private lateinit var featureFlags: FakeFeatureFlags private lateinit var navigationModeCallback: ModeChangedListener private lateinit var taskbarVisibilityCallback: OverviewProxyListener private lateinit var windowInsetsCallback: Consumer<WindowInsets> @@ -104,7 +101,6 @@ class NotificationsQSContainerControllerTest : SysuiTestCase() { fakeSystemClock = FakeSystemClock() delayableExecutor = FakeExecutor(fakeSystemClock) mSetFlagsRule.enableFlags(KeyguardShadeMigrationNssl.FLAG_NAME) - featureFlags = FakeFeatureFlags().apply { set(Flags.QS_CONTAINER_GRAPH_OPTIMIZER, true) } mContext.ensureTestableResources() whenever(view.context).thenReturn(mContext) whenever(view.resources).thenReturn(mContext.resources) @@ -122,7 +118,6 @@ class NotificationsQSContainerControllerTest : SysuiTestCase() { shadeInteractor, fragmentService, delayableExecutor, - featureFlags, notificationStackScrollLayoutController, ResourcesSplitShadeStateController(), largeScreenHeaderHelperLazy = { largeScreenHeaderHelper } @@ -513,7 +508,6 @@ class NotificationsQSContainerControllerTest : SysuiTestCase() { shadeInteractor, fragmentService, delayableExecutor, - featureFlags, notificationStackScrollLayoutController, ResourcesSplitShadeStateController(), largeScreenHeaderHelperLazy = { largeScreenHeaderHelper } 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 3e0a647d464e..72c52ec17934 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerBaseTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerBaseTest.java @@ -35,7 +35,6 @@ import android.view.accessibility.AccessibilityManager; 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; @@ -51,13 +50,9 @@ import com.android.systemui.flags.FeatureFlags; 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 +73,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.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; @@ -103,7 +97,6 @@ import com.android.systemui.statusbar.phone.StatusBarTouchableRegionManager; import com.android.systemui.statusbar.policy.CastController; import com.android.systemui.statusbar.policy.KeyguardStateController; import com.android.systemui.statusbar.policy.ResourcesSplitShadeStateController; -import com.android.systemui.statusbar.policy.data.repository.FakeDeviceProvisioningRepository; import com.android.systemui.statusbar.policy.data.repository.FakeUserSetupRepository; import com.android.systemui.user.domain.interactor.SelectedUserInteractor; import com.android.systemui.user.domain.interactor.UserSwitcherInteractor; @@ -205,9 +198,7 @@ public class QuickSettingsControllerBaseTest extends SysuiTestCase { mStatusBarStateController = mKosmos.getStatusBarStateController(); mInteractionJankMonitor = mKosmos.getInteractionJankMonitor(); - FakeDeviceProvisioningRepository deviceProvisioningRepository = - new FakeDeviceProvisioningRepository(); - deviceProvisioningRepository.setDeviceProvisioned(true); + mKosmos.getFakeDeviceProvisioningRepository().setDeviceProvisioned(true); FakeFeatureFlagsClassic featureFlags = new FakeFeatureFlagsClassic(); FakeConfigurationRepository configurationRepository = new FakeConfigurationRepository(); @@ -245,45 +236,9 @@ public class QuickSettingsControllerBaseTest extends SysuiTestCase { () -> mFromLockscreenTransitionInteractor, () -> mFromPrimaryBouncerTransitionInteractor); - 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(); @@ -294,7 +249,7 @@ public class QuickSettingsControllerBaseTest extends SysuiTestCase { mShadeInteractor = new ShadeInteractorImpl( mTestScope.getBackgroundScope(), - deviceProvisioningRepository, + mKosmos.getDeviceProvisioningInteractor(), mDisableFlagsRepository, mDozeParameters, mKeyguardRepository, 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 50349bea390f..0dd988d424b9 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 @@ class NotificationSettingsRepositoryTest : SysuiTestCase() { 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 8cb064dd39a6..8fd9c808f6d1 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.bouncer.data.repository.FakeKeyguardBouncerRepositor 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.kosmos.testDispatcher import com.android.systemui.kosmos.testScope import com.android.systemui.plugins.statusbar.StatusBarStateController @@ -56,8 +53,8 @@ import com.android.systemui.shade.domain.interactor.ShadeInteractorLegacyImpl import com.android.systemui.statusbar.disableflags.data.repository.FakeDisableFlagsRepository import com.android.systemui.statusbar.notification.stack.domain.interactor.SharedNotificationContainerInteractor import com.android.systemui.statusbar.policy.ResourcesSplitShadeStateController -import com.android.systemui.statusbar.policy.data.repository.FakeDeviceProvisioningRepository import com.android.systemui.statusbar.policy.data.repository.FakeUserSetupRepository +import com.android.systemui.statusbar.policy.domain.interactor.deviceProvisioningInteractor import com.android.systemui.testKosmos import com.android.systemui.util.mockito.mock import kotlinx.coroutines.flow.emptyFlow @@ -74,8 +71,8 @@ import org.mockito.ArgumentMatchers.eq 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) @@ -144,55 +141,14 @@ class StatusBarStateControllerImplTest : SysuiTestCase() { { 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 - ) + fromLockscreenTransitionInteractor = kosmos.fromLockscreenTransitionInteractor + fromPrimaryBouncerTransitionInteractor = kosmos.fromPrimaryBouncerTransitionInteractor whenever(deviceEntryUdfpsInteractor.isUdfpsSupported).thenReturn(emptyFlow()) shadeInteractor = ShadeInteractorImpl( testScope.backgroundScope, - FakeDeviceProvisioningRepository(), + kosmos.deviceProvisioningInteractor, FakeDisableFlagsRepository(), mock(), keyguardRepository, 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 350ed2d9ff22..7d99d05e3f0b 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.platform.test.annotations.EnableFlags 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 57dac3ac19a6..cac4a8d2d37f 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 @@ public class FooterViewTest extends SysuiTestCase { } @Test + @DisableFlags(FooterViewRefactor.FLAG_NAME) public void setHistoryShown() { mView.showHistory(true); assertTrue(mView.isHistoryShown()); @@ -103,6 +107,7 @@ public class FooterViewTest extends SysuiTestCase { } @Test + @DisableFlags(FooterViewRefactor.FLAG_NAME) public void setHistoryNotShown() { mView.showHistory(false); assertFalse(mView.isHistoryShown()); @@ -128,6 +133,62 @@ public class FooterViewTest extends SysuiTestCase { @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 class FooterViewTest extends SysuiTestCase { 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 class FooterViewTest extends SysuiTestCase { 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 class FooterViewTest extends SysuiTestCase { 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 class FooterViewTest extends SysuiTestCase { 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 8ab13f5aa720..620d97275949 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 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, - ) - ) + 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 - @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 @@ class FooterViewModelTest : SysuiTestCase() { @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 @@ class FooterViewModelTest : SysuiTestCase() { @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 @@ class FooterViewModelTest : SysuiTestCase() { @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 @@ class FooterViewModelTest : SysuiTestCase() { @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 @@ class FooterViewModelTest : SysuiTestCase() { @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 @@ class FooterViewModelTest : SysuiTestCase() { // 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 da68d9c743ce..54108642385f 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 @@ class VisualInterruptionDecisionProviderImplTest : VisualInterruptionDecisionPro systemClock, uiEventLogger, userTracker, + avalancheProvider ) } @@ -70,6 +74,114 @@ class VisualInterruptionDecisionProviderImplTest : VisualInterruptionDecisionPro } } + // 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 2ac0cb7499d2..f89b9cd7410f 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 @@ package com.android.systemui.statusbar.notification.interruption 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 android.provider.Settings.Global.HEADS_UP_ON 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 @@ abstract class VisualInterruptionDecisionProviderTestBase : SysuiTestCase() { 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 @@ abstract class VisualInterruptionDecisionProviderTestBase : SysuiTestCase() { 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 @@ abstract class VisualInterruptionDecisionProviderTestBase : SysuiTestCase() { 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 @@ abstract class VisualInterruptionDecisionProviderTestBase : SysuiTestCase() { var packageSuspended = false var visibilityOverride: Int? = null var suppressedVisualEffects: Int? = null + var isConversation = false private fun buildBubbleMetadata(): BubbleMetadata { val builder = @@ -1158,6 +1169,13 @@ abstract class VisualInterruptionDecisionProviderTestBase : SysuiTestCase() { 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 @@ abstract class VisualInterruptionDecisionProviderTestBase : SysuiTestCase() { 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 @@ abstract class VisualInterruptionDecisionProviderTestBase : SysuiTestCase() { 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 @@ abstract class VisualInterruptionDecisionProviderTestBase : SysuiTestCase() { } visibilityOverride?.let { mrb.setVisibilityOverride(it) } suppressedVisualEffects?.let { mrb.setSuppressedVisualEffects(it) } + mrb.setIsConversation(isConversation) } .build() } @@ -1287,7 +1311,7 @@ abstract class VisualInterruptionDecisionProviderTestBase : SysuiTestCase() { } } - 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 0356c2cdbcd0..620ad9c19bfa 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.hardware.display.AmbientDisplayConfiguration 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 @@ object VisualInterruptionDecisionProviderTestUtil { statusBarStateController: StatusBarStateController, systemClock: SystemClock, uiEventLogger: UiEventLogger, - userTracker: UserTracker + userTracker: UserTracker, + avalancheProvider: AvalancheProvider ): VisualInterruptionDecisionProvider { return if (VisualInterruptionRefactor.isEnabled) { VisualInterruptionDecisionProviderImpl( @@ -67,7 +69,8 @@ object VisualInterruptionDecisionProviderTestUtil { 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 dbe63f290407..7589a49e1963 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; 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 4188c5d34d98..88e4f5ab8d55 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,27 @@ import android.platform.test.annotations.EnableFlags 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.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.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.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.zenModeRepository +import com.android.systemui.statusbar.policy.fakeConfigurationController +import com.android.systemui.testKosmos 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 +53,18 @@ import org.mockito.MockitoAnnotations @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 activeNotificationListRepository = kosmos.activeNotificationListRepository + private val fakeKeyguardTransitionRepository = kosmos.fakeKeyguardTransitionRepository + private val fakeShadeRepository = kosmos.fakeShadeRepository + private val zenModeRepository = kosmos.zenModeRepository + private val fakeConfigurationController = kosmos.fakeConfigurationController - 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() - ) + val underTest = kosmos.notificationListViewModel @Before fun setUp() { @@ -110,11 +73,11 @@ class NotificationListViewModelTest : SysuiTestCase() { @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 +92,11 @@ class NotificationListViewModelTest : SysuiTestCase() { @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 +111,11 @@ class NotificationListViewModelTest : SysuiTestCase() { @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 +130,7 @@ class NotificationListViewModelTest : SysuiTestCase() { @Test fun testShouldShowEmptyShadeView_trueWhenNoNotifs() = - testComponent.runTest { + testScope.runTest { val shouldShow by collectLastValue(underTest.shouldShowEmptyShadeView) // WHEN has no notifs @@ -180,7 +143,7 @@ class NotificationListViewModelTest : SysuiTestCase() { @Test fun testShouldShowEmptyShadeView_falseWhenNotifs() = - testComponent.runTest { + testScope.runTest { val shouldShow by collectLastValue(underTest.shouldShowEmptyShadeView) // WHEN has notifs @@ -193,13 +156,13 @@ class NotificationListViewModelTest : SysuiTestCase() { @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 +171,16 @@ class NotificationListViewModelTest : SysuiTestCase() { @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 +189,13 @@ class NotificationListViewModelTest : SysuiTestCase() { @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 +211,13 @@ class NotificationListViewModelTest : SysuiTestCase() { @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 +230,7 @@ class NotificationListViewModelTest : SysuiTestCase() { @Test fun testAreNotificationsHiddenInShade_true() = - testComponent.runTest { + testScope.runTest { val hidden by collectLastValue(underTest.areNotificationsHiddenInShade) zenModeRepository.setSuppressedVisualEffects(Policy.SUPPRESSED_EFFECT_NOTIFICATION_LIST) @@ -279,7 +242,7 @@ class NotificationListViewModelTest : SysuiTestCase() { @Test fun testAreNotificationsHiddenInShade_false() = - testComponent.runTest { + testScope.runTest { val hidden by collectLastValue(underTest.areNotificationsHiddenInShade) zenModeRepository.setSuppressedVisualEffects(Policy.SUPPRESSED_EFFECT_NOTIFICATION_LIST) @@ -291,7 +254,7 @@ class NotificationListViewModelTest : SysuiTestCase() { @Test fun testHasFilteredOutSeenNotifications_true() = - testComponent.runTest { + testScope.runTest { val hasFilteredNotifs by collectLastValue(underTest.hasFilteredOutSeenNotifications) activeNotificationListRepository.hasFilteredOutSeenNotifications.value = true @@ -302,7 +265,7 @@ class NotificationListViewModelTest : SysuiTestCase() { @Test fun testHasFilteredOutSeenNotifications_false() = - testComponent.runTest { + testScope.runTest { val hasFilteredNotifs by collectLastValue(underTest.hasFilteredOutSeenNotifications) activeNotificationListRepository.hasFilteredOutSeenNotifications.value = false 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 849a13be58ff..b048949e0e76 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 @@ -153,6 +153,7 @@ import com.android.systemui.statusbar.notification.NotificationActivityStarter; 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; @@ -313,6 +314,7 @@ public class CentralSurfacesImplTest extends SysuiTestCase { @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 @@ public class CentralSurfacesImplTest extends SysuiTestCase { mStatusBarStateController, mFakeSystemClock, mock(UiEventLogger.class), - mUserTracker); + mUserTracker, + mAvalancheProvider); mVisualInterruptionDecisionProvider.start(); mContext.addMockSystemService(TrustManager.class, mock(TrustManager.class)); 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 3bde6e36a51f..99c2dc79b491 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 @@ public class ScrimControllerTest extends SysuiTestCase { @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 cb4531567e86..4015361dc277 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.dreams.DreamOverlayStateController; 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 @@ public class StatusBarKeyguardViewManagerTest extends SysuiTestCase { StandardTestDispatcher(null, null), () -> mock(WindowManagerLockscreenVisibilityInteractor.class), () -> mock(KeyguardDismissActionInteractor.class), - mSelectedUserInteractor) { + mSelectedUserInteractor, + () -> mock(KeyguardSurfaceBehindInteractor.class)) { @Override public ViewRootImpl getViewRootImpl() { return mViewRootImpl; @@ -730,7 +732,8 @@ public class StatusBarKeyguardViewManagerTest extends SysuiTestCase { 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/policy/SensitiveNotificationProtectionControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionControllerTest.kt index cd5d5ed0d08e..9919c6b78aab 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 @@ -16,6 +16,7 @@ package com.android.systemui.statusbar.policy +import android.app.ActivityOptions import android.app.Notification import android.media.projection.MediaProjectionInfo import android.media.projection.MediaProjectionManager @@ -23,7 +24,7 @@ import android.os.Handler import android.service.notification.StatusBarNotification import android.testing.AndroidTestingRunner 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 org.junit.Assert.assertFalse @@ -69,6 +70,8 @@ class SensitiveNotificationProtectionControllerTest : SysuiTestCase() { MockitoAnnotations.initMocks(this) mSetFlagsRule.enableFlags(Flags.FLAG_SCREENSHARE_NOTIFICATION_HIDING) + setShareFullScreen() + controller = SensitiveNotificationProtectionControllerImpl(mediaProjectionManager, handler) // Obtain useful MediaProjectionCallback @@ -195,6 +198,14 @@ class SensitiveNotificationProtectionControllerTest : SysuiTestCase() { } @Test + fun isSensitiveStateActive_projectionActive_singleActivity_false() { + setShareSingleApp() + mediaProjectionCallbackCaptor.value.onStart(mediaProjectionInfo) + + assertFalse(controller.isSensitiveStateActive) + } + + @Test fun shouldProtectNotification_projectionInactive_false() { val notificationEntry = mock(NotificationEntry::class.java) @@ -202,30 +213,74 @@ class SensitiveNotificationProtectionControllerTest : SysuiTestCase() { } @Test - fun shouldProtectNotification_projectionActive_fgsNotification_false() { + fun shouldProtectNotification_projectionActive_singleActivity_false() { + setShareSingleApp() mediaProjectionCallbackCaptor.value.onStart(mediaProjectionInfo) - 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.notification).thenReturn(notification) - `when`(notification.isFgsOrUij).thenReturn(true) + val notificationEntry = setupNotificationEntry(TEST_PACKAGE_NAME) + + assertFalse(controller.shouldProtectNotification(notificationEntry)) + } + + @Test + fun shouldProtectNotification_projectionActive_fgsNotificationFromProjectionApp_false() { + mediaProjectionCallbackCaptor.value.onStart(mediaProjectionInfo) + + val notificationEntry = setupFgsNotificationEntry(TEST_PROJECTION_PACKAGE_NAME) assertFalse(controller.shouldProtectNotification(notificationEntry)) } @Test + fun shouldProtectNotification_projectionActive_fgsNotificationNotFromProjectionApp_true() { + mediaProjectionCallbackCaptor.value.onStart(mediaProjectionInfo) + + val notificationEntry = setupFgsNotificationEntry(TEST_PACKAGE_NAME) + + assertTrue(controller.shouldProtectNotification(notificationEntry)) + } + + @Test fun shouldProtectNotification_projectionActive_notFgsNotification_true() { mediaProjectionCallbackCaptor.value.onStart(mediaProjectionInfo) + val notificationEntry = setupNotificationEntry(TEST_PROJECTION_PACKAGE_NAME) + + assertTrue(controller.shouldProtectNotification(notificationEntry)) + } + + private fun setShareFullScreen() { + `when`(mediaProjectionInfo.packageName).thenReturn(TEST_PROJECTION_PACKAGE_NAME) + `when`(mediaProjectionInfo.launchCookie).thenReturn(null) + } + + private fun setShareSingleApp() { + `when`(mediaProjectionInfo.packageName).thenReturn(TEST_PROJECTION_PACKAGE_NAME) + `when`(mediaProjectionInfo.launchCookie).thenReturn(ActivityOptions.LaunchCookie()) + } + + private fun setupNotificationEntry( + packageName: String, + isFgs: Boolean = false + ): NotificationEntry { 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(false) + `when`(notification.isFgsOrUij).thenReturn(isFgs) - assertTrue(controller.shouldProtectNotification(notificationEntry)) + return notificationEntry + } + + private fun setupFgsNotificationEntry(packageName: String): NotificationEntry { + return setupNotificationEntry(packageName, /* isFgs= */ true) + } + + companion object { + 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" } } 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 ba34ce6c90f0..bda339f7a022 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.os.Trace.TRACE_TAG_APP 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 @@ class TraceUtilsTest : SysuiTestCase() { } @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 @@ class TraceUtilsTest : SysuiTestCase() { 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/util/TestUtils.kt b/packages/SystemUI/tests/src/com/android/systemui/util/TestUtils.kt new file mode 100644 index 000000000000..27cd4b6b9885 --- /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/wmshell/BubblesTest.java b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java index 0a3c2d9a77cd..1d428c89333e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java @@ -91,11 +91,11 @@ import androidx.test.filters.SmallTest; 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; import com.android.systemui.bouncer.data.repository.FakeKeyguardBouncerRepository; +import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.colorextraction.SysuiColorExtractor; import com.android.systemui.common.ui.data.repository.FakeConfigurationRepository; import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractor; @@ -107,13 +107,9 @@ import com.android.systemui.flags.FakeFeatureFlagsClassic; 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 +133,6 @@ import com.android.systemui.shade.data.repository.FakeShadeRepository; 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 +146,7 @@ import com.android.systemui.statusbar.notification.collection.NotificationEntryB 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; @@ -444,45 +440,9 @@ public class BubblesTest extends SysuiTestCase { () -> mFromPrimaryBouncerTransitionInteractor); CommunalInteractor communalInteractor = mKosmos.getCommunalInteractor(); - 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(); @@ -494,7 +454,7 @@ public class BubblesTest extends SysuiTestCase { mShadeInteractor = new ShadeInteractorImpl( mTestScope.getBackgroundScope(), - deviceProvisioningRepository, + mKosmos.getDeviceProvisioningInteractor(), new FakeDisableFlagsRepository(), mDozeParameters, keyguardRepository, @@ -587,7 +547,9 @@ public class BubblesTest extends SysuiTestCase { 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 +578,7 @@ public class BubblesTest extends SysuiTestCase { mPositioner, mock(DisplayController.class), mOneHandedOptional, - Optional.of(mock(DragAndDropController.class)), + mock(DragAndDropController.class), syncExecutor, mock(Handler.class), mTaskViewTransitions, @@ -2098,6 +2060,26 @@ public class BubblesTest extends SysuiTestCase { } @Test + public void testShowOrHideAppBubble_addsFromOverflow() { + String appBubbleKey = Bubble.getAppBubbleKeyForApp(mAppBubbleIntent.getPackage(), mUser0); + mBubbleController.showOrHideAppBubble(mAppBubbleIntent, mUser0, mAppBubbleIcon); + + // Collapse the stack so we don't need to wait for the dismiss animation in the test + mBubbleController.collapseStack(); + + // Dismiss the app bubble so it's in the overflow + mBubbleController.dismissBubble(appBubbleKey, Bubbles.DISMISS_USER_GESTURE); + assertThat(mBubbleData.getOverflowBubbleWithKey(appBubbleKey)).isNotNull(); + + // Calling this while collapsed will re-add and expand the app bubble + mBubbleController.showOrHideAppBubble(mAppBubbleIntent, mUser0, mAppBubbleIcon); + assertThat(mBubbleData.getSelectedBubble().getKey()).isEqualTo(appBubbleKey); + assertThat(mBubbleController.isStackExpanded()).isTrue(); + assertThat(mBubbleData.getBubbles().size()).isEqualTo(1); + assertThat(mBubbleData.getOverflowBubbleWithKey(appBubbleKey)).isNull(); + } + + @Test public void testCreateBubbleFromOngoingNotification() { NotificationEntry notif = new NotificationEntryBuilder() .setFlag(mContext, Notification.FLAG_ONGOING_EVENT, true) 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 9ad234e16bb3..4a5ebd057835 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 @@ public class TestableBubbleController extends BubbleController { 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 fb51f0fec997..37e3a590dd65 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/systemui/biometrics/data/repository/BiometricStatusRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/BiometricStatusRepositoryKosmos.kt index 961022f0f426..a4f28f395a63 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 @@ package com.android.systemui.biometrics.data.repository 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 1c8bd3b58dfe..e9b7a69d1421 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 @@ class FakeBiometricStatusRepository : BiometricStatusRepository { 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 60f0448e89b3..3a61bf625804 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 @@ class FakePackageChangeRepository : PackageChangeRepository { 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 adc05e0ae6a2..57caa525d225 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/common/domain/interactor/PackageChangeInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/common/domain/interactor/PackageChangeInteractorKosmos.kt new file mode 100644 index 000000000000..e56d9d9a132f --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/common/domain/interactor/PackageChangeInteractorKosmos.kt @@ -0,0 +1,24 @@ +/* + * 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 com.android.systemui.common.data.repository.packageChangeRepository +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.user.domain.interactor.selectedUserInteractor + +var Kosmos.packageChangeInteractor by + Kosmos.Fixture { PackageChangeInteractor(packageChangeRepository, selectedUserInteractor) } 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 3ea3ccfb2909..1884a3264ed6 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 @@ class FakeCommunalMediaRepository : CommunalMediaRepository { 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/FakeCommunalWidgetRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalWidgetRepository.kt index fab64e38e1f8..7301404eada0 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalWidgetRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalWidgetRepository.kt @@ -36,7 +36,7 @@ class FakeCommunalWidgetRepository(private val coroutineScope: CoroutineScope) : } } - override fun deleteWidget(widgetId: Int) { + override fun deleteWidgetFromDb(widgetId: Int) { if (_communalWidgets.value.none { it.appWidgetId == widgetId }) { return } @@ -44,6 +44,10 @@ class FakeCommunalWidgetRepository(private val coroutineScope: CoroutineScope) : _communalWidgets.value = _communalWidgets.value.filter { it.appWidgetId != widgetId } } + override fun deleteWidgetFromHost(widgetId: Int) { + deleteWidgetFromDb(widgetId) + } + private fun onConfigured(id: Int, providerInfo: AppWidgetProviderInfo, priority: Int) { _communalWidgets.value += listOf(CommunalWidgetContentModel(id, providerInfo, priority)) } 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 c818e9c8971c..c47f020a3b83 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 @@ -24,12 +24,15 @@ import com.android.systemui.communal.widgets.EditWidgetsActivityStarter 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.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 { CommunalInteractor( + applicationScope = applicationCoroutineScope, communalRepository = communalRepository, widgetRepository = communalWidgetRepository, mediaRepository = communalMediaRepository, @@ -39,6 +42,8 @@ val Kosmos.communalInteractor by Fixture { appWidgetHost = mock(), keyguardInteractor = keyguardInteractor, editWidgetsActivityStarter = editWidgetsActivityStarter, + logBuffer = logcatLogBuffer("CommunalInteractor"), + 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 adaea7cbf64d..9776b436555d 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 @@ -21,6 +21,7 @@ import com.android.systemui.communal.data.repository.communalTutorialRepository import com.android.systemui.keyguard.domain.interactor.keyguardInteractor import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.applicationCoroutineScope +import com.android.systemui.util.mockito.mock val Kosmos.communalTutorialInteractor by Kosmos.Fixture { @@ -30,5 +31,6 @@ val Kosmos.communalTutorialInteractor by keyguardInteractor = keyguardInteractor, communalRepository = communalRepository, communalInteractor = communalInteractor, + 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 000000000000..109e113611c6 --- /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 a231212518ec..000000000000 --- 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 000000000000..ee5b7e560329 --- /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/keyguard/data/repository/FakeCommandQueueKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeCommandQueueKosmos.kt new file mode 100644 index 000000000000..d0a0e1d0ebcb --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeCommandQueueKosmos.kt @@ -0,0 +1,21 @@ +/* + * 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.data.repository + +import com.android.systemui.kosmos.Kosmos + +val Kosmos.fakeCommandQueue by Kosmos.Fixture { FakeCommandQueue() } 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 008f79a377e0..408157b7614f 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 @@ import com.android.systemui.kosmos.Kosmos 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/FromLockscreenTransitionInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractorKosmos.kt index 3b38342119e6..d9882fc2c140 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,7 +23,6 @@ import com.android.systemui.kosmos.applicationCoroutineScope 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 Kosmos.Fixture { @@ -38,7 +37,6 @@ val Kosmos.fromLockscreenTransitionInteractor by 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/KeyguardSurfaceBehindInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardSurfaceBehindInteractorKosmos.kt new file mode 100644 index 000000000000..d756f9a61c31 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardSurfaceBehindInteractorKosmos.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 android.content.applicationContext +import com.android.systemui.keyguard.data.repository.keyguardSurfaceBehindRepository +import com.android.systemui.kosmos.Kosmos + +val Kosmos.keyguardSurfaceBehindInteractor by + Kosmos.Fixture { + KeyguardSurfaceBehindInteractor( + repository = keyguardSurfaceBehindRepository, + context = applicationContext, + transitionInteractor = keyguardTransitionInteractor, + inWindowLauncherUnlockAnimationInteractor = { + inWindowLauncherUnlockAnimationInteractor + }, + swipeToDismissInteractor = swipeToDismissInteractor, + ) + } 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 000000000000..25aad04b723e --- /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/kosmos/KosmosJavaAdapter.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/KosmosJavaAdapter.kt index 083de107c971..10305f7d50c4 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.flags.fakeFeatureFlagsClassic 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 @@ -43,6 +45,8 @@ import com.android.systemui.scene.sceneContainerConfig import com.android.systemui.scene.shared.flag.fakeSceneContainerFlags import com.android.systemui.statusbar.phone.screenOffAnimationController 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.util.time.systemClock import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -80,6 +84,12 @@ class KosmosJavaAdapter( val deviceUnlockedInteractor by lazy { kosmos.deviceUnlockedInteractor } val communalInteractor by lazy { kosmos.communalInteractor } 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/qs/tiles/impl/fontscaling/FontScalingTileKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/fontscaling/FontScalingTileKosmos.kt new file mode 100644 index 000000000000..9410ce6d657a --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/fontscaling/FontScalingTileKosmos.kt @@ -0,0 +1,24 @@ +/* + * 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.qs.tiles.impl.fontscaling + +import com.android.systemui.accessibility.qs.QSAccessibilityModule +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.qs.qsEventLogger + +val Kosmos.qsFontScalingTileConfig by + Kosmos.Fixture { QSAccessibilityModule.provideFontScalingTileConfig(qsEventLogger) } 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 000000000000..207c3f7711b7 --- /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/settings/UserFileManagerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/settings/UserFileManagerKosmos.kt new file mode 100644 index 000000000000..4d7a40ab3380 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/settings/UserFileManagerKosmos.kt @@ -0,0 +1,22 @@ +/* + * 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 com.android.systemui.kosmos.Kosmos + +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 e13fa5207b33..82e0b8e83f24 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.concurrency.fakeExecutor 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 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi 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 f7005ab45ea0..4aab822f6659 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.Binds 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 @@ class FakeShadeRepository @Inject constructor() : ShadeRepository { 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 @@ class FakeShadeRepository @Inject constructor() : ShadeRepository { _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/ShadeInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeInteractorKosmos.kt index afd37b3f92dc..2bd76bec6852 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeInteractorKosmos.kt @@ -29,8 +29,8 @@ import com.android.systemui.shade.data.repository.shadeRepository import com.android.systemui.statusbar.disableflags.data.repository.disableFlagsRepository import com.android.systemui.statusbar.notification.stack.domain.interactor.sharedNotificationContainerInteractor import com.android.systemui.statusbar.phone.dozeParameters -import com.android.systemui.statusbar.policy.data.repository.deviceProvisioningRepository import com.android.systemui.statusbar.policy.data.repository.userSetupRepository +import com.android.systemui.statusbar.policy.domain.interactor.deviceProvisioningInteractor import com.android.systemui.user.domain.interactor.userSwitcherInteractor var Kosmos.baseShadeInteractor: BaseShadeInteractor by @@ -63,7 +63,7 @@ val Kosmos.shadeInteractorImpl by Kosmos.Fixture { ShadeInteractorImpl( scope = applicationCoroutineScope, - deviceProvisioningRepository = deviceProvisioningRepository, + deviceProvisioningInteractor = deviceProvisioningInteractor, disableFlagsRepository = disableFlagsRepository, dozeParams = dozeParameters, keyguardRepository = fakeKeyguardRepository, 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 ff22ca00a0a6..01cac4c1e030 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 @@ package com.android.systemui.statusbar.notification.footer.ui.viewmodel 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 748d04de1540..489598c4dba9 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 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 @@ val Kosmos.notificationListViewBinder by Fixture { 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/NotificationsPlaceholderViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModelKosmos.kt index d7e948eefc95..29faa58d674a 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 @@ val Kosmos.notificationsPlaceholderViewModel by Fixture { shadeInteractor = shadeInteractor, flags = sceneContainerFlags, featureFlags = featureFlagsClassic, + keyguardInteractor = keyguardInteractor, ) } 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 6ddc9df930f3..8042b5c7bf3e 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,7 +19,7 @@ package com.android.systemui.statusbar.phone 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 @@ -62,7 +62,7 @@ val Kosmos.statusBarNotificationActivityStarter by notificationClickNotifier, statusBarKeyguardViewManager, keyguardManager, - dreamManager, + dreamManagerInterface, Optional.of(bubblesManager), { assistManager }, notificationRemoteInputManager, 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 33ed7e61de4d..d4e9bfbd1500 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 300229954044..fc6a800e186c 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 @@ import kotlinx.coroutines.flow.MutableStateFlow @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/statusbar/policy/domain/interactor/DeviceProvisioningInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/domain/interactor/DeviceProvisioningInteractorKosmos.kt new file mode 100644 index 000000000000..84bd3e8ff08d --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/domain/interactor/DeviceProvisioningInteractorKosmos.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.policy.domain.interactor + +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.Kosmos.Fixture +import com.android.systemui.statusbar.policy.data.repository.deviceProvisioningRepository + +val Kosmos.deviceProvisioningInteractor by Fixture { + DeviceProvisioningInteractor( + repository = deviceProvisioningRepository, + ) +} diff --git a/packages/SystemUI/unfold/Android.bp b/packages/SystemUI/unfold/Android.bp index 81fd8ce12f05..32b7020ae22f 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 155c523a96a7..c134a4c5b2ad 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 com.android.internal.camera.flags.Flags; 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 @@ public class CameraExtensionsProxyService extends Service { // 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 @@ public class CameraExtensionsProxyService extends Service { (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 class CameraExtensionsProxyService extends Service { public boolean isPostviewAvailable() { return false; } + + @Override + public List<Pair<CameraCharacteristics.Key, Object>> + getAvailableCharacteristicsKeyValues() { + return Collections.emptyList(); + } }; } } @@ -1186,6 +1197,35 @@ public class CameraExtensionsProxyService extends Service { 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/README-ravenwood+mockito.md b/ravenwood/README-ravenwood+mockito.md deleted file mode 100644 index 6adb61441bb1..000000000000 --- a/ravenwood/README-ravenwood+mockito.md +++ /dev/null @@ -1,24 +0,0 @@ -# Ravenwood and Mockito - -Last update: 2023-11-13 - -- As of 2023-11-13, `external/mockito` is based on version 2.x. -- Mockito didn't support static mocking before 3.4.0. - See: https://javadoc.io/doc/org.mockito/mockito-core/latest/org/mockito/Mockito.html#48 - -- Latest Mockito is 5.*. According to https://github.com/mockito/mockito: - `Mockito 3 does not introduce any breaking API changes, but now requires Java 8 over Java 6 for Mockito 2. Mockito 4 removes deprecated API. Mockito 5 switches the default mockmaker to mockito-inline, and now requires Java 11.` - -- Mockito now supports Android natively. - See: https://javadoc.io/doc/org.mockito/mockito-core/latest/org/mockito/Mockito.html#0.1 - - But it's unclear at this point to omakoto@ how the `mockito-android` module is built. - -- Potential plan: - - Ideal option: - - If we can update `external/mockito`, that'd be great, but it may not work because - Mockito has removed the deprecated APIs. - - Second option: - - Import the latest mockito as `external/mockito-new`, and require ravenwood - to use this one. - - The latest mockito needs be exposed to all of 1) device tests, 2) host tests, and 3) ravenwood tests. - - This probably will require the latest `bytebuddy` and `objenesis`.
\ No newline at end of file diff --git a/ravenwood/bulk_enable.py b/ravenwood/bulk_enable.py index 36d398cc160c..aafaaff7561a 100644 --- a/ravenwood/bulk_enable.py +++ b/ravenwood/bulk_enable.py @@ -21,7 +21,7 @@ Currently only offers to include classes which are fully passing; ignores 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 @@ import sys 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_total = collections.defaultdict(int) 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 @@ for clazz in stats_class.keys(): 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 000000000000..9b7f8f7d2171 --- /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 000000000000..e58c282fb690 --- /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 000000000000..99b7a1ff226b --- /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 3670459c0300..7b5932b42b84 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 androidx.test.platform.app.InstrumentationRegistry; 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 @@ public class RavenwoodRuleImpl { 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 @@ public class RavenwoodRuleImpl { rule.mSystemProperties.getKeyReadablePredicate(), rule.mSystemProperties.getKeyWritablePredicate()); + ActivityManager.init$ravenwood(rule.mCurrentUser); + com.android.server.LocalServices.removeAllServicesForTest(); if (rule.mProvideMainThread) { @@ -78,6 +110,10 @@ public class RavenwoodRuleImpl { 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 @@ public class RavenwoodRuleImpl { 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 @@ public class RavenwoodRuleImpl { } 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 0285b386ed13..764573defd7a 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 @@ public class RavenwoodRule implements TestRule { 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 @@ public class RavenwoodRule implements TestRule { } } - 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 @@ public class RavenwoodRule implements TestRule { } } + 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 class RavenwoodRule implements TestRule { 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 class RavenwoodRule implements TestRule { 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 @@ public class RavenwoodRule implements TestRule { } }; } + + /** + * 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 7d172f2a83c0..e951351bd829 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 class RavenwoodRuleImpl { 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/list-ravenwood-tests.sh b/ravenwood/list-ravenwood-tests.sh new file mode 100755 index 000000000000..fb9b823ee93b --- /dev/null +++ b/ravenwood/list-ravenwood-tests.sh @@ -0,0 +1,18 @@ +#!/bin/bash +# 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. + +# List all the ravenwood test modules. + +jq -r 'to_entries[] | select( .value.compatibility_suites | index("ravenwood-tests") ) | .key' "$OUT/module-info.json" diff --git a/ravenwood/minimum-test/Android.bp b/ravenwood/minimum-test/Android.bp index bf3583cebd2c..e4ed3d5b0b73 100644 --- a/ravenwood/minimum-test/Android.bp +++ b/ravenwood/minimum-test/Android.bp @@ -13,6 +13,7 @@ android_ravenwood_test { 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 7abfecf0e424..03cfad5c7a70 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 @@ package com.android.ravenwood; 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/mockito/Android.bp b/ravenwood/mockito/Android.bp index 4135022d2bc9..a74bca47f692 100644 --- a/ravenwood/mockito/Android.bp +++ b/ravenwood/mockito/Android.bp @@ -7,16 +7,6 @@ package { default_applicable_licenses: ["frameworks_base_license"], } -// Ravenwood tests run on the hostside, so we need mockit of the host variant. -// But we need to use it in modules of the android variant, so we "wash" the variant with it. -java_host_for_device { - name: "mockito_ravenwood", - libs: [ - "mockito", - "objenesis", - ], -} - android_ravenwood_test { name: "RavenwoodMockitoTest", @@ -26,8 +16,6 @@ android_ravenwood_test { static_libs: [ "junit", "truth", - - "mockito_ravenwood", ], libs: [ "android.test.mock", diff --git a/ravenwood/mockito/test/com/android/ravenwood/mockito/RavenwoodMockitoTest.java b/ravenwood/mockito/test/com/android/ravenwood/mockito/RavenwoodMockitoTest.java index 364a86a0ec5f..1284d64b9a90 100644 --- a/ravenwood/mockito/test/com/android/ravenwood/mockito/RavenwoodMockitoTest.java +++ b/ravenwood/mockito/test/com/android/ravenwood/mockito/RavenwoodMockitoTest.java @@ -22,6 +22,7 @@ import static org.mockito.Mockito.when; import android.content.Context; import android.content.Intent; +import android.os.Parcel; import android.platform.test.ravenwood.RavenwoodRule; import org.junit.Rule; @@ -78,4 +79,13 @@ public class RavenwoodMockitoTest { assertThat(object.getPackageName()).isEqualTo("android"); } + + @Test + public void testMockFinalClass() { + var object = mock(Parcel.class); + + when(object.readInt()).thenReturn(123); + + assertThat(object.readInt()).isEqualTo(123); + } } diff --git a/ravenwood/ravenwood-annotation-allowed-classes.txt b/ravenwood/ravenwood-annotation-allowed-classes.txt index e49b64ebdbef..a5ecd20b8b4f 100644 --- a/ravenwood/ravenwood-annotation-allowed-classes.txt +++ b/ravenwood/ravenwood-annotation-allowed-classes.txt @@ -82,6 +82,8 @@ android.os.UidBatteryConsumer 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.graphics.RectF android.content.ContentProvider +android.app.ActivityManager android.app.Instrumentation android.metrics.LogMaker diff --git a/ravenwood/run-ravenwood-tests.sh b/ravenwood/run-ravenwood-tests.sh new file mode 100755 index 000000000000..3f4b8a79e864 --- /dev/null +++ b/ravenwood/run-ravenwood-tests.sh @@ -0,0 +1,22 @@ +#!/bin/bash +# 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. + +# Run all the ravenwood tests. + +# "echo" is to remove the newlines +all_tests=$(echo $(${0%/*}/list-ravenwood-tests.sh) ) + +echo "Running tests: $all_tests" +atest $all_tests diff --git a/ravenwood/test-authors.md b/ravenwood/test-authors.md index 5df827f11033..9179a621d0df 100644 --- a/ravenwood/test-authors.md +++ b/ravenwood/test-authors.md @@ -101,7 +101,7 @@ import android.platform.test.flag.junit.SetFlagsRule; @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/java/com/android/server/accessibility/AccessibilityManagerService.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java index 63784ba61150..c58cb72fe69d 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.ACTION_VIRTUAL_DEVI 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.R; 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 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub } mMainHandler.sendMessage(obtainMessage( AccessibilityManagerService::performAccessibilityShortcutInternal, this, - displayId, ShortcutConstants.UserShortcutType.SOFTWARE, targetName)); + displayId, ACCESSIBILITY_BUTTON, targetName)); } /** @@ -2199,12 +2201,11 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub } 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 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub } final Set<String> currentTargets = - userState.getShortcutTargetsLocked(ShortcutConstants.UserShortcutType.HARDWARE); + userState.getShortcutTargetsLocked(ACCESSIBILITY_SHORTCUT_KEY); if (targetsFromSetting.equals(currentTargets)) { return false; } @@ -3291,7 +3292,7 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub 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 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub */ 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 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub } 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 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub 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 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub 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 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub 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 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub } mMainHandler.sendMessage(obtainMessage( AccessibilityManagerService::performAccessibilityShortcutInternal, this, - Display.DEFAULT_DISPLAY, ShortcutConstants.UserShortcutType.HARDWARE, targetName)); + Display.DEFAULT_DISPLAY, ACCESSIBILITY_SHORTCUT_KEY, targetName)); } /** @@ -3765,7 +3764,7 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub * 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 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub } 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 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub /** * 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 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub * 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 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub 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 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub } 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 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub } @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 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub 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 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub } } // 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 class AccessibilityManagerService extends IAccessibilityManager.Stub 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 { @@ -5725,10 +5720,10 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub } @Override - public void attachAccessibilityOverlayToDisplay_enforcePermission( + public void attachAccessibilityOverlayToDisplay( int displayId, SurfaceControl sc) { mContext.enforceCallingPermission( - INTERNAL_SYSTEM_WINDOW, "attachAccessibilityOverlayToDisplay_enforcePermission"); + INTERNAL_SYSTEM_WINDOW, "attachAccessibilityOverlayToDisplay"); mMainHandler.sendMessage( obtainMessage( AccessibilityManagerService::attachAccessibilityOverlayToDisplayInternal, diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityUserState.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityUserState.java index 41165b6aed5a..68ee78076f3d 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_IGNORE 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 android.view.accessibility.IAccessibilityManagerClient; 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 @@ class AccessibilityUserState { * @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 @@ class AccessibilityUserState { /** * 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/autofill/features.aconfig b/services/autofill/features.aconfig index 727721d5319f..532db126bff2 100644 --- a/services/autofill/features.aconfig +++ b/services/autofill/features.aconfig @@ -12,4 +12,11 @@ flag { namespace: "autofill" description: "Guards against Autofill-Credman integration phase 2" bug: "320730001" -}
\ No newline at end of file +} + +flag { + name: "autofill_credman_dev_integration" + namespace: "autofill" + description: "Guards against Autofill-Credman Phase1 developer integration via new APIs" + bug: "320730001" +} diff --git a/services/contextualsearch/OWNERS b/services/contextualsearch/OWNERS new file mode 100644 index 000000000000..0c2612c9957b --- /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/SystemConfig.java b/services/core/java/com/android/server/SystemConfig.java index a493d7a57500..797a2e6e5128 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.app.ActivityManager; 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 @@ public class SystemConfig { // 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 @@ public class SystemConfig { 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 @@ public class SystemConfig { } } } 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 @@ public class SystemConfig { } } + 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).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/UiModeManagerService.java b/services/core/java/com/android/server/UiModeManagerService.java index cd8be338a031..82d9377031bc 100644 --- a/services/core/java/com/android/server/UiModeManagerService.java +++ b/services/core/java/com/android/server/UiModeManagerService.java @@ -17,6 +17,7 @@ package com.android.server; import static android.app.Flags.modesApi; +import static android.app.Flags.enableNightModeCache; import static android.app.UiModeManager.ContrastUtils.CONTRAST_DEFAULT_VALUE; import static android.app.UiModeManager.DEFAULT_PRIORITY; import static android.app.UiModeManager.MODE_ATTENTION_THEME_OVERLAY_OFF; @@ -135,7 +136,23 @@ final class UiModeManagerService extends SystemService { private int mDockState = Intent.EXTRA_DOCK_STATE_UNDOCKED; private int mLastBroadcastState = Intent.EXTRA_DOCK_STATE_UNDOCKED; - private int mNightMode = UiModeManager.MODE_NIGHT_NO; + + private final NightMode mNightMode = new NightMode(){ + private int mNightModeValue = UiModeManager.MODE_NIGHT_NO; + + @Override + public int get() { + return mNightModeValue; + } + + @Override + public void set(int mode) { + mNightModeValue = mode; + if (enableNightModeCache()) { + UiModeManager.invalidateNightModeCache(); + } + } + }; private int mNightModeCustomType = UiModeManager.MODE_NIGHT_CUSTOM_TYPE_UNKNOWN; private int mAttentionModeThemeOverlay = UiModeManager.MODE_ATTENTION_THEME_OVERLAY_OFF; private final LocalTime DEFAULT_CUSTOM_NIGHT_START_TIME = LocalTime.of(22, 0); @@ -297,7 +314,7 @@ final class UiModeManagerService extends SystemService { @Override public void onTwilightStateChanged(@Nullable TwilightState state) { synchronized (mLock) { - if (mNightMode == UiModeManager.MODE_NIGHT_AUTO && mSystemReady) { + if (mNightMode.get() == UiModeManager.MODE_NIGHT_AUTO && mSystemReady) { if (shouldApplyAutomaticChangesImmediately()) { updateLocked(0, 0); } else { @@ -392,7 +409,7 @@ final class UiModeManagerService extends SystemService { private void updateSystemProperties() { int mode = Secure.getIntForUser(getContext().getContentResolver(), Secure.UI_NIGHT_MODE, - mNightMode, 0); + mNightMode.get(), 0); if (mode == MODE_NIGHT_AUTO || mode == MODE_NIGHT_CUSTOM) { mode = MODE_NIGHT_YES; } @@ -412,7 +429,7 @@ final class UiModeManagerService extends SystemService { @Override public void onUserSwitching(@Nullable TargetUser from, @NonNull TargetUser to) { mCurrentUser = to.getUserIdentifier(); - if (mNightMode == MODE_NIGHT_AUTO) persistComputedNightMode(from.getUserIdentifier()); + if (mNightMode.get() == MODE_NIGHT_AUTO) persistComputedNightMode(from.getUserIdentifier()); getContext().getContentResolver().unregisterContentObserver(mSetupWizardObserver); verifySetupWizardCompleted(); synchronized (mLock) { @@ -471,12 +488,12 @@ final class UiModeManagerService extends SystemService { verifySetupWizardCompleted(); final Resources res = context.getResources(); + mNightMode.set(res.getInteger( + com.android.internal.R.integer.config_defaultNightMode)); mStartDreamImmediatelyOnDock = res.getBoolean( com.android.internal.R.bool.config_startDreamImmediatelyOnDock); mDreamsDisabledByAmbientModeSuppression = res.getBoolean( com.android.internal.R.bool.config_dreamsDisabledByAmbientModeSuppressionConfig); - mNightMode = res.getInteger( - com.android.internal.R.integer.config_defaultNightMode); mDefaultUiModeType = res.getInteger( com.android.internal.R.integer.config_defaultUiModeType); mCarModeKeepsScreenOn = (res.getInteger( @@ -510,7 +527,7 @@ final class UiModeManagerService extends SystemService { private final BroadcastReceiver mOnShutdown = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { - if (mNightMode == MODE_NIGHT_AUTO) { + if (mNightMode.get() == MODE_NIGHT_AUTO) { persistComputedNightMode(mCurrentUser); } } @@ -585,7 +602,7 @@ final class UiModeManagerService extends SystemService { } private void updateCustomTimeLocked() { - if (mNightMode != MODE_NIGHT_CUSTOM) return; + if (mNightMode.get() != MODE_NIGHT_CUSTOM) return; if (shouldApplyAutomaticChangesImmediately()) { updateLocked(0, 0); } else { @@ -606,9 +623,9 @@ final class UiModeManagerService extends SystemService { return; } if (mSetupWizardComplete) { - mNightMode = Secure.getIntForUser(context.getContentResolver(), + mNightMode.set(Secure.getIntForUser(context.getContentResolver(), Secure.UI_NIGHT_MODE, res.getInteger( - com.android.internal.R.integer.config_defaultNightMode), userId); + com.android.internal.R.integer.config_defaultNightMode), userId)); mNightModeCustomType = Secure.getIntForUser(context.getContentResolver(), Secure.UI_NIGHT_MODE_CUSTOM_TYPE, MODE_NIGHT_CUSTOM_TYPE_UNKNOWN, userId); mOverrideNightModeOn = Secure.getIntForUser(context.getContentResolver(), @@ -623,7 +640,7 @@ final class UiModeManagerService extends SystemService { Secure.getLongForUser(context.getContentResolver(), Secure.DARK_THEME_CUSTOM_END_TIME, DEFAULT_CUSTOM_NIGHT_END_TIME.toNanoOfDay() / 1000L, userId) * 1000); - if (mNightMode == MODE_NIGHT_AUTO) { + if (mNightMode.get() == MODE_NIGHT_AUTO) { mComputedNightMode = Secure.getIntForUser(context.getContentResolver(), Secure.UI_NIGHT_MODE_LAST_COMPUTED, 0, userId) != 0; } @@ -834,21 +851,23 @@ final class UiModeManagerService extends SystemService { final long ident = Binder.clearCallingIdentity(); try { synchronized (mLock) { - if (mNightMode != mode || mNightModeCustomType != customModeType) { - if (mNightMode == MODE_NIGHT_AUTO || mNightMode == MODE_NIGHT_CUSTOM) { + if (mNightMode.get() != mode || mNightModeCustomType != customModeType) { + if (mNightMode.get() == MODE_NIGHT_AUTO + || mNightMode.get() == MODE_NIGHT_CUSTOM) { unregisterDeviceInactiveListenerLocked(); cancelCustomAlarm(); } mNightModeCustomType = mode == MODE_NIGHT_CUSTOM ? customModeType : MODE_NIGHT_CUSTOM_TYPE_UNKNOWN; - mNightMode = mode; + mNightMode.set(mode); //deactivates AttentionMode if user toggles DarkTheme mAttentionModeThemeOverlay = MODE_ATTENTION_THEME_OVERLAY_OFF; resetNightModeOverrideLocked(); persistNightMode(user); // on screen off will update configuration instead - if ((mNightMode != MODE_NIGHT_AUTO && mNightMode != MODE_NIGHT_CUSTOM) + if ((mNightMode.get() != MODE_NIGHT_AUTO + && mNightMode.get() != MODE_NIGHT_CUSTOM) || shouldApplyAutomaticChangesImmediately()) { unregisterDeviceInactiveListenerLocked(); updateLocked(0, 0); @@ -865,7 +884,7 @@ final class UiModeManagerService extends SystemService { @Override public int getNightMode() { synchronized (mLock) { - return mNightMode; + return mNightMode.get(); } } @@ -999,18 +1018,19 @@ final class UiModeManagerService extends SystemService { synchronized (mLock) { final long ident = Binder.clearCallingIdentity(); try { - if (mNightMode == MODE_NIGHT_AUTO || mNightMode == MODE_NIGHT_CUSTOM) { + if (mNightMode.get() == MODE_NIGHT_AUTO + || mNightMode.get() == MODE_NIGHT_CUSTOM) { unregisterDeviceInactiveListenerLocked(); mOverrideNightModeOff = !active; mOverrideNightModeOn = active; mOverrideNightModeUser = mCurrentUser; persistNightModeOverrides(mCurrentUser); - } else if (mNightMode == UiModeManager.MODE_NIGHT_NO + } else if (mNightMode.get() == UiModeManager.MODE_NIGHT_NO && active) { - mNightMode = UiModeManager.MODE_NIGHT_YES; - } else if (mNightMode == UiModeManager.MODE_NIGHT_YES + mNightMode.set(UiModeManager.MODE_NIGHT_YES); + } else if (mNightMode.get() == UiModeManager.MODE_NIGHT_YES && !active) { - mNightMode = UiModeManager.MODE_NIGHT_NO; + mNightMode.set(UiModeManager.MODE_NIGHT_NO); } updateConfigurationLocked(); applyConfigurationExternallyLocked(); @@ -1414,7 +1434,7 @@ final class UiModeManagerService extends SystemService { private void onCustomTimeUpdated(int user) { persistNightMode(user); - if (mNightMode != MODE_NIGHT_CUSTOM) return; + if (mNightMode.get() != MODE_NIGHT_CUSTOM) return; if (shouldApplyAutomaticChangesImmediately()) { unregisterDeviceInactiveListenerLocked(); updateLocked(0, 0); @@ -1431,8 +1451,8 @@ final class UiModeManagerService extends SystemService { pw.print(" mStartDreamImmediatelyOnDock="); pw.print(mStartDreamImmediatelyOnDock); - pw.print(" mNightMode="); pw.print(mNightMode); pw.print(" ("); - pw.print(Shell.nightModeToStr(mNightMode, mNightModeCustomType)); pw.print(") "); + pw.print(" mNightMode="); pw.print(mNightMode.get()); pw.print(" ("); + pw.print(Shell.nightModeToStr(mNightMode.get(), mNightModeCustomType)); pw.print(") "); pw.print(" mOverrideOn/Off="); pw.print(mOverrideNightModeOn); pw.print("/"); pw.print(mOverrideNightModeOff); pw.print(" mAttentionModeThemeOverlay="); pw.print(mAttentionModeThemeOverlay); @@ -1623,7 +1643,7 @@ final class UiModeManagerService extends SystemService { // Only persist setting if not in car mode if (mCarModeEnabled || mCar) return; Secure.putIntForUser(getContext().getContentResolver(), - Secure.UI_NIGHT_MODE, mNightMode, user); + Secure.UI_NIGHT_MODE, mNightMode.get(), user); Secure.putLongForUser(getContext().getContentResolver(), Secure.UI_NIGHT_MODE_CUSTOM_TYPE, mNightModeCustomType, user); Secure.putLongForUser(getContext().getContentResolver(), @@ -1659,11 +1679,11 @@ final class UiModeManagerService extends SystemService { uiMode = Configuration.UI_MODE_TYPE_VR_HEADSET; } - if (mNightMode == MODE_NIGHT_YES || mNightMode == UiModeManager.MODE_NIGHT_NO) { - updateComputedNightModeLocked(mNightMode == MODE_NIGHT_YES); + if (mNightMode.get() == MODE_NIGHT_YES || mNightMode.get() == UiModeManager.MODE_NIGHT_NO) { + updateComputedNightModeLocked(mNightMode.get() == MODE_NIGHT_YES); } - if (mNightMode == MODE_NIGHT_AUTO) { + if (mNightMode.get() == MODE_NIGHT_AUTO) { boolean activateNightMode = mComputedNightMode; if (mTwilightManager != null) { mTwilightManager.registerListener(mTwilightListener, mHandler); @@ -1677,7 +1697,7 @@ final class UiModeManagerService extends SystemService { } } - if (mNightMode == MODE_NIGHT_CUSTOM) { + if (mNightMode.get() == MODE_NIGHT_CUSTOM) { if (mNightModeCustomType == MODE_NIGHT_CUSTOM_TYPE_BEDTIME) { updateComputedNightModeLocked(mLastBedtimeRequestedNightMode); } else { @@ -2010,7 +2030,7 @@ final class UiModeManagerService extends SystemService { private void updateComputedNightModeLocked(boolean activate) { boolean newComputedValue = activate; - if (mNightMode != MODE_NIGHT_YES && mNightMode != UiModeManager.MODE_NIGHT_NO) { + if (mNightMode.get() != MODE_NIGHT_YES && mNightMode.get() != UiModeManager.MODE_NIGHT_NO) { if (mOverrideNightModeOn && !newComputedValue) { newComputedValue = true; } else if (mOverrideNightModeOff && newComputedValue) { @@ -2029,7 +2049,7 @@ final class UiModeManagerService extends SystemService { mComputedNightMode = newComputedValue; } - if (mNightMode != MODE_NIGHT_AUTO || (mTwilightManager != null + if (mNightMode.get() != MODE_NIGHT_AUTO || (mTwilightManager != null && mTwilightManager.getLastTwilightState() != null)) { resetNightModeOverrideLocked(); } @@ -2279,4 +2299,13 @@ final class UiModeManagerService extends SystemService { Sandman.startDreamWhenDockedIfAppropriate(context); } } + + /** + * Interface to contain the value for system night mode. We make the night mode accessible + * through this class to ensure that the reassignment of this value invalidates the cache. + */ + private interface NightMode { + int get(); + void set(int mode); + } } diff --git a/services/core/java/com/android/server/am/AssistDataRequester.java b/services/core/java/com/android/server/am/AssistDataRequester.java index 98129ed12afe..856a15f0ecda 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 @@ public class AssistDataRequester extends IAssistDataReceiver.Stub { // 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/ProcessList.java b/services/core/java/com/android/server/am/ProcessList.java index 10cd6e5bb136..d110349c9fc1 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 @@ public final class ProcessList { mService.mNativeDebuggingApp = null; } - if (app.info.isEmbeddedDexUsed()) { + if (app.info.isEmbeddedDexUsed() + || (app.processInfo != null && app.processInfo.useEmbeddedDex)) { runtimeFlags |= Zygote.ONLY_USE_SYSTEM_OAT_FILES; } diff --git a/services/core/java/com/android/server/audio/AudioDeviceBroker.java b/services/core/java/com/android/server/audio/AudioDeviceBroker.java index cd295b521e89..f1eea728bedd 100644 --- a/services/core/java/com/android/server/audio/AudioDeviceBroker.java +++ b/services/core/java/com/android/server/audio/AudioDeviceBroker.java @@ -2820,6 +2820,10 @@ public class AudioDeviceBroker { return mDeviceInventory.isBluetoothAudioDeviceCategoryFixed(address); } + /*package*/ boolean isSADevice(AdiDeviceState deviceState) { + return mAudioService.isSADevice(deviceState); + } + //------------------------------------------------ // for testing purposes only void clearDeviceInventory() { diff --git a/services/core/java/com/android/server/audio/AudioDeviceInventory.java b/services/core/java/com/android/server/audio/AudioDeviceInventory.java index 690c37a9349a..102a960d35f6 100644 --- a/services/core/java/com/android/server/audio/AudioDeviceInventory.java +++ b/services/core/java/com/android/server/audio/AudioDeviceInventory.java @@ -307,9 +307,12 @@ public class AudioDeviceInventory { && di.mPeerDeviceAddress.equals(ads2.getDeviceAddress()))) { continue; } - ads2.setHasHeadTracker(updatedDevice.hasHeadTracker()); - ads2.setHeadTrackerEnabled(updatedDevice.isHeadTrackerEnabled()); - ads2.setSAEnabled(updatedDevice.isSAEnabled()); + if (mDeviceBroker.isSADevice(updatedDevice) + == mDeviceBroker.isSADevice(ads2)) { + ads2.setHasHeadTracker(updatedDevice.hasHeadTracker()); + ads2.setHeadTrackerEnabled(updatedDevice.isHeadTrackerEnabled()); + ads2.setSAEnabled(updatedDevice.isSAEnabled()); + } ads2.setAudioDeviceCategory(updatedDevice.getAudioDeviceCategory()); mDeviceBroker.postUpdatedAdiDeviceState(ads2); @@ -325,9 +328,12 @@ public class AudioDeviceInventory { && di.mDeviceAddress.equals(ads2.getDeviceAddress()))) { continue; } - ads2.setHasHeadTracker(updatedDevice.hasHeadTracker()); - ads2.setHeadTrackerEnabled(updatedDevice.isHeadTrackerEnabled()); - ads2.setSAEnabled(updatedDevice.isSAEnabled()); + if (mDeviceBroker.isSADevice(updatedDevice) + == mDeviceBroker.isSADevice(ads2)) { + ads2.setHasHeadTracker(updatedDevice.hasHeadTracker()); + ads2.setHeadTrackerEnabled(updatedDevice.isHeadTrackerEnabled()); + ads2.setSAEnabled(updatedDevice.isSAEnabled()); + } ads2.setAudioDeviceCategory(updatedDevice.getAudioDeviceCategory()); mDeviceBroker.postUpdatedAdiDeviceState(ads2); @@ -348,10 +354,11 @@ public class AudioDeviceInventory { || !updatedDevice.getDeviceAddress().equals(ads.getDeviceAddress())) { continue; } - - ads.setHasHeadTracker(updatedDevice.hasHeadTracker()); - ads.setHeadTrackerEnabled(updatedDevice.isHeadTrackerEnabled()); - ads.setSAEnabled(updatedDevice.isSAEnabled()); + if (mDeviceBroker.isSADevice(updatedDevice) == mDeviceBroker.isSADevice(ads)) { + ads.setHasHeadTracker(updatedDevice.hasHeadTracker()); + ads.setHeadTrackerEnabled(updatedDevice.isHeadTrackerEnabled()); + ads.setSAEnabled(updatedDevice.isSAEnabled()); + } ads.setAudioDeviceCategory(updatedDevice.getAudioDeviceCategory()); mDeviceBroker.postUpdatedAdiDeviceState(ads); diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java index 9f7c07e0af35..bbbba26303a7 100644 --- a/services/core/java/com/android/server/audio/AudioService.java +++ b/services/core/java/com/android/server/audio/AudioService.java @@ -7855,7 +7855,7 @@ public class AudioService extends IAudioService.Stub sDeviceLogger.enqueue(new EventLogger.StringEvent("BlutoothActiveDeviceChanged for " + BluetoothProfile.getProfileName(profile) + ", device update " + previousDevice - + " -> " + newDevice)); + + " -> " + newDevice).printLog(TAG)); AudioDeviceBroker.BtDeviceChangedData data = new AudioDeviceBroker.BtDeviceChangedData(newDevice, previousDevice, info, "AudioService"); @@ -10638,6 +10638,10 @@ public class AudioService extends IAudioService.Stub mSpatializerHelper.setFeatureEnabled(mHasSpatializerEffect); } + /*package*/ boolean isSADevice(AdiDeviceState deviceState) { + return mSpatializerHelper.isSADevice(deviceState); + } + private boolean isBluetoothPrividged() { if (!bluetoothMacAddressAnonymization()) { return true; diff --git a/services/core/java/com/android/server/audio/SpatializerHelper.java b/services/core/java/com/android/server/audio/SpatializerHelper.java index 8428f127e77d..8d767313d5ab 100644 --- a/services/core/java/com/android/server/audio/SpatializerHelper.java +++ b/services/core/java/com/android/server/audio/SpatializerHelper.java @@ -813,7 +813,7 @@ public class SpatializerHelper { return false; } - private boolean isSADevice(AdiDeviceState deviceState) { + /*package*/ boolean isSADevice(AdiDeviceState deviceState) { return deviceState.getDeviceType() == getCanonicalDeviceType(deviceState.getDeviceType(), deviceState.getInternalDeviceType()) && isDeviceCompatibleWithSpatializationModes( deviceState.getAudioDeviceAttributes()); 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 1ae4d6465c57..1dc882e5ed2b 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 @@ package com.android.server.biometrics.sensors; 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 @@ public class AuthenticationStateListeners implements IBinder.DeathRecipient { * @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 @@ public class AuthenticationStateListeners implements IBinder.DeathRecipient { } } + /** + * 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/fingerprint/aidl/FingerprintAuthenticationClient.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClient.java index e0fd44b9f6bb..8121a639ab0a 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.BiometricConstants; 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 @@ public class FingerprintAuthenticationClient private Runnable mAuthSuccessRunnable; private final Clock mClock; + public FingerprintAuthenticationClient( @NonNull Context context, @NonNull Supplier<AidlSession> lazyDaemon, @@ -280,6 +282,8 @@ public class FingerprintAuthenticationClient 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()); 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 60c532c26f5d..b6311afb5ea1 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.BiometricAuthenticator; 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 @@ class FingerprintAuthenticationClient @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 d2bdd643b0a2..51a85e48832e 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 a7965441274c..458fd82d9a65 100644 --- a/services/core/java/com/android/server/camera/CameraServiceProxy.java +++ b/services/core/java/com/android/server/camera/CameraServiceProxy.java @@ -1292,7 +1292,7 @@ public class CameraServiceProxy extends SystemService return; } if (DEBUG) Slog.v(TAG, "Setting NFC reader mode. enablePolling: " + enablePolling); - nfcAdapter.setReaderMode(enablePolling); + nfcAdapter.setReaderModePollingEnabled(enablePolling); } private static int[] toArray(Collection<Integer> c) { diff --git a/services/core/java/com/android/server/devicestate/DeviceStateManagerService.java b/services/core/java/com/android/server/devicestate/DeviceStateManagerService.java index 77cb08bc02bd..cb15abcc65fc 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 @@ public final class DeviceStateManagerService extends SystemService { } 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 5c4e2f3426ee..65e508557d06 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.annotation.NonNull; 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 @@ import com.android.server.policy.DeviceStatePolicyImpl; * * @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 50ab3f8b8b6c..d5945f4e8b52 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 static android.hardware.devicestate.DeviceStateManager.MINIMUM_DEVICE_STA 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 @@ import java.lang.annotation.RetentionPolicy; * * @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 93addcd84d12..7ebc311b5ee8 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 @@ public final class DisplayManagerService extends SystemService { 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(); } @@ -872,14 +872,15 @@ public final class DisplayManagerService extends SystemService { } @VisibleForTesting - void performTraversalInternal(SurfaceControl.Transaction t) { + void performTraversalInternal(SurfaceControl.Transaction t, + SparseArray<SurfaceControl.Transaction> displayTransactions) { synchronized (mSyncRoot) { if (!mPendingTraversal) { return; } mPendingTraversal = false; - performTraversalLocked(t); + performTraversalLocked(t, displayTransactions); } // List is self-synchronized copy-on-write. @@ -2593,7 +2594,8 @@ public final class DisplayManagerService extends SystemService { } } - private void performTraversalLocked(SurfaceControl.Transaction t) { + private void performTraversalLocked(SurfaceControl.Transaction t, + SparseArray<SurfaceControl.Transaction> displayTransactions) { // Clear all viewports before configuring displays so that we can keep // track of which ones we have configured. clearViewportsLocked(); @@ -2601,9 +2603,11 @@ public final class DisplayManagerService extends SystemService { // Configure each display device. mLogicalDisplayMapper.forEachLocked((LogicalDisplay display) -> { final DisplayDevice device = display.getPrimaryDisplayDeviceLocked(); + final SurfaceControl.Transaction displayTransaction = + displayTransactions.get(display.getDisplayIdLocked(), t); if (device != null) { - configureDisplayLocked(t, device); - device.performTraversalLocked(t); + configureDisplayLocked(displayTransaction, device); + device.performTraversalLocked(displayTransaction); } }); @@ -4680,8 +4684,9 @@ public final class DisplayManagerService extends SystemService { } @Override - public void performTraversal(SurfaceControl.Transaction t) { - performTraversalInternal(t); + public void performTraversal(SurfaceControl.Transaction t, + SparseArray<SurfaceControl.Transaction> displayTransactions) { + performTraversalInternal(t, displayTransactions); } @Override diff --git a/services/core/java/com/android/server/display/DisplayPowerController.java b/services/core/java/com/android/server/display/DisplayPowerController.java index 087cacf9a570..1ca3923b325c 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 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call } @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 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call 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 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call 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 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call } @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 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call if (mStopped) { return; } - handleSettingsChange(false /*userSwitch*/); + handleSettingsChange(); break; case MSG_UPDATE_RBC: @@ -2985,7 +2987,9 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call 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 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call @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 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call 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 13acb3f79c67..ecf1635a0cd2 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 @@ import java.io.PrintWriter; * 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 @@ public interface DisplayPowerControllerInterface { * 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 @@ public interface DisplayPowerControllerInterface { /** * 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 3bb798689ea6..f6d02dbc46df 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 @@ import java.io.PrintWriter; * 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 @@ public final class DisplayBrightnessController { * 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 @@ public final class DisplayBrightnessController { } /** + * 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 3c23b5c10671..3e6e09da9753 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 @@ public class AutomaticBrightnessStrategy { * 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/hdmi/AbsoluteVolumeAudioStatusAction.java b/services/core/java/com/android/server/hdmi/AbsoluteVolumeAudioStatusAction.java index 82786001ab68..202c8941c154 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 @@ final class AbsoluteVolumeAudioStatusAction extends HdmiCecFeatureAction { 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 a15cb102747b..a23c08a9e5c6 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 @@ public class HdmiCecMessageValidator { // 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 @@ public class HdmiCecMessageValidator { // 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/input/InputManagerService.java b/services/core/java/com/android/server/input/InputManagerService.java index 687def05b1d7..a61199ab908c 100644 --- a/services/core/java/com/android/server/input/InputManagerService.java +++ b/services/core/java/com/android/server/input/InputManagerService.java @@ -1392,6 +1392,11 @@ public class InputManagerService extends IInputManager.Stub } @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()")) { diff --git a/services/core/java/com/android/server/input/NativeInputManagerService.java b/services/core/java/com/android/server/input/NativeInputManagerService.java index e5f3484b4825..b16df0f8a28a 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 @@ interface NativeInputManagerService { */ boolean transferTouch(IBinder destChannelToken, int displayId); + int getMousePointerSpeed(); + void setPointerSpeed(int speed); void setMousePointerAccelerationEnabled(int displayId, boolean enabled); @@ -364,6 +366,9 @@ interface NativeInputManagerService { 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 f031b7b677ac..8aa038ff0ca4 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 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub @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. @@ -3659,6 +3653,15 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub Slog.e(TAG, "windowToken cannot be null."); return InputBindResult.NULL; } + // The user represented by userId, must be running. + if (!mUserManagerInternal.isUserRunning(userId)) { + // There is a chance that we hit here because of race condition. Let's just + // return an error code instead of crashing the caller process, which at + // least has INTERACT_ACROSS_USERS_FULL permission thus is likely to be an + // important process. + Slog.w(TAG, "User #" + userId + " is not running."); + return InputBindResult.INVALID_USER; + } try { Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "IMMS.startInputOrWindowGainedFocus"); @@ -3666,20 +3669,105 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub "InputMethodManagerService#startInputOrWindowGainedFocus"); final InputBindResult result; synchronized (ImfLock.class) { + // If the system is not yet ready, we shouldn't be running third party code. if (!mSystemReady) { - // If the system is not yet ready, we shouldn't be running third arty code. return new InputBindResult( InputBindResult.ResultCode.ERROR_SYSTEM_NOT_READY, null /* method */, null /* accessibilitySessions */, null /* channel */, getSelectedMethodIdLocked(), getSequenceNumberLocked(), false /* isInputMethodSuppressingSpellChecker */); } + final ClientState cs = mClientController.getClient(client.asBinder()); + if (cs == null) { + throw new IllegalArgumentException("Unknown client " + client.asBinder()); + } final long ident = Binder.clearCallingIdentity(); try { + // Verify if IMMS is in the process of switching user. + if (mUserSwitchHandlerTask != null) { + // There is already an on-going pending user switch task. + final int nextUserId = mUserSwitchHandlerTask.mToUserId; + if (userId == nextUserId) { + scheduleSwitchUserTaskLocked(userId, cs.mClient); + return InputBindResult.USER_SWITCHING; + } + final int[] profileIdsWithDisabled = mUserManagerInternal.getProfileIds( + mSettings.getUserId(), false /* enabledOnly */); + for (int profileId : profileIdsWithDisabled) { + if (profileId == userId) { + scheduleSwitchUserTaskLocked(userId, cs.mClient); + return InputBindResult.USER_SWITCHING; + } + } + 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, - unverifiedTargetSdkVersion, userId, imeDispatcher); + unverifiedTargetSdkVersion, userId, imeDispatcher, cs); } finally { Binder.restoreCallingIdentity(ident); } @@ -3708,7 +3796,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub IRemoteInputConnection inputContext, @Nullable IRemoteAccessibilityInputConnection remoteAccessibilityInputConnection, int unverifiedTargetSdkVersion, @UserIdInt int userId, - @NonNull ImeOnBackInvokedDispatcher imeDispatcher) { + @NonNull ImeOnBackInvokedDispatcher imeDispatcher, @NonNull ClientState cs) { if (DEBUG) { Slog.v(TAG, "startInputOrWindowGainedFocusInternalLocked: reason=" + InputMethodDebug.startInputReasonToString(startInputReason) @@ -3721,86 +3809,8 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub + " windowFlags=#" + Integer.toHexString(windowFlags) + " unverifiedTargetSdkVersion=" + unverifiedTargetSdkVersion + " userId=" + userId - + " imeDispatcher=" + imeDispatcher); - } - - if (!mUserManagerInternal.isUserRunning(userId)) { - // There is a chance that we hit here because of race condition. Let's just - // return an error code instead of crashing the caller process, which at - // least has INTERACT_ACROSS_USERS_FULL permission thus is likely to be an - // important process. - Slog.w(TAG, "User #" + userId + " is not running."); - return InputBindResult.INVALID_USER; - } - - final ClientState cs = mClientController.getClient(client.asBinder()); - if (cs == null) { - throw new IllegalArgumentException("Unknown client " + client.asBinder()); - } - - 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; - } - - if (mUserSwitchHandlerTask != null) { - // There is already an on-going pending user switch task. - final int nextUserId = mUserSwitchHandlerTask.mToUserId; - if (userId == nextUserId) { - scheduleSwitchUserTaskLocked(userId, cs.mClient); - return InputBindResult.USER_SWITCHING; - } - final int[] profileIdsWithDisabled = mUserManagerInternal.getProfileIds( - mSettings.getUserId(), false /* enabledOnly */); - for (int profileId : profileIdsWithDisabled) { - if (profileId == userId) { - scheduleSwitchUserTaskLocked(userId, cs.mClient); - return InputBindResult.USER_SWITCHING; - } - } - return InputBindResult.INVALID_USER; - } - - 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; + + " imeDispatcher=" + imeDispatcher + + " cs=" + cs); } final boolean sameWindowFocused = mCurFocusedWindow == windowToken; diff --git a/services/core/java/com/android/server/locksettings/LockSettingsService.java b/services/core/java/com/android/server/locksettings/LockSettingsService.java index a06607b68fa9..7fb3e001c4c3 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.Handler; 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.IndentingPrintWriter; 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.NoSuchElementException; 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 @@ public class LockSettingsService extends ILockSettings.Stub { 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 @@ public class LockSettingsService extends ILockSettings.Stub { } 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 @@ public class LockSettingsService extends ILockSettings.Stub { } @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 000000000000..f829bc6189f9 --- /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 8ce7b57c55e0..c863cbf327b8 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 @@ class WatchlistLoggingHandler extends Handler { 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 @@ class WatchlistLoggingHandler extends Handler { 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 @@ class WatchlistLoggingHandler extends Handler { 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/NotificationAttentionHelper.java b/services/core/java/com/android/server/notification/NotificationAttentionHelper.java index f852b8173f30..097daf2e51e6 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 @@ public final class NotificationAttentionHelper { 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 @@ public final class NotificationAttentionHelper { 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 4da2cc9bbe20..638382e1f5ae 100755 --- a/services/core/java/com/android/server/notification/NotificationManagerService.java +++ b/services/core/java/com/android/server/notification/NotificationManagerService.java @@ -2049,7 +2049,9 @@ public class NotificationManagerService extends SystemService { if (!mUserProfiles.isProfileUser(userId)) { mConditionProviders.onUserUnlocked(userId); mListeners.onUserUnlocked(userId); - mZenModeHelper.onUserUnlocked(userId); + if (!android.app.Flags.modesApi()) { + mZenModeHelper.onUserUnlocked(userId); + } } } } @@ -7953,8 +7955,8 @@ public class NotificationManagerService extends SystemService { && 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; @@ -8842,19 +8844,26 @@ public class NotificationManagerService extends SystemService { } } + 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); } @@ -8862,6 +8871,16 @@ public class NotificationManagerService extends SystemService { @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) @@ -9892,21 +9911,7 @@ public class NotificationManagerService extends SystemService { 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); diff --git a/services/core/java/com/android/server/notification/NotificationRecord.java b/services/core/java/com/android/server/notification/NotificationRecord.java index 1786ac53eeab..6ab4b994df73 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 @@ public final class NotificationRecord { 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 1bafcfe94267..4f3cdbc52259 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 @@ public class PreferencesHelper implements RankingConfig { 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 @@ public class PreferencesHelper implements RankingConfig { 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 7204d05fdce7..8a0e595176ec 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 @@ public final class VibratorHelper { 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/ZenModeHelper.java b/services/core/java/com/android/server/notification/ZenModeHelper.java index 41ff4150eb23..2f20bbe7f29c 100644 --- a/services/core/java/com/android/server/notification/ZenModeHelper.java +++ b/services/core/java/com/android/server/notification/ZenModeHelper.java @@ -328,6 +328,7 @@ public class ZenModeHelper { } } + // TODO: b/310620812 - Remove when MODES_API is inlined (no more callers). public void onUserUnlocked(int user) { loadConfigForUser(user, "onUserUnlocked"); } diff --git a/services/core/java/com/android/server/notification/flags.aconfig b/services/core/java/com/android/server/notification/flags.aconfig index dbff4423d10c..722654a9102c 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 { } 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/om/IdmapManager.java b/services/core/java/com/android/server/om/IdmapManager.java index 86d05d92c95b..25a39cc8456f 100644 --- a/services/core/java/com/android/server/om/IdmapManager.java +++ b/services/core/java/com/android/server/om/IdmapManager.java @@ -257,7 +257,7 @@ final class IdmapManager { private boolean matchesActorSignature(@NonNull AndroidPackage targetPackage, @NonNull AndroidPackage overlayPackage, int userId) { String targetOverlayableName = overlayPackage.getOverlayTargetOverlayableName(); - if (targetOverlayableName != null && !mPackageManager.getNamedActors().isEmpty()) { + if (targetOverlayableName != null) { try { OverlayableInfo overlayableInfo = mPackageManager.getOverlayableForTarget( targetPackage.getPackageName(), targetOverlayableName, userId); diff --git a/services/core/java/com/android/server/om/OverlayManagerService.java b/services/core/java/com/android/server/om/OverlayManagerService.java index a61b03fdbb39..b9464d96a019 100644 --- a/services/core/java/com/android/server/om/OverlayManagerService.java +++ b/services/core/java/com/android/server/om/OverlayManagerService.java @@ -32,7 +32,6 @@ import static android.os.Process.INVALID_UID; import static android.os.Trace.TRACE_TAG_RRO; import static android.os.Trace.traceBegin; import static android.os.Trace.traceEnd; - import static com.android.server.om.OverlayManagerServiceImpl.OperationFailedException; import android.annotation.NonNull; @@ -363,7 +362,7 @@ public final class OverlayManagerService extends SystemService { defaultPackages.add(packageName); } } - return defaultPackages.toArray(new String[0]); + return defaultPackages.toArray(new String[defaultPackages.size()]); } private final class OverlayManagerPackageMonitor extends PackageMonitor { @@ -1144,10 +1143,9 @@ public final class OverlayManagerService extends SystemService { }; private static final class PackageManagerHelperImpl implements PackageManagerHelper { - private static final class PackageStateUsers { + private static class PackageStateUsers { private PackageState mPackageState; - private Boolean mDefinesOverlayable = null; - private final ArraySet<Integer> mInstalledUsers = new ArraySet<>(); + private final Set<Integer> mInstalledUsers = new ArraySet<>(); private PackageStateUsers(@NonNull PackageState packageState) { this.mPackageState = packageState; } @@ -1162,7 +1160,7 @@ public final class OverlayManagerService extends SystemService { // state may lead to contradictions within OMS. Better then to lag // behind until all pending intents have been processed. private final ArrayMap<String, PackageStateUsers> mCache = new ArrayMap<>(); - private final ArraySet<Integer> mInitializedUsers = new ArraySet<>(); + private final Set<Integer> mInitializedUsers = new ArraySet<>(); PackageManagerHelperImpl(Context context) { mContext = context; @@ -1178,7 +1176,8 @@ public final class OverlayManagerService extends SystemService { */ @NonNull public ArrayMap<String, PackageState> initializeForUser(final int userId) { - if (mInitializedUsers.add(userId)) { + if (!mInitializedUsers.contains(userId)) { + mInitializedUsers.add(userId); mPackageManagerInternal.forEachPackageState((packageState -> { if (packageState.getPkg() != null && packageState.getUserStateOrDefault(userId).isInstalled()) { @@ -1197,11 +1196,13 @@ public final class OverlayManagerService extends SystemService { return userPackages; } - private PackageStateUsers getRawPackageStateForUser(@NonNull final String packageName, + @Override + @Nullable + public PackageState getPackageStateForUser(@NonNull final String packageName, final int userId) { final PackageStateUsers pkg = mCache.get(packageName); if (pkg != null && pkg.mInstalledUsers.contains(userId)) { - return pkg; + return pkg.mPackageState; } try { if (!mPackageManager.isPackageAvailable(packageName, userId)) { @@ -1215,14 +1216,8 @@ public final class OverlayManagerService extends SystemService { return addPackageUser(packageName, userId); } - @Override - public PackageState getPackageStateForUser(@NonNull final String packageName, - final int userId) { - final PackageStateUsers pkg = getRawPackageStateForUser(packageName, userId); - return pkg != null ? pkg.mPackageState : null; - } - - private PackageStateUsers addPackageUser(@NonNull final String packageName, + @NonNull + private PackageState addPackageUser(@NonNull final String packageName, final int user) { final PackageState pkg = mPackageManagerInternal.getPackageStateInternal(packageName); if (pkg == null) { @@ -1234,20 +1229,20 @@ public final class OverlayManagerService extends SystemService { } @NonNull - private PackageStateUsers addPackageUser(@NonNull final PackageState pkg, + private PackageState addPackageUser(@NonNull final PackageState pkg, final int user) { PackageStateUsers pkgUsers = mCache.get(pkg.getPackageName()); if (pkgUsers == null) { pkgUsers = new PackageStateUsers(pkg); mCache.put(pkg.getPackageName(), pkgUsers); - } else if (pkgUsers.mPackageState != pkg) { + } else { pkgUsers.mPackageState = pkg; - pkgUsers.mDefinesOverlayable = null; } pkgUsers.mInstalledUsers.add(user); - return pkgUsers; + return pkgUsers.mPackageState; } + @NonNull private void removePackageUser(@NonNull final String packageName, final int user) { final PackageStateUsers pkgUsers = mCache.get(packageName); @@ -1265,15 +1260,15 @@ public final class OverlayManagerService extends SystemService { } } + @Nullable public PackageState onPackageAdded(@NonNull final String packageName, final int userId) { - final var pu = addPackageUser(packageName, userId); - return pu != null ? pu.mPackageState : null; + return addPackageUser(packageName, userId); } + @Nullable public PackageState onPackageUpdated(@NonNull final String packageName, final int userId) { - final var pu = addPackageUser(packageName, userId); - return pu != null ? pu.mPackageState : null; + return addPackageUser(packageName, userId); } public void onPackageRemoved(@NonNull final String packageName, final int userId) { @@ -1313,30 +1308,22 @@ public final class OverlayManagerService extends SystemService { return (pkgs.length == 0) ? null : pkgs[0]; } + @Nullable @Override public OverlayableInfo getOverlayableForTarget(@NonNull String packageName, @NonNull String targetOverlayableName, int userId) throws IOException { - final var psu = getRawPackageStateForUser(packageName, userId); - final var pkg = (psu == null || psu.mPackageState == null) - ? null : psu.mPackageState.getAndroidPackage(); + var packageState = getPackageStateForUser(packageName, userId); + var pkg = packageState == null ? null : packageState.getAndroidPackage(); if (pkg == null) { throw new IOException("Unable to get target package"); } - if (Boolean.FALSE.equals(psu.mDefinesOverlayable)) { - return null; - } - ApkAssets apkAssets = null; try { apkAssets = ApkAssets.loadFromPath(pkg.getSplits().get(0).getPath(), ApkAssets.PROPERTY_ONLY_OVERLAYABLES); - if (psu.mDefinesOverlayable == null) { - psu.mDefinesOverlayable = apkAssets.definesOverlayable(); - } - return Boolean.FALSE.equals(psu.mDefinesOverlayable) - ? null : apkAssets.getOverlayableInfo(targetOverlayableName); + return apkAssets.getOverlayableInfo(targetOverlayableName); } finally { if (apkAssets != null) { try { @@ -1350,29 +1337,24 @@ public final class OverlayManagerService extends SystemService { @Override public boolean doesTargetDefineOverlayable(String targetPackageName, int userId) throws IOException { - final var psu = getRawPackageStateForUser(targetPackageName, userId); - var pkg = (psu == null || psu.mPackageState == null) - ? null : psu.mPackageState.getAndroidPackage(); + var packageState = getPackageStateForUser(targetPackageName, userId); + var pkg = packageState == null ? null : packageState.getAndroidPackage(); if (pkg == null) { throw new IOException("Unable to get target package"); } - if (psu.mDefinesOverlayable == null) { - ApkAssets apkAssets = null; - try { - apkAssets = ApkAssets.loadFromPath(pkg.getSplits().get(0).getPath(), - ApkAssets.PROPERTY_ONLY_OVERLAYABLES); - psu.mDefinesOverlayable = apkAssets.definesOverlayable(); - } finally { - if (apkAssets != null) { - try { - apkAssets.close(); - } catch (Throwable ignored) { - } + ApkAssets apkAssets = null; + try { + apkAssets = ApkAssets.loadFromPath(pkg.getSplits().get(0).getPath()); + return apkAssets.definesOverlayable(); + } finally { + if (apkAssets != null) { + try { + apkAssets.close(); + } catch (Throwable ignored) { } } } - return psu.mDefinesOverlayable; } @Override @@ -1563,7 +1545,8 @@ public final class OverlayManagerService extends SystemService { final OverlayPaths frameworkOverlays = mImpl.getEnabledOverlayPaths("android", userId, false); for (final String targetPackageName : targetPackageNames) { - final var list = new OverlayPaths.Builder(frameworkOverlays); + final OverlayPaths.Builder list = new OverlayPaths.Builder(); + list.addAll(frameworkOverlays); if (!"android".equals(targetPackageName)) { list.addAll(mImpl.getEnabledOverlayPaths(targetPackageName, userId, true)); } @@ -1575,21 +1558,17 @@ public final class OverlayManagerService extends SystemService { final HashSet<String> invalidPackages = new HashSet<>(); pm.setEnabledOverlayPackages(userId, pendingChanges, updatedPackages, invalidPackages); - if (DEBUG || !invalidPackages.isEmpty()) { - for (final String targetPackageName : targetPackageNames) { - if (DEBUG) { - Slog.d(TAG, - "-> Updating overlay: target=" + targetPackageName + " overlays=[" - + pendingChanges.get(targetPackageName) - + "] userId=" + userId); - } + for (final String targetPackageName : targetPackageNames) { + if (DEBUG) { + Slog.d(TAG, "-> Updating overlay: target=" + targetPackageName + " overlays=[" + + pendingChanges.get(targetPackageName) + + "] userId=" + userId); + } - if (invalidPackages.contains(targetPackageName)) { - Slog.e(TAG, TextUtils.formatSimple( - "Failed to change enabled overlays for %s user %d", - targetPackageName, - userId)); - } + if (invalidPackages.contains(targetPackageName)) { + Slog.e(TAG, TextUtils.formatSimple( + "Failed to change enabled overlays for %s user %d", targetPackageName, + userId)); } } return new ArrayList<>(updatedPackages); diff --git a/services/core/java/com/android/server/om/OverlayManagerServiceImpl.java b/services/core/java/com/android/server/om/OverlayManagerServiceImpl.java index c1b6ccc7e25c..972c78db9460 100644 --- a/services/core/java/com/android/server/om/OverlayManagerServiceImpl.java +++ b/services/core/java/com/android/server/om/OverlayManagerServiceImpl.java @@ -772,20 +772,24 @@ final class OverlayManagerServiceImpl { OverlayPaths getEnabledOverlayPaths(@NonNull final String targetPackageName, final int userId, boolean includeImmutableOverlays) { - final var paths = new OverlayPaths.Builder(); - mSettings.forEachMatching(userId, null, targetPackageName, oi -> { + final List<OverlayInfo> overlays = mSettings.getOverlaysForTarget(targetPackageName, + userId); + final OverlayPaths.Builder paths = new OverlayPaths.Builder(); + final int n = overlays.size(); + for (int i = 0; i < n; i++) { + final OverlayInfo oi = overlays.get(i); if (!oi.isEnabled()) { - return; + continue; } if (!includeImmutableOverlays && !oi.isMutable) { - return; + continue; } if (oi.isFabricated()) { paths.addNonApkPath(oi.baseCodePath); } else { paths.addApkPath(oi.baseCodePath); } - }); + } return paths.build(); } diff --git a/services/core/java/com/android/server/om/OverlayManagerSettings.java b/services/core/java/com/android/server/om/OverlayManagerSettings.java index b8b49f3eed2f..eae614ac9e77 100644 --- a/services/core/java/com/android/server/om/OverlayManagerSettings.java +++ b/services/core/java/com/android/server/om/OverlayManagerSettings.java @@ -47,7 +47,6 @@ import java.util.ArrayList; import java.util.List; import java.util.Objects; import java.util.Set; -import java.util.function.Consumer; import java.util.function.Predicate; import java.util.stream.Stream; @@ -183,23 +182,6 @@ final class OverlayManagerSettings { return CollectionUtils.map(items, SettingsItem::getOverlayInfo); } - void forEachMatching(int userId, String overlayName, String targetPackageName, - @NonNull Consumer<OverlayInfo> consumer) { - for (int i = 0, n = mItems.size(); i < n; i++) { - final SettingsItem item = mItems.get(i); - if (item.getUserId() != userId) { - continue; - } - if (overlayName != null && !item.mOverlay.getPackageName().equals(overlayName)) { - continue; - } - if (targetPackageName != null && !item.mTargetPackageName.equals(targetPackageName)) { - continue; - } - consumer.accept(item.getOverlayInfo()); - } - } - ArrayMap<String, List<OverlayInfo>> getOverlaysForUser(final int userId) { final List<SettingsItem> items = selectWhereUser(userId); diff --git a/services/core/java/com/android/server/pm/ComputerEngine.java b/services/core/java/com/android/server/pm/ComputerEngine.java index 0be8e6ed4c8e..ac89feccef7e 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 @@ public class ComputerEngine implements Computer { } } } 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 f1dc2849a391..b9aa28e58879 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.PackageInfo; 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 @@ public class CrossProfileAppsServiceImpl extends ICrossProfileApps.Stub { 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 @@ public class CrossProfileAppsServiceImpl extends ICrossProfileApps.Stub { 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 @@ public class CrossProfileAppsServiceImpl extends ICrossProfileApps.Stub { 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 @@ public class CrossProfileAppsServiceImpl extends ICrossProfileApps.Stub { 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 @@ public class CrossProfileAppsServiceImpl extends ICrossProfileApps.Stub { 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 28f3d59ee2ec..33f481cb2e2b 100644 --- a/services/core/java/com/android/server/pm/InstallPackageHelper.java +++ b/services/core/java/com/android/server/pm/InstallPackageHelper.java @@ -2204,6 +2204,9 @@ final class InstallPackageHelper { 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/PackageArchiver.java b/services/core/java/com/android/server/pm/PackageArchiver.java index 474b5907524c..dc97e5fa92af 100644 --- a/services/core/java/com/android/server/pm/PackageArchiver.java +++ b/services/core/java/com/android/server/pm/PackageArchiver.java @@ -19,7 +19,6 @@ package com.android.server.pm; import static android.app.ActivityManager.START_ABORTED; import static android.app.ActivityManager.START_CLASS_NOT_FOUND; import static android.app.ActivityManager.START_PERMISSION_DENIED; -import static android.app.ActivityManager.START_SUCCESS; import static android.app.AppOpsManager.MODE_ALLOWED; import static android.app.AppOpsManager.MODE_IGNORED; import static android.app.ComponentOptions.MODE_BACKGROUND_ACTIVITY_START_DENIED; @@ -29,6 +28,7 @@ import static android.content.pm.PackageInstaller.EXTRA_UNARCHIVE_STATUS; 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; @@ -183,62 +183,76 @@ public class PackageArchiver { 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); + } + + 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"); } - snapshot.enforceCrossUserPermission(binderUid, userId, true, 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; - } - - 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; - }); + final int deleteFlags = DELETE_ARCHIVE | DELETE_KEEP_DATA + | (deleteAllUsers ? DELETE_ALL_USERS : 0); + + 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; + } + ); } /** @@ -286,10 +300,16 @@ public class PackageArchiver { Slog.e(TAG, TextUtils.formatSimple( "Unexpected error occurred while unarchiving package %s: %s.", packageName, t.getLocalizedMessage())); - return START_ABORTED; } - return START_SUCCESS; + // We return STATUS_ABORTED because: + // 1. Archived App is not actually present during activity start. Hence the unarchival + // start should be treated as an error code. + // 2. STATUS_ABORTED is not visible to the end consumers. Hence, it will not change user + // experience. + // 3. Returning STATUS_ABORTED helps us avoid manually handling of different cases like + // aborting activity options, animations etc in the Windows Manager. + return START_ABORTED; } /** @@ -379,7 +399,7 @@ public class PackageArchiver { } /** 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, @@ -394,17 +414,18 @@ public class PackageArchiver { 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 @@ -798,6 +819,7 @@ public class PackageArchiver { * <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); @@ -823,7 +845,7 @@ public class PackageArchiver { // 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); @@ -879,6 +901,7 @@ public class PackageArchiver { return bitmap; } + @Nullable Bitmap includeCloudOverlay(Bitmap bitmap) { Drawable cloudDrawable = mContext.getResources() @@ -899,7 +922,9 @@ public class PackageArchiver { 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/PackageInstallerService.java b/services/core/java/com/android/server/pm/PackageInstallerService.java index c6d448d97673..abea56bfa433 100644 --- a/services/core/java/com/android/server/pm/PackageInstallerService.java +++ b/services/core/java/com/android/server/pm/PackageInstallerService.java @@ -1672,9 +1672,11 @@ public class PackageInstallerService extends IPackageInstaller.Stub implements 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/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java index b705e84d4dcd..b8960da15389 100644 --- a/services/core/java/com/android/server/pm/PackageManagerService.java +++ b/services/core/java/com/android/server/pm/PackageManagerService.java @@ -2559,12 +2559,20 @@ public class PackageManagerService implements PackageSender, TestUtilityService 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 +5239,21 @@ public class PackageManagerService implements PackageSender, TestUtilityService 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(); diff --git a/services/core/java/com/android/server/pm/PackageManagerShellCommand.java b/services/core/java/com/android/server/pm/PackageManagerShellCommand.java index 5c9c8c6d249a..81f9d1b68438 100644 --- a/services/core/java/com/android/server/pm/PackageManagerShellCommand.java +++ b/services/core/java/com/android/server/pm/PackageManagerShellCommand.java @@ -4651,6 +4651,7 @@ class PackageManagerShellCommand extends ShellCommand { private int runArchive() throws RemoteException { final PrintWriter pw = getOutPrintWriter(); + int flags = 0; int userId = UserHandle.USER_ALL; String opt; @@ -4678,13 +4679,16 @@ class PackageManagerShellCommand extends ShellCommand { 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() + "]"); diff --git a/services/core/java/com/android/server/pm/PackageSetting.java b/services/core/java/com/android/server/pm/PackageSetting.java index f474d32bc427..12eb88e518e6 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 @@ public class PackageSetting extends SettingBase implements PackageStateInternal @Nullable private String mAppMetadataFilePath; + private int mAppMetadataSource = PackageManager.APP_METADATA_SOURCE_UNKNOWN; + private int mTargetSdkVersion; @Nullable @@ -716,6 +718,7 @@ public class PackageSetting extends SettingBase implements PackageStateInternal 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 @@ public class PackageSetting extends SettingBase implements PackageStateInternal 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 @@ public class PackageSetting extends SettingBase implements PackageStateInternal } @DataClass.Generated.Member + public int getAppMetadataSource() { + return mAppMetadataSource; + } + + @DataClass.Generated.Member public int getTargetSdkVersion() { return mTargetSdkVersion; } @@ -1828,10 +1845,10 @@ public class PackageSetting extends SettingBase implements PackageStateInternal } @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 c7ee6492f595..04e820534c6d 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 @@ public final class Settings implements Watchable, Snappable, ResilientAtomicFile 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 @@ public final class Settings implements Watchable, Snappable, ResilientAtomicFile pkg.getAppMetadataFilePath()); } + serializer.attributeInt(null, "appMetadataSource", + pkg.getAppMetadataSource()); + writeUsesSdkLibLPw(serializer, pkg.getUsesSdkLibraries(), pkg.getUsesSdkLibrariesVersionsMajor(), pkg.getUsesSdkLibrariesOptional()); @@ -3226,6 +3230,9 @@ public final class Settings implements Watchable, Snappable, ResilientAtomicFile 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 @@ public final class Settings implements Watchable, Snappable, ResilientAtomicFile } 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 @@ public final class Settings implements Watchable, Snappable, ResilientAtomicFile 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 @@ public final class Settings implements Watchable, Snappable, ResilientAtomicFile 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 @@ public final class Settings implements Watchable, Snappable, ResilientAtomicFile .setLoadingProgress(loadingProgress) .setLoadingCompletedTime(loadingCompletedTime) .setAppMetadataFilePath(appMetadataFilePath) + .setAppMetadataSource(appMetadataSource) .setTargetSdkVersion(targetSdkVersion) .setRestrictUpdateHash(restrictUpdateHash) .setScannedAsStoppedSystemApp(isScannedAsStoppedSystemApp); @@ -5223,6 +5237,8 @@ public final class Settings implements Watchable, Snappable, ResilientAtomicFile } 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/UserManagerService.java b/services/core/java/com/android/server/pm/UserManagerService.java index c0596bb10823..796edde7d9bd 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 @@ public class UserManagerService extends IUserManager.Stub { @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 @@ public class UserManagerService extends IUserManager.Stub { * 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 @@ public class UserManagerService extends IUserManager.Stub { @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 @@ public class UserManagerService extends IUserManager.Stub { */ @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 @@ public class UserManagerService extends IUserManager.Stub { 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 @@ public class UserManagerService extends IUserManager.Stub { 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 @@ public class UserManagerService extends IUserManager.Stub { } 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 @@ public class UserManagerService extends IUserManager.Stub { @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 23d0230a6080..b7203045543e 100644 --- a/services/core/java/com/android/server/pm/UserTypeFactory.java +++ b/services/core/java/com/android/server/pm/UserTypeFactory.java @@ -304,7 +304,8 @@ public final class UserTypeFactory { UserProperties.CROSS_PROFILE_INTENT_FILTER_ACCESS_LEVEL_SYSTEM) .setInheritDevicePolicy(UserProperties.INHERIT_DEVICE_POLICY_FROM_PARENT) .setCrossProfileContentSharingStrategy( - UserProperties.CROSS_PROFILE_CONTENT_SHARING_DELEGATE_FROM_PARENT); + UserProperties.CROSS_PROFILE_CONTENT_SHARING_DELEGATE_FROM_PARENT) + .setItemsRestrictedOnHomeScreen(true); if (android.multiuser.Flags.supportHidingProfiles()) { userPropertiesBuilder.setProfileApiVisibility( UserProperties.PROFILE_API_VISIBILITY_HIDDEN); 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 6ed2d3126455..a9e5a5434189 100644 --- a/services/core/java/com/android/server/pm/parsing/PackageInfoUtils.java +++ b/services/core/java/com/android/server/pm/parsing/PackageInfoUtils.java @@ -831,7 +831,7 @@ public class PackageInfoUtils { 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 f7603b5cfb57..85ea83ab1599 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 @@ public interface PackageStateInternal extends PackageState { @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/policy/DeviceStatePolicyImpl.java b/services/core/java/com/android/server/policy/DeviceStatePolicyImpl.java index 7754944c008c..07cc7753b4f3 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 final class DeviceStatePolicyImpl extends DeviceStatePolicy { 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 3644054e3b78..afcf5a094bd4 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.File; 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 @@ public final class DeviceStateProviderImpl implements DeviceStateProvider, // 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 @@ public final class DeviceStateProviderImpl implements DeviceStateProvider, 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 51790b875465..775a3615905c 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 @@ public class PhoneWindowManager implements WindowManagerPolicy { 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 @@ public class PhoneWindowManager implements WindowManagerPolicy { 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 906da2f4cdf5..b05a421e6e87 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 @@ class TalkbackShortcutController { 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 @@ class TalkbackShortcutController { 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/tv/TvInputManagerService.java b/services/core/java/com/android/server/tv/TvInputManagerService.java index 089a88691af3..e0a822604058 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 @@ public final class TvInputManagerService extends SystemService { } @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/wallpaper/WallpaperCropper.java b/services/core/java/com/android/server/wallpaper/WallpaperCropper.java index 8549957f46b8..37f38252698e 100644 --- a/services/core/java/com/android/server/wallpaper/WallpaperCropper.java +++ b/services/core/java/com/android/server/wallpaper/WallpaperCropper.java @@ -347,7 +347,7 @@ public class WallpaperCropper { for (Rect crop : relativeCropHints) { Rect originalRect = new Rect(crop); originalRect.scale(wallpaper.mSampleSize); - originalRect.offset(wallpaper.cropHint.left, wallpaper.cropHint.right); + originalRect.offset(wallpaper.cropHint.left, wallpaper.cropHint.top); result.add(originalRect); } return result; diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java index b1d04c9ddb16..2c2ebe08f9f9 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; 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.ActivityMetricsLogger.TransitionInfoSnapshot; 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; @@ -986,6 +988,9 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A // 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 +2228,7 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A // No such property name. } mAppActivityEmbeddingSplitsEnabled = appActivityEmbeddingEnabled; + mAllowUntrustedEmbeddingStateSharing = getAllowUntrustedEmbeddingStateSharingProperty(); mOptInOnBackInvoked = WindowOnBackInvokedDispatcher .isOnBackInvokedCallbackEnabled(info, info.applicationInfo, @@ -3078,6 +3084,32 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A 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 d6f52b89819e..85580ac6810a 100644 --- a/services/core/java/com/android/server/wm/ActivityStarter.java +++ b/services/core/java/com/android/server/wm/ActivityStarter.java @@ -1034,20 +1034,19 @@ class ActivityStarter { } if (err == ActivityManager.START_SUCCESS && aInfo == null) { + // We couldn't find the specific class specified in the Intent. + err = ActivityManager.START_CLASS_NOT_FOUND; + if (isArchivingEnabled()) { PackageArchiver packageArchiver = mService .getPackageManagerInternalLocked() .getPackageArchiver(); if (packageArchiver.isIntentResolvedToArchivedApp(intent, mRequest.userId)) { - return packageArchiver + err = packageArchiver .requestUnarchiveOnActivityStart( intent, callingPackage, mRequest.userId, realCallingUid); } } - - // We couldn't find the specific class specified in the Intent. - // Also the end of the line. - err = ActivityManager.START_CLASS_NOT_FOUND; } if (err == ActivityManager.START_SUCCESS && sourceRecord != null diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java index 3397a3dd2290..0def5a1861ea 100644 --- a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java +++ b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java @@ -3505,8 +3505,9 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub { } @Override - public boolean isAssistDataAllowedOnCurrentActivity() { + public boolean isAssistDataAllowed() { int userId; + boolean hasRestrictedWindow; synchronized (mGlobalLock) { final Task focusedRootTask = getTopDisplayFocusedRootTask(); if (focusedRootTask == null || focusedRootTask.isActivityTypeAssistant()) { @@ -3518,8 +3519,17 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub { 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/BackNavigationController.java b/services/core/java/com/android/server/wm/BackNavigationController.java index 13f6a5f1a27b..c2dfa21016fa 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 @@ package com.android.server.wm; 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.annotations.VisibleForTesting; 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 @@ class BackNavigationController { 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 c7df83a9ccf9..e84026cf4155 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 @@ class ClientLifecycleManager { 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 @@ -157,7 +161,7 @@ class ClientLifecycleManager { 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); } } mPendingTransactions.clear(); diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java index 82dbf8d4242b..e7431723789d 100644 --- a/services/core/java/com/android/server/wm/DisplayContent.java +++ b/services/core/java/com/android/server/wm/DisplayContent.java @@ -1429,14 +1429,6 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp return mTokenMap.get(binder); } - ActivityRecord getActivityRecord(IBinder binder) { - final WindowToken token = getWindowToken(binder); - if (token == null) { - return null; - } - return token.asActivityRecord(); - } - void addWindowToken(IBinder binder, WindowToken token) { final DisplayContent dc = mWmService.mRoot.getWindowTokenDisplay(token); if (dc != null) { @@ -2250,7 +2242,6 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp } } - mWmService.mDisplayManagerInternal.performTraversal(transaction); if (shellTransitions) { // Before setDisplayProjection is applied by the start transaction of transition, // set the transform hint to avoid using surface in old rotation. @@ -5159,6 +5150,15 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp /** @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; @@ -6985,7 +6985,7 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp @Override public void onAppTransitionFinishedLocked(IBinder token) { - final ActivityRecord r = getActivityRecord(token); + final ActivityRecord r = ActivityRecord.forTokenLocked(token); // Ignore the animating recents so the fixed rotation transform won't be switched twice // by finishing the recents animation and moving it to top. That also avoids flickering // due to wait for previous activity to be paused if it supports PiP that ignores the diff --git a/services/core/java/com/android/server/wm/InputConsumerImpl.java b/services/core/java/com/android/server/wm/InputConsumerImpl.java index 34d765117a57..420467087224 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 @@ class InputConsumerImpl implements IBinder.DeathRecipient { 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 d9dda4aeb96a..cd968067e289 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 @@ class InsetsSourceProvider { 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 f6aad4c86220..e66321ae8219 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 static android.window.TaskConstants.TASK_CHILD_LAYER_LETTERBOX_BACKGROUND 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 @@ public class Letterbox { 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 @@ public class Letterbox { } } - 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 @@ public class Letterbox { 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/OWNERS b/services/core/java/com/android/server/wm/OWNERS index cd704478aa83..e06f2158dc14 100644 --- a/services/core/java/com/android/server/wm/OWNERS +++ b/services/core/java/com/android/server/wm/OWNERS @@ -17,6 +17,7 @@ mariiasand@google.com 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 eae9951d0679..d08f0438f853 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 @@ package com.android.server.wm; 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 @@ class PerfettoTransitionTracer implements TransitionTracer { 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 @@ class PerfettoTransitionTracer implements TransitionTracer { 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 @@ class PerfettoTransitionTracer implements TransitionTracer { 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 @@ class PerfettoTransitionTracer implements TransitionTracer { 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 587cc7489763..25646f19c461 100644 --- a/services/core/java/com/android/server/wm/RootWindowContainer.java +++ b/services/core/java/com/android/server/wm/RootWindowContainer.java @@ -249,6 +249,8 @@ class RootWindowContainer extends WindowContainer<DisplayContent> /** Reference to default display so we can quickly look it up. */ private DisplayContent mDefaultDisplay; private final SparseArray<IntArray> mDisplayAccessUIDs = new SparseArray<>(); + private final SparseArray<SurfaceControl.Transaction> mDisplayTransactions = + new SparseArray<>(); /** The current user */ int mCurrentUser; @@ -569,22 +571,6 @@ class RootWindowContainer extends WindowContainer<DisplayContent> }, true /* traverseTopToBottom */); } - /** - * Returns the app window token for the input binder if it exist in the system. - * NOTE: Only one AppWindowToken is allowed to exist in the system for a binder token, since - * AppWindowToken represents an activity which can only exist on one display. - */ - ActivityRecord getActivityRecord(IBinder binder) { - for (int i = mChildren.size() - 1; i >= 0; --i) { - final DisplayContent dc = mChildren.get(i); - final ActivityRecord activity = dc.getActivityRecord(binder); - if (activity != null) { - return activity; - } - } - return null; - } - /** Returns the window token for the input binder if it exist in the system. */ WindowToken getWindowToken(IBinder binder) { for (int i = mChildren.size() - 1; i >= 0; --i) { @@ -991,11 +977,13 @@ class RootWindowContainer extends WindowContainer<DisplayContent> for (int j = 0; j < count; ++j) { final DisplayContent dc = mChildren.get(j); dc.applySurfaceChangesTransaction(); + mDisplayTransactions.append(dc.mDisplayId, dc.getSyncTransaction()); } // Give the display manager a chance to adjust properties like display rotation if it needs // to. - mWmService.mDisplayManagerInternal.performTraversal(t); + mWmService.mDisplayManagerInternal.performTraversal(t, mDisplayTransactions); + mDisplayTransactions.clear(); } /** diff --git a/services/core/java/com/android/server/wm/ScreenRecordingCallbackController.java b/services/core/java/com/android/server/wm/ScreenRecordingCallbackController.java index bdb45884887c..967f415d2c11 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.annotations.GuardedBy; 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 @@ public class ScreenRecordingCallbackController { } @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 @@ public class ScreenRecordingCallbackController { } 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 @@ public class ScreenRecordingCallbackController { 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 @@ public class ScreenRecordingCallbackController { return; } - dispatchCallbacks(Set.of(uid), processVisible); + ArraySet<Integer> uidSet = new ArraySet<>(); + uidSet.add(uid); + dispatchCallbacks(uidSet, processVisible); } @GuardedBy("WindowManagerService.mGlobalLock") @@ -233,8 +237,8 @@ public class ScreenRecordingCallbackController { } @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 @@ public class ScreenRecordingCallbackController { } @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 083872a03edd..95e6ca6f428e 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 @@ class Session extends IWindowSession.Stub implements IBinder.DeathRecipient { 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/Task.java b/services/core/java/com/android/server/wm/Task.java index 8f9ed8353456..5b5177683252 100644 --- a/services/core/java/com/android/server/wm/Task.java +++ b/services/core/java/com/android/server/wm/Task.java @@ -5070,7 +5070,7 @@ class Task extends TaskFragment { mHandler.removeMessages(TRANSLUCENT_TIMEOUT_MSG); if (waitingActivity != null) { - mWmService.setWindowOpaqueLocked(waitingActivity.token, false); + waitingActivity.setMainWindowOpaque(false); if (waitingActivity.attachedToProcess()) { try { waitingActivity.app.getThread().scheduleTranslucentConversionComplete( diff --git a/services/core/java/com/android/server/wm/TaskFragmentOrganizerController.java b/services/core/java/com/android/server/wm/TaskFragmentOrganizerController.java index 81fe453cc692..8d054db8994a 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 @@ public class TaskFragmentOrganizerController extends ITaskFragmentOrganizerContr + " 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 27cc2d6b4aac..7fc61e1ac7f3 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.ArrayList; 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 @@ class TaskOrganizerController extends ITaskOrganizerController.Stub { */ 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 @@ class TaskOrganizerController extends ITaskOrganizerController.Stub { 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 @@ class TaskOrganizerController extends ITaskOrganizerController.Stub { // 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 @@ class TaskOrganizerController extends ITaskOrganizerController.Stub { } /** - * 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/TrustedOverlayHost.java b/services/core/java/com/android/server/wm/TrustedOverlayHost.java index f8edc2b871be..debe7946dc95 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 @@ class TrustedOverlayHost { 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 1688a1a91114..817901f96ad4 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.HandlerThread; 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 @@ public class TrustedPresentationListenerController { 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 @@ public class TrustedPresentationListenerController { } } - 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 @@ public class TrustedPresentationListenerController { 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 2d2857aba781..80894b201942 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 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer< 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/WindowManagerInternal.java b/services/core/java/com/android/server/wm/WindowManagerInternal.java index d0b9a6ec8775..ae4c3b9a510a 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 @@ public abstract class WindowManagerInternal { * * {@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 426694d178af..b3983e731cf0 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 static com.android.window.flags.Flags.multiCrop; 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.policy.WindowManagerPolicy.ScreenOffListener; 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; @@ -1073,7 +1075,7 @@ public class WindowManagerService extends IWindowManager.Stub @Override public void onAppTransitionFinishedLocked(IBinder token) { - final ActivityRecord atoken = mRoot.getActivityRecord(token); + final ActivityRecord atoken = ActivityRecord.forTokenLocked(token); if (atoken == null) { return; } @@ -3105,13 +3107,6 @@ public class WindowManagerService extends IWindowManager.Stub return mRecentsAnimationController != null && mRecentsAnimationController.isTargetApp(r); } - void setWindowOpaqueLocked(IBinder token, boolean isOpaque) { - final ActivityRecord wtoken = mRoot.getActivityRecord(token); - if (wtoken != null) { - wtoken.setMainWindowOpaque(isOpaque); - } - } - boolean isValidPictureInPictureAspectRatio(DisplayContent displayContent, float aspectRatio) { return displayContent.getPinnedTaskController().isValidPictureInPictureAspectRatio( aspectRatio); @@ -3292,7 +3287,7 @@ public class WindowManagerService extends IWindowManager.Stub } } - @android.annotation.EnforcePermission(android.Manifest.permission.DISABLE_KEYGUARD) + @EnforcePermission(android.Manifest.permission.DISABLE_KEYGUARD) /** * @see android.app.KeyguardManager#exitKeyguardSecurely */ @@ -4520,7 +4515,7 @@ public class WindowManagerService extends IWindowManager.Stub } } - @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) { @@ -4539,7 +4534,7 @@ public class WindowManagerService extends IWindowManager.Stub } } - @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) { @@ -4562,7 +4557,7 @@ public class WindowManagerService extends IWindowManager.Stub } } - @android.annotation.EnforcePermission(android.Manifest.permission.MANAGE_APP_TOKENS) + @EnforcePermission(android.Manifest.permission.MANAGE_APP_TOKENS) @Override public void setDisplayWindowInsetsController( int displayId, IDisplayWindowInsetsController insetsController) { @@ -4581,7 +4576,7 @@ public class WindowManagerService extends IWindowManager.Stub } } - @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) { @@ -5841,7 +5836,7 @@ public class WindowManagerService extends IWindowManager.Stub } } - @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(); @@ -5859,7 +5854,7 @@ public class WindowManagerService extends IWindowManager.Stub } } - @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(); @@ -5947,7 +5942,7 @@ public class WindowManagerService extends IWindowManager.Stub 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(); @@ -6010,7 +6005,7 @@ public class WindowManagerService extends IWindowManager.Stub 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(); @@ -6036,7 +6031,7 @@ public class WindowManagerService extends IWindowManager.Stub } } - @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(); @@ -6536,7 +6531,7 @@ public class WindowManagerService extends IWindowManager.Stub } } - @android.annotation.EnforcePermission(android.Manifest.permission.STATUS_BAR) + @EnforcePermission(android.Manifest.permission.STATUS_BAR) public void setNavBarVirtualKeyHapticFeedbackEnabled(boolean enabled) { setNavBarVirtualKeyHapticFeedbackEnabled_enforcePermission(); @@ -6578,7 +6573,7 @@ public class WindowManagerService extends IWindowManager.Stub } } - @android.annotation.EnforcePermission(android.Manifest.permission.RESTRICTED_VR_ACCESS) + @EnforcePermission(android.Manifest.permission.RESTRICTED_VR_ACCESS) @Override public Region getCurrentImeTouchRegion() { getCurrentImeTouchRegion_enforcePermission(); @@ -8468,12 +8463,13 @@ public class WindowManagerService extends IWindowManager.Stub 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); @@ -8634,6 +8630,24 @@ public class WindowManagerService extends IWindowManager.Stub } } } + + @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 { @@ -9167,18 +9181,48 @@ public class WindowManagerService extends IWindowManager.Stub 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; @@ -9227,12 +9271,13 @@ public class WindowManagerService extends IWindowManager.Stub if (topRunningActivity == null) { return false; } - moveDisplayToTopInternal(topRunningActivity.getDisplayId()); - handleTaskFocusChange(topRunningActivity.getTask(), topRunningActivity); - if (fromWin.isFocused()) { - return false; - } - return true; + moveFocusToActivity(topRunningActivity); + return !fromWin.isFocused(); + } + + private void moveFocusToActivity(@NonNull ActivityRecord activity) { + moveDisplayToTopInternal(activity.getDisplayId()); + handleTaskFocusChange(activity.getTask(), activity); } /** Return whether layer tracing is enabled */ @@ -9956,13 +10001,17 @@ public class WindowManagerService extends IWindowManager.Stub 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/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java index 24e50c54aa61..f5806c07c572 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 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP parentWindow.addChild(this, sWindowSubLayerComparator); } - if (token.mRoundedCornerOverlay) { - mWmService.mTrustedPresentationListenerController.addIgnoredWindowTokens( - getWindowToken()); - } } @Override @@ -1163,6 +1159,9 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP 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 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP mSession.onWindowRemoved(this); mWmService.postWindowRemoveCleanupLocked(this); - mWmService.mTrustedPresentationListenerController.removeIgnoredWindowTokens( - getWindowToken()); - consumeInsetsChange(); } diff --git a/services/core/java/com/android/server/wm/WindowStateAnimator.java b/services/core/java/com/android/server/wm/WindowStateAnimator.java index 44cd23d037c6..09c4f7ca3c6c 100644 --- a/services/core/java/com/android/server/wm/WindowStateAnimator.java +++ b/services/core/java/com/android/server/wm/WindowStateAnimator.java @@ -499,6 +499,10 @@ class WindowStateAnimator { } void applyEnterAnimationLocked() { + if (mWin.mActivityRecord != null && mWin.mActivityRecord.hasStartingWindow()) { + // It's unnecessary to play enter animation below starting window. + return; + } final int transit; if (mEnterAnimationPending) { mEnterAnimationPending = false; diff --git a/services/core/jni/OWNERS b/services/core/jni/OWNERS index cc08488742b2..df7fb991e39e 100644 --- a/services/core/jni/OWNERS +++ b/services/core/jni/OWNERS @@ -31,5 +31,8 @@ per-file com_android_server_vibrator_* = file:/services/core/java/com/android/se 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 e892d23460c9..49a3ad35649b 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 @@ static memevents::MemEventListener memevent_listener; * @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 @@ static jobjectArray android_server_am_OomConnection_waitOom(JNIEnv* env, jobject } 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 4a6b31c29471..810090adbf8e 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 @@ public: 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 @@ void NativeInputManager::setPointerDisplayId(int32_t displayId) { } } +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 jboolean nativeTransferTouch(JNIEnv* env, jobject nativeImplObj, jobject } } +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 @@ static const JNINativeMethod gInputManagerMethods[] = { {"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/CredentialManagerUi.java b/services/credentials/java/com/android/server/credentials/CredentialManagerUi.java index 4203576ac521..84b5cb785a41 100644 --- a/services/credentials/java/com/android/server/credentials/CredentialManagerUi.java +++ b/services/credentials/java/com/android/server/credentials/CredentialManagerUi.java @@ -148,8 +148,8 @@ public class CredentialManagerUi { * 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 +170,8 @@ public class CredentialManagerUi { .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 +179,7 @@ public class CredentialManagerUi { // 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 7e709fec128b..1a9a0e6888c0 100644 --- a/services/credentials/java/com/android/server/credentials/GetCandidateRequestSession.java +++ b/services/credentials/java/com/android/server/credentials/GetCandidateRequestSession.java @@ -116,7 +116,7 @@ public class GetCandidateRequestSession extends RequestSession<GetCredentialRequ mRequestId, mClientRequest, mClientAppInfo.getPackageName(), PermissionUtils.hasPermission(mContext, mClientAppInfo.getPackageName(), Manifest.permission.CREDENTIAL_MANAGER_SET_ALLOWED_PROVIDERS)), - providerDataList, + /*providerDataList=*/ null, /*isRequestForAllOptions=*/ true); List<GetCredentialProviderData> candidateProviderDataList = new ArrayList<>(); diff --git a/services/credentials/java/com/android/server/credentials/ProviderGetSession.java b/services/credentials/java/com/android/server/credentials/ProviderGetSession.java index fcaef9ffb11b..ca23d62601bb 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.AuthenticationEntry; 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 @@ public final class ProviderGetSession extends ProviderSession<BeginGetCredential @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 @@ public final class ProviderGetSession extends ProviderSession<BeginGetCredential 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 @@ public final class ProviderGetSession extends ProviderSession<BeginGetCredential invokeCallbackOnInternalInvalidState(); return; } - onCredentialEntrySelected(providerPendingIntentResponse); + onCredentialEntrySelected(providerPendingIntentResponse, entryKey); break; case ACTION_ENTRY_KEY: Action actionEntry = mProviderResponseDataHandler.getActionEntry(entryKey); @@ -307,7 +312,7 @@ public final class ProviderGetSession extends ProviderSession<BeginGetCredential invokeCallbackOnInternalInvalidState(); return; } - onActionEntrySelected(providerPendingIntentResponse); + onActionEntrySelected(providerPendingIntentResponse, entryKey); break; case AUTHENTICATION_ACTION_ENTRY_KEY: Action authenticationEntry = mProviderResponseDataHandler @@ -337,7 +342,7 @@ public final class ProviderGetSession extends ProviderSession<BeginGetCredential 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 @@ public final class ProviderGetSession extends ProviderSession<BeginGetCredential 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 @@ public final class ProviderGetSession extends ProviderSession<BeginGetCredential .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 @@ public final class ProviderGetSession extends ProviderSession<BeginGetCredential } 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 @@ public final class ProviderGetSession extends ProviderSession<BeginGetCredential 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 @@ public final class ProviderGetSession extends ProviderSession<BeginGetCredential /** 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 @@ public final class ProviderGetSession extends ProviderSession<BeginGetCredential 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/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java index 519c9bb16eed..74d544fcbf8a 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java @@ -23128,6 +23128,10 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { 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()); 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 d5a3cffd71dd..82d5247ebed8 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.SensorManager; 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.HingeAngle; 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 @@ import java.util.function.Supplier; * 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 @@ public class BookStyleClosedStatePredicate implements Predicate<FoldableDeviceSt } + @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 @@ public class BookStyleClosedStatePredicate implements Predicate<FoldableDeviceSt /** * 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 @@ public class BookStyleClosedStatePredicate implements Predicate<FoldableDeviceSt 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 ad938aff396a..8b22718786e5 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.FoldableDeviceStateProvider.DeviceStateConfigur 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 class BookStyleDeviceStatePolicy extends DeviceStatePolicy implements 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 8977422a90a8..69d793e5f989 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 @@ import java.util.Objects; * * 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 class BookStylePreferredScreenCalculator { */ 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 @@ public class BookStylePreferredScreenCalculator { + ", likelyReverseWedge = " + likelyReverseWedge); } - final State oldState = mState; mState = newState; if (mState.mPreferredScreen == PreferredScreen.INVALID) { @@ -103,6 +116,13 @@ public class BookStylePreferredScreenCalculator { + 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 @@ public class BookStylePreferredScreenCalculator { + 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 ba7297790dc9..021a667113e7 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.Handler; 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.devicestate.DeviceStateProvider; 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 @@ public final class FoldableDeviceStateProvider implements DeviceStateProvider, // 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 @@ public final class FoldableDeviceStateProvider implements DeviceStateProvider, mHingeAngleSensor = hingeAngleSensor; mHallSensor = hallSensor; mDisplayManager = displayManager; + mConfigurations = deviceStateConfigurations; mIsDualDisplayBlockingEnabled = featureFlags.enableDualDisplayBlocking(); sensorManager.registerListener(this, mHingeAngleSensor, SENSOR_DELAY_FASTEST); @@ -350,16 +355,20 @@ public final class FoldableDeviceStateProvider implements DeviceStateProvider, @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 @@ public final class FoldableDeviceStateProvider implements DeviceStateProvider, } } + @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 fd21a326b640..2359422c9b89 100644 --- a/services/java/com/android/server/SystemConfigService.java +++ b/services/java/com/android/server/SystemConfigService.java @@ -22,6 +22,8 @@ import android.Manifest; 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 @@ public class SystemConfigService extends SystemService { 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 2b8bcc77281a..b79d20a14d32 100644 --- a/services/java/com/android/server/SystemServer.java +++ b/services/java/com/android/server/SystemServer.java @@ -269,6 +269,8 @@ public final class SystemServer implements Dumpable { "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 +2071,24 @@ public final class SystemServer implements Dumpable { 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(); } diff --git a/services/profcollect/OWNERS b/services/profcollect/OWNERS index b380e39529e3..be9e61fa298b 100644 --- a/services/profcollect/OWNERS +++ b/services/profcollect/OWNERS @@ -1,3 +1 @@ -srhines@google.com -yabinc@google.com -yikong@google.com +include platform/prebuilts/clang/host/linux-x86:/OWNERS diff --git a/services/profcollect/src/com/android/server/profcollect/ProfcollectForwardingService.java b/services/profcollect/src/com/android/server/profcollect/ProfcollectForwardingService.java index 582b712ec3fc..50e426ca0282 100644 --- a/services/profcollect/src/com/android/server/profcollect/ProfcollectForwardingService.java +++ b/services/profcollect/src/com/android/server/profcollect/ProfcollectForwardingService.java @@ -62,7 +62,7 @@ public final class ProfcollectForwardingService extends SystemService { private static final boolean DEBUG = Log.isLoggable(LOG_TAG, Log.DEBUG); private static final String INTENT_UPLOAD_PROFILES = "com.android.server.profcollect.UPLOAD_PROFILES"; - private static final long BG_PROCESS_PERIOD = TimeUnit.HOURS.toMillis(4); // every 4 hours. + private static final long BG_PROCESS_INTERVAL = TimeUnit.HOURS.toMillis(4); // every 4 hours. private IProfCollectd mIProfcollect; private static ProfcollectForwardingService sSelfService; @@ -226,7 +226,7 @@ public final class ProfcollectForwardingService extends SystemService { js.schedule(new JobInfo.Builder(JOB_IDLE_PROCESS, JOB_SERVICE_NAME) .setRequiresDeviceIdle(true) .setRequiresCharging(true) - .setPeriodic(BG_PROCESS_PERIOD) + .setPeriodic(BG_PROCESS_INTERVAL) .setPriority(JobInfo.PRIORITY_MIN) .build()); } @@ -327,7 +327,7 @@ public final class ProfcollectForwardingService extends SystemService { // 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/InputMethodSystemServerTests/Android.bp b/services/tests/InputMethodSystemServerTests/Android.bp index 56423b961813..afd6dbd7f6a7 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" @@ -73,7 +74,6 @@ android_ravenwood_test { "androidx.annotation_annotation", "androidx.test.rules", "framework", - "mockito_ravenwood", "ravenwood-runtime", "ravenwood-utils", "services", diff --git a/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/Android.bp b/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/Android.bp index e1fd2b34d881..8a12dcd0add4 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 00850a5e5be0..e3919a5e449e 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 ad7af44d4089..f15e533fee2b 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 99211062964e..6da503d5df45 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 f8accc309931..4f27bc2bba5d 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 129efc630be6..005cad125115 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.os.UserManager; 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.pm.permission.PermissionManagerService; 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 @@ public class CrossProfileAppsServiceImplTest { private IApplicationThread mIApplicationThread; private SparseArray<Boolean> mUserEnabled = new SparseArray<>(); + @Rule + public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); @Before public void initCrossProfileAppsServiceImpl() { @@ -123,8 +127,9 @@ public class CrossProfileAppsServiceImplTest { 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/PackageManagerSettingsTests.java b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageManagerSettingsTests.java index 6fff012cd9ca..8ad557c7bbfd 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.PersistableBundle; 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 @@ public class PackageManagerSettingsTests { 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 8505983894a8..c93f48225e48 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/ParsedProcessTest.kt b/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/parsing/parcelling/ParsedProcessTest.kt index 93bdeaeb90e3..d538f255c5aa 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 @@ class ParsedProcessTest : ParcelableComponentTest(ParsedProcess::class, ParsedPr ParsedProcess::getGwpAsanMode, ParsedProcess::getMemtagMode, ParsedProcess::getNativeHeapZeroInitialized, + ParsedProcess::isUseEmbeddedDex, ) override fun extraParams() = listOf( 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 75febd902dcf..b142334db9e9 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.Context; 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,10 @@ import android.os.MessageQueue; 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; import android.view.DisplayAdjustments; @@ -209,6 +213,10 @@ public class DisplayManagerServiceTest { private int mPreferredHdrOutputType; + private Handler mPowerHandler; + + private UserManager mUserManager; + private final DisplayManagerService.Injector mShortMockedInjector = new DisplayManagerService.Injector() { @Override @@ -369,11 +377,14 @@ public class DisplayManagerServiceTest { 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(); @@ -432,7 +443,7 @@ public class DisplayManagerServiceTest { verify(mMockProjectionService, never()).setContentRecordingSession(any(), nullable(IMediaProjection.class)); - displayManager.performTraversalInternal(mock(SurfaceControl.Transaction.class)); + performTraversalInternal(displayManager); // flush the handler displayManager.getDisplayHandler().runWithScissors(() -> {}, 0 /* now */); @@ -507,7 +518,7 @@ public class DisplayManagerServiceTest { assertTrue(expectedDisplayTypeToViewPortTypeMapping.keySet().contains(info.type)); } - displayManager.performTraversalInternal(mock(SurfaceControl.Transaction.class)); + performTraversalInternal(displayManager); // flush the handler displayManager.getDisplayHandler().runWithScissors(() -> {}, 0 /* now */); @@ -559,7 +570,7 @@ public class DisplayManagerServiceTest { verify(mMockProjectionService, never()).setContentRecordingSession(any(), nullable(IMediaProjection.class)); - displayManager.performTraversalInternal(mock(SurfaceControl.Transaction.class)); + performTraversalInternal(displayManager); // flush the handler displayManager.getDisplayHandler().runWithScissors(() -> {}, 0 /* now */); @@ -594,7 +605,7 @@ public class DisplayManagerServiceTest { verify(mMockProjectionService, never()).setContentRecordingSession(any(), nullable(IMediaProjection.class)); - displayManager.performTraversalInternal(mock(SurfaceControl.Transaction.class)); + performTraversalInternal(displayManager); // flush the handler displayManager.getDisplayHandler().runWithScissors(() -> {}, /* now= */ 0); @@ -632,7 +643,7 @@ public class DisplayManagerServiceTest { verify(mMockProjectionService, never()).setContentRecordingSession(any(), nullable(IMediaProjection.class)); - displayManager.performTraversalInternal(mock(SurfaceControl.Transaction.class)); + performTraversalInternal(displayManager); // flush the handler displayManager.getDisplayHandler().runWithScissors(() -> {}, /* now= */ 0); @@ -667,7 +678,7 @@ public class DisplayManagerServiceTest { verify(mMockProjectionService, never()).setContentRecordingSession(any(), nullable(IMediaProjection.class)); - displayManager.performTraversalInternal(mock(SurfaceControl.Transaction.class)); + performTraversalInternal(displayManager); // flush the handler displayManager.getDisplayHandler().runWithScissors(() -> {}, /* now= */ 0); @@ -947,7 +958,7 @@ public class DisplayManagerServiceTest { PACKAGE_NAME); verify(mMockProjectionService, never()).setContentRecordingSession(any(), nullable(IMediaProjection.class)); - displayManager.performTraversalInternal(mock(SurfaceControl.Transaction.class)); + performTraversalInternal(displayManager); // flush the handler displayManager.getDisplayHandler().runWithScissors(() -> {}, 0 /* now */); @@ -1439,7 +1450,7 @@ public class DisplayManagerServiceTest { PACKAGE_NAME); verify(mMockProjectionService, never()).setContentRecordingSession(any(), nullable(IMediaProjection.class)); - displayManager.performTraversalInternal(mock(SurfaceControl.Transaction.class)); + performTraversalInternal(displayManager); // flush the handler displayManager.getDisplayHandler().runWithScissors(() -> {}, 0 /* now */); @@ -1694,7 +1705,7 @@ public class DisplayManagerServiceTest { verify(mMockProjectionService, never()).setContentRecordingSession(any(), nullable(IMediaProjection.class)); - displayManager.performTraversalInternal(mock(SurfaceControl.Transaction.class)); + performTraversalInternal(displayManager); // flush the handler displayManager.getDisplayHandler().runWithScissors(() -> {}, 0 /* now */); @@ -1728,7 +1739,7 @@ public class DisplayManagerServiceTest { null /* projection */, PACKAGE_NAME); verify(mMockProjectionService, never()).setContentRecordingSession(any(), nullable(IMediaProjection.class)); - displayManager.performTraversalInternal(mock(SurfaceControl.Transaction.class)); + performTraversalInternal(displayManager); displayManager.getDisplayHandler().runWithScissors(() -> {}, 0 /* now */); DisplayDeviceInfo ddi = displayManager.getDisplayDeviceInfoInternal(displayId); assertNotNull(ddi); @@ -1797,7 +1808,7 @@ public class DisplayManagerServiceTest { mock(DisplayWindowPolicyController.class), PACKAGE_NAME); verify(mMockProjectionService, never()).setContentRecordingSession(any(), nullable(IMediaProjection.class)); - displayManager.performTraversalInternal(mock(SurfaceControl.Transaction.class)); + performTraversalInternal(displayManager); displayManager.getDisplayHandler().runWithScissors(() -> {}, 0 /* now */); DisplayDeviceInfo ddi = displayManager.getDisplayDeviceInfoInternal(displayId); assertNotNull(ddi); @@ -2788,6 +2799,85 @@ public class DisplayManagerServiceTest { 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 @@ -2819,7 +2909,7 @@ public class DisplayManagerServiceTest { public void releaseSuspendBlocker(String id) { } - }, new Handler(Looper.getMainLooper()), mSensorManager); + }, mPowerHandler, mSensorManager); } private void testDisplayInfoFrameRateOverrideModeCompat(boolean compatChangeEnabled) { @@ -2912,6 +3002,11 @@ public class DisplayManagerServiceTest { assertEquals(expectedMode, displayInfo.getMode()); } + private void performTraversalInternal(DisplayManagerService displayManager) { + displayManager.performTraversalInternal(mock(SurfaceControl.Transaction.class), + new SparseArray<>()); + } + private int getDisplayIdForDisplayDevice( DisplayManagerService displayManager, DisplayManagerService.BinderService displayManagerBinderService, 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 88a9758991f4..57b86324e171 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 @@ public final class DisplayPowerControllerTest { 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 @@ public final class DisplayPowerControllerTest { 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 @@ public final class DisplayPowerControllerTest { 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 @@ public final class DisplayPowerControllerTest { 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 @@ public final class DisplayPowerControllerTest { 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 @@ public final class DisplayPowerControllerTest { 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 @@ public final class DisplayPowerControllerTest { 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 @@ public final class DisplayPowerControllerTest { 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 @@ public final class DisplayPowerControllerTest { 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 @@ public final class DisplayPowerControllerTest { .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 c92ce254cae4..b99ecf3978ae 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 @@ public class LocalDisplayAdapterTest { .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 2d0c3fdb6352..289d54b86c7d 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 @@ package com.android.server.display.brightness; 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 @@ public final class DisplayBrightnessControllerTest { 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 78ec2ff31161..5408e1143330 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 @@ public class AutomaticBrightnessStrategyTest { 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 @@ public class AutomaticBrightnessStrategyTest { 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 @@ public class AutomaticBrightnessStrategyTest { 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 @@ public class AutomaticBrightnessStrategyTest { 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 @@ public class AutomaticBrightnessStrategyTest { 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 @@ public class AutomaticBrightnessStrategyTest { 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 @@ public class AutomaticBrightnessStrategyTest { 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 @@ public class AutomaticBrightnessStrategyTest { 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/src/com/android/server/job/JobSchedulerServiceTest.java b/services/tests/mockingservicestests/src/com/android/server/job/JobSchedulerServiceTest.java index eaf0ffdc3cbd..3355a6cff2ad 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.ACTIVE_INDEX; 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; @@ -60,12 +62,15 @@ 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.BatteryManagerInternal; 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,6 +87,7 @@ import com.android.server.usage.AppStandbyInternal; import org.junit.After; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.mockito.Mock; import org.mockito.MockitoSession; @@ -105,6 +111,9 @@ public class JobSchedulerServiceTest { @Mock private PackageManagerInternal mPackageManagerInternal; + @Rule + public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); + private class TestJobSchedulerService extends JobSchedulerService { TestJobSchedulerService(Context context) { super(context); @@ -1711,29 +1720,283 @@ public class JobSchedulerServiceTest { /** Tests that rare job batching works as expected. */ @Test - public void testRareJobBatching() { + 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.MIN_READY_NON_ACTIVE_JOBS_COUNT = 5; - mService.mConstants.MAX_NON_ACTIVE_JOB_BATCH_DELAY_MS = HOUR_IN_MILLIS; + 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( - "testRareJobBatching", + "testConnectivityJobBatching", createJobInfo().setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY)); - job.setStandbyBucket(RARE_INDEX); + 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() { + 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_NON_ACTIVE_JOBS_COUNT = 5; + mService.mConstants.MAX_NON_ACTIVE_JOB_BATCH_DELAY_MS = HOUR_IN_MILLIS; // 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 +2007,11 @@ public class JobSchedulerServiceTest { 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 +2019,17 @@ public class JobSchedulerServiceTest { 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 +2046,11 @@ public class JobSchedulerServiceTest { - 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()); } 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 4958f1c1c214..f27d0c2ce5a9 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.inOrder; 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.PackageManager; 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.junit.MockitoJUnitRunner; 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 @@ public class ConnectivityControllerTest { 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 9a143d5b3743..1e81951bb00d 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 @@ public class AltitudeConverterTest { 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/PackageArchiverTest.java b/services/tests/mockingservicestests/src/com/android/server/pm/PackageArchiverTest.java index bf00b75e9f7b..4535ecee8097 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 @@ public class PackageArchiverTest { 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/powerstatstests/Android.bp b/services/tests/powerstatstests/Android.bp index f49f6383b3c8..64fef68e387a 100644 --- a/services/tests/powerstatstests/Android.bp +++ b/services/tests/powerstatstests/Android.bp @@ -81,7 +81,6 @@ android_ravenwood_test { "androidx.annotation_annotation", "androidx.test.rules", "truth", - "mockito_ravenwood", ], srcs: [ ":power_stats_ravenwood_tests", diff --git a/services/tests/servicestests/Android.bp b/services/tests/servicestests/Android.bp index 5611415c8fb1..ad6e2c657c96 100644 --- a/services/tests/servicestests/Android.bp +++ b/services/tests/servicestests/Android.bp @@ -153,7 +153,6 @@ android_ravenwood_test { static_libs: [ "androidx.annotation_annotation", "androidx.test.rules", - "mockito_ravenwood", "services.core", ], srcs: [ diff --git a/services/tests/servicestests/res/xml/usertypes_test_profile.xml b/services/tests/servicestests/res/xml/usertypes_test_profile.xml index 6537d47106a9..f9c6794d485d 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/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClientTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClientTest.java index f96d9e841eba..9cdaec647a43 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 @@ package com.android.server.biometrics.sensors.fingerprint.aidl; 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; @@ -517,6 +518,16 @@ public class FingerprintAuthenticationClientTest { } @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/companion/virtual/VirtualDeviceTest.java b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceTest.java index 07dd59d2e2d8..a4628ee3b52b 100644 --- a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceTest.java +++ b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceTest.java @@ -18,6 +18,8 @@ package com.android.server.companion.virtual; import static android.companion.virtual.VirtualDeviceParams.DEVICE_POLICY_CUSTOM; import static android.companion.virtual.VirtualDeviceParams.DEVICE_POLICY_DEFAULT; +import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_AUDIO; +import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_CAMERA; import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_SENSORS; import static android.content.Context.DEVICE_ID_DEFAULT; import static android.content.Context.DEVICE_ID_INVALID; @@ -135,4 +137,34 @@ public class VirtualDeviceTest { when(mVirtualDevice.getDevicePolicy(POLICY_TYPE_SENSORS)).thenReturn(DEVICE_POLICY_CUSTOM); assertThat(virtualDevice.hasCustomSensorSupport()).isTrue(); } + + @Test + public void virtualDevice_hasCustomAudioInputSupport() throws Exception { + mSetFlagsRule.enableFlags(Flags.FLAG_VDM_PUBLIC_APIS); + + VirtualDevice virtualDevice = + new VirtualDevice( + mVirtualDevice, VIRTUAL_DEVICE_ID, /*persistentId=*/null, /*name=*/null); + + when(mVirtualDevice.getDevicePolicy(POLICY_TYPE_AUDIO)).thenReturn(DEVICE_POLICY_DEFAULT); + assertThat(virtualDevice.hasCustomAudioInputSupport()).isFalse(); + + when(mVirtualDevice.getDevicePolicy(POLICY_TYPE_AUDIO)).thenReturn(DEVICE_POLICY_CUSTOM); + assertThat(virtualDevice.hasCustomAudioInputSupport()).isTrue(); + } + + @Test + public void virtualDevice_hasCustomCameraSupport() throws Exception { + mSetFlagsRule.enableFlags(Flags.FLAG_VDM_PUBLIC_APIS); + + VirtualDevice virtualDevice = + new VirtualDevice( + mVirtualDevice, VIRTUAL_DEVICE_ID, /*persistentId=*/null, /*name=*/null); + + when(mVirtualDevice.getDevicePolicy(POLICY_TYPE_CAMERA)).thenReturn(DEVICE_POLICY_DEFAULT); + assertThat(virtualDevice.hasCustomCameraSupport()).isFalse(); + + when(mVirtualDevice.getDevicePolicy(POLICY_TYPE_CAMERA)).thenReturn(DEVICE_POLICY_CUSTOM); + assertThat(virtualDevice.hasCustomCameraSupport()).isTrue(); + } } 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 7dcfc88e998c..fa3936443f31 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.assertNotNull; 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.Before; 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 @@ public final class DeviceStateManagerServiceTest { } onComplete.run(); } + + @Override + public void dump(@NonNull PrintWriter writer, @Nullable String[] args) { + } } private static final class TestDeviceStateProvider implements DeviceStateProvider { @@ -1001,6 +1007,10 @@ public final class DeviceStateManagerServiceTest { 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 e7da26ea38b1..98789ac96e98 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 @@ public abstract class BaseAbsoluteVolumeBehaviorTest { 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 @@ public abstract class BaseAbsoluteVolumeBehaviorTest { 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 @@ public abstract class BaseAbsoluteVolumeBehaviorTest { 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 a410702e202d..9ad2652e9c63 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 @@ public abstract class BaseTvToAudioSystemAvbTest extends BaseAbsoluteVolumeBehav 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 @@ public abstract class BaseTvToAudioSystemAvbTest extends BaseAbsoluteVolumeBehav 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 @@ public abstract class BaseTvToAudioSystemAvbTest extends BaseAbsoluteVolumeBehav 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/job/PendingJobQueueTest.java b/services/tests/servicestests/src/com/android/server/job/PendingJobQueueTest.java index 213e05eb788e..2b07d33ea541 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 @@ package com.android.server.job; 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 com.android.server.job.controllers.JobStatus; import org.junit.Test; -import java.util.ArrayList; -import java.util.List; import java.util.Random; public class PendingJobQueueTest { @@ -68,7 +67,7 @@ public class PendingJobQueueTest { @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 @@ public class PendingJobQueueTest { 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 @@ public class PendingJobQueueTest { @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 @@ public class PendingJobQueueTest { @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 @@ public class PendingJobQueueTest { @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 @@ public class PendingJobQueueTest { 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 @@ public class PendingJobQueueTest { @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 @@ public class PendingJobQueueTest { @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 @@ public class PendingJobQueueTest { 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 @@ public class PendingJobQueueTest { 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 @@ public class PendingJobQueueTest { 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 5081198f0058..705359708bc7 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.verify; 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 android.text.TextUtils; 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 @@ public class LockSettingsServiceTests extends BaseLockSettingsServiceTests { 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 @@ public class LockSettingsServiceTests extends BaseLockSettingsServiceTests { 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 @@ public class LockSettingsServiceTests extends BaseLockSettingsServiceTests { 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 @@ public class LockSettingsServiceTests extends BaseLockSettingsServiceTests { 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 000000000000..5df7a5e6d788 --- /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/os/BugreportManagerServiceImplTest.java b/services/tests/servicestests/src/com/android/server/os/BugreportManagerServiceImplTest.java index 1c6d36b0a0d2..ea84eb2fbf73 100644 --- a/services/tests/servicestests/src/com/android/server/os/BugreportManagerServiceImplTest.java +++ b/services/tests/servicestests/src/com/android/server/os/BugreportManagerServiceImplTest.java @@ -36,6 +36,7 @@ import android.os.IBinder; import android.os.IDumpstateListener; import android.os.Process; import android.os.RemoteException; +import android.platform.test.annotations.RequiresFlagsDisabled; import android.platform.test.annotations.RequiresFlagsEnabled; import android.platform.test.flag.junit.CheckFlagsRule; import android.platform.test.flag.junit.DeviceFlagsValueProvider; @@ -48,6 +49,7 @@ import androidx.test.runner.AndroidJUnit4; import org.junit.After; import org.junit.Before; +import org.junit.Ignore; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; @@ -118,6 +120,7 @@ public class BugreportManagerServiceImplTest { } @Test + @RequiresFlagsDisabled(Flags.FLAG_ONBOARDING_BUGREPORT_V2_ENABLED) public void testBugreportFileManagerFileExists() { Pair<Integer, String> callingInfo = new Pair<>(mCallingUid, mCallingPackage); mBugreportFileManager.addBugreportFileForCaller( @@ -137,6 +140,7 @@ public class BugreportManagerServiceImplTest { @Test @RequiresFlagsEnabled(Flags.FLAG_ONBOARDING_BUGREPORT_V2_ENABLED) + @Ignore public void testBugreportFileManagerKeepFilesOnRetrieval() { Pair<Integer, String> callingInfo = new Pair<>(mCallingUid, mCallingPackage); mBugreportFileManager.addBugreportFileForCaller( @@ -150,6 +154,7 @@ public class BugreportManagerServiceImplTest { } @Test + @RequiresFlagsDisabled(Flags.FLAG_ONBOARDING_BUGREPORT_V2_ENABLED) public void testBugreportFileManagerMultipleFiles() { Pair<Integer, String> callingInfo = new Pair<>(mCallingUid, mCallingPackage); mBugreportFileManager.addBugreportFileForCaller( 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 c81fbb443dce..cee738711473 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.os.UserManager; 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.pm.permission.PermissionManagerService; 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 @@ public class CrossProfileAppsServiceImplTest { private IApplicationThread mIApplicationThread; private SparseArray<Boolean> mUserEnabled = new SparseArray<>(); + @Rule + public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); @Before public void initCrossProfileAppsServiceImpl() { @@ -123,8 +127,9 @@ public class CrossProfileAppsServiceImplTest { 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 39cc6537c759..6decf438f95b 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 @@ public class UserManagerServiceCreateProfileTest { 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 8d8dc9cc45b1..3778a32b34c3 100644 --- a/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceUserPropertiesTest.java +++ b/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceUserPropertiesTest.java @@ -79,6 +79,7 @@ public class UserManagerServiceUserPropertiesTest { .setAlwaysVisible(false) .setCrossProfileContentSharingStrategy(0) .setProfileApiVisibility(34) + .setItemsRestrictedOnHomeScreen(false) .build(); final UserProperties actualProps = new UserProperties(defaultProps); actualProps.setShowInLauncher(14); @@ -97,6 +98,7 @@ public class UserManagerServiceUserPropertiesTest { actualProps.setAlwaysVisible(true); actualProps.setCrossProfileContentSharingStrategy(1); actualProps.setProfileApiVisibility(36); + actualProps.setItemsRestrictedOnHomeScreen(true); // Write the properties to xml. final ByteArrayOutputStream baos = new ByteArrayOutputStream(); @@ -144,6 +146,7 @@ public class UserManagerServiceUserPropertiesTest { .setAllowStoppingUserWithDelayedLocking(false) .setAlwaysVisible(true) .setProfileApiVisibility(110) + .setItemsRestrictedOnHomeScreen(false) .build(); final UserProperties orig = new UserProperties(defaultProps); orig.setShowInLauncher(2841); @@ -154,6 +157,7 @@ public class UserManagerServiceUserPropertiesTest { 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 +204,8 @@ public class UserManagerServiceUserPropertiesTest { 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 +289,7 @@ public class UserManagerServiceUserPropertiesTest { 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 1ee604e78d5f..6cdbc7428f7e 100644 --- a/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceUserTypeTest.java +++ b/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceUserTypeTest.java @@ -102,7 +102,8 @@ public class UserManagerServiceUserTypeTest { .setDeleteAppWithParent(true) .setAlwaysVisible(true) .setCrossProfileContentSharingStrategy(1) - .setProfileApiVisibility(34); + .setProfileApiVisibility(34) + .setItemsRestrictedOnHomeScreen(true); final UserTypeDetails type = new UserTypeDetails.Builder() .setName("a.name") @@ -186,6 +187,7 @@ public class UserManagerServiceUserTypeTest { assertEquals(1, type.getDefaultUserPropertiesReference() .getCrossProfileContentSharingStrategy()); assertEquals(34, type.getDefaultUserPropertiesReference().getProfileApiVisibility()); + assertTrue(type.getDefaultUserPropertiesReference().areItemsRestrictedOnHomeScreen()); assertEquals(23, type.getBadgeLabel(0)); assertEquals(24, type.getBadgeLabel(1)); @@ -343,7 +345,8 @@ public class UserManagerServiceUserTypeTest { .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 +398,7 @@ public class UserManagerServiceUserTypeTest { 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 +456,7 @@ public class UserManagerServiceUserTypeTest { 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 db561c436269..9323b482d5e2 100644 --- a/services/tests/servicestests/src/com/android/server/pm/UserManagerTest.java +++ b/services/tests/servicestests/src/com/android/server/pm/UserManagerTest.java @@ -355,6 +355,8 @@ public final class UserManagerTest { privateProfileUserProperties::getAllowStoppingUserWithDelayedLocking); assertThrows(SecurityException.class, privateProfileUserProperties::getProfileApiVisibility); + assertThrows(SecurityException.class, + privateProfileUserProperties::areItemsRestrictedOnHomeScreen); compareDrawables(mUserManager.getUserBadge(), Resources.getSystem().getDrawable(userTypeDetails.getBadgePlain())); 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 aca96ad20385..03cdbbd8397b 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 com.google.common.truth.Truth.assertThat; 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 @@ public class SystemConfigTest { @Rule public TemporaryFolder mTemporaryFolder = new TemporaryFolder(); + @Rule public CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule(); + @Before public void setUp() throws Exception { mSysConfig = new SystemConfigTestClass(); @@ -696,6 +703,43 @@ public class SystemConfigTest { 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).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/utils/com/android/server/testutils/StubTransaction.java b/services/tests/servicestests/utils/com/android/server/testutils/StubTransaction.java index b8dcecdbfb95..e27bb4c8c3b6 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 @@ public class StubTransaction extends SurfaceControl.Transaction { 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/UiModeManagerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/UiModeManagerServiceTest.java index 839cf7ce4926..c247c08c8010 100644 --- a/services/tests/uiservicestests/src/com/android/server/UiModeManagerServiceTest.java +++ b/services/tests/uiservicestests/src/com/android/server/UiModeManagerServiceTest.java @@ -82,6 +82,7 @@ import android.content.res.Resources; import android.os.Bundle; import android.os.Handler; import android.os.IBinder; +import android.os.IpcDataCache; import android.os.PowerManager; import android.os.PowerManagerInternal; import android.os.PowerSaveState; @@ -174,6 +175,11 @@ public class UiModeManagerServiceTest extends UiServiceTestCase { @Before + public void disableProcessCaches() { + IpcDataCache.disableForTestMode(); + } + + @Before public void setUp() { // The AIDL stub will use PermissionEnforcer to check permission from the caller. mPermissionEnforcer = new FakePermissionEnforcer(); 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 9c2cba8ecf96..8261dee0340f 100755 --- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java @@ -227,8 +227,6 @@ import android.permission.PermissionManager; import android.platform.test.annotations.DisableFlags; import android.platform.test.annotations.EnableFlags; import android.platform.test.flag.junit.SetFlagsRule; -import android.platform.test.rule.DeniedDevices; -import android.platform.test.rule.DeviceProduct; import android.platform.test.rule.LimitDevicesRule; import android.provider.DeviceConfig; import android.provider.MediaStore; @@ -336,7 +334,6 @@ import java.util.function.Consumer; @RunWith(AndroidTestingRunner.class) @SuppressLint("GuardedBy") // It's ok for this test to access guarded methods from the service. @RunWithLooper -@DeniedDevices(denied = {DeviceProduct.CF_AUTO}) public class NotificationManagerServiceTest extends UiServiceTestCase { private static final String TEST_CHANNEL_ID = "NotificationManagerServiceTestChannelId"; private static final String TEST_PACKAGE = "The.name.is.Package.Test.Package"; @@ -593,7 +590,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { when(mAtm.getTaskToShowPermissionDialogOn(anyString(), anyInt())) .thenReturn(INVALID_TASK_ID); mContext.addMockSystemService(AppOpsManager.class, mock(AppOpsManager.class)); - when(mUm.getProfileIds(0, false)).thenReturn(new int[]{0}); + when(mUm.getProfileIds(eq(mUserId), eq(false))).thenReturn(new int[] { mUserId }); when(mPackageManagerClient.hasSystemFeature(FEATURE_TELECOM)).thenReturn(true); @@ -881,9 +878,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { private void simulatePackageRemovedBroadcast(String pkg, int uid) { // mimics receive broadcast that package is removed, but doesn't remove the package. final Bundle extras = new Bundle(); - extras.putStringArray(Intent.EXTRA_CHANGED_PACKAGE_LIST, - new String[]{pkg}); - extras.putIntArray(Intent.EXTRA_CHANGED_UID_LIST, new int[]{uid}); + extras.putInt(Intent.EXTRA_UID, uid); final Intent intent = new Intent(Intent.ACTION_PACKAGE_REMOVED); intent.setData(Uri.parse("package:" + pkg)); @@ -1031,7 +1026,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { private NotificationRecord generateNotificationRecord(NotificationChannel channel, long postTime) { - final StatusBarNotification sbn = generateSbn(PKG, mUid, postTime, 0); + final StatusBarNotification sbn = generateSbn(PKG, mUid, postTime, mUserId); return new NotificationRecord(mContext, sbn, channel); } @@ -1259,6 +1254,11 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { 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 @@ -1766,7 +1766,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { mBinderService.enqueueNotificationWithTag(PKG, PKG, "testEnqueueNotification_appBlocked", 0, - generateNotificationRecord(null).getNotification(), 0); + generateNotificationRecord(null).getNotification(), mUserId); waitForIdle(); verify(mWorkerHandler, never()).post( any(NotificationManagerService.EnqueueNotificationRunnable.class)); @@ -1776,7 +1776,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { public void testEnqueueNotificationWithTag_PopulatesGetActiveNotifications() throws Exception { mBinderService.enqueueNotificationWithTag(PKG, PKG, "testEnqueueNotificationWithTag_PopulatesGetActiveNotifications", 0, - generateNotificationRecord(null).getNotification(), 0); + generateNotificationRecord(null).getNotification(), mUserId); waitForIdle(); StatusBarNotification[] notifs = mBinderService.getActiveNotifications(PKG); assertEquals(1, notifs.length); @@ -1787,7 +1787,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { public void testEnqueueNotificationWithTag_WritesExpectedLogs() throws Exception { final String tag = "testEnqueueNotificationWithTag_WritesExpectedLog"; mBinderService.enqueueNotificationWithTag(PKG, PKG, tag, 0, - generateNotificationRecord(null).getNotification(), 0); + generateNotificationRecord(null).getNotification(), mUserId); waitForIdle(); assertEquals(1, mNotificationRecordLogger.numCalls()); @@ -1828,12 +1828,12 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { Notification original = new Notification.Builder(mContext, mTestNotificationChannel.getId()) .setSmallIcon(android.R.drawable.sym_def_app_icon).build(); - mBinderService.enqueueNotificationWithTag(PKG, PKG, tag, 0, original, 0); + mBinderService.enqueueNotificationWithTag(PKG, PKG, tag, 0, original, mUserId); Notification update = new Notification.Builder(mContext, mTestNotificationChannel.getId()) .setSmallIcon(android.R.drawable.sym_def_app_icon) .setCategory(Notification.CATEGORY_ALARM).build(); - mBinderService.enqueueNotificationWithTag(PKG, PKG, tag, 0, update, 0); + mBinderService.enqueueNotificationWithTag(PKG, PKG, tag, 0, update, mUserId); waitForIdle(); assertEquals(2, mNotificationRecordLogger.numCalls()); @@ -1853,9 +1853,9 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { public void testEnqueueNotificationWithTag_DoesNotLogOnMinorUpdate() throws Exception { final String tag = "testEnqueueNotificationWithTag_DoesNotLogOnMinorUpdate"; mBinderService.enqueueNotificationWithTag(PKG, PKG, tag, 0, - generateNotificationRecord(null).getNotification(), 0); + generateNotificationRecord(null).getNotification(), mUserId); mBinderService.enqueueNotificationWithTag(PKG, PKG, tag, 0, - generateNotificationRecord(null).getNotification(), 0); + generateNotificationRecord(null).getNotification(), mUserId); waitForIdle(); assertEquals(2, mNotificationRecordLogger.numCalls()); assertTrue(mNotificationRecordLogger.get(0).wasLogged); @@ -1869,10 +1869,10 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { final String tag = "testEnqueueNotificationWithTag_DoesNotLogOnTitleUpdate"; mBinderService.enqueueNotificationWithTag(PKG, PKG, tag, 0, generateNotificationRecord(null).getNotification(), - 0); + mUserId); final Notification notif = generateNotificationRecord(null).getNotification(); notif.extras.putString(Notification.EXTRA_TITLE, "Changed title"); - mBinderService.enqueueNotificationWithTag(PKG, PKG, tag, 0, notif, 0); + mBinderService.enqueueNotificationWithTag(PKG, PKG, tag, 0, notif, mUserId); waitForIdle(); assertEquals(2, mNotificationRecordLogger.numCalls()); assertEquals(NOTIFICATION_POSTED, mNotificationRecordLogger.event(0)); @@ -1885,11 +1885,11 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { Notification notification = new Notification.Builder(mContext, mTestNotificationChannel.getId()) .setSmallIcon(android.R.drawable.sym_def_app_icon).build(); - mBinderService.enqueueNotificationWithTag(PKG, PKG, tag, 0, notification, 0); + mBinderService.enqueueNotificationWithTag(PKG, PKG, tag, 0, notification, mUserId); waitForIdle(); - mBinderService.cancelNotificationWithTag(PKG, PKG, tag, 0, 0); + mBinderService.cancelNotificationWithTag(PKG, PKG, tag, 0, mUserId); waitForIdle(); - mBinderService.enqueueNotificationWithTag(PKG, PKG, tag, 0, notification, 0); + mBinderService.enqueueNotificationWithTag(PKG, PKG, tag, 0, notification, mUserId); waitForIdle(); assertEquals(3, mNotificationRecordLogger.numCalls()); @@ -1949,7 +1949,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { .build(); n.actions[1] = null; - mBinderService.enqueueNotificationWithTag(PKG, PKG, "tag", 0, n, 0); + mBinderService.enqueueNotificationWithTag(PKG, PKG, "tag", 0, n, mUserId); waitForIdle(); StatusBarNotification[] posted = mBinderService.getActiveNotifications(PKG); @@ -1970,7 +1970,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { n.actions[0] = null; n.actions[1] = null; - mBinderService.enqueueNotificationWithTag(PKG, PKG, "tag", 0, n, 0); + mBinderService.enqueueNotificationWithTag(PKG, PKG, "tag", 0, n, mUserId); waitForIdle(); StatusBarNotification[] posted = mBinderService.getActiveNotifications(PKG); @@ -1982,7 +1982,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { public void enqueueNotificationWithTag_usesAndFinishesTracker() throws Exception { mBinderService.enqueueNotificationWithTag(PKG, PKG, "testEnqueueNotificationWithTag_PopulatesGetActiveNotifications", 0, - generateNotificationRecord(null).getNotification(), 0); + generateNotificationRecord(null).getNotification(), mUserId); assertThat(mPostNotificationTrackerFactory.mCreatedTrackers).hasSize(1); assertThat(mPostNotificationTrackerFactory.mCreatedTrackers.get(0).isOngoing()).isTrue(); @@ -2000,7 +2000,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { assertThrows(Exception.class, () -> mBinderService.enqueueNotificationWithTag(PKG, PKG, "testEnqueueNotificationWithTag_PopulatesGetActiveNotifications", 0, - /* notification= */ null, 0)); + /* notification= */ null, mUserId)); waitForIdle(); @@ -2017,7 +2017,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { mBinderService.enqueueNotificationWithTag(PKG, PKG, "testEnqueueNotificationWithTag_PopulatesGetActiveNotifications", 0, - generateNotificationRecord(null).getNotification(), 0); + generateNotificationRecord(null).getNotification(), mUserId); waitForIdle(); assertThat(mBinderService.getActiveNotifications(PKG)).hasLength(0); @@ -2032,7 +2032,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { mBinderService.enqueueNotificationWithTag(PKG, PKG, "testEnqueueNotificationWithTag_PopulatesGetActiveNotifications", 0, - generateNotificationRecord(null).getNotification(), 0); + generateNotificationRecord(null).getNotification(), mUserId); waitForIdle(); assertThat(mBinderService.getActiveNotifications(PKG)).hasLength(0); @@ -2044,7 +2044,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { public void enqueueNotification_acquiresAndReleasesWakeLock() throws Exception { mBinderService.enqueueNotificationWithTag(PKG, PKG, "enqueueNotification_acquiresAndReleasesWakeLock", 0, - generateNotificationRecord(null).getNotification(), 0); + generateNotificationRecord(null).getNotification(), mUserId); verify(mPowerManager).newWakeLock(eq(PARTIAL_WAKE_LOCK), anyString()); assertThat(mAcquiredWakeLocks).hasSize(1); @@ -2062,7 +2062,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { assertThrows(Exception.class, () -> mBinderService.enqueueNotificationWithTag(PKG, PKG, "enqueueNotification_throws_acquiresAndReleasesWakeLock", 0, - /* notification= */ null, 0)); + /* notification= */ null, mUserId)); verify(mPowerManager).newWakeLock(eq(PARTIAL_WAKE_LOCK), anyString()); assertThat(mAcquiredWakeLocks).hasSize(1); @@ -2077,7 +2077,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { mBinderService.enqueueNotificationWithTag(PKG, PKG, "enqueueNotification_notEnqueued_acquiresAndReleasesWakeLock", 0, - generateNotificationRecord(null).getNotification(), 0); + generateNotificationRecord(null).getNotification(), mUserId); verify(mPowerManager).newWakeLock(eq(PARTIAL_WAKE_LOCK), anyString()); assertThat(mAcquiredWakeLocks).hasSize(1); @@ -2098,7 +2098,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { mBinderService.enqueueNotificationWithTag(PKG, PKG, "enqueueNotification_notPosted_acquiresAndReleasesWakeLock", 0, - notif, 0); + notif, mUserId); verify(mPowerManager).newWakeLock(eq(PARTIAL_WAKE_LOCK), anyString()); assertThat(mAcquiredWakeLocks).hasSize(1); @@ -2123,7 +2123,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { mBinderService.enqueueNotificationWithTag(PKG, PKG, "enqueueNotification_setsWakeLockWorkSource", 0, - generateNotificationRecord(null).getNotification(), 0); + generateNotificationRecord(null).getNotification(), mUserId); waitForIdle(); InOrder inOrder = inOrder(mPowerManager, wakeLock); @@ -2137,7 +2137,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { @Test public void testCancelNonexistentNotification() throws Exception { mBinderService.cancelNotificationWithTag(PKG, PKG, - "testCancelNonexistentNotification", 0, 0); + "testCancelNonexistentNotification", 0, mUserId); waitForIdle(); // The notification record logger doesn't even get called when a nonexistent notification // is cancelled, because that happens very frequently and is not interesting. @@ -2148,9 +2148,9 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { public void testCancelNotificationImmediatelyAfterEnqueue() throws Exception { mBinderService.enqueueNotificationWithTag(PKG, PKG, "testCancelNotificationImmediatelyAfterEnqueue", 0, - generateNotificationRecord(null).getNotification(), 0); + generateNotificationRecord(null).getNotification(), mUserId); mBinderService.cancelNotificationWithTag(PKG, PKG, - "testCancelNotificationImmediatelyAfterEnqueue", 0, 0); + "testCancelNotificationImmediatelyAfterEnqueue", 0, mUserId); waitForIdle(); StatusBarNotification[] notifs = mBinderService.getActiveNotifications(PKG); @@ -2185,13 +2185,13 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { public void testCancelNotificationWhilePostedAndEnqueued() throws Exception { mBinderService.enqueueNotificationWithTag(PKG, PKG, "testCancelNotificationWhilePostedAndEnqueued", 0, - generateNotificationRecord(null).getNotification(), 0); + generateNotificationRecord(null).getNotification(), mUserId); waitForIdle(); mBinderService.enqueueNotificationWithTag(PKG, PKG, "testCancelNotificationWhilePostedAndEnqueued", 0, - generateNotificationRecord(null).getNotification(), 0); + generateNotificationRecord(null).getNotification(), mUserId); mBinderService.cancelNotificationWithTag(PKG, PKG, - "testCancelNotificationWhilePostedAndEnqueued", 0, 0); + "testCancelNotificationWhilePostedAndEnqueued", 0, mUserId); waitForIdle(); StatusBarNotification[] notifs = mBinderService.getActiveNotifications(PKG); @@ -3406,12 +3406,12 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { @Test public void testPostNotification_appPermissionFixed() throws Exception { when(mPermissionHelper.hasPermission(mUid)).thenReturn(true); - when(mPermissionHelper.isPermissionFixed(PKG, 0)).thenReturn(true); + when(mPermissionHelper.isPermissionFixed(PKG, mUserId)).thenReturn(true); NotificationRecord temp = generateNotificationRecord(mTestNotificationChannel); mBinderService.enqueueNotificationWithTag(PKG, PKG, "testPostNotification_appPermissionFixed", 0, - temp.getNotification(), 0); + temp.getNotification(), mUserId); waitForIdle(); assertThat(mService.getNotificationRecordCount()).isEqualTo(1); StatusBarNotification[] notifs = @@ -3443,7 +3443,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { Notification.TvExtender tv = new Notification.TvExtender().setChannelId("foo"); mBinderService.enqueueNotificationWithTag(PKG, PKG, "testTvExtenderChannelOverride_onTv", 0, - generateNotificationRecord(null, tv).getNotification(), 0); + generateNotificationRecord(null, tv).getNotification(), mUserId); verify(mPreferencesHelper, times(1)).getConversationNotificationChannel( anyString(), anyInt(), eq("foo"), eq(null), anyBoolean(), anyBoolean()); } @@ -3458,7 +3458,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { Notification.TvExtender tv = new Notification.TvExtender().setChannelId("foo"); mBinderService.enqueueNotificationWithTag(PKG, PKG, "testTvExtenderChannelOverride_notOnTv", - 0, generateNotificationRecord(null, tv).getNotification(), 0); + 0, generateNotificationRecord(null, tv).getNotification(), mUserId); verify(mPreferencesHelper, times(1)).getConversationNotificationChannel( anyString(), anyInt(), eq(mTestNotificationChannel.getId()), eq(null), anyBoolean(), anyBoolean()); @@ -11686,7 +11686,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { // 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(); @@ -11769,7 +11769,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { // 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); @@ -11859,10 +11859,10 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { @Test public void testGetActiveNotification_filtersUsers() throws Exception { - when(mUm.getProfileIds(0, false)).thenReturn(new int[]{0, 10}); + when(mUm.getProfileIds(mUserId, false)).thenReturn(new int[]{mUserId, 10}); NotificationRecord nr0 = - generateNotificationRecord(mTestNotificationChannel, 0); + generateNotificationRecord(mTestNotificationChannel, mUserId); mBinderService.enqueueNotificationWithTag(PKG, PKG, "tag0", nr0.getSbn().getId(), nr0.getSbn().getNotification(), nr0.getSbn().getUserId()); @@ -12316,7 +12316,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { .setFullScreenIntent(mock(PendingIntent.class), true) .build(); - mService.fixNotification(n, PKG, "tag", 9, 0, mUid, NOT_FOREGROUND_SERVICE, true); + mService.fixNotification(n, PKG, "tag", 9, mUserId, mUid, NOT_FOREGROUND_SERVICE, true); final int stickyFlag = n.flags & Notification.FLAG_FSI_REQUESTED_BUT_DENIED; 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 670d09795e12..130a8ca54c7c 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.net.Uri; 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 @@ public class NotificationRecordTest extends UiServiceTestCase { 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 @@ public class NotificationRecordTest extends UiServiceTestCase { 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 @@ public class NotificationRecordTest extends UiServiceTestCase { 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 @@ public class NotificationRecordTest extends UiServiceTestCase { 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 @@ public class NotificationRecordTest extends UiServiceTestCase { 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 @@ public class NotificationRecordTest extends UiServiceTestCase { 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 5ddac034d116..8b557789195a 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 @@ public class PreferencesHelperTest extends UiServiceTestCase { 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 @@ public class PreferencesHelperTest extends UiServiceTestCase { 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/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java index 5d114f4bb702..74622014aa5c 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java @@ -5202,7 +5202,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { // rules for a missing package, created a long time ago and deleted a long time ago config.deletedRules.put("del6", newZenRule("bad_pkg", twoMonthsAgo, twoMonthsAgo)); - mZenModeHelper.onUserUnlocked(42); // copies config and cleans it up. + mZenModeHelper.onUserSwitched(42); // copies config and cleans it up. assertThat(mZenModeHelper.mConfig.automaticRules.keySet()) .containsExactly("ar1", "ar2", "ar3", "ar4"); 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 56c3ec0ffe2d..c2a5824d4ffd 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 @@ public class AssistDataRequesterTest { 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/ClientLifecycleManagerTests.java b/services/tests/wmtests/src/com/android/server/wm/ClientLifecycleManagerTests.java index 09f677e701df..46e14d51b5fd 100644 --- a/services/tests/wmtests/src/com/android/server/wm/ClientLifecycleManagerTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/ClientLifecycleManagerTests.java @@ -16,8 +16,6 @@ package com.android.server.wm; -import static android.platform.test.flag.junit.SetFlagsRule.DefaultInitValueType.DEVICE_DEFAULT; - import static com.android.dx.mockito.inline.extended.ExtendedMockito.spy; import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify; @@ -38,13 +36,12 @@ import android.app.servertransaction.ClientTransactionItem; import android.os.IBinder; import android.os.RemoteException; import android.platform.test.annotations.Presubmit; -import android.platform.test.flag.junit.SetFlagsRule; import androidx.test.filters.SmallTest; import org.junit.Before; -import org.junit.Rule; import org.junit.Test; +import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; import org.mockito.Captor; import org.mockito.Mock; @@ -56,13 +53,8 @@ import org.mockito.MockitoAnnotations; */ @SmallTest @Presubmit -public class ClientLifecycleManagerTests { - - @Rule(order = 0) - public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(DEVICE_DEFAULT); - - @Rule(order = 1) - public final SystemServicesTestRule mSystemServices = new SystemServicesTestRule(); +@RunWith(WindowTestRunner.class) +public class ClientLifecycleManagerTests extends SystemServiceTestsBase { @Mock private IBinder mClientBinder; @@ -86,7 +78,7 @@ public class ClientLifecycleManagerTests { public void setup() { MockitoAnnotations.initMocks(this); - mWms = mSystemServices.getWindowManagerService(); + mWms = mSystemServicesTestRule.getWindowManagerService(); mLifecycleManager = spy(new ClientLifecycleManager()); mLifecycleManager.setWindowManager(mWms); 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 95850ac2f3b2..f63ff6ec66e1 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 @@ public class DisplayContentTests extends WindowTestsBase { 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/TaskFragmentOrganizerControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java index a88285ac4c8f..897a3da07473 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 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase { @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 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase { } @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 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase { 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 98ca0947a1cf..01bd96b8a772 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 @@ public class TaskFragmentTest extends WindowTestsBase { 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 74aabe15ba34..aa9c0c8457f5 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 androidx.test.filters.SmallTest; 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 @@ public class WindowOrganizerTests extends WindowTestsBase { 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/WindowTestsBase.java b/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java index be837441ef94..28e03bfeaadb 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 @@ class WindowTestsBase extends SystemServiceTestsBase { TestStartingWindowOrganizer(ActivityTaskManagerService service) { mAtm = service; mWMService = mAtm.mWindowManager; - mAtm.mTaskOrganizerController.setDeferTaskOrgCallbacksConsumer(Runnable::run); mAtm.mTaskOrganizerController.registerTaskOrganizer(this); } @@ -1802,8 +1801,7 @@ class WindowTestsBase extends SystemServiceTestsBase { @Override public void addStartingWindow(StartingWindowInfo info) { synchronized (mWMService.mGlobalLock) { - final ActivityRecord activity = mWMService.mRoot.getActivityRecord( - info.appToken); + final ActivityRecord activity = ActivityRecord.forTokenLocked(info.appToken); IWindow iWindow = mock(IWindow.class); doReturn(mock(IBinder.class)).when(iWindow).asBinder(); final WindowState window = WindowTestsBase.createWindow(null, @@ -1825,8 +1823,7 @@ class WindowTestsBase extends SystemServiceTestsBase { final IBinder appToken = mTaskAppMap.get(removalInfo.taskId); if (appToken != null) { mTaskAppMap.remove(removalInfo.taskId); - final ActivityRecord activity = mWMService.mRoot.getActivityRecord( - appToken); + final ActivityRecord activity = ActivityRecord.forTokenLocked(appToken); WindowState win = mAppWindowMap.remove(appToken); activity.removeChild(win); activity.mStartingWindow = null; diff --git a/telecomm/OWNERS b/telecomm/OWNERS index b57b7c79326e..bb2ac51a24f1 100644 --- a/telecomm/OWNERS +++ b/telecomm/OWNERS @@ -6,4 +6,5 @@ xiaotonj@google.com 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 a2105b02b97a..1df6cf78047c 100644 --- a/telecomm/java/android/telecom/Connection.java +++ b/telecomm/java/android/telecom/Connection.java @@ -21,6 +21,7 @@ import static android.Manifest.permission.MODIFY_PHONE_STATE; 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 android.view.Surface; 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 @@ public abstract class Connection extends Conferenceable { /** * 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 @@ public abstract class Connection extends Conferenceable { * {@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 @@ public abstract class Connection extends Conferenceable { } /** + * 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/TelecomManager.java b/telecomm/java/android/telecom/TelecomManager.java index 2c6e1e48b57f..e62bd90807d1 100644 --- a/telecomm/java/android/telecom/TelecomManager.java +++ b/telecomm/java/android/telecom/TelecomManager.java @@ -1359,8 +1359,9 @@ public class TelecomManager { /** * 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 @@ public class TelecomManager { * * @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 f1bfd227298c..412e8273b5e6 100644 --- a/telecomm/java/com/android/internal/telecom/ITelecomService.aidl +++ b/telecomm/java/com/android/internal/telecom/ITelecomService.aidl @@ -395,7 +395,7 @@ interface ITelecomService { * @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 287aa653ef9a..7607c64150d8 100644 --- a/telephony/OWNERS +++ b/telephony/OWNERS @@ -15,4 +15,4 @@ per-file CarrierConfigManager.java=set noparent 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/telephony/CarrierConfigManager.java b/telephony/java/android/telephony/CarrierConfigManager.java index e5a94c302c89..f7793f373916 100644 --- a/telephony/java/android/telephony/CarrierConfigManager.java +++ b/telephony/java/android/telephony/CarrierConfigManager.java @@ -7517,8 +7517,8 @@ public class CarrierConfigManager { * * 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 @@ public class CarrierConfigManager { * * 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 @@ public class CarrierConfigManager { * 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 @@ public class CarrierConfigManager { * 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 @@ public class CarrierConfigManager { /** * 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 @@ public class CarrierConfigManager { * {{@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 @@ public class CarrierConfigManager { * {{@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 @@ public class CarrierConfigManager { * 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 @@ public class CarrierConfigManager { * {@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 @@ public class CarrierConfigManager { * 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 @@ public class CarrierConfigManager { * 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 @@ public class CarrierConfigManager { /** * 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 @@ public class CarrierConfigManager { * {@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 @@ public class CarrierConfigManager { * 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 @@ public class CarrierConfigManager { * 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 @@ public class CarrierConfigManager { * 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 @@ public class CarrierConfigManager { * 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 @@ public class CarrierConfigManager { * 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 @@ public class CarrierConfigManager { * @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 @@ public class CarrierConfigManager { * 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 @@ public class CarrierConfigManager { * 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 @@ public class CarrierConfigManager { * 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 @@ public class CarrierConfigManager { 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 @@ public class CarrierConfigManager { "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 @@ public class CarrierConfigManager { 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/DomainSelectionService.java b/telephony/java/android/telephony/DomainSelectionService.java index abcce5f61aee..0f54e8d9457e 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.ITransportSelectorCallback; 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,12 +58,38 @@ import java.util.concurrent.Executor; 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 */ +@SystemApi +@FlaggedApi(Flags.FLAG_USE_OEM_DOMAIN_SELECTION_SERVICE) public class DomainSelectionService extends Service { private static final String LOG_TAG = "DomainSelectionService"; @@ -78,16 +107,13 @@ public class DomainSelectionService extends Service { @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,51 +136,52 @@ public class DomainSelectionService extends Service { /** * 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; /** - * @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; + 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; @@ -167,14 +194,14 @@ public class DomainSelectionService extends Service { * @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; @@ -188,16 +215,16 @@ public class DomainSelectionService extends Service { } /** - * @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 @@ public class DomainSelectionService extends Service { } /** - * @return The dialed number. + * @return The dialed address. */ - public @Nullable String getNumber() { - return mNumber; + public @Nullable Uri getAddress() { + return mAddress; } /** @@ -237,18 +264,19 @@ public class DomainSelectionService extends Service { } /** + * @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. @@ -274,13 +302,14 @@ public class DomainSelectionService extends Service { @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 @@ -293,13 +322,13 @@ public class DomainSelectionService extends Service { 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); @@ -307,9 +336,9 @@ public class DomainSelectionService extends Service { @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, + mEmergencyRegResult, mSlotIndex, mSubId, mSelectorType, mCause); } @Override @@ -319,30 +348,31 @@ public class DomainSelectionService extends Service { @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); } 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(); @@ -370,16 +400,17 @@ public class DomainSelectionService extends Service { /** * 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; @@ -387,9 +418,9 @@ public class DomainSelectionService extends Service { /** * 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; } @@ -405,63 +436,60 @@ public class DomainSelectionService extends Service { } /** - * 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(@NonNull 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 the request caused the device to move out of airplane mode. + * Sets whether it's a test emergency number or not. * - * @param exited {@code true} if the request caused the device to move out of - * airplane mode. + * @param isTest Indicates it's a test emergency number. * @return The same instance of the builder. */ - public @NonNull Builder setExitedFromAirplaneMode(boolean exited) { - mIsExitedFromAirplaneMode = exited; + public @NonNull Builder setTestEmergencyNumber(boolean isTest) { + mIsTestEmergencyNumber = isTest; return this; } /** - * Sets the Ut service attributes. - * Only applicable for SELECTOR_TYPE_UT + * Sets whether the request caused the device to move out of airplane mode. * - * @param attr Ut services attributes. + * @param exited {@code true} if the request caused the device to move out of + * airplane mode. * @return The same instance of the builder. */ - /* - public @NonNull Builder setUtAttributes(@NonNull UtAttributes attr); - mUtAttributes = attr; + public @NonNull Builder setExitedFromAirplaneMode(boolean exited) { + mIsExitedFromAirplaneMode = exited; return this; } - */ /** * Sets an optional reason why the last PS attempt failed. @@ -501,9 +529,10 @@ public class DomainSelectionService extends Service { * @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, mEmergencyRegResult); } } } @@ -546,19 +575,6 @@ public class DomainSelectionService extends Service { } @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 +643,6 @@ public class DomainSelectionService extends Service { } @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 +695,15 @@ public class DomainSelectionService extends Service { @Override public void onRequestEmergencyNetworkScan(@NonNull List<Integer> preferredNetworks, - @EmergencyScanType int scanType, @NonNull CancellationSignal signal, + @EmergencyScanType int scanType, boolean resetScan, + @NonNull CancellationSignal signal, @NonNull Consumer<EmergencyRegResult> 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); } @@ -738,7 +746,15 @@ public class DomainSelectionService extends Service { 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. @@ -748,23 +764,24 @@ public class DomainSelectionService extends Service { } /** - * 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 +796,19 @@ public class DomainSelectionService extends Service { } @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 +836,8 @@ public class DomainSelectionService extends Service { /** @hide */ @Override - public IBinder onBind(Intent intent) { + public @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 +846,13 @@ public class DomainSelectionService extends Service { } /** - * 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; } @@ -845,7 +866,7 @@ public class DomainSelectionService extends Service { public @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 087183122670..5d25d3a8037b 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.java b/telephony/java/android/telephony/EmergencyRegResult.java index 5aed41254cf6..15579be2d786 100644 --- a/telephony/java/android/telephony/EmergencyRegResult.java +++ b/telephony/java/android/telephony/EmergencyRegResult.java @@ -16,17 +16,25 @@ 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 */ +@SystemApi +@FlaggedApi(Flags.FLAG_USE_OEM_DOMAIN_SELECTION_SERVICE) public final class EmergencyRegResult implements Parcelable { /** @@ -77,7 +85,7 @@ public final class EmergencyRegResult implements Parcelable { * 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 @@ -108,7 +116,7 @@ public final class EmergencyRegResult implements Parcelable { mNwProvidedEmf = emf; mMcc = mcc; mMnc = mnc; - mIso = iso; + mCountryIso = iso; } /** @@ -127,7 +135,7 @@ public final class EmergencyRegResult implements Parcelable { mNwProvidedEmf = s.mNwProvidedEmf; mMcc = s.mMcc; mMnc = s.mMnc; - mIso = s.mIso; + mCountryIso = s.mCountryIso; } /** @@ -226,8 +234,8 @@ public final class EmergencyRegResult implements Parcelable { * * @return Country code. */ - public @NonNull String getIso() { - return mIso; + public @NonNull String getCountryIso() { + return mCountryIso; } @Override @@ -242,7 +250,7 @@ public final class EmergencyRegResult implements Parcelable { + ", emf=" + mNwProvidedEmf + ", mcc=" + mMcc + ", mnc=" + mMnc - + ", iso=" + mIso + + ", iso=" + mCountryIso + " }"; } @@ -260,7 +268,7 @@ public final class EmergencyRegResult implements Parcelable { && 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 @@ public final class EmergencyRegResult implements Parcelable { return Objects.hash(mAccessNetworkType, mRegState, mDomain, mIsVopsSupported, mIsEmcBearerSupported, mNwProvidedEmc, mNwProvidedEmf, - mMcc, mMnc, mIso); + mMcc, mMnc, mCountryIso); } @Override @@ -287,7 +295,7 @@ public final class EmergencyRegResult implements Parcelable { out.writeInt(mNwProvidedEmf); out.writeString8(mMcc); out.writeString8(mMnc); - out.writeString8(mIso); + out.writeString8(mCountryIso); } private void readFromParcel(@NonNull Parcel in) { @@ -300,7 +308,7 @@ public final class EmergencyRegResult implements Parcelable { mNwProvidedEmf = in.readInt(); mMcc = in.readString8(); mMnc = in.readString8(); - mIso = in.readString8(); + mCountryIso = in.readString8(); } public static final @NonNull Creator<EmergencyRegResult> CREATOR = diff --git a/telephony/java/android/telephony/NetworkRegistrationInfo.java b/telephony/java/android/telephony/NetworkRegistrationInfo.java index 7ccc27e2f94e..0c324e6061cb 100644 --- a/telephony/java/android/telephony/NetworkRegistrationInfo.java +++ b/telephony/java/android/telephony/NetworkRegistrationInfo.java @@ -649,12 +649,19 @@ public final class NetworkRegistrationInfo implements Parcelable { } /** - * @return Reason for denial if the registration state is {@link #REGISTRATION_STATE_DENIED}. - * Depending on {@code accessNetworkTechnology}, the values are defined in 3GPP TS 24.008 - * 10.5.3.6 for UMTS, 3GPP TS 24.301 9.9.3.9 for LTE, and 3GPP2 A.S0001 6.2.2.44 for CDMA - * @hide + * Get the 3GPP/3GPP2 reason code indicating why registration failed. + * + * Returns the reason code for non-transient registration failures. Typically this method will + * only return the last reason code received during a network selection procedure. The reason + * code is system-specific; however, the reason codes for both 3GPP and 3GPP2 systems are + * largely equivalent across generations. + * + * @return registration reject cause if available, otherwise 0. Depending on + * {@link #getAccessNetworkTechnology}, the values are defined in 3GPP TS 24.008 10.5.3.6 for + * WCDMA/UMTS, 3GPP TS 24.301 9.9.3.9 for LTE/EPS, 3GPP 24.501 Annex A for NR/5GS, or 3GPP2 + * A.S0001 6.2.2.44 for CDMA. */ - @SystemApi + @FlaggedApi(Flags.FLAG_NETWORK_REGISTRATION_INFO_REJECT_CAUSE) public int getRejectCause() { return mRejectCause; } diff --git a/telephony/java/android/telephony/PreciseDisconnectCause.java b/telephony/java/android/telephony/PreciseDisconnectCause.java index 1cfd22cc4d65..d9437ab29881 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 @@ public final class PreciseDisconnectCause { /** * 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/SubscriptionManager.java b/telephony/java/android/telephony/SubscriptionManager.java index eb7e67dccfd5..174954542b94 100644 --- a/telephony/java/android/telephony/SubscriptionManager.java +++ b/telephony/java/android/telephony/SubscriptionManager.java @@ -1927,34 +1927,25 @@ public class SubscriptionManager { * 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 +1961,8 @@ public class SubscriptionManager { if (activeList != null) { activeList = activeList.stream().filter(subInfo -> isSubscriptionVisible(subInfo)) .collect(Collectors.toList()); + } else { + activeList = Collections.emptyList(); } return activeList; } @@ -1998,12 +1991,7 @@ public class SubscriptionManager { * {@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 +2020,7 @@ public class SubscriptionManager { * * @hide */ - public @Nullable List<SubscriptionInfo> getActiveSubscriptionInfoList(boolean userVisibleOnly) { + public @NonNull List<SubscriptionInfo> getActiveSubscriptionInfoList(boolean userVisibleOnly) { List<SubscriptionInfo> activeList = null; try { @@ -2045,11 +2033,13 @@ public class SubscriptionManager { // 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 +2076,7 @@ public class SubscriptionManager { * @hide */ @SystemApi - public List<SubscriptionInfo> getAvailableSubscriptionInfoList() { + public @Nullable List<SubscriptionInfo> getAvailableSubscriptionInfoList() { List<SubscriptionInfo> result = null; try { @@ -2098,7 +2088,7 @@ public class SubscriptionManager { } catch (RemoteException ex) { // ignore it } - return result; + return (result == null) ? Collections.emptyList() : result; } /** @@ -2128,7 +2118,7 @@ public class SubscriptionManager { * @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 +2129,7 @@ public class SubscriptionManager { } catch (RemoteException ex) { // ignore it } - return result; + return (result == null) ? Collections.emptyList() : result; } /** diff --git a/telephony/java/android/telephony/TelephonyManager.java b/telephony/java/android/telephony/TelephonyManager.java index 89661a4368ec..61c7a420b604 100644 --- a/telephony/java/android/telephony/TelephonyManager.java +++ b/telephony/java/android/telephony/TelephonyManager.java @@ -18709,9 +18709,9 @@ public class TelephonyManager { */ @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 @@ public class TelephonyManager { * @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 @@ public class TelephonyManager { * @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 @@ public class TelephonyManager { } /** - * 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 @@ public class TelephonyManager { @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 @@ public class TelephonyManager { * 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 @@ public class TelephonyManager { @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 @@ public class TelephonyManager { .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 @@ public class TelephonyManager { /** * 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 04752e418466..0f5dd27120e3 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 @@ public interface TransportSelectorCallback { 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 f9c2620cfaf8..ea83815c146d 100644 --- a/telephony/java/android/telephony/WwanSelectorCallback.java +++ b/telephony/java/android/telephony/WwanSelectorCallback.java @@ -16,29 +16,38 @@ 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, + @EmergencyScanType int scanType, boolean resetScan, @NonNull CancellationSignal signal, @NonNull Consumer<EmergencyRegResult> consumer); /** diff --git a/telephony/java/com/android/internal/telephony/IDomainSelector.aidl b/telephony/java/com/android/internal/telephony/IDomainSelector.aidl index d94840b112ee..0eeadee78abd 100644 --- a/telephony/java/com/android/internal/telephony/IDomainSelector.aidl +++ b/telephony/java/com/android/internal/telephony/IDomainSelector.aidl @@ -19,7 +19,6 @@ package com.android.internal.telephony; 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/ITelephony.aidl b/telephony/java/com/android/internal/telephony/ITelephony.aidl index a1fc064d0d81..213fbc55d597 100644 --- a/telephony/java/com/android/internal/telephony/ITelephony.aidl +++ b/telephony/java/com/android/internal/telephony/ITelephony.aidl @@ -3203,6 +3203,18 @@ interface ITelephony { 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 6777256d171e..26daacdeca43 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.IDomainSelector; 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 94e7c871066e..87955ac5a4b2 100644 --- a/telephony/java/com/android/internal/telephony/IWwanSelectorCallback.aidl +++ b/telephony/java/com/android/internal/telephony/IWwanSelectorCallback.aidl @@ -20,7 +20,7 @@ import com.android.internal.telephony.IWwanSelectorResultCallback; 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/tests/Input/Android.bp b/tests/Input/Android.bp index cf2d5d69552f..d17cd1fa2c3b 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 5434c82b07bd..5f1bc8748db8 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 @@ public class InputDeviceTest { 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 @@ public class InputDeviceTest { .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 58ceb3f3edf4..5ed8d8d528c4 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 23efe548c82a..27511411c97e 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 ddb3649a8320..12d43383a6e2 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 ed34fa9fc1d0..0f21035b3cf8 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 0bdb3a8c6b14..38355530b8c4 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 5d3abc63519f..92764020b120 100644 --- a/tools/aapt/ApkBuilder.h +++ b/tools/aapt/ApkBuilder.h @@ -44,7 +44,7 @@ public: 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 2f2ef92e72b4..66a05101c2a1 100644 --- a/tools/aapt/Main.cpp +++ b/tools/aapt/Main.cpp @@ -187,7 +187,7 @@ void usage(void) " 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" |